import type { FileInfo, FilesPlugin } from "../index.js"; /** * A trashed object, as returned by {@link SoftDeleteApi.trashed}. Pass its * {@link TrashedFile.key} (the original, live key) back to * {@link SoftDeleteApi.restoreTrashed} to bring it back, or * {@link SoftDeleteApi.purge} to delete it for good. */ export interface TrashedFile { /** The original key the object was deleted from — hand it to `restoreTrashed()` / `purge()`. */ key: string; /** The underlying storage key the trashed copy lives at, under the trash prefix. */ trashKey: string; /** Byte length of the trashed object. */ size: number; /** * The trashed copy's last-modified time (ms epoch), when the adapter reports * one. On most adapters a soft delete is a server-side copy, so this is * roughly when the object was trashed. */ lastModified?: number; /** The trashed copy's ETag, when the adapter reports one. */ etag?: string; } /** Options for {@link SoftDeleteApi.trashed}. */ export interface TrashedOptions { /** * Only list trashed objects whose original key starts with this prefix — * the walk itself is bounded to it, so a per-tenant view (`"users/42/"`) * never reads anyone else's trash. */ prefix?: string; } /** * The methods {@link softDelete} grafts onto a {@link Files} instance. A `type` * rather than an `interface` so it satisfies the `Record` * constraint on {@link FilesPlugin}'s extension parameter — an interface has no * implicit index signature and wouldn't be assignable. */ export type SoftDeleteApi = { /** * List everything currently in the trash — or, with `prefix`, only what was * deleted from under it — each entry carrying the original `key` you'd pass * to {@link SoftDeleteApi.restoreTrashed}. Returns an empty array when * nothing matches. */ trashed: (options?: TrashedOptions) => Promise; /** * Bring a soft-deleted object back to its original key, removing it from the * trash. Resolves to the restored key's {@link FileInfo} (via `head`). Throws * when nothing is trashed for `key`. A live object at `key` (e.g. one * re-created after the delete) is overwritten. */ restoreTrashed: (key: string) => Promise; /** * Permanently delete a trashed object — the one for `key`, or the **entire** * trash when `key` is omitted. Idempotent: purging a key with nothing trashed * is a no-op. This is the only way the data actually leaves storage. A * whole-trash purge removes everything it can and then throws (carrying the * first failure as `cause`) when any trashed object couldn't be deleted. */ purge: (key?: string) => Promise; }; export interface SoftDeleteOptions { /** * Where deleted objects are moved, as a key prefix. Defaults to `".trash"`. * A delete of `photos/a.jpg` relocates it to `".trash/photos/a.jpg"`. Objects * under this prefix are hidden from `list()` (unless you list within it) and a * `delete` of one is a **real** delete — that's how `purge()` works. Don't * store your own data under it. */ prefix?: string; } /** * Turn `delete` into a recoverable move into a trash prefix, and add `trashed()` * / `restoreTrashed()` / `purge()` so you can list, recover, and permanently remove * what's been deleted. Instead of destroying an object, a `delete` server-side * **moves** it to a time-of-deletion copy under a trash prefix (`.trash/` by * default); the bytes only ever leave storage when you `purge()`. * * Like `versioning()`, it's **body-transparent** — it never buffers, transforms, * or reads the body, so streaming, range downloads, `url()`, and * `signedUploadUrl()` all keep working — and it has **no native dependencies**. * Because it relocates whatever the rest of the pipeline stored, place it * **first** (outermost): `plugins: [softDelete(), encryption(key)]`. * * It uses `extend`, so reach for {@link createFiles} to surface * `files.trashed()` / `files.restoreTrashed()` / `files.purge()` on the type. * * Trade-offs, by design: * - **One copy per key.** A delete relocates to `"/"`, so deleting * a key whose trashed copy still exists **replaces** that copy (latest delete * wins). Reach for `versioning()` if you need every deleted generation kept. * On a hierarchical store (fs, SFTP, …) a trashed `a` and a trashed `a/b` * can't coexist, so the second delete throws a `Conflict` naming the * trashed entry to `purge()` first. * - **`delete` becomes a `copy` + `delete`.** A soft delete is a move, so it * costs an extra round-trip versus a hard delete. Deleting a key that doesn't * exist stays a no-op, the same as a plain `delete`. * - **Only deletes through the instance are trashed.** Overwrites (including * presigned uploads) and deletes made directly against the provider aren't — * reach for `versioning()` to keep overwritten bytes. It's a safety net, not * a security control, so it doesn't fail closed the way `validation()` does. * - **Trash grows until you `purge()`.** Nothing expires on its own. * - **Pairs with `versioning()`, versioning outermost.** Put `versioning()` * before this plugin so deletes are snapshotted, and hand it the trash prefix * as `ignore` so a `purge()` isn't itself versioned: * `plugins: [versioning({ ignore: [".trash"] }), softDelete()]`. * * @param options optional `{ prefix }` — where trashed objects live. * @example * ```ts * import { createFiles } from "files-sdk"; * import { s3 } from "files-sdk/s3"; * import { softDelete } from "files-sdk/soft-delete"; * * const files = createFiles({ * adapter: s3({ bucket: "uploads" }), * plugins: [softDelete()], * }); * * await files.upload("notes.txt", "hi"); * await files.delete("notes.txt"); // moved to .trash/notes.txt, not destroyed * * await files.trashed(); // [{ key: "notes.txt", trashKey: ".trash/notes.txt", … }] * await files.restoreTrashed("notes.txt"); // back to "notes.txt" * await files.delete("notes.txt"); * await files.purge("notes.txt"); // now it's really gone * ``` */ export declare const softDelete: (options?: SoftDeleteOptions) => FilesPlugin; //# sourceMappingURL=index.d.ts.map