git-fs-s3 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as GitRefNotFoundError, B as fromHex, C as GitConflictError, D as GitPathNotFoundError, E as GitObjectNotFoundError, F as concat, G as sha1, I as decodeAscii, K as toBase64, L as decodeUtf8, M as formatErrorResponse, N as isAbsentError, O as GitProtocolError, P as isNotFoundError, R as deflate, S as GitAuthorizationError, T as GitInvalidRequestError, V as hasNullByte, W as readBlobContent, _ as READABLE_PACK_FILE_KINDS, a as isSafeRepoPath, b as parsePackFileName, g as PACK_FILE_KINDS, i as isSafeRefName, j as GitRepositoryNotFoundError, k as GitRateLimitError, n as isSafeBranchName, o as isSafeTreePath, q as toHex, r as isSafeFullRefName, s as qualifyBranchRef, t as isFullSha, v as packFileName, w as GitError, x as GitAuthenticationError, y as parsePackDirectory, z as encodeUtf8 } from "./refs-Cb_NfKWI.js";
2
- import { i as parseLooseObjectPath, n as classifyGitdirPath, r as isMutableGitdirEntry, t as getObjectSize } from "./object-size-BJAsnZzw.js";
2
+ import { a as parseLooseObjectPath, i as isMutableGitdirEntry, n as getObjectSizes, r as classifyGitdirPath, t as getObjectSize } from "./object-size-D1Anvn8i.js";
3
3
  import { t as MemoryObjectStore } from "./memory-M2cCuVF-.js";
4
4
  import { LRUCache } from "lru-cache";
5
5
  //#region src/cache.ts
@@ -653,6 +653,6 @@ function createRetryStore(store, options = {}) {
653
653
  };
654
654
  }
655
655
  //#endregion
656
- export { CircuitOpenError, FsError, GitAuthenticationError, GitAuthorizationError, GitConflictError, GitError, GitInvalidRequestError, GitObjectNotFoundError, GitPathNotFoundError, GitProtocolError, GitRateLimitError, GitRefNotFoundError, GitRepositoryNotFoundError, MemoryObjectStore, PACK_FILE_KINDS, READABLE_PACK_FILE_KINDS, classifyGitdirPath, concat, createCachedStore, createGitFs, createRetryStore, decodeAscii, decodeUtf8, deflate, deletePrefix, encodeUtf8, formatErrorResponse, fromHex, getObjectSize, hasNullByte, isAbsentError, isFullSha, isMutableGitdirEntry, isNotFoundError, isRetryableError, isSafeBranchName, isSafeFullRefName, isSafeRefName, isSafeRepoPath, isSafeTreePath, movePrefix, packFileName, parseLooseObjectPath, parsePackDirectory, parsePackFileName, qualifyBranchRef, readBlobContent, sha1, toBase64, toHex };
656
+ export { CircuitOpenError, FsError, GitAuthenticationError, GitAuthorizationError, GitConflictError, GitError, GitInvalidRequestError, GitObjectNotFoundError, GitPathNotFoundError, GitProtocolError, GitRateLimitError, GitRefNotFoundError, GitRepositoryNotFoundError, MemoryObjectStore, PACK_FILE_KINDS, READABLE_PACK_FILE_KINDS, classifyGitdirPath, concat, createCachedStore, createGitFs, createRetryStore, decodeAscii, decodeUtf8, deflate, deletePrefix, encodeUtf8, formatErrorResponse, fromHex, getObjectSize, getObjectSizes, hasNullByte, isAbsentError, isFullSha, isMutableGitdirEntry, isNotFoundError, isRetryableError, isSafeBranchName, isSafeFullRefName, isSafeRefName, isSafeRepoPath, isSafeTreePath, movePrefix, packFileName, parseLooseObjectPath, parsePackDirectory, parsePackFileName, qualifyBranchRef, readBlobContent, sha1, toBase64, toHex };
657
657
 
658
658
  //# sourceMappingURL=index.js.map
@@ -123,11 +123,19 @@ const ENTRY_PROBE_BYTES = 64;
123
123
  const DELTA_PROBE_BYTES = 4096;
124
124
  /** The most a delta header can occupy once inflated: two 10-byte varints. */
125
125
  const DELTA_HEADER_MAX_BYTES = 20;
126
+ /**
127
+ * No pack directory yet means no packs; any other listing failure throws, so
128
+ * a storage outage is never mistaken for "no packed objects".
129
+ */
130
+ function noPackDirectory(error) {
131
+ if (error?.code === "ENOENT") return [];
132
+ throw error;
133
+ }
126
134
  /** The pack holding `oid`, with the offset of its entry. */
