@artemjs/vfskit-front 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/dist/conformance.d.ts +10 -0
- package/dist/conformance.js +1 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +1 -0
- package/dist/types-CMveBA-B.d.ts +69 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://cdn.jsdelivr.net/gh/artemjs/vfskit@6dffa9f/assets/logo.svg" alt="vfskit" width="160" height="160">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">vfskit</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/version-1.1.0-7c8cff?style=flat-square" alt="version">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-MIT-56e6c4?style=flat-square" alt="license">
|
|
10
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square" alt="typescript">
|
|
11
|
+
<img src="https://img.shields.io/badge/module-ESM-f0db4f?style=flat-square" alt="esm">
|
|
12
|
+
<img src="https://img.shields.io/badge/runtime%20deps-0-1f9d55?style=flat-square" alt="zero deps">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<b>One <code>VFS</code> interface over any backend</b>: in-memory, real disk, S3, or your own.<br>
|
|
17
|
+
Composable adapters, encryption, caching, optimistic concurrency, and a browser ⇆ server bridge.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
vfskit wraps any kind of storage behind a single, small `VFS` interface, then lets you
|
|
23
|
+
**compose** behavior on top of it - encryption, caching, a remote bridge - and drive it from
|
|
24
|
+
the browser exactly as you would on the server. Anything you can read, write, and list
|
|
25
|
+
becomes a structured file system with files and metadata.
|
|
26
|
+
|
|
27
|
+
It ships in two faces under one brand:
|
|
28
|
+
|
|
29
|
+
- **`/vfskit`** (npm, Node) - the full kit: core + memory + node-fs + s3 + encrypt + cache + serve + remote.
|
|
30
|
+
- **`/vfskit-front`** (npm + jsDelivr, browser) - core + memory + encrypt + cache + a remote client.
|
|
31
|
+
|
|
32
|
+
Both expose **identical API names**, so your code looks the same on either side.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm i @artemjs/vfskit # backend / Node
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// browser, no build step
|
|
42
|
+
import { remote, wsTransport } from 'https://cdn.jsdelivr.net/npm/@artemjs/vfskit-front/+esm'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Everything is a VFS
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="https://cdn.jsdelivr.net/gh/artemjs/vfskit@6dffa9f/assets/architecture.svg" alt="vfskit architecture" width="820">
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
interface VFS {
|
|
53
|
+
read(path): Promise<Uint8Array>
|
|
54
|
+
write(path, data, opts?): Promise<void>
|
|
55
|
+
list(path, opts?): Promise<Entry[]>
|
|
56
|
+
stat(path): Promise<Stat>
|
|
57
|
+
exists(path): Promise<boolean>
|
|
58
|
+
mkdir(path, opts?): Promise<void>
|
|
59
|
+
remove(path, opts?): Promise<void>
|
|
60
|
+
move(from, to): Promise<void>
|
|
61
|
+
copy(from, to): Promise<void>
|
|
62
|
+
getMeta(path): Promise<Meta>
|
|
63
|
+
setMeta(path, meta): Promise<void>
|
|
64
|
+
watch(path, cb): Unsubscribe
|
|
65
|
+
capabilities(): Capabilities
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **Adapters** implement `VFS` over a backend: `memory()`, `nodeFs(dir)`, `s3({ client })`.
|
|
70
|
+
- **Middleware** wraps a `VFS` and returns a `VFS`: `encrypt(vfs, { passphrase })`, `cache(vfs)`.
|
|
71
|
+
- **Bridge** connects them across the wire: `serve(vfs)` on the server, `remote(transport)` on the client.
|
|
72
|
+
|
|
73
|
+
Compose freely. `encrypt(remote(transport))` is end-to-end encryption - the server only ever
|
|
74
|
+
stores ciphertext.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { memory, nodeFs, encrypt, serve, remote, toText } from '@artemjs/vfskit'
|
|
80
|
+
|
|
81
|
+
const store = encrypt(nodeFs('./data'), { passphrase: 'hunter2' })
|
|
82
|
+
await store.write('/notes/todo.md', '# buy milk', { meta: { tag: 'home' } })
|
|
83
|
+
console.log(toText(await store.read('/notes/todo.md')))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Expose a backend, drive it from anywhere:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// server
|
|
90
|
+
const server = serve(nodeFs('./data'))
|
|
91
|
+
// wire server.fetch (HTTP) or server.socket (WebSocket) into your runtime
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// client (browser or Node)
|
|
96
|
+
import { remote, wsTransport } from '@artemjs/vfskit-front'
|
|
97
|
+
const fs = remote(wsTransport('ws://localhost:3000'))
|
|
98
|
+
await fs.write('/hello.txt', 'hi')
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Adapters
|
|
102
|
+
|
|
103
|
+
| Adapter | Where | Metadata | Notes |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `memory()` | anywhere | native | reference implementation; synchronous `watch` |
|
|
106
|
+
| `nodeFs(dir)` | Node | sidecar `.vfskit/meta.json` | rooted at `dir`; native streaming; `watch` via `fs.watch` |
|
|
107
|
+
| `s3({ client, prefix?, pollMs? })` | Node | native object metadata | inject any `S3Like` client; POSIX dirs emulated with markers; `watch` by polling |
|
|
108
|
+
|
|
109
|
+
Every adapter passes the same conformance suite, so a new one "just works" once it does too.
|
|
110
|
+
|
|
111
|
+
## Bring your own storage
|
|
112
|
+
|
|
113
|
+
vfskit is just an interface. To put *any* backend behind the same API, write a function that
|
|
114
|
+
returns a `VFS` - a plain object literal implementing the methods above - over your store
|
|
115
|
+
(a database, a KV cache, `localStorage`, a blob service, whatever):
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { type VFS, normalize, toBytes, notFound } from '@artemjs/vfskit'
|
|
119
|
+
|
|
120
|
+
export function myVfs(store: MyStore): VFS {
|
|
121
|
+
return {
|
|
122
|
+
capabilities: () => ({ streaming: false, watch: false, atomicMove: false, nativeMeta: true, randomAccess: false, conditionalWrite: false }),
|
|
123
|
+
async read(path) { /* ... */ },
|
|
124
|
+
async write(path, data, opts) { /* ... */ },
|
|
125
|
+
// ...the rest of the interface
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then validate it against the exact same battery every built-in adapter must pass:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { conformanceCases } from '@artemjs/vfskit/conformance'
|
|
134
|
+
import { describe, it } from 'vitest'
|
|
135
|
+
|
|
136
|
+
describe('my adapter', () => {
|
|
137
|
+
for (const c of conformanceCases) it(c.name, () => c.run(() => myVfs(new MyStore())))
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If it passes, your storage now works everywhere vfskit works - behind `encrypt(...)`, behind
|
|
142
|
+
`serve(...)`, driven by a browser `remote(...)`. A complete worked example (a key-value
|
|
143
|
+
backend) lives in [`examples/custom-adapter`](examples/custom-adapter). `conformanceCases` is
|
|
144
|
+
framework-agnostic (`{ name, run(makeVfs) }[]`), so you can drive it from any test runner.
|
|
145
|
+
|
|
146
|
+
## Encryption
|
|
147
|
+
|
|
148
|
+
AES-256-GCM via WebCrypto. A raw key, or a passphrase derived per file with PBKDF2 (random
|
|
149
|
+
salt, 210k iterations). Tamper fails closed with a typed error. Content is encrypted by
|
|
150
|
+
default; metadata stays as the backend stores it.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const vault = encrypt(memory(), { passphrase: 'open sesame' })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Caching
|
|
157
|
+
|
|
158
|
+
`cache(vfs, { ttlMs? })` serves reads from an in-memory store (write-through,
|
|
159
|
+
subtree-invalidated on write/remove/move/copy). Wrap a `remote(...)` to avoid round-trips for
|
|
160
|
+
hot files:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { cache, remote, wsTransport } from '@artemjs/vfskit-front'
|
|
164
|
+
const fs = cache(remote(wsTransport(url)), { ttlMs: 5000 })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Pass your own `store` to back the cache with anything (e.g. `localStorage`).
|
|
168
|
+
|
|
169
|
+
## Concurrent writes
|
|
170
|
+
|
|
171
|
+
Adapters that report `conditionalWrite` give every file an opaque `version` token (via
|
|
172
|
+
`stat`). Pass it back as `ifMatch` to make a write succeed only if nobody changed the file in
|
|
173
|
+
between - otherwise it fails with a typed `CONFLICT`. `ifAbsent` makes a create-only write.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const { version } = await fs.stat('/doc')
|
|
177
|
+
await fs.write('/doc', next, { ifMatch: version }) // CONFLICT if it moved on
|
|
178
|
+
await fs.write('/new', data, { ifAbsent: true }) // ALREADY_EXISTS if it exists
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Supported by `memory`, `nodeFs`, `s3`, and transparently over `remote(...)`.
|
|
182
|
+
|
|
183
|
+
## Streaming
|
|
184
|
+
|
|
185
|
+
`readStream(vfs, path)` and `writeStream(vfs, path)` give Web `ReadableStream` /
|
|
186
|
+
`WritableStream` over any adapter - native where supported (`nodeFs` streams real file
|
|
187
|
+
handles), buffered otherwise, so the API is uniform:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { readStream, writeStream, collect, toBytes } from '@artemjs/vfskit'
|
|
191
|
+
|
|
192
|
+
const w = (await writeStream(fs, '/big.log')).getWriter()
|
|
193
|
+
await w.write(toBytes('line 1\n')); await w.close()
|
|
194
|
+
const all = await collect(await readStream(fs, '/big.log'))
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`encrypt` and `cache` buffer through their own `read`/`write`, so streaming stays correct
|
|
198
|
+
behind them (the stream still yields plaintext; the disk still holds ciphertext).
|
|
199
|
+
|
|
200
|
+
## Transports
|
|
201
|
+
|
|
202
|
+
- `httpTransport(url)` - request/response; works on serverless/edge. No `watch`.
|
|
203
|
+
- `wsTransport(url)` - multiplexed; enables `watch`/events.
|
|
204
|
+
|
|
205
|
+
## Errors
|
|
206
|
+
|
|
207
|
+
Typed hierarchy with stable wire codes, reconstructed on the client across the bridge:
|
|
208
|
+
`NOT_FOUND`, `ALREADY_EXISTS`, `NOT_A_DIRECTORY`, `IS_A_DIRECTORY`, `PERMISSION_DENIED`,
|
|
209
|
+
`UNSUPPORTED`, `CONFLICT`, `IO`. Detect with `isVfsError(e)` (brand-based - survives bundling
|
|
210
|
+
and the RPC round-trip).
|
|
211
|
+
|
|
212
|
+
## Example
|
|
213
|
+
|
|
214
|
+
[`examples/cloud-ide`](examples/cloud-ide) - Monaco editing files on a real-disk VFS over a
|
|
215
|
+
WebSocket bridge, with per-user isolation. Swapping the backend to S3 is one line.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { V as VFS } from './types-CMveBA-B.js';
|
|
2
|
+
|
|
3
|
+
interface ConformanceCase {
|
|
4
|
+
name: string;
|
|
5
|
+
run(make: () => VFS): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
declare const conformanceCases: ConformanceCase[];
|
|
8
|
+
declare function runConformance(make: () => VFS): void;
|
|
9
|
+
|
|
10
|
+
export { type ConformanceCase, conformanceCases, runConformance };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var h=new TextEncoder,b=new TextDecoder;function w(e){return typeof e=="string"?h.encode(e):e instanceof Uint8Array?e:new Uint8Array(e)}function s(e){return b.decode(e)}function d(e){let t=0;for(let i of e)t+=i.length;let a=new Uint8Array(t),n=0;for(let i of e)a.set(i,n),n+=i.length;return a}var m=Symbol.for("vfskit.VfsError"),f=class extends Error{code;path;[m]=!0;constructor(t,a,n){super(a),this.name="VfsError",this.code=t,this.path=n}};function u(e){return typeof e=="object"&&e!==null&&e[m]===!0}async function y(e,t,a){if(e.readStream)return e.readStream(t,a);let n=await e.read(t,a);return new ReadableStream({start(i){i.enqueue(n),i.close()}})}async function p(e,t,a){if(e.writeStream)return e.writeStream(t,a);let n=[];return new WritableStream({write(i){n.push(i)},async close(){await e.write(t,d(n),a)}})}async function l(e){let t=[],a=e.getReader();for(;;){let{done:n,value:i}=await a.read();if(n)break;i&&t.push(i)}return d(t)}function x(e){throw new Error("conformance: "+e)}function c(e,t="expected truthy"){e||x(t)}function r(e,t,a){e!==t&&x(a??`expected ${JSON.stringify(t)}, got ${JSON.stringify(e)}`)}async function o(e,t){let a;try{await e()}catch(n){a=n}c(u(a)&&a.code===t,`expected error ${t}, got ${u(a)?a.code:a}`)}async function S(e){let t;try{await e()}catch(a){t=a}c(t,"expected throw")}async function E(e,t=2e3){for(let n=0;n<t;n+=10){if(e())return;await new Promise(i=>setTimeout(i,10))}}var g=[{name:"writes and reads a file",async run(e){let t=e();await t.write("/a.txt","hello"),r(s(await t.read("/a.txt")),"hello")}},{name:"reports existence",async run(e){let t=e();r(await t.exists("/a.txt"),!1),await t.write("/a.txt","x"),r(await t.exists("/a.txt"),!0)}},{name:"stats a file",async run(e){let t=e();await t.write("/a.txt","hello");let a=await t.stat("/a.txt");r(a.type,"file"),c(a.size>0)}},{name:"throws NOT_FOUND for missing read",async run(e){let t=e();await o(()=>t.read("/nope"),"NOT_FOUND")}},{name:"lists directory children",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await t.write("/d/b","2"),r((await t.list("/d")).map(a=>a.name).sort().join(","),"a,b")}},{name:"lists recursively",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/d/sub"),await t.write("/d/sub/a","1"),c((await t.list("/d",{recursive:!0})).some(a=>a.path==="/d/sub/a"))}},{name:"removes a file",async run(e){let t=e();await t.write("/a","1"),await t.remove("/a"),r(await t.exists("/a"),!1)}},{name:"requires recursive to remove a non-empty dir",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await S(()=>t.remove("/d")),await t.remove("/d",{recursive:!0}),r(await t.exists("/d"),!1)}},{name:"moves a file",async run(e){let t=e();await t.write("/a","1"),await t.move("/a","/b"),r(await t.exists("/a"),!1),r(s(await t.read("/b")),"1")}},{name:"copies a file",async run(e){let t=e();await t.write("/a","1"),await t.copy("/a","/b"),r(s(await t.read("/a")),"1"),r(s(await t.read("/b")),"1")}},{name:"stores and reads metadata",async run(e){let t=e();await t.write("/a","1",{meta:{tag:"x"}}),r((await t.getMeta("/a")).tag,"x"),await t.setMeta("/a",{tag:"y"}),r((await t.getMeta("/a")).tag,"y")}},{name:"emits watch events when supported",async run(e){let t=e();if(!t.capabilities().watch)return;let a=[],n=t.watch("/",i=>a.push(i.type+":"+i.path));await new Promise(i=>setTimeout(i,60)),await t.write("/a","1"),await E(()=>a.includes("create:/a")),n(),c(a.includes("create:/a"))}},{name:"moves a directory subtree",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/d/sub"),await t.write("/d/sub/a","1"),await t.move("/d","/e"),r(await t.exists("/d"),!1),r(s(await t.read("/e/sub/a")),"1")}},{name:"copies a directory subtree deeply",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await t.copy("/d","/e"),await t.write("/e/a","2"),r(s(await t.read("/d/a")),"1"),r(s(await t.read("/e/a")),"2")}},{name:"isolates returned read buffers from the store",async run(e){let t=e();await t.write("/a","abc");let a=await t.read("/a");a[0]=0,r(s(await t.read("/a")),"abc")}},{name:"does not confuse sibling prefixes",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/dx"),await t.write("/dx/a","1"),c(!(await t.list("/d")).some(a=>a.name==="a"))}},{name:"reports byte size matching written content",async run(e){let t=e();await t.write("/a","hello"),r((await t.stat("/a")).size,5)}},{name:"throws ALREADY_EXISTS creating an existing dir without recursive",async run(e){let t=e();await t.mkdir("/d"),await o(()=>t.mkdir("/d"),"ALREADY_EXISTS")}},{name:"preserves metadata when overwriting content without new meta",async run(e){let t=e();await t.write("/a","1",{meta:{tag:"x"}}),await t.write("/a","2"),r((await t.getMeta("/a")).tag,"x")}},{name:"throws NOT_A_DIRECTORY creating a directory under a file",async run(e){let t=e();await t.write("/f","1"),await o(()=>t.mkdir("/f/sub",{recursive:!0}),"NOT_A_DIRECTORY")}},{name:"throws ALREADY_EXISTS moving or copying onto an existing path",async run(e){let t=e();await t.write("/a","1"),await t.write("/b","2"),await o(()=>t.move("/a","/b"),"ALREADY_EXISTS"),await o(()=>t.copy("/a","/b"),"ALREADY_EXISTS")}},{name:"conditional write succeeds on matching version and CONFLICTs on stale (when capable)",async run(e){let t=e();if(!t.capabilities().conditionalWrite)return;await t.write("/a","1");let a=(await t.stat("/a")).version;c(typeof a=="string"&&a.length>0,"expected a version token"),await t.write("/a","2",{ifMatch:a}),r(s(await t.read("/a")),"2"),await o(()=>t.write("/a","3",{ifMatch:a}),"CONFLICT"),r(s(await t.read("/a")),"2")}},{name:"ifAbsent rejects overwriting an existing file (when capable)",async run(e){let t=e();t.capabilities().conditionalWrite&&(await t.write("/a","1"),await o(()=>t.write("/a","2",{ifAbsent:!0}),"ALREADY_EXISTS"),r(s(await t.read("/a")),"1"))}},{name:"streams content in and back out (helper, native or buffered)",async run(e){let t=e(),n=(await p(t,"/s.bin")).getWriter();await n.write(w("chunk-one;")),await n.write(w("chunk-two")),await n.close(),r(s(await l(await y(t,"/s.bin"))),"chunk-one;chunk-two"),r(s(await t.read("/s.bin")),"chunk-one;chunk-two")}}];function A(e){let t=globalThis;t.describe("vfs conformance",()=>{for(let a of g)t.it(a.name,()=>a.run(e))})}export{g as conformanceCases,A as runConformance};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { B as BytesLike, V as VFS, R as ReadOpts, W as WriteOpts, a as WatchCb, U as Unsubscribe, C as Capabilities } from './types-CMveBA-B.js';
|
|
2
|
+
export { E as Entry, F as FileType, L as ListOpts, M as Meta, b as MkdirOpts, c as RemoveOpts, S as Stat, d as WatchEvent } from './types-CMveBA-B.js';
|
|
3
|
+
|
|
4
|
+
declare function normalize(p: string): string;
|
|
5
|
+
declare function join(...parts: string[]): string;
|
|
6
|
+
declare function dirname(p: string): string;
|
|
7
|
+
declare function basename(p: string): string;
|
|
8
|
+
declare function segments(p: string): string[];
|
|
9
|
+
|
|
10
|
+
type ErrorCode = 'NOT_FOUND' | 'ALREADY_EXISTS' | 'NOT_A_DIRECTORY' | 'IS_A_DIRECTORY' | 'PERMISSION_DENIED' | 'UNSUPPORTED' | 'CONFLICT' | 'IO';
|
|
11
|
+
declare const BRAND: unique symbol;
|
|
12
|
+
declare class VfsError extends Error {
|
|
13
|
+
code: ErrorCode;
|
|
14
|
+
path?: string;
|
|
15
|
+
readonly [BRAND] = true;
|
|
16
|
+
constructor(code: ErrorCode, message: string, path?: string);
|
|
17
|
+
}
|
|
18
|
+
declare const notFound: (p: string) => VfsError;
|
|
19
|
+
declare const alreadyExists: (p: string) => VfsError;
|
|
20
|
+
declare const notADirectory: (p: string) => VfsError;
|
|
21
|
+
declare const isADirectory: (p: string) => VfsError;
|
|
22
|
+
declare const permissionDenied: (p: string) => VfsError;
|
|
23
|
+
declare const unsupported: (op: string) => VfsError;
|
|
24
|
+
declare const conflict: (p: string) => VfsError;
|
|
25
|
+
declare const io: (message: string, path?: string) => VfsError;
|
|
26
|
+
declare function isVfsError(e: unknown): e is VfsError;
|
|
27
|
+
|
|
28
|
+
declare function toBytes(d: BytesLike): Uint8Array;
|
|
29
|
+
declare function toText(d: Uint8Array): string;
|
|
30
|
+
declare function concat(parts: Uint8Array[]): Uint8Array;
|
|
31
|
+
|
|
32
|
+
declare function readStream(vfs: VFS, path: string, opts?: ReadOpts): Promise<ReadableStream<Uint8Array>>;
|
|
33
|
+
declare function writeStream(vfs: VFS, path: string, opts?: WriteOpts): Promise<WritableStream<Uint8Array>>;
|
|
34
|
+
declare function collect(s: ReadableStream<Uint8Array>): Promise<Uint8Array>;
|
|
35
|
+
|
|
36
|
+
declare function memory(): VFS;
|
|
37
|
+
|
|
38
|
+
interface EncryptOpts {
|
|
39
|
+
key?: Uint8Array;
|
|
40
|
+
passphrase?: string;
|
|
41
|
+
}
|
|
42
|
+
declare function encrypt(inner: VFS, opts: EncryptOpts): VFS;
|
|
43
|
+
|
|
44
|
+
interface CacheStore {
|
|
45
|
+
get(k: string): {
|
|
46
|
+
data: Uint8Array;
|
|
47
|
+
exp: number;
|
|
48
|
+
} | undefined;
|
|
49
|
+
set(k: string, v: {
|
|
50
|
+
data: Uint8Array;
|
|
51
|
+
exp: number;
|
|
52
|
+
}): void;
|
|
53
|
+
delete(k: string): void;
|
|
54
|
+
keys(): Iterable<string>;
|
|
55
|
+
}
|
|
56
|
+
interface CacheOpts {
|
|
57
|
+
store?: CacheStore;
|
|
58
|
+
ttlMs?: number;
|
|
59
|
+
}
|
|
60
|
+
declare function cache(inner: VFS, opts?: CacheOpts): VFS;
|
|
61
|
+
|
|
62
|
+
interface Transport {
|
|
63
|
+
request(bytes: Uint8Array): Promise<Uint8Array>;
|
|
64
|
+
watch?(path: string, cb: WatchCb): Unsubscribe;
|
|
65
|
+
}
|
|
66
|
+
interface RemoteOpts {
|
|
67
|
+
transport: Transport;
|
|
68
|
+
capabilities?: Capabilities;
|
|
69
|
+
}
|
|
70
|
+
declare function remote(opts: Transport | RemoteOpts): VFS;
|
|
71
|
+
|
|
72
|
+
type FetchLike = (url: string, init: {
|
|
73
|
+
method: string;
|
|
74
|
+
body: Uint8Array;
|
|
75
|
+
}) => Promise<{
|
|
76
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
77
|
+
}>;
|
|
78
|
+
declare function httpTransport(url: string, fetchImpl?: FetchLike): Transport;
|
|
79
|
+
|
|
80
|
+
interface SocketLike {
|
|
81
|
+
send(data: Uint8Array): void;
|
|
82
|
+
readyState: number;
|
|
83
|
+
binaryType?: string;
|
|
84
|
+
onmessage: ((ev: {
|
|
85
|
+
data: any;
|
|
86
|
+
}) => void) | null;
|
|
87
|
+
onopen?: ((ev?: any) => void) | null;
|
|
88
|
+
addEventListener?(type: string, cb: (ev?: any) => void): void;
|
|
89
|
+
}
|
|
90
|
+
declare function wsTransport(url: string, factory?: () => SocketLike): Transport;
|
|
91
|
+
|
|
92
|
+
export { BRAND, BytesLike, type CacheOpts, type CacheStore, Capabilities, type EncryptOpts, type ErrorCode, type FetchLike, ReadOpts, type RemoteOpts, type SocketLike, type Transport, Unsubscribe, VFS, VfsError, WatchCb, WriteOpts, alreadyExists, basename, cache, collect, concat, conflict, dirname, encrypt, httpTransport, io, isADirectory, isVfsError, join, memory, normalize, notADirectory, notFound, permissionDenied, readStream, remote, segments, toBytes, toText, unsupported, writeStream, wsTransport };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function f(t){let a=[];for(let o of t.split("/"))if(!(o===""||o===".")){if(o===".."){a.pop();continue}a.push(o)}return"/"+a.join("/")}function tt(...t){return f(t.join("/"))}function E(t){let a=f(t),o=a.lastIndexOf("/");return o<=0?"/":a.slice(0,o)}function et(t){let a=f(t);return a.slice(a.lastIndexOf("/")+1)}function rt(t){return f(t).split("/").filter(Boolean)}var D=Symbol.for("vfskit.VfsError"),w=class extends Error{code;path;[D]=!0;constructor(a,o,n){super(o),this.name="VfsError",this.code=a,this.path=n}},O=t=>new w("NOT_FOUND",`not found: ${t}`,t),x=t=>new w("ALREADY_EXISTS",`already exists: ${t}`,t),S=t=>new w("NOT_A_DIRECTORY",`not a directory: ${t}`,t),C=t=>new w("IS_A_DIRECTORY",`is a directory: ${t}`,t),at=t=>new w("PERMISSION_DENIED",`permission denied: ${t}`,t),T=t=>new w("UNSUPPORTED",`unsupported: ${t}`),R=t=>new w("CONFLICT",`version conflict: ${t}`,t),g=(t,a)=>new w("IO",t,a);function ot(t){return typeof t=="object"&&t!==null&&t[D]===!0}var L=new TextEncoder,B=new TextDecoder;function h(t){return typeof t=="string"?L.encode(t):t instanceof Uint8Array?t:new Uint8Array(t)}function st(t){return B.decode(t)}function b(t){let a=0;for(let i of t)a+=i.length;let o=new Uint8Array(a),n=0;for(let i of t)o.set(i,n),n+=i.length;return o}async function yt(t,a,o){if(t.readStream)return t.readStream(a,o);let n=await t.read(a,o);return new ReadableStream({start(i){i.enqueue(n),i.close()}})}async function dt(t,a,o){if(t.writeStream)return t.writeStream(a,o);let n=[];return new WritableStream({write(i){n.push(i)},async close(){await t.write(a,b(n),o)}})}async function ut(t){let a=[],o=t.getReader();for(;;){let{done:n,value:i}=await o.read();if(n)break;i&&a.push(i)}return b(a)}var W={streaming:!1,watch:!0,atomicMove:!0,nativeMeta:!0,randomAccess:!1,conditionalWrite:!0};function _(){let t=()=>Date.now(),a=0,o=()=>String(++a),n=new Map([["/",{type:"dir",meta:{},mtime:t(),ctime:t()}]]),i=new Set,c=(r,s)=>s===r||s.startsWith(r==="/"?"/":r+"/"),m=(r,s)=>{for(let e of i)c(e.base,s)&&e.cb({type:r,path:s})},d=r=>{let s=n.get(r);if(!s)throw O(r);return s},p=r=>{let s=E(r),e=n.get(s);if(!e)throw O(s);if(e.type!=="dir")throw S(s)};return{capabilities:()=>W,async read(r){let s=f(r),e=d(s);if(e.type==="dir")throw C(s);return e.data.slice()},async write(r,s,e){let y=f(r);p(y);let l=n.get(y);if(l&&l.type==="dir")throw C(y);if(e?.ifAbsent&&l)throw x(y);if(e?.ifMatch!==void 0&&l?.version!==e.ifMatch)throw R(y);let u=l?l.ctime:t();n.set(y,{type:"file",data:h(s).slice(),meta:e?.meta?{...e.meta}:l?.meta??{},ctime:u,mtime:t(),version:o()}),m(l?"update":"create",y)},async list(r,s){let e=f(r);if(d(e).type!=="dir")throw S(e);let l=[];for(let[u,I]of n)u===e||!c(e,u)||!s?.recursive&&E(u)!==e||l.push({name:u.slice(u.lastIndexOf("/")+1),path:u,type:I.type});return l},async stat(r){let s=f(r),e=d(s);return{type:e.type,size:e.type==="file"?e.data.length:0,mtime:e.mtime,ctime:e.ctime,meta:{...e.meta},version:e.type==="file"?e.version:void 0}},async exists(r){return n.has(f(r))},async mkdir(r,s){let e=f(r);if(n.has(e)){if(s?.recursive)return;throw x(e)}if(s?.recursive){let y="";for(let l of e.split("/").filter(Boolean)){y+="/"+l;let u=n.get(y);if(u){if(u.type!=="dir")throw S(y);continue}n.set(y,{type:"dir",meta:{},mtime:t(),ctime:t()}),m("create",y)}return}p(e),n.set(e,{type:"dir",meta:{},mtime:t(),ctime:t()}),m("create",e)},async remove(r,s){let e=f(r);d(e);let y=[...n.keys()].filter(l=>l!==e&&c(e,l));if(y.length&&!s?.recursive)throw g("directory not empty",e);for(let l of[...y,e])n.delete(l),m("remove",l)},async move(r,s){let e=f(r),y=f(s);if(d(e),p(y),n.has(y))throw x(y);if(c(e,y))throw g("cannot move into itself",y);for(let l of[...n.keys()].filter(u=>u===e||c(e,u))){let u=n.get(l);n.delete(l),n.set(y+l.slice(e.length),u)}m("remove",e),m("create",y)},async copy(r,s){let e=f(r),y=f(s);if(d(e),p(y),n.has(y))throw x(y);for(let l of[...n.keys()].filter(u=>u===e||c(e,u))){let u=n.get(l);n.set(y+l.slice(e.length),u.type==="file"?{type:"file",data:u.data.slice(),meta:{...u.meta},mtime:u.mtime,ctime:u.ctime,version:o()}:{type:"dir",meta:{...u.meta},mtime:u.mtime,ctime:u.ctime})}m("create",y)},async getMeta(r){return{...d(f(r)).meta}},async setMeta(r,s){let e=d(f(r));e.meta={...s},e.mtime=t()},watch(r,s){let e={base:f(r),cb:s};return i.add(e),()=>{i.delete(e)}}}}var v=new Uint8Array([86,75,1]),M=47,k=globalThis.crypto.subtle;function P(t){let a=new Uint8Array(t);return globalThis.crypto.getRandomValues(a),a}async function q(t,a){let o=await k.importKey("raw",new TextEncoder().encode(t),"PBKDF2",!1,["deriveKey"]);return k.deriveKey({name:"PBKDF2",salt:a.slice(),iterations:21e4,hash:"SHA-256"},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function K(t,a){if(!a.key&&!a.passphrase)throw new Error("encrypt: key or passphrase required");let o=a.key?k.importKey("raw",a.key.slice(),"AES-GCM",!1,["encrypt","decrypt"]):null,n=c=>o??q(a.passphrase,c),i={...t.capabilities(),streaming:!1,randomAccess:!1};return{...t,readStream:void 0,writeStream:void 0,capabilities:()=>i,async stat(c){let m=await t.stat(c);return m.type==="file"?{...m,size:Math.max(0,m.size-M)}:m},async write(c,m,d){let p=P(16),r=P(12),s=await n(p),e=new Uint8Array(await k.encrypt({name:"AES-GCM",iv:r},s,h(m).slice()));await t.write(c,b([v,p,r,e]),d)},async read(c,m){let d=await t.read(c,m);if(d.length<M||d[0]!==v[0]||d[1]!==v[1]||d[2]!==v[2])throw g("invalid ciphertext",c);let p=await n(d.slice(3,19)),r;try{r=await k.decrypt({name:"AES-GCM",iv:d.slice(19,31)},p,d.slice(31))}catch{throw g("decryption failed",c)}return new Uint8Array(r)}}}function Y(t,a={}){let o=a.store??new Map,n=a.ttlMs??0,i=()=>n?Date.now():0,c=()=>n?Date.now()+n:1/0,m=(p,r)=>r===p||r.startsWith(p==="/"?"/":p+"/"),d=p=>{for(let r of[...o.keys()])m(p,r)&&o.delete(r)};return{...t,readStream:void 0,writeStream:void 0,async read(p,r){let s=f(p),e=o.get(s);if(e&&e.exp>i())return e.data.slice();let y=await t.read(s,r);return o.set(s,{data:y.slice(),exp:c()}),y.slice()},async write(p,r,s){let e=f(p),y=h(r).slice();await t.write(e,y,s),o.set(e,{data:y,exp:c()})},async remove(p,r){let s=f(p);await t.remove(s,r),d(s)},async move(p,r){let s=f(p),e=f(r);await t.move(s,e),d(s),d(e)},async copy(p,r){let s=f(p),e=f(r);await t.copy(s,e),d(e)}}}var $=new TextEncoder,z=new TextDecoder,A=new Uint8Array(0);function G(t,a=A){let o=$.encode(JSON.stringify(t)),n=new Uint8Array(4+o.length+a.length);return new DataView(n.buffer).setUint32(0,o.length),n.set(o,4),n.set(a,4+o.length),n}function j(t){let o=new DataView(t.buffer,t.byteOffset,t.byteLength).getUint32(0);return{header:JSON.parse(z.decode(t.subarray(4,4+o))),data:t.subarray(4+o)}}function F(t,a,o=[],n){return G({m:t,p:a,a:o},n)}function N(t){let{header:a,data:o}=j(t);return a.ok?{ok:!0,value:a.v,data:o}:{ok:!1,data:A,code:a.c,message:a.e,path:a.p}}function U(t,a,o=A){let n=new Uint8Array(5+o.length);return n[0]=t,new DataView(n.buffer).setUint32(1,a),n.set(o,5),n}function V(t){let a=new DataView(t.buffer,t.byteOffset,t.byteLength);return{type:t[0],id:a.getUint32(1),payload:t.subarray(5)}}function J(t){let a=t.request?t:t.transport,o=t.capabilities??{streaming:!1,watch:!!a.watch,atomicMove:!1,nativeMeta:!0,randomAccess:!1,conditionalWrite:!0},n=async(i,c,m,d)=>{let p=m?[...m]:[];for(;p.length&&p[p.length-1]===void 0;)p.pop();let r=N(await a.request(F(i,c,p,d)));if(!r.ok)throw new w(r.code,r.message??"",r.path);return r};return{capabilities:()=>o,async read(i,c){return(await n("read",i,[c])).data},async write(i,c,m){await n("write",i,[m],h(c))},async list(i,c){return(await n("list",i,[c])).value},async stat(i){return(await n("stat",i)).value},async exists(i){return(await n("exists",i)).value},async mkdir(i,c){await n("mkdir",i,[c])},async remove(i,c){await n("remove",i,[c])},async move(i,c){await n("move",i,[c])},async copy(i,c){await n("copy",i,[c])},async getMeta(i){return(await n("getMeta",i)).value},async setMeta(i,c){await n("setMeta",i,[c])},watch(i,c){if(!a.watch)throw T("watch");return a.watch(i,c)}}}function H(t,a){let o=a??((n,i)=>fetch(n,i));return{async request(n){let i=await o(t,{method:"POST",body:n});return new Uint8Array(await i.arrayBuffer())}}}var X=new TextEncoder,Q=new TextDecoder;function Z(t,a){let o=a?a():new WebSocket(t);try{o.binaryType="arraybuffer"}catch{}let n=new Map,i=new Map,c=0,m=o.readyState===1?Promise.resolve():new Promise(d=>{o.addEventListener?o.addEventListener("open",()=>d()):o.onopen=()=>d()});return o.onmessage=d=>{let{type:p,id:r,payload:s}=V(new Uint8Array(d.data));p===1?(n.get(r)?.(s),n.delete(r)):p===4&&i.get(r)?.(JSON.parse(Q.decode(s)))},{async request(d){await m;let p=++c;return new Promise(r=>{n.set(p,r),o.send(U(0,p,d))})},watch(d,p){let r=++c;return i.set(r,p),m.then(()=>o.send(U(2,r,X.encode(JSON.stringify({path:d}))))),()=>{i.delete(r),m.then(()=>o.send(U(3,r,A)))}}}}export{D as BRAND,w as VfsError,x as alreadyExists,et as basename,Y as cache,ut as collect,b as concat,R as conflict,E as dirname,K as encrypt,H as httpTransport,g as io,C as isADirectory,ot as isVfsError,tt as join,_ as memory,f as normalize,S as notADirectory,O as notFound,at as permissionDenied,yt as readStream,J as remote,rt as segments,h as toBytes,st as toText,T as unsupported,dt as writeStream,Z as wsTransport};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
type BytesLike = Uint8Array | ArrayBuffer | string;
|
|
2
|
+
type FileType = 'file' | 'dir';
|
|
3
|
+
interface Meta {
|
|
4
|
+
[k: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface Stat {
|
|
7
|
+
type: FileType;
|
|
8
|
+
size: number;
|
|
9
|
+
mtime: number;
|
|
10
|
+
ctime: number;
|
|
11
|
+
meta: Meta;
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
interface Entry {
|
|
15
|
+
name: string;
|
|
16
|
+
path: string;
|
|
17
|
+
type: FileType;
|
|
18
|
+
}
|
|
19
|
+
interface ReadOpts {
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
interface WriteOpts {
|
|
23
|
+
meta?: Meta;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
ifMatch?: string;
|
|
26
|
+
ifAbsent?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface ListOpts {
|
|
29
|
+
recursive?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface MkdirOpts {
|
|
32
|
+
recursive?: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface RemoveOpts {
|
|
35
|
+
recursive?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface Capabilities {
|
|
38
|
+
streaming: boolean;
|
|
39
|
+
watch: boolean;
|
|
40
|
+
atomicMove: boolean;
|
|
41
|
+
nativeMeta: boolean;
|
|
42
|
+
randomAccess: boolean;
|
|
43
|
+
conditionalWrite: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface WatchEvent {
|
|
46
|
+
type: 'create' | 'update' | 'remove';
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
type WatchCb = (e: WatchEvent) => void;
|
|
50
|
+
type Unsubscribe = () => void;
|
|
51
|
+
interface VFS {
|
|
52
|
+
read(path: string, opts?: ReadOpts): Promise<Uint8Array>;
|
|
53
|
+
write(path: string, data: BytesLike, opts?: WriteOpts): Promise<void>;
|
|
54
|
+
list(path: string, opts?: ListOpts): Promise<Entry[]>;
|
|
55
|
+
stat(path: string): Promise<Stat>;
|
|
56
|
+
exists(path: string): Promise<boolean>;
|
|
57
|
+
mkdir(path: string, opts?: MkdirOpts): Promise<void>;
|
|
58
|
+
remove(path: string, opts?: RemoveOpts): Promise<void>;
|
|
59
|
+
move(from: string, to: string): Promise<void>;
|
|
60
|
+
copy(from: string, to: string): Promise<void>;
|
|
61
|
+
getMeta(path: string): Promise<Meta>;
|
|
62
|
+
setMeta(path: string, meta: Meta): Promise<void>;
|
|
63
|
+
watch(path: string, cb: WatchCb): Unsubscribe;
|
|
64
|
+
capabilities(): Capabilities;
|
|
65
|
+
readStream?(path: string, opts?: ReadOpts): Promise<ReadableStream<Uint8Array>>;
|
|
66
|
+
writeStream?(path: string, opts?: WriteOpts): Promise<WritableStream<Uint8Array>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type { BytesLike as B, Capabilities as C, Entry as E, FileType as F, ListOpts as L, Meta as M, ReadOpts as R, Stat as S, Unsubscribe as U, VFS as V, WriteOpts as W, WatchCb as a, MkdirOpts as b, RemoveOpts as c, WatchEvent as d };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@artemjs/vfskit-front",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Universal VFS abstraction for the browser - in-memory + encryption + a remote client that drives any vfskit backend. Frontend kit, served via jsDelivr.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vfs",
|
|
7
|
+
"filesystem",
|
|
8
|
+
"fs",
|
|
9
|
+
"browser",
|
|
10
|
+
"storage",
|
|
11
|
+
"abstraction",
|
|
12
|
+
"virtual-file-system",
|
|
13
|
+
"encryption",
|
|
14
|
+
"remote",
|
|
15
|
+
"jsdelivr"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./conformance": {
|
|
25
|
+
"types": "./dist/conformance.d.ts",
|
|
26
|
+
"import": "./dist/conformance.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/artemjs/vfskit.git",
|
|
40
|
+
"directory": "facades/vfskit-front"
|
|
41
|
+
},
|
|
42
|
+
"bugs": "https://github.com/artemjs/vfskit/issues",
|
|
43
|
+
"homepage": "https://github.com/artemjs/vfskit#readme",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@vfskit/core": "*",
|
|
49
|
+
"@vfskit/memory": "*",
|
|
50
|
+
"@vfskit/encrypt": "*",
|
|
51
|
+
"@vfskit/remote": "*",
|
|
52
|
+
"@vfskit/transport-http": "*",
|
|
53
|
+
"@vfskit/transport-ws": "*",
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"@vfskit/cache": "*"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|