Separate completed transfers and retain original file extensions

This commit is contained in:
2026-10-14 16:10:59 +09:00
parent 3852c87e2e
commit 69e7449ba7
11 changed files with 736 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
import { execFileSync } from 'node:child_process';
import { randomBytes, randomInt, randomUUID } from 'node:crypto';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
const suffix = randomUUID().slice(0, 8),
@@ -151,6 +151,15 @@ try {
`import {createHash} from 'node:crypto';const state=${JSON.stringify(fileState.pending)},base='http://127.0.0.1:${port}',path='/api/uploads/resumable/'+state.uploadId;const headers={Origin:'https://hop.test',Cookie:state.cookie,'X-CSRF-Token':state.csrf};const r=await fetch(base+path,{headers});const progress=await r.json();if(!r.ok||progress.offset!==8*1024*1024)throw Error('partial offset not preserved');const last=await fetch(base+path,{method:'PUT',headers:{...headers,'Content-Type':'application/octet-stream','Upload-Offset':String(progress.offset)},body:Buffer.alloc(7,120)});if(!last.ok)throw Error(await last.text());const complete=await fetch(base+path+'/complete',{method:'POST',headers:{...headers,'Content-Type':'application/json'},body:'{}'});if(!complete.ok)throw Error(await complete.text());const list=await fetch(base+'/api/device/items',{headers:{Cookie:${JSON.stringify(fileState.cookie)}}});const item=(await list.json()).items.find(i=>i.file?.name==='resume.bin');const downloaded=await fetch(base+'/api/access/'+item.accessId+'/download',{headers:{Cookie:${JSON.stringify(fileState.cookie)}}});const content=Buffer.from(await downloaded.arrayBuffer());if(!downloaded.ok||content.length!==8*1024*1024+7||!content.equals(Buffer.alloc(content.length,120)))throw Error('resumed content mismatch');console.log('resumed');`,
);
assert.equal(resumed, 'resumed');
// Completed physical paths and the original download URL survive another restart.
docker('restart', name);
wait();
assert.equal(
node(
`const r=await fetch('http://127.0.0.1:${port}/api/access/${fileState.accessId}/download',{headers:{Cookie:${JSON.stringify(fileState.cookie)}}});if(!r.ok)throw Error('completed download failed after restart');console.log(await r.text());`,
),
'Docker file persistence',
);
const integrity = node(
`import Database from 'better-sqlite3';const db=new Database('/data/database/hop.sqlite');console.log(db.pragma('integrity_check',{simple:true}));db.close();`,
);
@@ -163,12 +172,14 @@ try {
),
);
assert.deepEqual(mode, { uid: 12001, gid: 12001, mode: 0o700 });
const originals = resolve(folder, 'originals');
const stored = readdirSync(originals).find(
(key) => readFileSync(resolve(originals, key)).toString() === 'Docker file persistence',
const completed = resolve(folder, 'completed');
const stored = readdirSync(completed).find(
(key) => readFileSync(resolve(completed, key)).toString() === 'Docker file persistence',
);
assert(stored, 'uploaded file is visible in the host project directory');
unlinkSync(resolve(originals, stored));
assert(stored, 'downloaded anonymous file is visible in the host completed directory');
assert.match(stored, /^[a-f0-9-]{36}\.txt$/);
assert.equal(existsSync(resolve(folder, 'originals', stored)), false);
unlinkSync(resolve(completed, stored));
const missing = JSON.parse(
node(
`const r=await fetch('http://127.0.0.1:${port}/api/access/${fileState.accessId}/download',{headers:{Cookie:${JSON.stringify(fileState.cookie)}}});console.log(JSON.stringify({status:r.status,body:await r.json()}));`,