castle-web-cli 0.4.99 → 0.4.100

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.
Files changed (2) hide show
  1. package/dist/imports.js +26 -15
  2. package/package.json +1 -1
package/dist/imports.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as fs from 'fs';
2
- import * as os from 'os';
3
2
  import * as path from 'path';
4
3
  import { nanoid } from 'nanoid';
5
4
  import * as api from './api.js';
@@ -248,20 +247,32 @@ export async function restoreMissingImports(deckDir) {
248
247
  }
249
248
  return restored;
250
249
  }
251
- // Download an archive and unpack it at `destDir`, via a temp dir so a failure
252
- // partway leaves nothing behind.
250
+ // Download an archive and unpack it at `destDir`, replacing whatever is there.
251
+ //
252
+ // Staged NEXT TO destDir rather than in the OS temp dir. In a sandbox `/tmp` and
253
+ // the workspace are separate filesystems, and renaming across them fails with
254
+ // EXDEV -- which, when the old copy had already been cleared to make room, left
255
+ // the deck with no kit at all. Staging inside `imports/` keeps the move on one
256
+ // filesystem, and nothing existing is touched until the new copy is unpacked and
257
+ // whole: a failed download or a bad archive now leaves the deck exactly as it
258
+ // was.
253
259
  async function fetchDeckSourceInto(archiveUrl, destDir) {
254
- const res = await fetch(archiveUrl, { signal: AbortSignal.timeout(60000) });
255
- if (!res.ok)
256
- throw new Error(`Archive fetch failed: HTTP ${res.status}`);
257
- const buf = Buffer.from(await res.arrayBuffer());
258
- const tmpFile = path.join(os.tmpdir(), `castle-import-${nanoid(8)}.tar.gz`);
259
- const tmpDir = path.join(os.tmpdir(), `castle-import-${nanoid(8)}`);
260
- fs.writeFileSync(tmpFile, buf);
260
+ const importsDir = path.dirname(destDir);
261
+ fs.mkdirSync(importsDir, { recursive: true });
262
+ const tmpFile = path.join(importsDir, `.castle-import-${nanoid(8)}.tar.gz`);
263
+ const tmpDir = path.join(importsDir, `.castle-import-${nanoid(8)}`);
261
264
  fs.mkdirSync(tmpDir, { recursive: true });
262
265
  try {
266
+ const res = await fetch(archiveUrl, { signal: AbortSignal.timeout(60000) });
267
+ if (!res.ok)
268
+ throw new Error(`Archive fetch failed: HTTP ${res.status}`);
269
+ fs.writeFileSync(tmpFile, Buffer.from(await res.arrayBuffer()));
263
270
  await runTar(['-xzf', tmpFile, '-C', tmpDir]);
264
- fs.mkdirSync(path.dirname(destDir), { recursive: true });
271
+ // The new copy is whole; only now is it safe to drop the old one.
272
+ if (fs.existsSync(destDir)) {
273
+ unlockImportTree(destDir);
274
+ fs.rmSync(destDir, { recursive: true, force: true });
275
+ }
265
276
  fs.renameSync(tmpDir, destDir);
266
277
  }
267
278
  finally {
@@ -347,11 +358,11 @@ function backupImport(deckDir, alias) {
347
358
  async function placeImport(deckDir, alias, deckId, source, via) {
348
359
  const destDir = path.join(deckDir, IMPORTS_DIR, alias);
349
360
  const replaced = fs.existsSync(destDir);
350
- if (replaced) {
361
+ // Back up first, but leave the old copy in place: fetchDeckSourceInto only
362
+ // clears it once the replacement is unpacked, so a failed update is a no-op
363
+ // rather than a deck with its kit missing.
364
+ if (replaced)
351
365
  backupImport(deckDir, alias);
352
- unlockImportTree(destDir);
353
- fs.rmSync(destDir, { recursive: true, force: true });
354
- }
355
366
  await fetchDeckSourceInto(source.archiveUrl, destDir);
356
367
  lockImportTree(destDir);
357
368
  clearStatusCache(deckDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.99",
3
+ "version": "0.4.100",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"