Preserve empty directories during persistent home seeding

Stage directory entries under the existing exact upload token and no-follow directory handles before atomic publication. Preserve adoption of existing homes and bounded file streaming.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-11 09:29:52 -05:00
1 parent cdde159de3
commit 54deda1baa
6 files changed
+54 -5

No files matched your search

@@ -1,5 +1,6 @@
import {
existsSync,
unlinkSync,
readFileSync,
readdirSync,
statSync,
@@ -511,6 +512,24 @@ describe("confined computer files", () => {
error: "invalid",
});
});
it("stages empty seed directories with exact upload ownership and no-follow paths", () => {
const f = fixture(); const seedId = randomUUID();
f.call({ action: "seed-begin", seedId });
expect(f.call({ action: "seed-directory", seedId, path: "empty/nested" })).toEqual({});
expect(existsSync(f.root)).toBe(false);
expect(f.call({ action: "seed-directory", seedId: randomUUID(), path: "foreign" })).toEqual({ error: "conflict" });
expect(f.call({ action: "seed-directory", seedId, path: "../escape" })).toEqual({ error: "invalid" });
const staging = join(f.temp, `.paperclip-seed-home-${seedId}`, "tree");
const outside = join(f.temp, "outside"); mkdirSync(outside);
symlinkSync(outside, join(staging, "alias"));
expect(f.call({ action: "seed-directory", seedId, path: "alias/escape" })).toEqual({ error: "invalid" });
expect(readdirSync(outside)).toEqual([]);
// A malicious link must not be included in the successful fixture publication.
unlinkSync(join(staging, "alias"));
expect(f.call({ action: "seed-commit", seedId })).toEqual({ seeded: true });
expect(statSync(join(f.root, "empty/nested")).isDirectory()).toBe(true);
expect(statSync(join(f.root, "empty/nested")).mode & 0o777).toBe(0o700);
});
it("publishes bounded seed chunks atomically and aborts partial homes", () => {
const f = fixture(); const seedId = randomUUID();
expect(f.call({ action: "seed-begin", seedId })).toEqual({ started: true });
@@ -124,7 +124,7 @@ def directory(parts,create=False,start=None):
except BaseException:os.close(fd);raise
# Initial uploads stay in a private sibling until a no-replace rename publishes them.
# Each request carries at most one bounded chunk, never the whole home.
seed_actions=('seed-begin','seed-chunk','seed-commit','seed-abort')
seed_actions=('seed-begin','seed-directory','seed-chunk','seed-commit','seed-abort')
# The durable receipt names one exact upload. Root listings (including home
# admission) reclaim expired uploads even after a controller crash or publish.
if act in seed_actions or (act=='list' and not p.get('path') and os.path.lexists(os.path.join(os.path.dirname(root),'.paperclip-seed-'+os.path.basename(root)+'.json'))):
@@ -209,7 +209,14 @@ if act in seed_actions or (act=='list' and not p.get('path') and os.path.lexists
if receipt is None or receipt['seedId']!=token:fail('conflict')
stagefd=os.open(staging,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parentfd)
if read_metadata(stagefd,'owner.json')!=identity(token):fail('invalid')
if act=='seed-chunk':
if act=='seed-directory':
relative=p.get('path','');safe(relative)
parts=[part for part in relative.split('/') if part not in ('','.')]
if not parts:fail('invalid')
fd=directory(['tree']+parts,create=True,start=stagefd);os.close(fd)
receipt['updatedAt']=time.time();save_receipt(receipt)
print('{}')
elif act=='seed-chunk':
relative=p.get('path','');safe(relative)
parts=[part for part in relative.split('/') if part not in ('','.')]
if not parts:fail('invalid')