Skip to content
This repository was archived by the owner on Oct 4, 2025. It is now read-only.

Commit ecc9652

Browse files
committed
Support for Upload API (#9)
* upload api: - POST /c/<coll>/upload POST with {url, headers} body to start uploading to url, instead of download - GET /c/<coll>/upload GET return upload progress, including lastUploadTime / lastUploadId - DELETE /c/<coll>/upload' to delete upload info (though not actual upload itself) - when uploading set 'filename' to download WACZ name, 'name' to name of archive collection - support for uploading WACZ files to external browsertrix cloud endpoint, 'PUT c/<coll>/upload' - add support for aborting upload, via POST with abortUpload to upload endpoint API - support for pinging status of current upload, including size and estimated totalSize 'GET c/<coll>/upload' - store upload progress in counter object - store uploadId and uploadTime to determine if new upload is needed (for potential automatic sync) - deps: bump to wabac.js 2.16.4 for including upload metadata + rewriting fidelity bump to 0.4.0
1 parent b169932 commit ecc9652

File tree

3 files changed

+181
-8
lines changed

3 files changed

+181
-8
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@webrecorder/awp-sw",
33
"browser": "dist/sw.js",
4-
"version": "0.3.3",
4+
"version": "0.4.0",
55
"main": "index.js",
66
"type": "module",
77
"repository": {
@@ -18,7 +18,7 @@
1818
"dependencies": {
1919
"@ipld/car": "^5.1.1",
2020
"@ipld/unixfs": "^2.1.1",
21-
"@webrecorder/wabac": "^2.16.3",
21+
"@webrecorder/wabac": "^2.16.4",
2222
"auto-js-ipfs": "^2.3.0",
2323
"client-zip": "^2.3.0",
2424
"hash-wasm": "^4.9.0",

src/api.js

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ class ExtAPI extends API
1515
constructor(collections, {softwareString = "", replaceSoftwareString = false} = {}) {
1616
super(collections);
1717
this.softwareString = replaceSoftwareString ? softwareString : softwareString + DEFAULT_SOFTWARE_STRING;
18+
19+
this.uploading = new Map();
1820
}
1921

2022
get routes() {
2123
return {
2224
...super.routes,
2325
"downloadPages": "c/:coll/dl",
26+
"upload": ["c/:coll/upload", "POST"],
27+
"uploadStatus": "c/:coll/upload",
28+
"uploadDelete": ["c/:coll/upload", "DELETE"],
2429
"recPending": "c/:coll/recPending",
2530
"pageTitle": ["c/:coll/pageTitle", "POST"],
2631
"ipfsAdd": ["c/:coll/ipfs", "POST"],
@@ -43,6 +48,15 @@ class ExtAPI extends API
4348
case "downloadPages":
4449
return await this.handleDownload(params);
4550

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+
4660
case "recPending":
4761
return await this.recordingPending(params);
4862

@@ -67,6 +81,11 @@ class ExtAPI extends API
6781
}
6882

6983
async handleDownload(params) {
84+
const dl = await this.getDownloader(params);
85+
return dl.download();
86+
}
87+
88+
async getDownloader(params) {
7089
const coll = await this.collections.loadColl(params.coll);
7190
if (!coll) {
7291
return {error: "collection_not_found"};
@@ -78,8 +97,120 @@ class ExtAPI extends API
7897
const format = params._query.get("format") || "wacz";
7998
let filename = params._query.get("filename");
8099

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;
83214
}
84215

85216
async recordingPending(params) {
@@ -226,4 +357,40 @@ async function runIPFSAdd(collId, coll, client, opts, collections, replayOpts) {
226357
await collections.updateMetadata(coll.name, coll.config.metadata);
227358
}
228359

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+
229396
export { ExtAPI };

yarn.lock

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -512,15 +512,16 @@
512512
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.0.tgz#f08ea194e01ed45379383a8886e8c85a65a5f26a"
513513
integrity sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w==
514514

515-
"@webrecorder/wabac@^2.16.3":
516-
version "2.16.3"
517-
resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.16.3.tgz#27179e04cac142db2e9eb0e311222778f422cd31"
518-
integrity sha512-hXtW8X0iASF/xUNhplvllcV+g1T0N3vUo99ZiSPXCWJFj04zCI7rl500IG+Cuo92X/mwmWhUXZMbnc2NPgdUWw==
515+
"@webrecorder/wabac@^2.16.4":
516+
version "2.16.4"
517+
resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.16.4.tgz#18a7b33947bd4ed038c7a60f1d43f561b247f52f"
518+
integrity sha512-ZlP8csompVd24MJQnSDkLCQStbv0GpbNy0JsTFlV0Podp8GQ5nKOp8ktx5tOvp62M90kdl3jeb+khY+9KeAdFg==
519519
dependencies:
520520
"@peculiar/asn1-ecc" "^2.3.4"
521521
"@peculiar/asn1-schema" "^2.3.3"
522522
"@peculiar/x509" "^1.9.2"
523523
"@webrecorder/wombat" "^3.5.2"
524+
acorn "^8.9.0"
524525
auto-js-ipfs "^2.1.1"
525526
base64-js "^1.5.1"
526527
brotli "^1.3.3"
@@ -572,6 +573,11 @@ acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0:
572573
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
573574
integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
574575

576+
acorn@^8.9.0:
577+
version "8.10.0"
578+
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
579+
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
580+
575581
actor@^2.3.1:
576582
version "2.3.1"
577583
resolved "https://registry.yarnpkg.com/actor/-/actor-2.3.1.tgz#80ce158bb41338a0c38863bddf0947c1850b6e20"

0 commit comments

Comments
 (0)