Make admin quotas editable and simplify uploader storage

This commit is contained in:
2026-10-15 12:51:42 +09:00
parent 4d527f74e6
commit 328fad2e36
26 changed files with 1101 additions and 205 deletions

View File

@@ -115,10 +115,11 @@ try {
if (verifiedAccount) {
const saved = JSON.parse(
node(
`const base='http://127.0.0.1:${port}', cookie=${JSON.stringify(verifiedAccount.cookie)};const session=await(await fetch(base+'/api/guest/session',{method:'POST',headers:{Origin:'https://hop.test',Cookie:cookie,'Content-Type':'application/json'},body:'{}'})).json();const r=await fetch(base+'/api/admin/settings',{method:'PATCH',headers:{Origin:'https://hop.test',Cookie:cookie,'X-CSRF-Token':session.csrfToken,'Content-Type':'application/json'},body:JSON.stringify({revision:0,maxConcurrentUploads:3,blockedCountries:['CN'],allowedIps:['127.0.0.1'],autoBlockEnabled:true})});if(!r.ok)throw Error(await r.text());console.log(JSON.stringify(await r.json()));`,
`const base='http://127.0.0.1:${port}', cookie=${JSON.stringify(verifiedAccount.cookie)};const session=await(await fetch(base+'/api/guest/session',{method:'POST',headers:{Origin:'https://hop.test',Cookie:cookie,'Content-Type':'application/json'},body:'{}'})).json();const r=await fetch(base+'/api/admin/settings',{method:'PATCH',headers:{Origin:'https://hop.test',Cookie:cookie,'X-CSRF-Token':session.csrfToken,'Content-Type':'application/json'},body:JSON.stringify({revision:0,maxConcurrentUploads:3,accountBytes:3e9,deviceBytes:4e9,devicesTotalBytes:12e9,perFileBytes:1e9,minDiskFreeBytes:0,blockedCountries:['CN'],allowedIps:['127.0.0.1'],autoBlockEnabled:true})});if(!r.ok)throw Error(await r.text());console.log(JSON.stringify(await r.json()));`,
),
);
assert.equal(saved.maxConcurrentUploads, 3);
assert.equal(saved.accountBytes, 3e9);
node(
`import Database from 'better-sqlite3';const db=new Database('/data/database/hop.sqlite');db.prepare('INSERT INTO automatic_blocks (ip,countryCode,reason,createdAt,expiresAt) VALUES (?,?,?,?,?)').run('8.8.8.8','KR','login-failures',Date.now(),Date.now()+1800000);db.close();`,
);
@@ -153,6 +154,14 @@ try {
),
);
assert.equal(settings.maxConcurrentUploads, 3);
for (const [key, value] of Object.entries({
accountBytes: 3e9,
deviceBytes: 4e9,
devicesTotalBytes: 12e9,
perFileBytes: 1e9,
minDiskFreeBytes: 0,
}))
assert.equal(settings[key], value, key);
assert.deepEqual(settings.blockedCountries, ['CN']);
assert.deepEqual(settings.allowedIps, ['127.0.0.1']);
assert.equal(settings.autoBlockEnabled, true);
@@ -216,7 +225,7 @@ try {
assert.deepEqual(mode, { uid: 12001, gid: 12001, mode: 0o700 });
const originals = resolve(folder, 'originals');
const stored = readdirSync(originals)
.filter((key) => key.startsWith('guest-'))
.filter((key) => /^[a-f0-9-]{36}$/.test(key))
.flatMap((owner) =>
readdirSync(resolve(originals, owner)).map((key) => ({
owner,
@@ -226,7 +235,7 @@ try {
)
.find(({ path }) => readFileSync(path).toString() === 'Docker file persistence');
assert(stored, 'uploaded file is visible under its guest owner in the host project');
assert.match(stored.owner, /^guest-[a-f0-9-]{36}$/);
assert.match(stored.owner, /^[a-f0-9-]{36}$/);
assert.match(stored.key, /^[a-f0-9-]{36}\.txt$/);
assert.equal(existsSync(resolve(folder, 'completed')), false);
unlinkSync(stored.path);
@@ -249,8 +258,46 @@ try {
assert(visible.some((item) => item.file?.name === 'resume.bin'));
}
assert.equal(docker('inspect', '--format', '{{json .NetworkSettings.Ports}}', name), '{}');
if (process.env.HOP_CHECK_RESET === '1') {
const protectedState = `import Database from 'better-sqlite3';import {createHash} from 'node:crypto';const db=new Database('/data/database/hop.sqlite');const tables=['users','account_sessions','account_tokens','user_rules','admin_settings','user_activity'];const rows=tables.map(t=>db.prepare('SELECT * FROM '+t).all());console.log(createHash('sha256').update(JSON.stringify(rows)).digest('hex'));db.close();`;
const beforeReset = node(protectedState);
docker('stop', name);
const reset = docker(
'run',
'--rm',
'--network',
'none',
'--volumes-from',
name,
'--user',
runtimeUser,
'--mount',
`type=bind,source=${resolve('scripts/reset-transfer-records.mjs')},target=/app/scripts/reset-transfer-records.mjs,readonly`,
'--env',
'DATABASE_PATH=/data/database/hop.sqlite',
'--entrypoint',
'node',
process.env.HOP_CHECK_IMAGE ?? 'hop-phase1-check:local',
'/app/scripts/reset-transfer-records.mjs',
'--confirm-reset-transfer-records',
);
assert.match(reset, /초기화 완료/);
docker('start', name);
wait();
assert.equal(
node(protectedState),
beforeReset,
'offline reset preserves user/auth/admin data exactly',
);
const emptied = JSON.parse(
node(
`import Database from 'better-sqlite3';const db=new Database('/data/database/hop.sqlite');console.log(JSON.stringify({files:db.prepare('SELECT count(*) AS n FROM files').get().n,requests:db.prepare('SELECT count(*) AS n FROM transfer_requests').get().n,guests:db.prepare('SELECT count(*) AS n FROM guest_sessions').get().n,integrity:db.pragma('integrity_check',{simple:true})}));db.close();`,
),
);
assert.deepEqual(emptied, { files: 0, requests: 0, guests: 0, integrity: 'ok' });
}
console.log(
`PASS: production Docker startup, HTTPS Secure session cookie, migration, anonymous session/name/file and Unicode text persistence and partial chunk resume after restart, SQLite integrity, no host ports published.${bindUploads ? ' Bind uploads visible on host, custom NAS UID ownership, manual host deletion reconciliation.' : ''}${verifiedAccount ? ' Verified account auto-login, protected cookie, previous session revocation, one-use token, admin role and persisted admin controls after restart.' : ''}`,
`PASS: production Docker startup, HTTPS Secure session cookie, migration, anonymous session/name/file and Unicode text persistence and partial chunk resume after restart, SQLite integrity, no host ports published.${process.env.HOP_CHECK_RESET === '1' ? ' Offline transfer reset preserves accounts and admin settings.' : ''}${bindUploads ? ' Bind uploads visible on host, custom NAS UID ownership, manual host deletion reconciliation.' : ''}${verifiedAccount ? ' Verified account auto-login, protected cookie, previous session revocation, one-use token, admin role and persisted admin controls after restart.' : ''}`,
);
} catch (error) {
console.error(execFileSync('docker', ['logs', name], { encoding: 'utf8', stdio: 'inherit' }));