filebrowser/frontend/src/api/tus.ts
Jon_K 0a3d1642f7 Add user quota management system
Introduces comprehensive quota management with real-time usage tracking, frontend components, backend API endpoints, and TUS upload integration for storage limit enforcement.
2026-01-03 01:07:26 -05:00

145 lines
4.2 KiB
TypeScript

import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { removePrefix } from "@/api/utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
export async function upload(
filePath: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
throw new Error("Tus.io settings are not defined");
}
filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
if (content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
endpoint: `${origin}${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": authStore.jwt,
},
onShouldRetry: function (err) {
const status = err.originalResponse
? err.originalResponse.getStatus()
: 0;
// Do not retry for file conflict.
if (status === 409) {
return false;
}
// Do not retry for quota exceeded (413 Request Entity Too Large)
if (status === 413) {
return false;
}
return true;
},
onError: function (error: Error | tus.DetailedError) {
delete CURRENT_UPLOAD_LIST[filePath];
if (error.message === "Upload aborted") {
return reject(error);
}
let message = "Upload failed";
if (error instanceof tus.DetailedError) {
if (error.originalResponse === null) {
message = "000 No connection";
} else {
const status = error.originalResponse.getStatus();
// Handle quota exceeded error (413)
if (status === 413) {
const quotaUsed = error.originalResponse.getHeader("X-Quota-Used");
const quotaLimit = error.originalResponse.getHeader("X-Quota-Limit");
if (quotaUsed && quotaLimit) {
message = `Cannot upload file: storage quota exceeded. Current usage: ${quotaUsed}, Limit: ${quotaLimit}`;
} else {
message = "Cannot upload file: storage quota exceeded";
}
} else {
message = error.originalResponse.getBody();
}
}
}
console.error(error);
reject(new Error(message));
},
onProgress: function (bytesUploaded) {
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
delete CURRENT_UPLOAD_LIST[filePath];
resolve();
},
});
CURRENT_UPLOAD_LIST[filePath] = upload;
upload.start();
});
}
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return undefined;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
const retryDelays = [];
let delay = 0;
for (let i = 0; i < tusSettings.retryCount; i++) {
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
delay =
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
}
return retryDelays;
}
export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob;
}
function isTusSupported() {
return tus.isSupported === true;
}
export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath]) {
CURRENT_UPLOAD_LIST[filePath].abort(true);
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
new Error("Upload aborted")
);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}