refactor: split off tus upload to getTusOptions so it can be reusable to external libraries
This commit is contained in:
parent
b4d1e0ec67
commit
ba370250a2
|
@ -8,6 +8,7 @@ import {
|
||||||
uploadLargeFileRequest,
|
uploadLargeFileRequest,
|
||||||
uploadWebapp,
|
uploadWebapp,
|
||||||
uploadWebappRequest,
|
uploadWebappRequest,
|
||||||
|
getTusOptions,
|
||||||
} from "./methods/upload.js";
|
} from "./methods/upload.js";
|
||||||
import {
|
import {
|
||||||
downloadBlob,
|
downloadBlob,
|
||||||
|
@ -53,6 +54,7 @@ export class S5Client {
|
||||||
getEntry = getEntry;
|
getEntry = getEntry;
|
||||||
accountPins = accountPins;
|
accountPins = accountPins;
|
||||||
// Download
|
// Download
|
||||||
|
getTusOptions = getTusOptions;
|
||||||
protected uploadSmallFile = uploadSmallFile;
|
protected uploadSmallFile = uploadSmallFile;
|
||||||
protected uploadSmallFileRequest = uploadSmallFileRequest;
|
protected uploadSmallFileRequest = uploadSmallFileRequest;
|
||||||
protected uploadLargeFile = uploadLargeFile;
|
protected uploadLargeFile = uploadLargeFile;
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { AxiosProgressEvent } from "axios";
|
import { AxiosProgressEvent } from "axios";
|
||||||
import { DetailedError, HttpRequest, Upload } from "tus-js-client";
|
import {
|
||||||
|
DetailedError,
|
||||||
|
HttpRequest,
|
||||||
|
Upload,
|
||||||
|
UploadOptions,
|
||||||
|
} from "tus-js-client";
|
||||||
|
|
||||||
import { blake3 } from "@noble/hashes/blake3";
|
import { blake3 } from "@noble/hashes/blake3";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
@ -17,6 +22,8 @@ import {
|
||||||
import { BaseCustomOptions } from "#methods/registry.js";
|
import { BaseCustomOptions } from "#methods/registry.js";
|
||||||
import { optionsToConfig } from "#utils/options.js";
|
import { optionsToConfig } from "#utils/options.js";
|
||||||
import { buildRequestUrl } from "#request.js";
|
import { buildRequestUrl } from "#request.js";
|
||||||
|
import defer from "p-defer";
|
||||||
|
import { Multihash } from "@lumeweb/libs5/lib/multihash.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tus chunk size is (4MiB - encryptionOverhead) * dataPieces, set as default.
|
* The tus chunk size is (4MiB - encryptionOverhead) * dataPieces, set as default.
|
||||||
|
@ -173,6 +180,45 @@ export async function uploadLargeFileRequest(
|
||||||
file: File,
|
file: File,
|
||||||
customOptions: CustomUploadOptions = {},
|
customOptions: CustomUploadOptions = {},
|
||||||
): Promise<UploadResult> {
|
): Promise<UploadResult> {
|
||||||
|
const p = defer<UploadResult>();
|
||||||
|
|
||||||
|
const options = await this.getTusOptions(
|
||||||
|
file,
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (!upload.url) {
|
||||||
|
p.reject(new Error("'upload.url' was not set"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.resolve({ cid });
|
||||||
|
},
|
||||||
|
onError: (error: Error | DetailedError) => {
|
||||||
|
// Return error body rather than entire error.
|
||||||
|
const res = (error as DetailedError).originalResponse;
|
||||||
|
const newError = res ? new Error(res.getBody().trim()) || error : error;
|
||||||
|
p.reject(newError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customOptions,
|
||||||
|
);
|
||||||
|
const cid = CID.fromHash(
|
||||||
|
Multihash.fromBase64Url(<string>options.metadata?.hash).fullBytes,
|
||||||
|
file.size,
|
||||||
|
CID_TYPES.RAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
const upload = new Upload(file, options);
|
||||||
|
|
||||||
|
return p.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTusOptions(
|
||||||
|
this: S5Client,
|
||||||
|
file: File,
|
||||||
|
tusOptions: Partial<UploadOptions> = {},
|
||||||
|
customOptions: CustomUploadOptions = {},
|
||||||
|
): Promise<UploadOptions> {
|
||||||
const config = optionsToConfig(this, DEFAULT_UPLOAD_OPTIONS, customOptions);
|
const config = optionsToConfig(this, DEFAULT_UPLOAD_OPTIONS, customOptions);
|
||||||
|
|
||||||
// Validation.
|
// Validation.
|
||||||
|
@ -182,104 +228,41 @@ export async function uploadLargeFileRequest(
|
||||||
|
|
||||||
file = ensureFileObjectConsistency(file);
|
file = ensureFileObjectConsistency(file);
|
||||||
|
|
||||||
const onProgress =
|
|
||||||
config.onUploadProgress &&
|
|
||||||
function (bytesSent: number, bytesTotal: number) {
|
|
||||||
const progress = bytesSent / bytesTotal;
|
|
||||||
|
|
||||||
// @ts-expect-error TS complains.
|
|
||||||
config.onUploadProgress(progress, {
|
|
||||||
loaded: bytesSent,
|
|
||||||
total: bytesTotal,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasher = blake3.create({});
|
const hasher = blake3.create({});
|
||||||
|
|
||||||
const chunkSize = 1024 * 1024;
|
const chunkSize = 1024 * 1024;
|
||||||
|
|
||||||
let position = 0;
|
let position = 0;
|
||||||
|
|
||||||
while (position <= file.size) {
|
while (position <= file.size) {
|
||||||
const chunk = file.slice(position, position + chunkSize);
|
const chunk = file.slice(position, position + chunkSize);
|
||||||
hasher.update(new Uint8Array(await chunk.arrayBuffer()));
|
hasher.update(new Uint8Array(await chunk.arrayBuffer()));
|
||||||
position += chunkSize;
|
position += chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
const b3hash = hasher.digest();
|
const b3hash = hasher.digest();
|
||||||
const hash = Buffer.concat([
|
|
||||||
|
const filename = new Multihash(
|
||||||
|
Buffer.concat([
|
||||||
Buffer.alloc(1, CID_HASH_TYPES.BLAKE3),
|
Buffer.alloc(1, CID_HASH_TYPES.BLAKE3),
|
||||||
Buffer.from(b3hash),
|
Buffer.from(b3hash),
|
||||||
]);
|
]),
|
||||||
const cid = Buffer.concat([
|
).toBase64Url();
|
||||||
Buffer.alloc(1, CID_TYPES.RAW),
|
|
||||||
hash,
|
|
||||||
numberToBuffer(file.size),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* convert a number to Buffer.
|
|
||||||
*
|
|
||||||
* @param value - File objects to upload, indexed by their path strings.
|
|
||||||
* @returns - The returned cid.
|
|
||||||
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
|
||||||
*/
|
|
||||||
function numberToBuffer(value: number) {
|
|
||||||
const view = Buffer.alloc(16);
|
|
||||||
let lastIndex = 15;
|
|
||||||
for (let index = 0; index <= 15; ++index) {
|
|
||||||
if (value % 256 !== 0) {
|
|
||||||
lastIndex = index;
|
|
||||||
}
|
|
||||||
view[index] = value % 256;
|
|
||||||
value = value >> 8;
|
|
||||||
}
|
|
||||||
return view.subarray(0, lastIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const filename = hash
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace("=", "");
|
|
||||||
|
|
||||||
const tusOpts = {
|
|
||||||
endpoint: url,
|
endpoint: url,
|
||||||
// retryDelays: opts.retryDelays,
|
|
||||||
metadata: {
|
metadata: {
|
||||||
hash: filename,
|
hash: filename,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
filetype: file.type,
|
filetype: file.type,
|
||||||
},
|
},
|
||||||
config: config.headers,
|
headers: config.headers as any,
|
||||||
onProgress,
|
|
||||||
onBeforeRequest: function (req: HttpRequest) {
|
onBeforeRequest: function (req: HttpRequest) {
|
||||||
const xhr = req.getUnderlyingObject();
|
const xhr = req.getUnderlyingObject();
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
},
|
},
|
||||||
onError: (error: Error | DetailedError) => {
|
...tusOptions,
|
||||||
// Return error body rather than entire error.
|
|
||||||
const res = (error as DetailedError).originalResponse;
|
|
||||||
const newError = res ? new Error(res.getBody().trim()) || error : error;
|
|
||||||
reject(newError);
|
|
||||||
},
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (!upload.url) {
|
|
||||||
reject(new Error("'upload.url' was not set"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resCid =
|
|
||||||
"u" +
|
|
||||||
cid
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace("=", "");
|
|
||||||
resolve({ cid: CID.decode(resCid) });
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = new Upload(file, tusOpts);
|
|
||||||
upload.start();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -392,3 +375,31 @@ export async function uploadWebappRequest(
|
||||||
function ensureFileObjectConsistency(file: File): File {
|
function ensureFileObjectConsistency(file: File): File {
|
||||||
return new File([file], file.name, { type: getFileMimeType(file) });
|
return new File([file], file.name, { type: getFileMimeType(file) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert a number to Buffer.
|
||||||
|
*
|
||||||
|
* @param value - File objects to upload, indexed by their path strings.
|
||||||
|
* @returns - The returned cid.
|
||||||
|
* @throws - Will throw if the request is successful but the upload response does not contain a complete response.
|
||||||
|
*/
|
||||||
|
function numberToBuffer(value: number) {
|
||||||
|
const view = Buffer.alloc(16);
|
||||||
|
let lastIndex = 15;
|
||||||
|
for (let index = 0; index <= 15; ++index) {
|
||||||
|
if (value % 256 !== 0) {
|
||||||
|
lastIndex = index;
|
||||||
|
}
|
||||||
|
view[index] = value % 256;
|
||||||
|
value = value >> 8;
|
||||||
|
}
|
||||||
|
return view.subarray(0, lastIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64Decode(data) {
|
||||||
|
const paddedData = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
||||||
|
|
||||||
|
const base64 = paddedData.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
|
||||||
|
return Buffer.from(base64, "base64");
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue