/** * Files stored in the repo, as a server extension: the platform passes the * factory to core's `extensions` option and core serves the Drive section's * routes under /account/api/drive. * * This module must stay free of node-only imports; it runs on Cloudflare * Workers. */ /** @typedef {import('./account.js').DriveSpaceAdmin} DriveSpaceAdmin */ import { cborDecodeJson } from '@pdsjs/core/repo'; import { createDriveAccountRoutes } from './account.js'; import { createDriveBrowser } from './browser.js'; import { DRIVE_FILE_COLLECTION, DRIVE_FOLDER_COLLECTION } from './lexicon.js'; import { createDriveWriter } from './writer.js'; /** * The part of the space admin port a drive needs, built on first use over * the host's space storage from @pdsjs/spaces, an optional peer imported * only then. * @param {import('@pdsjs/core/ports').SpaceStoragePort} spaceStorage * @param {() => Promise} getDid * @returns {DriveSpaceAdmin} */ function lazySpaceAdmin(spaceStorage, getDid) { /** @type {Promise|null} */ let admin = null; const load = () => { admin ??= import('@pdsjs/spaces/admin').then(({ createSpaceAdmin }) => createSpaceAdmin({ spaceStorage, getDid }), ); return admin; }; return { createSpace: async (opts) => (await load()).createSpace(opts), }; } /** * @param {Object} [options] * @param {boolean} [options.upload] - accept uploads (default on); off, the drive lists and downloads only * @param {number} [options.maxFileSize] - largest file accepted, in bytes * @returns {import('@pdsjs/core/ports').ExtensionFactory} */ export function createDriveExtension({ upload = true, maxFileSize } = {}) { return (host) => { const { actorStorage, blobs, spaceStorage, spaceBrowser } = host; const ports = { actorStorage, blobs, getDid: host.getDid }; // The account page is the owner's own surface, so a drive held in a // space reads from storage rather than the authenticated space // endpoints. /** @type {import('./ports.js').DriveSpaceReader|undefined} */ const spaceReader = spaceBrowser && spaceStorage ? { listRecords: (space, collection, limit) => spaceBrowser.listRecords(space, collection, limit), getRecord: async (space, rkey) => { const row = await spaceStorage.getSpaceRecord( space, DRIVE_FILE_COLLECTION, rkey, ); return row ? { cid: row.cid, value: cborDecodeJson(row.value) } : null; }, getBlob: async (cid) => { const did = await host.getDid(); if (!did) return null; const result = await blobs.get(did, cid); if (!result) return null; return result.data instanceof Uint8Array ? result.data : new Uint8Array(/** @type {ArrayBuffer} */ (result.data)); }, } : undefined; const browser = createDriveBrowser({ ...ports, spaceReader }); const writer = upload ? createDriveWriter({ ...ports, createCommit: host.createCommit, chunkSize: host.blobUploadLimit, maxFileSize, hasher: host.hasher, spaceReader, // The spaces package is an optional peer, imported only when a // drive is written into a space. spaceWrite: spaceStorage ? async (space, writes) => { const { applyWrites } = await import('@pdsjs/spaces'); return applyWrites(spaceStorage, { space, writes, owner: await host.getDid(), }); } : undefined, }) : null; return { name: 'drive', collections: [DRIVE_FILE_COLLECTION, DRIVE_FOLDER_COLLECTION], routes: createDriveAccountRoutes({ host, browser, writer, spaceBrowser: spaceBrowser ?? null, spaceAdmin: spaceStorage ? lazySpaceAdmin(spaceStorage, host.getDid) : null, }), features: { drive: true, driveUpload: Boolean(writer) }, stats: (count) => ({ driveFiles: count(DRIVE_FILE_COLLECTION) }), }; }; }