refactor: split off tus upload to getTusOptions so it can be reusable to external libraries

This commit is contained in:
Derrick Hammer 2024-03-17 14:06:10 -04:00
parent b4d1e0ec67
commit ba370250a2
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
2 changed files with 100 additions and 87 deletions

View File

@ -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;

View File

@ -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");
}