File size: 2,091 Bytes
0d57d79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
874bd3c
 
 
 
 
0d57d79
874bd3c
d594446
 
874bd3c
0d57d79
 
 
 
 
 
 
 
 
 
 
 
874bd3c
0d57d79
 
 
 
 
d594446
 
 
 
 
 
 
0d57d79
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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);