127
135
  async function locatePackedObject(repo, oid) {
128
136
  const fsp = rawFs(repo);
129
137
  const packDir = `${repo.gitdir}/objects/pack`;
130
- const indexes = parsePackDirectory(await fsp.readdir(packDir).catch(() => [])).filter((file) => file.kind === "idx");
138
+ const indexes = parsePackDirectory(await fsp.readdir(packDir).catch(noPackDirectory)).filter((file) => file.kind === "idx");
131
139
  for (const { base } of indexes) {
132
140
  const idx = await fsp.readFile(`${packDir}/${packFileName(base, "idx")}`);
133
141
  if (!(idx instanceof Uint8Array)) continue;
@@ -185,7 +193,106 @@ async function getObjectSize(repo, oid) {
185
193
  }
186
194
  return deltaResultSize(read, packKey, dataStart);
187
195
  }
196
+ /** Gap between two entries that one batched read still spans. */
197
+ const BATCH_MAX_GAP_BYTES = 65536;
198
+ /** Run `tasks`, at most `limit` at a time. */
199
+ async function runPooled(tasks, limit) {
200
+ let next = 0;
201
+ const worker = async () => {
202
+ while (next < tasks.length) {
203
+ const task = tasks[next++];
204
+ if (task) await task();
205
+ }
206
+ };
207
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
208
+ }
209
+ /** The most one batched read fetches. */
210
+ const BATCH_MAX_SPAN_BYTES = 1048576;
211
+ /** Every pack index in the repo, read once. */
212
+ async function packIndexes(repo) {
213
+ const fsp = rawFs(repo);
214
+ const packDir = `${repo.gitdir}/objects/pack`;
215
+ const files = parsePackDirectory(await fsp.readdir(packDir).catch(noPackDirectory)).filter((file) => file.kind === "idx");
216
+ return (await Promise.all(files.map(async ({ base }) => ({
217
+ packName: packFileName(base, "pack"),
218
+ idx: await fsp.readFile(`${packDir}/${packFileName(base, "idx")}`)
219
+ })))).filter((entry) => entry.idx instanceof Uint8Array);
220
+ }
221
+ /**
222
+ * {@link getObjectSize} for many objects at once, in few range reads: the
223
+ * pack indexes are read once, and entries that sit near each other in a pack
224
+ * (a tree's blobs usually do: they were written together) share one read of
225
+ * at most 1 MiB. A delta whose header runs past its window costs one more
226
+ * small read. Each oid maps to its size, or `null` exactly as
227
+ * {@link getObjectSize} would answer.
228
+ */
229
+ async function getObjectSizes(repo, oids) {
230
+ const sizes = new Map(oids.map((oid) => [oid, null]));
231
+ const read = rangeReader(repo);
232
+ if (!read || oids.length === 0) return sizes;
233
+ const byPack = /* @__PURE__ */ new Map();
234
+ const indexes = await packIndexes(repo);
235
+ for (const oid of sizes.keys()) for (const { packName, idx } of indexes) {
236
+ const offset = findPackIndexOffset(idx, oid);
237
+ if (offset === null) continue;
238
+ const list = byPack.get(packName) ?? [];
239
+ list.push({
240
+ oid,
241
+ offset
242
+ });
243
+ byPack.set(packName, list);
244
+ break;
245
+ }
246
+ const work = [];
247
+ for (const [packName, entries] of byPack) {
248
+ const packKey = `${repo.gitdir}/objects/pack/${packName}`;
249
+ entries.sort((a, b) => a.offset - b.offset);
250
+ const windows = [];
251
+ for (const entry of entries) {
252
+ const current = windows.at(-1);
253
+ const first = current?.[0];
254
+ const last = current?.at(-1);
255
+ if (current && first && last && entry.offset - last.offset <= BATCH_MAX_GAP_BYTES && entry.offset + ENTRY_PROBE_BYTES - first.offset <= BATCH_MAX_SPAN_BYTES) current.push(entry);
256
+ else windows.push([entry]);
257
+ }
258
+ for (const window of windows) work.push(async () => {
259
+ const start = window[0]?.offset ?? 0;
260
+ const end = (window.at(-1)?.offset ?? start) + ENTRY_PROBE_BYTES;
261
+ const bytes = await read(packKey, start, end - 1);
262
+ if (!bytes) return;
263
+ for (const { oid, offset } of window) sizes.set(oid, await sizeFromWindow(read, packKey, bytes, start, offset));
264
+ });
265
+ }
266
+ await runPooled(work, 8);
267
+ return sizes;
268
+ }
269
+ /** One entry's size, parsed from a window read at `windowStart`. */
270
+ async function sizeFromWindow(read, packKey, window, windowStart, offset) {
271
+ const at = offset - windowStart;
272
+ if (at >= window.byteLength) return null;
273
+ const probe = window.subarray(at, at + ENTRY_PROBE_BYTES);
274
+ const header = readEntryHeader(probe, 0);
275
+ if (header.length > probe.byteLength) return null;
276
+ if (header.type !== 6 && header.type !== 7) return header.size;
277
+ let bodyLength;
278
+ if (header.type === 7) bodyLength = 20;
279
+ else {
280
+ const base = readOfsDeltaOffset(probe, header.length);
281
+ if (header.length + base.length > probe.byteLength) return null;
282
+ bodyLength = base.length;
283
+ }
284
+ const dataStart = offset + header.length + bodyLength;
285
+ const local = window.subarray(dataStart - windowStart, dataStart - windowStart + DELTA_PROBE_BYTES);
286
+ if (local.byteLength > 0) {
287
+ const inflated = await inflatePrefix(new Uint8Array(local), DELTA_HEADER_MAX_BYTES);
288
+ if (inflated) {
289
+ const delta = readDeltaHeader(inflated, 0);
290
+ if (delta.length <= inflated.byteLength) return delta.resultSize;
291
+ }
292
+ }
293
+ return deltaResultSize(read, packKey, dataStart);
294
+ }
188
295
  //#endregion
189
- export { parseLooseObjectPath as i, classifyGitdirPath as n, isMutableGitdirEntry as r, getObjectSize as t };
296
+ export { parseLooseObjectPath as a, isMutableGitdirEntry as i, getObjectSizes as n, classifyGitdirPath as r, getObjectSize as t };
190
297
 
191
- //# sourceMappingURL=object-size-BJAsnZzw.js.map
298
+ //# sourceMappingURL=object-size-D1Anvn8i.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-size-D1Anvn8i.js","names":[],"sources":["../src/git-layout.ts","../src/object-size.ts"],"sourcesContent":["/**\n * Typed classification of paths inside a git directory, so callers can\n * reason about \"a ref\", \"a loose object\" or \"the pack listing\" as values\n * instead of matching path strings against patterns.\n *\n * Edge-compatible: no node: imports.\n */\n\nimport { type PackFileName, parsePackFileName } from \"./pack-files.js\";\n\n/** What a gitdir-relative path refers to. Listing prefixes end in `/`. */\nexport type GitdirEntry =\n\t| { readonly kind: \"head\" }\n\t| { readonly kind: \"config\" }\n\t/** A ref file (`refs/heads/main`) or a refs listing (`refs/heads/`). */\n\t| { readonly kind: \"refs\"; readonly path: string }\n\t/** The `objects/` listing. */\n\t| { readonly kind: \"objects-dir\" }\n\t/** The `objects/pack/` listing. */\n\t| { readonly kind: \"pack-dir\" }\n\t| { readonly kind: \"pack-file\"; readonly file: PackFileName }\n\t| { readonly kind: \"loose-object\"; readonly oid: string }\n\t| { readonly kind: \"other\" };\n\nconst HEX = new Set(\"0123456789abcdef\");\n\nfunction isHex(value: string, length: number): boolean {\n\tif (value.length !== length) return false;\n\tfor (const char of value) if (!HEX.has(char)) return false;\n\treturn true;\n}\n\n/** Classify a path relative to a gitdir (no leading slash). */\nexport function classifyGitdirPath(path: string): GitdirEntry {\n\tconst segments = path.split(\"/\");\n\tconst [first, second, third, ...rest] = segments;\n\tif (segments.length === 1) {\n\t\tif (first === \"HEAD\") return { kind: \"head\" };\n\t\tif (first === \"config\") return { kind: \"config\" };\n\t\tif (first === \"refs\") return { kind: \"refs\", path };\n\t\treturn { kind: \"other\" };\n\t}\n\tif (first === \"refs\") return { kind: \"refs\", path };\n\tif (first !== \"objects\" || second === undefined) return { kind: \"other\" };\n\tif (segments.length === 2 && second === \"\") return { kind: \"objects-dir\" };\n\tif (second === \"pack\" && third !== undefined && rest.length === 0) {\n\t\tif (third === \"\") return { kind: \"pack-dir\" };\n\t\tconst file = parsePackFileName(third);\n\t\treturn file ? { kind: \"pack-file\", file } : { kind: \"other\" };\n\t}\n\tif (\n\t\tthird !== undefined &&\n\t\trest.length === 0 &&\n\t\tisHex(second, 2) &&\n\t\tisHex(third, 38)\n\t) {\n\t\treturn { kind: \"loose-object\", oid: second + third };\n\t}\n\treturn { kind: \"other\" };\n}\n\n/**\n * Whether reads of this entry can change between pushes: refs, `HEAD`, and\n * the object/pack listings. Everything else is content-addressed (objects)\n * or written once (config, a given pack file name).\n */\nexport function isMutableGitdirEntry(entry: GitdirEntry): boolean {\n\tswitch (entry.kind) {\n\t\tcase \"head\":\n\t\tcase \"refs\":\n\t\tcase \"objects-dir\":\n\t\tcase \"pack-dir\":\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * A loose-object path anywhere in the fs, split into its gitdir and oid —\n * for callers that don't know where the gitdir boundary is: a loose object\n * is always `<gitdir>/objects/<2 hex>/<38 hex>`.\n */\nexport function parseLooseObjectPath(\n\tpath: string,\n): { readonly gitdir: string; readonly oid: string } | null {\n\tconst segments = path.split(\"/\");\n\tif (segments.length < 3) return null;\n\tconst gitdirSegments = segments.slice(0, -3);\n\tconst entry = classifyGitdirPath(segments.slice(-3).join(\"/\"));\n\tif (entry.kind !== \"loose-object\") return null;\n\treturn { gitdir: gitdirSegments.join(\"/\"), oid: entry.oid };\n}\n","/**\n * An object's size without reading (or inflating) the object.\n *\n * Why this exists: a git object's length lives in its pack entry, but the only\n * way isomorphic-git will hand you an object's bytes is by inflating the whole\n * thing — so \"is this blob too large to render or diff\" could only be asked\n * *after* loading it, which is exactly the cost a size limit is meant to avoid.\n * A 500 MB blob committed to a public repository would be materialised in\n * memory by a single page view before any limit could reject it.\n *\n * Reading the answer is cheap but not trivial, because git stores an object two\n * ways:\n *\n * - **whole** — the entry header records the size, so a small range read at the\n * entry's offset answers it;\n * - **as a delta** — the entry header records the size of the *delta stream*,\n * and the reconstructed size is the second varint inside the delta data, which\n * is itself deflated. Only a bounded prefix of that stream is needed, so\n * {@link inflatePrefix} recovers it without inflating the object.\n *\n * Either way the answer is exact, which matters because it decides whether a\n * caller may load the object at all. The cost is one `.idx` read (served from\n * the fs cache after the first) plus one or two small range reads of the pack,\n * so a caller can ask about a hundred objects without fetching a pack.\n *\n * `null` means \"not a packed object whose size could be read\" — a loose object,\n * a genuinely absent one, an unreadable index, a store without range reads, or\n * a delta prefix too short to yield its header. A caller must then read the\n * object rather than treating `null` as \"too large\"; see the call sites.\n */\nimport { inflatePrefix } from \"./edge-utils.js\";\nimport { rawFs } from \"./http/types.js\";\nimport type { Repo } from \"./ops/types.js\";\nimport {\n\tOBJ_OFS_DELTA,\n\tOBJ_REF_DELTA,\n\tOID_BYTES,\n\treadDeltaHeader,\n\treadEntryHeader,\n\treadOfsDeltaOffset,\n} from \"./pack-entry.js\";\nimport { packFileName, parsePackDirectory } from \"./pack-files.js\";\nimport { findPackIndexOffset } from \"./pack-index.js\";\n\n/**\n * Bytes fetched at an entry's offset. The entry header itself is at most 10\n * bytes for a 64-bit length, a `REF_DELTA`'s base id is 20, and an `OFS_DELTA`'s\n * base offset at most 10 — 40 bytes, so 64 answers the layout without a second\n * request.\n */\nconst ENTRY_PROBE_BYTES = 64;\n\n/**\n * Compressed bytes fetched from a delta's data. A delta header is two varints\n * (at most 10 bytes each) whose compressed form is smaller still, so this is\n * generous by orders of magnitude while staying a bounded read.\n */\nconst DELTA_PROBE_BYTES = 4 * 1024;\n\n/** The most a delta header can occupy once inflated: two 10-byte varints. */\nconst DELTA_HEADER_MAX_BYTES = 20;\n\n/**\n * No pack directory yet means no packs; any other listing failure throws, so\n * a storage outage is never mistaken for \"no packed objects\".\n */\nfunction noPackDirectory(error: unknown): string[] {\n\tif ((error as { code?: string } | null)?.code === \"ENOENT\") return [];\n\tthrow error;\n}\n\n/** The pack holding `oid`, with the offset of its entry. */\nasync function locatePackedObject(\n\trepo: Repo,\n\toid: string,\n): Promise<{ packName: string; offset: number } | null> {\n\tconst fsp = rawFs(repo);\n\tconst packDir = `${repo.gitdir}/objects/pack`;\n\tconst indexes = parsePackDirectory(\n\t\tawait fsp.readdir(packDir).catch(noPackDirectory),\n\t).filter((file) => file.kind === \"idx\");\n\n\tfor (const { base } of indexes) {\n\t\tconst idx = await fsp.readFile(`${packDir}/${packFileName(base, \"idx\")}`);\n\t\tif (!(idx instanceof Uint8Array)) continue;\n\t\tconst offset = findPackIndexOffset(idx, oid);\n\t\tif (offset !== null) {\n\t\t\treturn { packName: packFileName(base, \"pack\"), offset };\n\t\t}\n\t}\n\treturn null;\n}\n\n/** The store's range reader, or `null` when it has none. */\nfunction rangeReader(repo: Repo) {\n\tconst promises = rawFs(repo) as {\n\t\tgetRange?: (\n\t\t\tkey: string,\n\t\t\tstart: number,\n\t\t\tend: number,\n\t\t) => Promise<Uint8Array | null>;\n\t};\n\tif (!promises.getRange) return null;\n\treturn (key: string, start: number, end: number) => {\n\t\tif (!promises.getRange) return Promise.resolve(null);\n\t\treturn promises.getRange(key, start, end);\n\t};\n}\n\n/**\n * A delta entry's reconstructed size, read from the header its deflated data\n * opens with. `null` when the bounded prefix doesn't yield a complete header.\n */\nasync function deltaResultSize(\n\tread: NonNullable<ReturnType<typeof rangeReader>>,\n\tpackKey: string,\n\tdataStart: number,\n): Promise<number | null> {\n\tconst compressed = await read(\n\t\tpackKey,\n\t\tdataStart,\n\t\tdataStart + DELTA_PROBE_BYTES - 1,\n\t);\n\tif (!compressed || compressed.byteLength === 0) return null;\n\n\tconst inflated = await inflatePrefix(\n\t\tnew Uint8Array(compressed),\n\t\tDELTA_HEADER_MAX_BYTES,\n\t);\n\tif (!inflated) return null;\n\n\tconst header = readDeltaHeader(inflated, 0);\n\treturn header.length <= inflated.byteLength ? header.resultSize : null;\n}\n\n/**\n * `oid`'s size in bytes, or `null` when it isn't a packed object this can read\n * (see the module comment for what that means to a caller).\n */\nexport async function getObjectSize(\n\trepo: Repo,\n\toid: string,\n): Promise<number | null> {\n\t// Without range reads the pack would have to be fetched whole, which is the\n\t// cost this exists to avoid — so report \"unknown\" and let the caller read.\n\tconst read = rangeReader(repo);\n\tif (!read) return null;\n\n\tconst located = await locatePackedObject(repo, oid);\n\tif (!located) return null;\n\n\tconst packKey = `${repo.gitdir}/objects/pack/${located.packName}`;\n\tconst probe = await read(\n\t\tpackKey,\n\t\tlocated.offset,\n\t\tlocated.offset + ENTRY_PROBE_BYTES - 1,\n\t);\n\tif (!probe || probe.byteLength === 0) return null;\n\n\tconst header = readEntryHeader(probe, 0);\n\t// A header that ran off the end of the probe is a header this read, not one\n\t// it can interpret.\n\tif (header.length > probe.byteLength) return null;\n\tif (header.type !== OBJ_OFS_DELTA && header.type !== OBJ_REF_DELTA) {\n\t\treturn header.size;\n\t}\n\n\t// The base reference sits between the entry header and the deflated delta\n\t// data: 20 bytes of oid for `REF_DELTA`, or a backwards offset for\n\t// `OFS_DELTA`.\n\tconst bodyStart = located.offset + header.length;\n\tlet dataStart: number;\n\tif (header.type === OBJ_REF_DELTA) {\n\t\tdataStart = bodyStart + OID_BYTES;\n\t} else {\n\t\tconst base = readOfsDeltaOffset(probe, header.length);\n\t\tif (header.length + base.length > probe.byteLength) return null;\n\t\tdataStart = bodyStart + base.length;\n\t}\n\treturn deltaResultSize(read, packKey, dataStart);\n}\n\n/** Gap between two entries that one batched read still spans. */\nconst BATCH_MAX_GAP_BYTES = 64 * 1024;\n/** Range reads one batch keeps in flight at once. */\nexport const BATCH_MAX_IN_FLIGHT = 8;\n\n/** Run `tasks`, at most `limit` at a time. */\nasync function runPooled(\n\ttasks: Array<() => Promise<void>>,\n\tlimit: number,\n): Promise<void> {\n\tlet next = 0;\n\tconst worker = async () => {\n\t\twhile (next < tasks.length) {\n\t\t\tconst task = tasks[next++];\n\t\t\tif (task) await task();\n\t\t}\n\t};\n\tawait Promise.all(\n\t\tArray.from({ length: Math.min(limit, tasks.length) }, worker),\n\t);\n}\n\n/** The most one batched read fetches. */\nconst BATCH_MAX_SPAN_BYTES = 1024 * 1024;\n\n/** Every pack index in the repo, read once. */\nasync function packIndexes(\n\trepo: Repo,\n): Promise<Array<{ packName: string; idx: Uint8Array }>> {\n\tconst fsp = rawFs(repo);\n\tconst packDir = `${repo.gitdir}/objects/pack`;\n\tconst files = parsePackDirectory(\n\t\tawait fsp.readdir(packDir).catch(noPackDirectory),\n\t).filter((file) => file.kind === \"idx\");\n\tconst indexes = await Promise.all(\n\t\tfiles.map(async ({ base }) => ({\n\t\t\tpackName: packFileName(base, \"pack\"),\n\t\t\tidx: await fsp.readFile(`${packDir}/${packFileName(base, \"idx\")}`),\n\t\t})),\n\t);\n\treturn indexes.filter(\n\t\t(entry): entry is { packName: string; idx: Uint8Array } =>\n\t\t\tentry.idx instanceof Uint8Array,\n\t);\n}\n\n/**\n * {@link getObjectSize} for many objects at once, in few range reads: the\n * pack indexes are read once, and entries that sit near each other in a pack\n * (a tree's blobs usually do: they were written together) share one read of\n * at most 1 MiB. A delta whose header runs past its window costs one more\n * small read. Each oid maps to its size, or `null` exactly as\n * {@link getObjectSize} would answer.\n */\nexport async function getObjectSizes(\n\trepo: Repo,\n\toids: readonly string[],\n): Promise<Map<string, number | null>> {\n\tconst sizes = new Map<string, number | null>(oids.map((oid) => [oid, null]));\n\tconst read = rangeReader(repo);\n\tif (!read || oids.length === 0) return sizes;\n\n\tconst byPack = new Map<string, Array<{ oid: string; offset: number }>>();\n\tconst indexes = await packIndexes(repo);\n\tfor (const oid of sizes.keys()) {\n\t\tfor (const { packName, idx } of indexes) {\n\t\t\tconst offset = findPackIndexOffset(idx, oid);\n\t\t\tif (offset === null) continue;\n\t\t\tconst list = byPack.get(packName) ?? [];\n\t\t\tlist.push({ oid, offset });\n\t\t\tbyPack.set(packName, list);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst work: Array<() => Promise<void>> = [];\n\tfor (const [packName, entries] of byPack) {\n\t\tconst packKey = `${repo.gitdir}/objects/pack/${packName}`;\n\t\tentries.sort((a, b) => a.offset - b.offset);\n\t\t// Coalesce neighbouring entries into windows.\n\t\tconst windows: Array<typeof entries> = [];\n\t\tfor (const entry of entries) {\n\t\t\tconst current = windows.at(-1);\n\t\t\tconst first = current?.[0];\n\t\t\tconst last = current?.at(-1);\n\t\t\tif (\n\t\t\t\tcurrent &&\n\t\t\t\tfirst &&\n\t\t\t\tlast &&\n\t\t\t\tentry.offset - last.offset <= BATCH_MAX_GAP_BYTES &&\n\t\t\t\tentry.offset + ENTRY_PROBE_BYTES - first.offset <= BATCH_MAX_SPAN_BYTES\n\t\t\t) {\n\t\t\t\tcurrent.push(entry);\n\t\t\t} else {\n\t\t\t\twindows.push([entry]);\n\t\t\t}\n\t\t}\n\t\tfor (const window of windows) {\n\t\t\twork.push(async () => {\n\t\t\t\tconst start = window[0]?.offset ?? 0;\n\t\t\t\tconst end = (window.at(-1)?.offset ?? start) + ENTRY_PROBE_BYTES;\n\t\t\t\tconst bytes = await read(packKey, start, end - 1);\n\t\t\t\tif (!bytes) return;\n\t\t\t\t// Sequential: a delta's follow-up read stays inside this slot.\n\t\t\t\tfor (const { oid, offset } of window) {\n\t\t\t\t\tsizes.set(\n\t\t\t\t\t\toid,\n\t\t\t\t\t\tawait sizeFromWindow(read, packKey, bytes, start, offset),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\tawait runPooled(work, BATCH_MAX_IN_FLIGHT);\n\treturn sizes;\n}\n\n/** One entry's size, parsed from a window read at `windowStart`. */\nasync function sizeFromWindow(\n\tread: NonNullable<ReturnType<typeof rangeReader>>,\n\tpackKey: string,\n\twindow: Uint8Array,\n\twindowStart: number,\n\toffset: number,\n): Promise<number | null> {\n\tconst at = offset - windowStart;\n\tif (at >= window.byteLength) return null;\n\tconst probe = window.subarray(at, at + ENTRY_PROBE_BYTES);\n\tconst header = readEntryHeader(probe, 0);\n\tif (header.length > probe.byteLength) return null;\n\tif (header.type !== OBJ_OFS_DELTA && header.type !== OBJ_REF_DELTA) {\n\t\treturn header.size;\n\t}\n\tlet bodyLength: number;\n\tif (header.type === OBJ_REF_DELTA) {\n\t\tbodyLength = OID_BYTES;\n\t} else {\n\t\tconst base = readOfsDeltaOffset(probe, header.length);\n\t\tif (header.length + base.length > probe.byteLength) return null;\n\t\tbodyLength = base.length;\n\t}\n\tconst dataStart = offset + header.length + bodyLength;\n\t// The delta's opening bytes are often already in the window.\n\tconst local = window.subarray(\n\t\tdataStart - windowStart,\n\t\tdataStart - windowStart + DELTA_PROBE_BYTES,\n\t);\n\tif (local.byteLength > 0) {\n\t\tconst inflated = await inflatePrefix(\n\t\t\tnew Uint8Array(local),\n\t\t\tDELTA_HEADER_MAX_BYTES,\n\t\t);\n\t\tif (inflated) {\n\t\t\tconst delta = readDeltaHeader(inflated, 0);\n\t\t\tif (delta.length <= inflated.byteLength) return delta.resultSize;\n\t\t}\n\t}\n\treturn deltaResultSize(read, packKey, dataStart);\n}\n"],"mappings":"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"}
package/dist/ops.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { D as GitPathNotFoundError, H as inflate, T as GitInvalidRequestError, W as readBlobContent, o as isSafeTreePath, s as qualifyBranchRef } from "./refs-Cb_NfKWI.js";
2
- import { i as parseLooseObjectPath, t as getObjectSize } from "./object-size-BJAsnZzw.js";
2
+ import { a as parseLooseObjectPath, t as getObjectSize } from "./object-size-D1Anvn8i.js";
3
3
  import { A as upsertTree, C as resolveCommitOid, D as deleteFromTree, E as runStep, O as findTreeEntry, S as resolveCommit, T as resultKeyPrefixes, _ as getCommitHistory, a as storePack, b as getTreeFromRef, c as createBranchFrom, f as listTags, g as getCommit, h as getBlob, k as listTreeEntries, l as deleteBranchByName, m as HISTORY_WALK_DEPTH, n as encodePack, o as assertBranchExists, p as resolveLooseRefFast, s as assertSafeBranchName, t as createSemaphore, u as listBranches, v as getCommitLog, w as summarizeCommit, x as peelToCommit, y as getFileContent } from "./semaphore-CPjn7Iqh.js";
4
4
  import git from "isomorphic-git";
5
5
  import { createTwoFilesPatch } from "diff";
@@ -4,7 +4,7 @@ export { FsError } from "./errors.js";
4
4
  export { formatErrorResponse, GitAuthenticationError, GitAuthorizationError, GitConflictError, GitError, GitInvalidRequestError, GitObjectNotFoundError, GitPathNotFoundError, GitProtocolError, GitRateLimitError, GitRefNotFoundError, GitRepositoryNotFoundError, isAbsentError, isNotFoundError, type MergeConflictDetail, } from "./git-errors.js";
5
5
  export { createGitFs, type GitFs } from "./git-fs.js";
6
6
  export { classifyGitdirPath, type GitdirEntry, isMutableGitdirEntry, parseLooseObjectPath, } from "./git-layout.js";
7
- export { getObjectSize } from "./object-size.js";
7
+ export { getObjectSize, getObjectSizes } from "./object-size.js";
8
8
  export { PACK_FILE_KINDS, type PackFileKind, type PackFileName, packFileName, parsePackDirectory, parsePackFileName, READABLE_PACK_FILE_KINDS, } from "./pack-files.js";
9
9
  export { deletePrefix, movePrefix } from "./prefix.js";
10
10
  export { isFullSha, isSafeBranchName, isSafeFullRefName, isSafeRefName, isSafeRepoPath, isSafeTreePath, qualifyBranchRef, } from "./refs.js";
@@ -4,3 +4,14 @@ import type { Repo } from "./ops/types.js";
4
4
  * (see the module comment for what that means to a caller).
5
5
  */
6
6
  export declare function getObjectSize(repo: Repo, oid: string): Promise<number | null>;
7
+ /** Range reads one batch keeps in flight at once. */
8
+ export declare const BATCH_MAX_IN_FLIGHT = 8;
9
+ /**
10
+ * {@link getObjectSize} for many objects at once, in few range reads: the
11
+ * pack indexes are read once, and entries that sit near each other in a pack
12
+ * (a tree's blobs usually do: they were written together) share one read of
13
+ * at most 1 MiB. A delta whose header runs past its window costs one more
14
+ * small read. Each oid maps to its size, or `null` exactly as
15
+ * {@link getObjectSize} would answer.
16
+ */
17
+ export declare function getObjectSizes(repo: Repo, oids: readonly string[]): Promise<Map<string, number | null>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-fs-s3",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "isomorphic-git filesystem backend for S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) — run git servers without a disk",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"object-size-BJAsnZzw.js","names":[],"sources":["../src/git-layout.ts","../src/object-size.ts"],"sourcesContent":["/**\n * Typed classification of paths inside a git directory, so callers can\n * reason about \"a ref\", \"a loose object\" or \"the pack listing\" as values\n * instead of matching path strings against patterns.\n *\n * Edge-compatible: no node: imports.\n */\n\nimport { type PackFileName, parsePackFileName } from \"./pack-files.js\";\n\n/** What a gitdir-relative path refers to. Listing prefixes end in `/`. */\nexport type GitdirEntry =\n\t| { readonly kind: \"head\" }\n\t| { readonly kind: \"config\" }\n\t/** A ref file (`refs/heads/main`) or a refs listing (`refs/heads/`). */\n\t| { readonly kind: \"refs\"; readonly path: string }\n\t/** The `objects/` listing. */\n\t| { readonly kind: \"objects-dir\" }\n\t/** The `objects/pack/` listing. */\n\t| { readonly kind: \"pack-dir\" }\n\t| { readonly kind: \"pack-file\"; readonly file: PackFileName }\n\t| { readonly kind: \"loose-object\"; readonly oid: string }\n\t| { readonly kind: \"other\" };\n\nconst HEX = new Set(\"0123456789abcdef\");\n\nfunction isHex(value: string, length: number): boolean {\n\tif (value.length !== length) return false;\n\tfor (const char of value) if (!HEX.has(char)) return false;\n\treturn true;\n}\n\n/** Classify a path relative to a gitdir (no leading slash). */\nexport function classifyGitdirPath(path: string): GitdirEntry {\n\tconst segments = path.split(\"/\");\n\tconst [first, second, third, ...rest] = segments;\n\tif (segments.length === 1) {\n\t\tif (first === \"HEAD\") return { kind: \"head\" };\n\t\tif (first === \"config\") return { kind: \"config\" };\n\t\tif (first === \"refs\") return { kind: \"refs\", path };\n\t\treturn { kind: \"other\" };\n\t}\n\tif (first === \"refs\") return { kind: \"refs\", path };\n\tif (first !== \"objects\" || second === undefined) return { kind: \"other\" };\n\tif (segments.length === 2 && second === \"\") return { kind: \"objects-dir\" };\n\tif (second === \"pack\" && third !== undefined && rest.length === 0) {\n\t\tif (third === \"\") return { kind: \"pack-dir\" };\n\t\tconst file = parsePackFileName(third);\n\t\treturn file ? { kind: \"pack-file\", file } : { kind: \"other\" };\n\t}\n\tif (\n\t\tthird !== undefined &&\n\t\trest.length === 0 &&\n\t\tisHex(second, 2) &&\n\t\tisHex(third, 38)\n\t) {\n\t\treturn { kind: \"loose-object\", oid: second + third };\n\t}\n\treturn { kind: \"other\" };\n}\n\n/**\n * Whether reads of this entry can change between pushes: refs, `HEAD`, and\n * the object/pack listings. Everything else is content-addressed (objects)\n * or written once (config, a given pack file name).\n */\nexport function isMutableGitdirEntry(entry: GitdirEntry): boolean {\n\tswitch (entry.kind) {\n\t\tcase \"head\":\n\t\tcase \"refs\":\n\t\tcase \"objects-dir\":\n\t\tcase \"pack-dir\":\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * A loose-object path anywhere in the fs, split into its gitdir and oid —\n * for callers that don't know where the gitdir boundary is: a loose object\n * is always `<gitdir>/objects/<2 hex>/<38 hex>`.\n */\nexport function parseLooseObjectPath(\n\tpath: string,\n): { readonly gitdir: string; readonly oid: string } | null {\n\tconst segments = path.split(\"/\");\n\tif (segments.length < 3) return null;\n\tconst gitdirSegments = segments.slice(0, -3);\n\tconst entry = classifyGitdirPath(segments.slice(-3).join(\"/\"));\n\tif (entry.kind !== \"loose-object\") return null;\n\treturn { gitdir: gitdirSegments.join(\"/\"), oid: entry.oid };\n}\n","/**\n * An object's size without reading (or inflating) the object.\n *\n * Why this exists: a git object's length lives in its pack entry, but the only\n * way isomorphic-git will hand you an object's bytes is by inflating the whole\n * thing — so \"is this blob too large to render or diff\" could only be asked\n * *after* loading it, which is exactly the cost a size limit is meant to avoid.\n * A 500 MB blob committed to a public repository would be materialised in\n * memory by a single page view before any limit could reject it.\n *\n * Reading the answer is cheap but not trivial, because git stores an object two\n * ways:\n *\n * - **whole** — the entry header records the size, so a small range read at the\n * entry's offset answers it;\n * - **as a delta** — the entry header records the size of the *delta stream*,\n * and the reconstructed size is the second varint inside the delta data, which\n * is itself deflated. Only a bounded prefix of that stream is needed, so\n * {@link inflatePrefix} recovers it without inflating the object.\n *\n * Either way the answer is exact, which matters because it decides whether a\n * caller may load the object at all. The cost is one `.idx` read (served from\n * the fs cache after the first) plus one or two small range reads of the pack,\n * so a caller can ask about a hundred objects without fetching a pack.\n *\n * `null` means \"not a packed object whose size could be read\" — a loose object,\n * a genuinely absent one, an unreadable index, a store without range reads, or\n * a delta prefix too short to yield its header. A caller must then read the\n * object rather than treating `null` as \"too large\"; see the call sites.\n */\nimport { inflatePrefix } from \"./edge-utils.js\";\nimport { rawFs } from \"./http/types.js\";\nimport type { Repo } from \"./ops/types.js\";\nimport {\n\tOBJ_OFS_DELTA,\n\tOBJ_REF_DELTA,\n\tOID_BYTES,\n\treadDeltaHeader,\n\treadEntryHeader,\n\treadOfsDeltaOffset,\n} from \"./pack-entry.js\";\nimport { packFileName, parsePackDirectory } from \"./pack-files.js\";\nimport { findPackIndexOffset } from \"./pack-index.js\";\n\n/**\n * Bytes fetched at an entry's offset. The entry header itself is at most 10\n * bytes for a 64-bit length, a `REF_DELTA`'s base id is 20, and an `OFS_DELTA`'s\n * base offset at most 10 — 40 bytes, so 64 answers the layout without a second\n * request.\n */\nconst ENTRY_PROBE_BYTES = 64;\n\n/**\n * Compressed bytes fetched from a delta's data. A delta header is two varints\n * (at most 10 bytes each) whose compressed form is smaller still, so this is\n * generous by orders of magnitude while staying a bounded read.\n */\nconst DELTA_PROBE_BYTES = 4 * 1024;\n\n/** The most a delta header can occupy once inflated: two 10-byte varints. */\nconst DELTA_HEADER_MAX_BYTES = 20;\n\n/** The pack holding `oid`, with the offset of its entry. */\nasync function locatePackedObject(\n\trepo: Repo,\n\toid: string,\n): Promise<{ packName: string; offset: number } | null> {\n\tconst fsp = rawFs(repo);\n\tconst packDir = `${repo.gitdir}/objects/pack`;\n\tconst indexes = parsePackDirectory(\n\t\tawait fsp.readdir(packDir).catch(() => [] as string[]),\n\t).filter((file) => file.kind === \"idx\");\n\n\tfor (const { base } of indexes) {\n\t\tconst idx = await fsp.readFile(`${packDir}/${packFileName(base, \"idx\")}`);\n\t\tif (!(idx instanceof Uint8Array)) continue;\n\t\tconst offset = findPackIndexOffset(idx, oid);\n\t\tif (offset !== null) {\n\t\t\treturn { packName: packFileName(base, \"pack\"), offset };\n\t\t}\n\t}\n\treturn null;\n}\n\n/** The store's range reader, or `null` when it has none. */\nfunction rangeReader(repo: Repo) {\n\tconst promises = rawFs(repo) as {\n\t\tgetRange?: (\n\t\t\tkey: string,\n\t\t\tstart: number,\n\t\t\tend: number,\n\t\t) => Promise<Uint8Array | null>;\n\t};\n\tif (!promises.getRange) return null;\n\treturn (key: string, start: number, end: number) => {\n\t\tif (!promises.getRange) return Promise.resolve(null);\n\t\treturn promises.getRange(key, start, end);\n\t};\n}\n\n/**\n * A delta entry's reconstructed size, read from the header its deflated data\n * opens with. `null` when the bounded prefix doesn't yield a complete header.\n */\nasync function deltaResultSize(\n\tread: NonNullable<ReturnType<typeof rangeReader>>,\n\tpackKey: string,\n\tdataStart: number,\n): Promise<number | null> {\n\tconst compressed = await read(\n\t\tpackKey,\n\t\tdataStart,\n\t\tdataStart + DELTA_PROBE_BYTES - 1,\n\t);\n\tif (!compressed || compressed.byteLength === 0) return null;\n\n\tconst inflated = await inflatePrefix(\n\t\tnew Uint8Array(compressed),\n\t\tDELTA_HEADER_MAX_BYTES,\n\t);\n\tif (!inflated) return null;\n\n\tconst header = readDeltaHeader(inflated, 0);\n\treturn header.length <= inflated.byteLength ? header.resultSize : null;\n}\n\n/**\n * `oid`'s size in bytes, or `null` when it isn't a packed object this can read\n * (see the module comment for what that means to a caller).\n */\nexport async function getObjectSize(\n\trepo: Repo,\n\toid: string,\n): Promise<number | null> {\n\t// Without range reads the pack would have to be fetched whole, which is the\n\t// cost this exists to avoid — so report \"unknown\" and let the caller read.\n\tconst read = rangeReader(repo);\n\tif (!read) return null;\n\n\tconst located = await locatePackedObject(repo, oid);\n\tif (!located) return null;\n\n\tconst packKey = `${repo.gitdir}/objects/pack/${located.packName}`;\n\tconst probe = await read(\n\t\tpackKey,\n\t\tlocated.offset,\n\t\tlocated.offset + ENTRY_PROBE_BYTES - 1,\n\t);\n\tif (!probe || probe.byteLength === 0) return null;\n\n\tconst header = readEntryHeader(probe, 0);\n\t// A header that ran off the end of the probe is a header this read, not one\n\t// it can interpret.\n\tif (header.length > probe.byteLength) return null;\n\tif (header.type !== OBJ_OFS_DELTA && header.type !== OBJ_REF_DELTA) {\n\t\treturn header.size;\n\t}\n\n\t// The base reference sits between the entry header and the deflated delta\n\t// data: 20 bytes of oid for `REF_DELTA`, or a backwards offset for\n\t// `OFS_DELTA`.\n\tconst bodyStart = located.offset + header.length;\n\tlet dataStart: number;\n\tif (header.type === OBJ_REF_DELTA) {\n\t\tdataStart = bodyStart + OID_BYTES;\n\t} else {\n\t\tconst base = readOfsDeltaOffset(probe, header.length);\n\t\tif (header.length + base.length > probe.byteLength) return null;\n\t\tdataStart = bodyStart + base.length;\n\t}\n\treturn deltaResultSize(read, packKey, dataStart);\n}\n"],"mappings":"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"}