fix(uploads): reclaim abandoned front-submit attachments
Mark new regular and chunked uploads as pending and claim persisted article references, including partial saves and large reference sets. Protect associated media from cleanup and add upload lifecycle regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9b7997ec2
commit
32ac2112b0
10 files changed
+960
-26
No files matched your search
@@ -2,9 +2,20 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||||
let behavior;
|
||||
const harness = () => {
|
||||
if (!behavior) {
|
||||
const run = spawnSync('php', [path.join(__dirname, 'pending-upload-harness.php')], { encoding: 'utf8' });
|
||||
assert.equal(run.status, 0, `${run.stderr}${run.stdout}`);
|
||||
assert.equal(run.stderr, '');
|
||||
behavior = JSON.parse(run.stdout);
|
||||
}
|
||||
return behavior;
|
||||
};
|
||||
|
||||
test('upload marks pending and returns attachment id', () => {
|
||||
const controller = read('src/Admin/UserController.php');
|
||||
@@ -40,3 +51,60 @@ test('front-end remove buttons discard the pending upload', () => {
|
||||
assert.match(core, /data-aksrt-ticket-image-id/);
|
||||
assert.match(report, /'\/upload\/pending\/'/);
|
||||
});
|
||||
|
||||
test('front submission uploads are marked and saved references are claimed without close-time deletion', () => {
|
||||
const upload = read('src/Core/FrontSubmit.php');
|
||||
const media = read('src/Core/FrontSubmitMedia.php');
|
||||
const editor = read('assets/js/front-submit-editor.js');
|
||||
assert.match(upload, /PendingUploads::mark\(\$attachment_id\)/);
|
||||
assert.match(media, /PendingUploads::claim_ids\(array_keys\(\$desired\), \$user_id\)/);
|
||||
assert.match(media, /PendingUploads::claim_urls\(\$download_urls, \$user_id, null\)/);
|
||||
assert.match(media, /PendingUploads::claim_text\(\(string\) \$post->post_content, \$user_id, null\)/);
|
||||
assert.doesNotMatch(editor, /\/upload\/pending\//);
|
||||
});
|
||||
|
||||
test('cleanup is scheduled once hourly and discard requires a logged-in user', () => {
|
||||
const state = harness().registration;
|
||||
assert.ok(state.hooks.includes('aksrt_pending_upload_cleanup'));
|
||||
assert.ok(state.hooks.includes('init'));
|
||||
assert.equal(state.schedule_count, 1);
|
||||
assert.equal(state.recurrence, 'hourly');
|
||||
assert.equal(state.namespace, 'aksrt/v1');
|
||||
assert.equal(state.method, 'DELETE');
|
||||
assert.equal(state.member_allowed, true);
|
||||
assert.equal(state.guest_allowed, false);
|
||||
});
|
||||
|
||||
test('ID claims handle more than 20 attachments but only clear the owner’s pending marker', () => {
|
||||
assert.deepEqual(harness().claim_ids, { remaining: [26], marked_recently: true, invalid_marked: false });
|
||||
});
|
||||
|
||||
test('URL and text claims retain their default bound while saved content may claim all references', () => {
|
||||
assert.deepEqual(harness().claim_urls, { default_remaining: [21, 22, 23, 24, 25], unlimited_remaining: [] });
|
||||
assert.deepEqual(harness().claim_text, { default_remaining: [21, 22, 23, 24, 25, 26, 27], unlimited_remaining: [26, 27] });
|
||||
});
|
||||
|
||||
test('discard refuses foreign, claimed and parented media and permanently deletes only an owned pending upload', () => {
|
||||
assert.deepEqual(harness().discard, { denied: [false, false, false, false], success: true, repeat_status: 404, deleted: [[1, true]] });
|
||||
});
|
||||
|
||||
test('cleanup deletes only expired unattached pending uploads and uses prepared bounded SQL', () => {
|
||||
const state = harness().cleanup;
|
||||
assert.deepEqual(state.deleted, [[1, true]]);
|
||||
assert.deepEqual(state.remaining, [2, 3, 4]);
|
||||
assert.match(state.query.sql, /p\.post_type = 'attachment' AND p\.post_parent = 0/);
|
||||
assert.match(state.query.sql, /m\.meta_key = %s/);
|
||||
assert.match(state.query.sql, /LIMIT %d/);
|
||||
assert.equal(state.query.params[0], '_aksrt_pending_upload');
|
||||
assert.equal(state.query.params[2], 100);
|
||||
assert.ok(Math.abs(state.query.params[1] - (Math.floor(Date.now() / 1000) - 86400)) < 10);
|
||||
});
|
||||
|
||||
test('cleanup rechecks selected candidates after claim, attach, refresh or removal', () => {
|
||||
assert.deepEqual(harness().stale_selection.deleted, [[5, true]]);
|
||||
});
|
||||
|
||||
test('cleanup processes a bounded batch and retries unsuccessful deletions later', () => {
|
||||
assert.deepEqual(harness().batch, { first: 100, total: 105, remaining: 0 });
|
||||
assert.deepEqual(harness().retry, { retained: true, deleted_after_retry: true });
|
||||
});
|
||||
Reference in new issue
Block a user