aboutsummaryrefslogtreecommitdiffstats
path: root/src/git/multipart.ts
blob: 0c653ad50c92f6fc490e53ee853c245db66fd968 (plain) (blame)
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { concat } from "./util";
 
/** In-progress R2 multipart upload for one pack object (clone cache or raw store). */
export interface MultipartPackUpload {
  uploadPart(data: Uint8Array): Promise<void>;
  complete(): Promise<void>;
  abort(): Promise<void>;
}
 
/** Uniform multipart part size. R2 requires every part but the last to be the
 * same size and >= 5 MiB; 16 MiB parts cap a pack at 16 MiB * 10,000 = 160 GiB
 * while holding at most one part in isolate memory at a time. */
export const R2_PART_SIZE = 16 * 1024 * 1024;
 
/**
 * Buffers a streamed pack into fixed-size R2 multipart parts. Chunks are copied
 * on push (callers hand out views into buffers that later reads evict). `drain`
 * uploads every whole 16 MiB part and keeps the sub-part remainder, so isolate
 * memory never holds more than ~one part; `finish` flushes the final (short)
 * part and completes; any failure aborts so R2 is left with no partial object.
 * Uniform part size (exactly R2_PART_SIZE, last excepted) is what satisfies R2's
 * equal-size-parts rule.
 */
export class MultipartCapture {
  private chunks: Uint8Array[] = [];
  private len = 0;
  private failed = false;
  private completed = false;
  constructor(private mp: MultipartPackUpload) {}
 
  /** True once a part upload or complete/abort has failed (no R2 object exists). */
  get aborted(): boolean {
    return this.failed;
  }
 
  push(chunk: Uint8Array): void {
    if (this.failed) return;
    this.chunks.push(chunk.slice());
    this.len += chunk.length;
  }
 
  async drain(): Promise<void> {
    while (!this.failed && this.len >= R2_PART_SIZE) {
      const merged = concat(this.chunks);
      try {
        await this.mp.uploadPart(merged.subarray(0, R2_PART_SIZE));
      } catch {
        await this.abort();
        return;
      }
      const rest = merged.slice(R2_PART_SIZE);
      this.chunks = rest.length ? [rest] : [];
      this.len = rest.length;
    }
  }
 
  async finish(): Promise<void> {
    if (this.failed) return;
    try {
      if (this.len) await this.mp.uploadPart(concat(this.chunks));
      await this.mp.complete();
      this.completed = true;
    } catch {
      await this.abort();
    }
    this.chunks = [];
    this.len = 0;
  }
 
  async abort(): Promise<void> {
    if (this.failed || this.completed) return;
    this.failed = true;
    this.chunks = [];
    this.len = 0;
    try {
      await this.mp.abort();
    } catch {
      // best effort: an un-aborted multipart auto-expires
    }
  }
}
 
/** Begin a raw R2 multipart upload under `key`. Returns null (caller falls back)
 * if R2 refuses — celld deliberately makes createMultipartUpload throw. */
export async function beginRawMultipart(
  bucket: R2Bucket,
  key: string
): Promise<MultipartPackUpload | null> {
  try {
    const mp = await bucket.createMultipartUpload(key);
    const parts: R2UploadedPart[] = [];
    return {
      uploadPart: async (data) => {
        parts.push(await mp.uploadPart(parts.length + 1, data));
      },
      complete: async () => {
        await mp.complete(parts);
      },
      abort: async () => {
        await mp.abort();
      },
    };
  } catch {
    return null;
  }
}