76 lines · 2.3 KB
1 /**
2 * Git object storage service backed by R2.
3 *
4 * Objects are stored as zlib-deflated loose objects at:
5 * {owner}/{repo}/objects/{sha[0:2]}/{sha[2:]}
6 */
7
8 import { serializeGitObject, type GitObjectType, parseRawGitObject } from '@gitfastr/shared';
9 import { deflate, inflate } from '@gitfastr/shared/git/zlib.js';
10
11 export class ObjectStore {
12 constructor(
13 private bucket: R2Bucket,
14 private owner: string,
15 private repo: string
16 ) {}
17
18 private objectKey(sha: string): string {
19 return `${this.owner}/${this.repo}/objects/${sha.slice(0, 2)}/${sha.slice(2)}`;
20 }
21
22 /** Store a git object. Returns the R2 key. */
23 async put(sha: string, type: GitObjectType, content: Uint8Array): Promise<string> {
24 const raw = serializeGitObject(type, content);
25 const compressed = await deflate(raw);
26 const key = this.objectKey(sha);
27 await this.bucket.put(key, compressed);
28 return key;
29 }
30
31 /** Retrieve a git object by SHA. Returns null if not found. */
32 async get(sha: string): Promise<{ type: GitObjectType; content: Uint8Array } | null> {
33 const key = this.objectKey(sha);
34 const obj = await this.bucket.get(key);
35 if (!obj) return null;
36
37 const compressed = new Uint8Array(await obj.arrayBuffer());
38 const raw = await inflate(compressed);
39 return parseRawGitObject(raw);
40 }
41
42 /** Check if an object exists. */
43 async exists(sha: string): Promise<boolean> {
44 const key = this.objectKey(sha);
45 const head = await this.bucket.head(key);
46 return head !== null;
47 }
48
49 /** Delete an object. */
50 async delete(sha: string): Promise<void> {
51 const key = this.objectKey(sha);
52 await this.bucket.delete(key);
53 }
54
55 /** List all object SHAs in the repo (for pack generation). */
56 async listAll(): Promise<string[]> {
57 const prefix = `${this.owner}/${this.repo}/objects/`;
58 const shas: string[] = [];
59 let cursor: string | undefined;
60
61 do {
62 const listed = await this.bucket.list({ prefix, cursor });
63 for (const obj of listed.objects) {
64 // Key format: owner/repo/objects/ab/cdef...
65 const parts = obj.key.slice(prefix.length).split('/');
66 if (parts.length === 2) {
67 shas.push(parts[0] + parts[1]);
68 }
69 }
70 cursor = listed.truncated ? listed.cursor : undefined;
71 } while (cursor);
72
73 return shas;
74 }
75 }
76