gm-pantry 0.3.0 → 0.4.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/builder.d.ts +6 -0
- package/dist/builder.js +6 -2
- package/dist/parse.d.ts +3 -0
- package/dist/parse.js +25 -0
- package/dist/s3.js +31 -3
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/builder.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface NewVersion {
|
|
|
14
14
|
version?: string;
|
|
15
15
|
notes?: string;
|
|
16
16
|
released?: string;
|
|
17
|
+
/** What is inside the archive (paths relative to its root) and its README text, if any. */
|
|
18
|
+
contents?: {
|
|
19
|
+
path: string;
|
|
20
|
+
size: number;
|
|
21
|
+
}[];
|
|
22
|
+
readme?: string;
|
|
17
23
|
format?: string;
|
|
18
24
|
file: {
|
|
19
25
|
url: string;
|
package/dist/builder.js
CHANGED
|
@@ -18,6 +18,10 @@ export function addVersion(repo, v) {
|
|
|
18
18
|
for (const old of game.versions ?? []) {
|
|
19
19
|
if (old.file?.url === v.file.url) {
|
|
20
20
|
old.file = { ...old.file, sha256: v.file.sha256, size: v.file.size };
|
|
21
|
+
if (v.contents)
|
|
22
|
+
old.contents = v.contents;
|
|
23
|
+
if (v.readme)
|
|
24
|
+
old.readme = v.readme;
|
|
21
25
|
return game;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -40,8 +44,8 @@ export function addVersion(repo, v) {
|
|
|
40
44
|
;
|
|
41
45
|
else if (taken.has(label))
|
|
42
46
|
throw new Error(`game ${id} already has a version ${label}`);
|
|
43
|
-
const { notes, released, format, file } = v;
|
|
44
|
-
game.versions.unshift({ version: label, file, ...(format && { format }), ...(notes && { notes }), ...(released && { released }) });
|
|
47
|
+
const { notes, released, format, file, contents, readme } = v;
|
|
48
|
+
game.versions.unshift({ version: label, file, ...(format && { format }), ...(notes && { notes }), ...(contents && { contents }), ...(readme && { readme }), ...(released && { released }) });
|
|
45
49
|
return game;
|
|
46
50
|
}
|
|
47
51
|
/** Adds an archive by its url, deriving the game id and title from the file name. */
|
package/dist/parse.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare const LIMITS: {
|
|
|
13
13
|
readonly metaText: 2000;
|
|
14
14
|
readonly author: 120;
|
|
15
15
|
readonly notes: 2000;
|
|
16
|
+
readonly contents: 2000;
|
|
17
|
+
readonly contentPath: 260;
|
|
18
|
+
readonly readme: 20000;
|
|
16
19
|
readonly tags: 20;
|
|
17
20
|
readonly tag: 40;
|
|
18
21
|
readonly screenshots: 20;
|
package/dist/parse.js
CHANGED
|
@@ -14,6 +14,9 @@ export const LIMITS = {
|
|
|
14
14
|
metaText: 2000,
|
|
15
15
|
author: 120,
|
|
16
16
|
notes: 2000,
|
|
17
|
+
contents: 2000,
|
|
18
|
+
contentPath: 260,
|
|
19
|
+
readme: 20000,
|
|
17
20
|
tags: 20,
|
|
18
21
|
tag: 40,
|
|
19
22
|
screenshots: 20,
|
|
@@ -202,12 +205,34 @@ function parseVersion(v, path, baseUrl, warn) {
|
|
|
202
205
|
version.notes = notes;
|
|
203
206
|
else if (v.notes !== undefined)
|
|
204
207
|
warn(`${path}.notes`, "dropped");
|
|
208
|
+
const contents = parseContents(v.contents);
|
|
209
|
+
if (contents)
|
|
210
|
+
version.contents = contents;
|
|
211
|
+
else if (v.contents !== undefined)
|
|
212
|
+
warn(`${path}.contents`, "dropped: not a list of {path, size} of at most 2000 entries");
|
|
213
|
+
if (typeof v.readme === "string" && v.readme.trim() !== "" && v.readme.length <= LIMITS.readme)
|
|
214
|
+
version.readme = v.readme;
|
|
215
|
+
else if (v.readme !== undefined)
|
|
216
|
+
warn(`${path}.readme`, "dropped");
|
|
205
217
|
if (validDate(v.released))
|
|
206
218
|
version.released = v.released;
|
|
207
219
|
else if (v.released !== undefined)
|
|
208
220
|
warn(`${path}.released`, "dropped: not an ISO-8601 date");
|
|
209
221
|
return version;
|
|
210
222
|
}
|
|
223
|
+
function parseContents(c) {
|
|
224
|
+
if (!Array.isArray(c) || c.length === 0 || c.length > LIMITS.contents)
|
|
225
|
+
return undefined;
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const e of c) {
|
|
228
|
+
if (!isObject(e) || typeof e.path !== "string" || e.path === "" || e.path.length > LIMITS.contentPath)
|
|
229
|
+
return undefined;
|
|
230
|
+
if (!Number.isSafeInteger(e.size) || e.size < 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
out.push({ path: e.path, size: e.size });
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
211
236
|
function parseWork(w) {
|
|
212
237
|
if (!isObject(w))
|
|
213
238
|
return undefined;
|
package/dist/s3.js
CHANGED
|
@@ -287,6 +287,36 @@ export class S3Store {
|
|
|
287
287
|
}
|
|
288
288
|
const GENERATED_SHARD = /^shards\/[0-9a-f]{16}\.json$/;
|
|
289
289
|
const keyOf = (url) => decodeURIComponent(url);
|
|
290
|
+
/**
|
|
291
|
+
* repo.json with its guard. A 412 is a real conflict only if the object's ETag is no longer the expected one:
|
|
292
|
+
* some services answer 412 for an ETag they handed out themselves (quoted versus bare), so when the ETag still
|
|
293
|
+
* matches, retry once with the bare form, then without a guard (the ETag was checked just now).
|
|
294
|
+
*/
|
|
295
|
+
async function putGuarded(store, body, put, guard, expected, result) {
|
|
296
|
+
const conflict = () => new ConflictError("repo.json changed while publishing; reload and apply the edit again");
|
|
297
|
+
try {
|
|
298
|
+
await store.putObject("repo.json", body, { ...put, ...guard });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
if (!(e instanceof S3Error) || e.status !== 412)
|
|
303
|
+
throw e;
|
|
304
|
+
if (!expected || (await store.headObject("repo.json"))?.etag !== expected)
|
|
305
|
+
throw conflict();
|
|
306
|
+
}
|
|
307
|
+
const bare = expected.replace(/^W\//, "").replace(/^"(.*)"$/, "$1");
|
|
308
|
+
try {
|
|
309
|
+
await store.putObject("repo.json", body, { ...put, ifMatch: bare });
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
if (!(e instanceof S3Error) || e.status !== 412)
|
|
313
|
+
throw e;
|
|
314
|
+
if ((await store.headObject("repo.json"))?.etag !== expected)
|
|
315
|
+
throw conflict();
|
|
316
|
+
result.conditional = false;
|
|
317
|
+
await store.putObject("repo.json", body, put);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
290
320
|
/**
|
|
291
321
|
* Uploads archives and new shards first and repo.json last, so a reader never sees a repo.json that points at missing files;
|
|
292
322
|
* then deletes the shards nothing lists any more. repo.json is written with `If-Match` (or `If-None-Match: *`) where the service supports it.
|
|
@@ -327,11 +357,9 @@ export async function publishRepo(store, input, o = {}) {
|
|
|
327
357
|
const guard = expected ? { ifMatch: expected } : { ifNoneMatch: "*" };
|
|
328
358
|
const put = { contentType: "application/json", cacheControl: "no-cache" };
|
|
329
359
|
try {
|
|
330
|
-
await store
|
|
360
|
+
await putGuarded(store, input.repoJson, put, guard, expected, result);
|
|
331
361
|
}
|
|
332
362
|
catch (e) {
|
|
333
|
-
if (e instanceof S3Error && e.status === 412)
|
|
334
|
-
throw new ConflictError("repo.json changed while publishing; reload and apply the edit again");
|
|
335
363
|
if (!(e instanceof S3Error) || (e.status !== 501 && e.code !== "NotImplemented"))
|
|
336
364
|
throw e;
|
|
337
365
|
result.conditional = false;
|
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface Version {
|
|
|
13
13
|
/** Claimed hash of the normalised game payload; a hint, consumers recompute. */
|
|
14
14
|
build?: string;
|
|
15
15
|
notes?: string;
|
|
16
|
+
/** What is inside the archive, so a consumer can tell what it holds without downloading it. Advisory. */
|
|
17
|
+
contents?: {
|
|
18
|
+
path: string;
|
|
19
|
+
size: number;
|
|
20
|
+
}[];
|
|
21
|
+
/** The archive's README text, so it can be shown without downloading the archive. Advisory. */
|
|
22
|
+
readme?: string;
|
|
16
23
|
/** YYYY-MM-DD, or a full ISO-8601 timestamp. */
|
|
17
24
|
released?: string;
|
|
18
25
|
}
|