~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
118 lines · 3.7 KB Raw History
// 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]);
  });
});