@@ -15,12 +15,17 @@ class ExtAPI extends API
15
15
constructor ( collections , { softwareString = "" , replaceSoftwareString = false } = { } ) {
16
16
super ( collections ) ;
17
17
this . softwareString = replaceSoftwareString ? softwareString : softwareString + DEFAULT_SOFTWARE_STRING ;
18
+
19
+ this . uploading = new Map ( ) ;
18
20
}
19
21
20
22
get routes ( ) {
21
23
return {
22
24
...super . routes ,
23
25
"downloadPages" : "c/:coll/dl" ,
26
+ "upload" : [ "c/:coll/upload" , "POST" ] ,
27
+ "uploadStatus" : "c/:coll/upload" ,
28
+ "uploadDelete" : [ "c/:coll/upload" , "DELETE" ] ,
24
29
"recPending" : "c/:coll/recPending" ,
25
30
"pageTitle" : [ "c/:coll/pageTitle" , "POST" ] ,
26
31
"ipfsAdd" : [ "c/:coll/ipfs" , "POST" ] ,
@@ -43,6 +48,15 @@ class ExtAPI extends API
43
48
case "downloadPages" :
44
49
return await this . handleDownload ( params ) ;
45
50
51
+ case "upload" :
52
+ return await this . handleUpload ( params , request , event ) ;
53
+
54
+ case "uploadStatus" :
55
+ return await this . getUploadStatus ( params ) ;
56
+
57
+ case "uploadDelete" :
58
+ return await this . deleteUpload ( params ) ;
59
+
46
60
case "recPending" :
47
61
return await this . recordingPending ( params ) ;
48
62
@@ -67,6 +81,11 @@ class ExtAPI extends API
67
81
}
68
82
69
83
async handleDownload ( params ) {
84
+ const dl = await this . getDownloader ( params ) ;
85
+ return dl . download ( ) ;
86
+ }
87
+
88
+ async getDownloader ( params ) {
70
89
const coll = await this . collections . loadColl ( params . coll ) ;
71
90
if ( ! coll ) {
72
91
return { error : "collection_not_found" } ;
@@ -78,8 +97,120 @@ class ExtAPI extends API
78
97
const format = params . _query . get ( "format" ) || "wacz" ;
79
98
let filename = params . _query . get ( "filename" ) ;
80
99
81
- const dl = new Downloader ( { ...this . downloaderOpts ( ) , coll, format, filename, pageList} ) ;
82
- return dl . download ( ) ;
100
+ return new Downloader ( { ...this . downloaderOpts ( ) , coll, format, filename, pageList} ) ;
101
+ }
102
+
103
+ async handleUpload ( params , request , event ) {
104
+ const uploading = this . uploading ;
105
+
106
+ const prevUpload = uploading . get ( params . coll ) ;
107
+
108
+ const { url, headers, abortUpload} = await request . json ( ) ;
109
+
110
+ if ( prevUpload && prevUpload . status === "uploading" ) {
111
+ if ( abortUpload && prevUpload . abort ) {
112
+ prevUpload . abort ( ) ;
113
+ return { aborted : true } ;
114
+ }
115
+ return { error : "already_uploading" } ;
116
+ } else if ( abortUpload ) {
117
+ return { error : "not_uploading" } ;
118
+ }
119
+
120
+ const dl = await this . getDownloader ( params ) ;
121
+ const dlResp = await dl . download ( ) ;
122
+ const filename = dlResp . filename ;
123
+
124
+ const abort = new AbortController ( ) ;
125
+ const signal = abort . signal ;
126
+
127
+ const counter = new CountingStream ( dl . metadata . size , abort ) ;
128
+
129
+ const body = dlResp . body . pipeThrough ( counter . transformStream ( ) ) ;
130
+
131
+ try {
132
+ const urlObj = new URL ( url ) ;
133
+ urlObj . searchParams . set ( "filename" , filename ) ;
134
+ urlObj . searchParams . set ( "name" , dl . metadata . title || filename ) ;
135
+ const fetchPromise = fetch ( urlObj . href , { method : "PUT" , headers, duplex : "half" , body, signal} ) ;
136
+ uploading . set ( params . coll , counter ) ;
137
+ if ( event . waitUntil ) {
138
+ event . waitUntil ( this . uploadFinished ( fetchPromise , params . coll , dl . metadata , filename , counter ) ) ;
139
+ }
140
+ return { uploading : true } ;
141
+ } catch ( e ) {
142
+ uploading . delete ( params . coll ) ;
143
+ return { error : "upload_failed" , details : e . toString ( ) } ;
144
+ }
145
+ }
146
+
147
+ async uploadFinished ( fetchPromise , collId , metadata , filename , counter ) {
148
+ try {
149
+ const resp = await fetchPromise ;
150
+ const json = await resp . json ( ) ;
151
+
152
+ console . log ( `Upload finished for ${ filename } ${ collId } ` ) ;
153
+
154
+ metadata . uploadTime = new Date ( ) . getTime ( ) ;
155
+ metadata . uploadId = json . id ;
156
+ if ( ! metadata . mtime ) {
157
+ metadata . mtime = metadata . uploadTime ;
158
+ }
159
+ if ( ! metadata . ctime ) {
160
+ metadata . ctime = metadata . uploadTime ;
161
+ }
162
+ await this . collections . updateMetadata ( collId , metadata ) ;
163
+ counter . status = "done" ;
164
+
165
+ } catch ( e ) {
166
+ console . log ( `Upload failed for ${ filename } ${ collId } ` ) ;
167
+ console . log ( e ) ;
168
+ counter . status = counter . aborted ? "aborted" : "failed" ;
169
+ }
170
+ }
171
+
172
+ async deleteUpload ( params ) {
173
+ const collId = params . coll ;
174
+
175
+ this . uploading . delete ( collId ) ;
176
+
177
+ const coll = await this . collections . loadColl ( collId ) ;
178
+
179
+ if ( coll && coll . metadata ) {
180
+ coll . metadata . uploadTime = null ;
181
+ coll . metadata . uploadId = null ;
182
+ await this . collections . updateMetadata ( collId , coll . metadata ) ;
183
+ return { deleted : true } ;
184
+ }
185
+
186
+ return { deleted : false } ;
187
+ }
188
+
189
+ async getUploadStatus ( params ) {
190
+ let result = null ;
191
+ const counter = this . uploading . get ( params . coll ) ;
192
+
193
+ if ( ! counter ) {
194
+ result = { status : "idle" } ;
195
+ } else {
196
+ const { size, totalSize, status } = counter ;
197
+ result = { status, size, totalSize} ;
198
+
199
+ if ( status !== "uploading" ) {
200
+ this . uploading . delete ( params . coll ) ;
201
+ }
202
+ }
203
+
204
+ const coll = await this . collections . loadColl ( params . coll ) ;
205
+
206
+ if ( coll && coll . metadata ) {
207
+ result . uploadTime = coll . metadata . uploadTime ;
208
+ result . uploadId = coll . metadata . uploadId ;
209
+ result . ctime = coll . metadata . ctime ;
210
+ result . mtime = coll . metadata . mtime ;
211
+ }
212
+
213
+ return result ;
83
214
}
84
215
85
216
async recordingPending ( params ) {
@@ -226,4 +357,40 @@ async function runIPFSAdd(collId, coll, client, opts, collections, replayOpts) {
226
357
await collections . updateMetadata ( coll . name , coll . config . metadata ) ;
227
358
}
228
359
360
+
361
+ // ===========================================================================
362
+ class CountingStream
363
+ {
364
+ constructor ( totalSize , abort ) {
365
+ this . totalSize = totalSize || 0 ;
366
+ this . status = "uploading" ;
367
+ this . size = 0 ;
368
+ this . _abort = abort ;
369
+ this . aborted = false ;
370
+ }
371
+
372
+ abort ( ) {
373
+ if ( this . _abort ) {
374
+ this . _abort . abort ( ) ;
375
+ this . aborted = true ;
376
+ }
377
+ }
378
+
379
+ transformStream ( ) {
380
+ const counterStream = this ;
381
+
382
+ return new TransformStream ( {
383
+ start ( ) {
384
+ counterStream . size = 0 ;
385
+ } ,
386
+
387
+ transform ( chunk , controller ) {
388
+ counterStream . size += chunk . length ;
389
+ //console.log(`Uploaded: ${counterStream.size}`);
390
+ controller . enqueue ( chunk ) ;
391
+ }
392
+ } ) ;
393
+ }
394
+ }
395
+
229
396
export { ExtAPI } ;
0 commit comments