chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// A migrated account is the failure case here: loadRepositoryFromCar imports
// records without populating the record_blobs link table, so getOrphanedBlobs
// reports every referenced blob as an orphan. Cleanup must cross-check the
// blobs actually referenced by record content before deleting, or it silently
// destroys the account's blobs a day after migration.
import { PersonalDataServer } from '@pdsjs/core';
import { describe, expect, test } from 'vitest';
const DID = 'did:plc:blobcleanupaaaaaaaaaaaaa';
const REFERENCED = 'bafkreireferencedblobaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const ORPHAN = 'bafkreiorphanblobaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
/**
* Storage that mimics a freshly migrated account: a profile record references
* REFERENCED, but the orphan query (which trusts the empty link table) reports
* BOTH blobs as orphaned.
* @param {string[]} deleted - collects cids passed to deleteBlob
* @param {{cid: string, uri: string}[]} linked - collects rebuilt blob links
*/
function migratedStorage(deleted, linked) {
const profileUri = `at://${DID}/app.bsky.actor.profile/self`;
return {
async getDid() {
return DID;
},
async getOrphanedBlobs() {
return [REFERENCED, ORPHAN];
},
async linkBlobToRecord(
/** @type {string} */ cid,
/** @type {string} */ uri,
) {
linked.push({ cid, uri });
},
async listAllRecords() {
return [{ key: 'app.bsky.actor.profile/self', cid: 'bafyprofile' }];
},
async getRecord(/** @type {string} */ uri) {
if (uri !== profileUri) return null;
return {
cid: 'bafyprofile',
value: {
$type: 'app.bsky.actor.profile',
avatar: {
$type: 'blob',
ref: { $link: REFERENCED },
mimeType: 'image/jpeg',
size: 100,
},
},
};
},
async deleteBlob(/** @type {string} */ cid) {
deleted.push(cid);
},
};
}
describe('orphaned-blob cleanup', () => {
test('never deletes a blob a record still references', async () => {
/** @type {string[]} */
const deleted = [];
/** @type {{cid: string, uri: string}[]} */
const linked = [];
const pds = new PersonalDataServer({
actorStorage: /** @type {any} */ (migratedStorage(deleted, linked)),
sharedStorage: /** @type {any} */ ({}),
blobs: {
async get() {
return null;
},
async put() {},
async delete(/** @type {string} */ _did, /** @type {string} */ cid) {
deleted.push(cid);
},
},
jwtSecret: 'test-secret',
hostname: 'pds.example.com',
password: 'pw',
});
await pds.runBlobCleanup();
// The referenced blob survives; only the true orphan is reaped.
expect(deleted).not.toContain(REFERENCED);
expect(deleted).toContain(ORPHAN);
// And the cleanup healed the link table on its way through.
expect(linked).toEqual([
{ cid: REFERENCED, uri: `at://${DID}/app.bsky.actor.profile/self` },
]);
});
test('a reap limited to some blobs leaves every other orphan', async () => {
/** @type {string[]} */
const deleted = [];
const pds = new PersonalDataServer({
actorStorage: /** @type {any} */ (migratedStorage(deleted, [])),
sharedStorage: /** @type {any} */ ({}),
blobs: {
async get() {
return null;
},
async put() {},
async delete() {},
},
jwtSecret: 'test-secret',
hostname: 'pds.example.com',
password: 'pw',
});
await pds.reapOrphanedBlobs(DID, Date.now(), ['bafkreinotanorphan']);
expect(deleted).toEqual([]);
await pds.reapOrphanedBlobs(DID, Date.now(), [ORPHAN, REFERENCED]);
expect(deleted).toEqual([ORPHAN]);
});
});