splitwise / src /lib /server /storage /store.ts
assafvayner's picture
assafvayner HF Staff
fix(storage): skip integration suite safely, tighten list contract, drop expand
d594446
Raw History Blame Contribute Delete
2.09 kB
export interface StoreEntry {
path: string;
size: number;
uploadedAt?: Date;
}
/**
* Minimal object-store abstraction. Paths are bucket-relative, use `/` separators,
* and never start with `/`.
*/
export interface BucketStore {
/** Resolves null when the object does not exist. */
get(path: string): Promise<Uint8Array | null>;
/** Creates or overwrites. */
put(path: string, bytes: Uint8Array, contentType?: string): Promise<void>;
/**
* Appends bytes to an existing object, creating it if missing. Not atomic on every
* backend: a crash mid-append may leave a truncated final line, so readers must
* tolerate a trailing partial line.
*/
append(path: string, bytes: Uint8Array): Promise<void>;
/**
* Lists files whose path starts with `prefix`, sorted by path. The prefix must be `''`
* (the whole store) or end with `/`, so it always matches within a directory.
*/
list(prefix: string): Promise<StoreEntry[]>;
/** Deletes objects; missing ones are ignored. */
delete(paths: string[]): Promise<void>;
/** Server-side copy. Rejects if `src` does not exist. */
copy(src: string, dst: string): Promise<void>;
}
const SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
export function assertValidPath(path: string): void {
if (path.length === 0 || path.length > 512) throw new Error(`Invalid path: ${path}`);
if (path.startsWith('/') || path.endsWith('/')) throw new Error(`Invalid path: ${path}`);
if (path.endsWith('.tmp')) throw new Error(`Invalid path: ${path}`);
for (const seg of path.split('/')) {
if (seg === '' || seg === '.' || seg === '..' || !SEGMENT_RE.test(seg)) throw new Error(`Invalid path: ${path}`);
}
}
/** Validates a `list` prefix: either `''` or a valid path followed by `/`. */
export function assertValidPrefix(prefix: string): void {
if (prefix === '') return;
if (!prefix.endsWith('/')) throw new Error(`Invalid prefix: ${prefix}`);
assertValidPath(prefix.slice(0, -1));
}
export const encodeText = (s: string): Uint8Array => new TextEncoder().encode(s);
export const decodeText = (b: Uint8Array): string => new TextDecoder().decode(b);