251 lines
15 KiB
Markdown
251 lines
15 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
||
|
||
# Capability Spec Delta: storage-file (storage-file)
|
||
|
||
**Package**: `@suaveplan/storage-file`
|
||
**Status**: PROPOSED
|
||
|
||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: NodeFsStorage extends AbstractStorage
|
||
|
||
The package SHALL provide a `NodeFsStorage` class extending `AbstractStorage` from `@suaveplan/storage`. The class SHALL implement every abstract method on the base (`get`, `set`, `delete`, `exists`, `list`, `clear`, `getWithMetadata`, `getMetadata`, `getSize`, `getPartial`, `setPartial`, `getItemSize`, `createReadStream`, `createWriteStream`) with the signatures declared on the base, including the trailing `signal?: AbortSignal` parameter.
|
||
|
||
#### Scenario: instance is structurally an AbstractStorage
|
||
|
||
- **WHEN** a caller constructs `new NodeFsStorage({ rootDir })`
|
||
- **THEN** the instance is `instanceof AbstractStorage` and every abstract method is callable
|
||
|
||
#### Scenario: capabilities matrix declares native streams and partial reads
|
||
|
||
- **WHEN** a caller reads `storage.capabilities`
|
||
- **THEN** `partialReads === true`, `nativeStreams === true`, `atomicWrites === true`, `partialWrites === false`, `contentAddressable === false`
|
||
|
||
**Verified by:** `src/node-fs-storage/node-fs-storage.test.ts::"implements every AbstractStorage abstract method"` AND `src/node-fs-storage/node-fs-storage.spec-conformance.test.ts::"declares the documented capabilities matrix"`
|
||
|
||
---
|
||
|
||
### Requirement: CRUD operations under rootDir isolation
|
||
|
||
The adapter SHALL store every key under the configured `rootDir`. `set` SHALL create parent directories when `mkdirp` is enabled (default true). `get` SHALL return `undefined` for missing keys. `delete` SHALL return `true` on successful removal and `false` when the key did not exist. `exists` SHALL return a boolean without throwing on missing keys. `list` SHALL enumerate every key under an optional `prefix`, support `limit`/`offset`/`continuationToken` pagination, and return `{keys, total, hasMore, continuationToken}`. `clear` SHALL remove every data and metadata file under `rootDir`. Every method SHALL accept an optional `AbortSignal` and throw `STORAGE_FILE.ABORTED` when the signal is already aborted.
|
||
|
||
#### Scenario: round-trip set/get/delete preserves bytes
|
||
|
||
- **WHEN** a caller `await storage.set("k", bytes)` then `await storage.get("k")`
|
||
- **THEN** the returned `Uint8Array` is byte-equal to `bytes`; subsequent `await storage.delete("k")` returns `true`; final `await storage.get("k")` returns `undefined`
|
||
|
||
#### Scenario: AbortSignal short-circuits every method
|
||
|
||
- **WHEN** a caller passes an already-aborted signal to any method
|
||
- **THEN** the call rejects with `StorageFileError` whose `code === "STORAGE_FILE.ABORTED"` before any filesystem call is issued
|
||
|
||
**Verified by:** `src/node-fs-storage/node-fs-storage.test.ts::"round-trips Uint8Array values"` AND `src/node-fs-storage/node-fs-storage.test.ts::"rejects with STORAGE_FILE.ABORTED when signal is pre-aborted"`
|
||
|
||
---
|
||
|
||
### Requirement: Atomic writes via temp-file-then-rename
|
||
|
||
`set` and metadata writes SHALL write to a temp file in the destination directory and `fs.rename` it onto the destination path. On write failure the temp file SHALL be unlinked. Concurrent readers SHALL never observe a torn write — they either see the previous version or the new version.
|
||
|
||
#### Scenario: temp file is removed after successful write
|
||
|
||
- **WHEN** a caller `await storage.set("k", bytes)` completes
|
||
- **THEN** no temp file (with prefix `.tmp-`) remains in `rootDir`
|
||
|
||
#### Scenario: temp file is removed when the underlying write throws
|
||
|
||
- **WHEN** the injected `NodeFsLike.writeFile` throws after the temp file is created
|
||
- **THEN** the temp file is unlinked and the destination path is unchanged
|
||
|
||
**Verified by:** `src/node-fs-storage/node-fs-storage.test.ts::"writes atomically — no temp files remain after success"` AND `src/node-fs-storage/node-fs-storage.test.ts::"unlinks the temp file when the underlying writeFile throws"`
|
||
|
||
---
|
||
|
||
### Requirement: Strict key sanitisation at the boundary
|
||
|
||
Every method that accepts a key SHALL pass it through `sanitizeKey` before issuing any filesystem call. The sanitiser SHALL reject empty strings, keys containing `..` segments, keys starting with `/` or matching a drive-letter prefix (`C:\…`), keys containing null bytes (` |