Files
OpenCut/apps/web/src/services/storage/quota.ts
T

146 lines
3.2 KiB
TypeScript

const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024;
export interface StorageQuotaStatus {
quotaBytes: number | null;
usageBytes: number | null;
headroomBytes: number | null;
availableBytes: number | null;
}
export interface StorageCapacityCheckResult {
canStore: boolean;
reason: "enough-space" | "insufficient-space" | "estimate-unavailable";
availableBytes: number | null;
}
function normalizeByteValue({ value }: { value: unknown }): number | null {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return null;
}
return value;
}
export function formatStorageBytes({ bytes }: { bytes: number }): string {
if (!Number.isFinite(bytes) || bytes <= 0) {
return "0 B";
}
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) {
value /= 1024;
unitIndex += 1;
}
const precision = value >= 10 || unitIndex === 0 ? 0 : 1;
return `${value.toFixed(precision)} ${BYTE_UNITS[unitIndex]}`;
}
export async function readStorageQuotaStatus(): Promise<StorageQuotaStatus> {
if (
typeof navigator === "undefined" ||
!navigator.storage ||
typeof navigator.storage.estimate !== "function"
) {
return {
quotaBytes: null,
usageBytes: null,
headroomBytes: null,
availableBytes: null,
};
}
const estimate = await navigator.storage.estimate();
const quotaBytes = normalizeByteValue({ value: estimate.quota });
const usageBytes = normalizeByteValue({ value: estimate.usage });
if (quotaBytes === null || usageBytes === null) {
return {
quotaBytes,
usageBytes,
headroomBytes: null,
availableBytes: null,
};
}
const headroomBytes = Math.max(quotaBytes - usageBytes, 0);
const availableBytes = Math.max(
headroomBytes - STORAGE_HEADROOM_RESERVE_BYTES,
0,
);
return {
quotaBytes,
usageBytes,
headroomBytes,
availableBytes,
};
}
export function evaluateStorageCapacity({
requiredBytes,
quotaStatus,
}: {
requiredBytes: number;
quotaStatus: StorageQuotaStatus;
}): StorageCapacityCheckResult {
if (quotaStatus.availableBytes === null) {
return {
canStore: true,
reason: "estimate-unavailable",
availableBytes: null,
};
}
if (requiredBytes > quotaStatus.availableBytes) {
return {
canStore: false,
reason: "insufficient-space",
availableBytes: quotaStatus.availableBytes,
};
}
return {
canStore: true,
reason: "enough-space",
availableBytes: quotaStatus.availableBytes,
};
}
export class StorageQuotaExceededError extends Error {
requiredBytes: number;
constructor({ requiredBytes }: { requiredBytes: number }) {
super(
`Not enough browser storage to save a ${formatStorageBytes({ bytes: requiredBytes })} file.`,
);
this.name = "StorageQuotaExceededError";
this.requiredBytes = requiredBytes;
}
}
export function isStorageQuotaExceededError({
error,
}: {
error: unknown;
}): boolean {
if (error instanceof StorageQuotaExceededError) {
return true;
}
if (!(error instanceof Error)) {
return false;
}
return (
error.name === "QuotaExceededError" ||
error.name === "NS_ERROR_DOM_QUOTA_REACHED" ||
error.message.toLowerCase().includes("quota")
);
}