@statewalker/webrun-files 0.5.0 → 0.9.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/LICENSE +1 -1
- package/README.md +131 -114
- package/dist/cjs/index.cjs +85 -529
- package/dist/esm/index.js +78 -519
- package/dist/file-utils.d.ts +47 -0
- package/dist/file-utils.d.ts.map +1 -0
- package/dist/index.d.ts +14 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/{utils/path-utils.d.ts → path-utils.d.ts} +10 -1
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/types.d.ts +130 -51
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -10
- package/src/file-utils.ts +106 -0
- package/src/index.ts +38 -27
- package/src/{utils/path-utils.ts → path-utils.ts} +16 -2
- package/src/types.ts +136 -53
- package/dist/files-api.d.ts +0 -44
- package/dist/files-api.d.ts.map +0 -1
- package/dist/impl/index.d.ts +0 -6
- package/dist/impl/index.d.ts.map +0 -1
- package/dist/impl/mem-files-api.d.ts +0 -13
- package/dist/impl/mem-files-api.d.ts.map +0 -1
- package/dist/impl/node-files-api.d.ts +0 -24
- package/dist/impl/node-files-api.d.ts.map +0 -1
- package/dist/utils/collect-stream.d.ts +0 -16
- package/dist/utils/collect-stream.d.ts.map +0 -1
- package/dist/utils/file-info-utils.d.ts +0 -10
- package/dist/utils/file-info-utils.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/normalize-path.d.ts +0 -21
- package/dist/utils/normalize-path.d.ts.map +0 -1
- package/dist/utils/path-utils.d.ts.map +0 -1
- package/src/files-api.ts +0 -168
- package/src/impl/index.ts +0 -6
- package/src/impl/mem-files-api.ts +0 -323
- package/src/impl/node-files-api.ts +0 -271
- package/src/utils/collect-stream.ts +0 -35
- package/src/utils/file-info-utils.ts +0 -15
- package/src/utils/index.ts +0 -8
- package/src/utils/normalize-path.ts +0 -34
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,78 +1,126 @@
|
|
|
1
1
|
# @statewalker/webrun-files
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Core types and utilities for cross-platform file operations. This package defines the `FilesApi` interface that all storage backends implement, plus utility functions for common file operations.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @statewalker/webrun-files
|
|
9
|
-
# or
|
|
10
|
-
pnpm add @statewalker/webrun-files
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
For actual filesystem implementations, install one of these packages:
|
|
12
|
+
- `@statewalker/webrun-files-mem` - In-memory storage
|
|
13
|
+
- `@statewalker/webrun-files-node` - Node.js filesystem
|
|
14
|
+
- `@statewalker/webrun-files-browser` - Browser File System Access API
|
|
15
|
+
- `@statewalker/webrun-files-s3` - AWS S3 / S3-compatible storage
|
|
16
|
+
- `@statewalker/webrun-files-composite` - Mount multiple backends into a unified filesystem
|
|
17
|
+
|
|
18
|
+
## The FilesApi Interface
|
|
19
|
+
|
|
20
|
+
All implementations provide this interface:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
interface FilesApi {
|
|
24
|
+
// Read file content as async iterable of chunks
|
|
25
|
+
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
26
|
+
|
|
27
|
+
// Write content to file (creates parent directories)
|
|
28
|
+
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
29
|
+
|
|
30
|
+
// Create directory (and parents)
|
|
31
|
+
mkdir(path: string): Promise<void>;
|
|
32
|
+
|
|
33
|
+
// List directory contents
|
|
34
|
+
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
14
35
|
|
|
15
|
-
|
|
36
|
+
// Get file/directory metadata
|
|
37
|
+
stats(path: string): Promise<FileStats | undefined>;
|
|
38
|
+
|
|
39
|
+
// Check if path exists
|
|
40
|
+
exists(path: string): Promise<boolean>;
|
|
41
|
+
|
|
42
|
+
// Remove file or directory (recursively)
|
|
43
|
+
remove(path: string): Promise<boolean>;
|
|
44
|
+
|
|
45
|
+
// Move/rename file or directory
|
|
46
|
+
move(source: string, target: string): Promise<boolean>;
|
|
47
|
+
|
|
48
|
+
// Copy file or directory (recursively)
|
|
49
|
+
copy(source: string, target: string): Promise<boolean>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
16
54
|
|
|
17
55
|
```typescript
|
|
18
|
-
import {
|
|
56
|
+
import { MemFilesApi } from '@statewalker/webrun-files-mem';
|
|
57
|
+
import { readFile, writeText } from '@statewalker/webrun-files';
|
|
19
58
|
|
|
20
|
-
const files = new
|
|
59
|
+
const files = new MemFilesApi();
|
|
21
60
|
|
|
22
61
|
// Write a file
|
|
23
|
-
await files
|
|
24
|
-
new TextEncoder().encode('Remember to water the plants')
|
|
25
|
-
]);
|
|
62
|
+
await writeText(files, '/documents/notes.txt', 'Remember to water the plants');
|
|
26
63
|
|
|
27
64
|
// Read it back
|
|
28
|
-
const content = await
|
|
65
|
+
const content = await readFile(files, '/documents/notes.txt');
|
|
29
66
|
console.log(new TextDecoder().decode(content));
|
|
30
67
|
```
|
|
31
68
|
|
|
32
|
-
|
|
69
|
+
## Utility Functions
|
|
70
|
+
|
|
71
|
+
### Reading Files
|
|
33
72
|
|
|
34
73
|
```typescript
|
|
35
|
-
import {
|
|
74
|
+
import { readFile, readText, tryReadFile, tryReadText, readRange, readAt } from '@statewalker/webrun-files';
|
|
36
75
|
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
// Read entire file as Uint8Array
|
|
77
|
+
const content = await readFile(files, '/data.bin');
|
|
39
78
|
|
|
40
|
-
|
|
79
|
+
// Read as UTF-8 string
|
|
80
|
+
const text = await readText(files, '/config.json');
|
|
41
81
|
|
|
42
|
-
|
|
82
|
+
// Read with undefined return for missing files
|
|
83
|
+
const maybeContent = await tryReadFile(files, '/optional.txt');
|
|
84
|
+
const maybeText = await tryReadText(files, '/optional.txt');
|
|
43
85
|
|
|
44
|
-
|
|
86
|
+
// Read a specific byte range
|
|
87
|
+
const chunk = await readRange(files, '/large.bin', 1000, 500); // 500 bytes starting at position 1000
|
|
45
88
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
await files
|
|
89
|
+
// Read into a buffer at specific offset (like fs.read)
|
|
90
|
+
const buffer = new Uint8Array(100);
|
|
91
|
+
const bytesRead = await readAt(files, '/data.bin', buffer, 0, 100, 500);
|
|
49
92
|
```
|
|
50
93
|
|
|
51
|
-
|
|
94
|
+
### Writing Files
|
|
52
95
|
|
|
53
96
|
```typescript
|
|
54
|
-
|
|
55
|
-
```
|
|
97
|
+
import { writeText } from '@statewalker/webrun-files';
|
|
56
98
|
|
|
57
|
-
|
|
99
|
+
// Write string content as UTF-8
|
|
100
|
+
await writeText(files, '/greeting.txt', 'Hello, World!');
|
|
58
101
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
// Write binary content directly
|
|
103
|
+
await files.write('/data.bin', [new Uint8Array([1, 2, 3, 4])]);
|
|
104
|
+
|
|
105
|
+
// Write from multiple chunks
|
|
106
|
+
await files.write('/large.bin', generateChunks());
|
|
63
107
|
```
|
|
64
108
|
|
|
65
|
-
|
|
109
|
+
### Path Utilities
|
|
66
110
|
|
|
67
111
|
```typescript
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
import { normalizePath, joinPath, dirname, basename, extname } from '@statewalker/webrun-files';
|
|
113
|
+
|
|
114
|
+
normalizePath('//foo/./bar//baz/'); // '/foo/bar/baz'
|
|
115
|
+
joinPath('/foo', 'bar', 'baz.txt'); // '/foo/bar/baz.txt'
|
|
116
|
+
dirname('/foo/bar/baz.txt'); // '/foo/bar'
|
|
117
|
+
basename('/foo/bar/baz.txt'); // 'baz.txt'
|
|
118
|
+
extname('/foo/bar/baz.txt'); // '.txt'
|
|
71
119
|
```
|
|
72
120
|
|
|
73
|
-
##
|
|
121
|
+
## Working with Files
|
|
74
122
|
|
|
75
|
-
Checking
|
|
123
|
+
### Checking Existence
|
|
76
124
|
|
|
77
125
|
```typescript
|
|
78
126
|
if (await files.exists('/config.json')) {
|
|
@@ -80,34 +128,54 @@ if (await files.exists('/config.json')) {
|
|
|
80
128
|
}
|
|
81
129
|
```
|
|
82
130
|
|
|
83
|
-
|
|
131
|
+
### Getting Metadata
|
|
132
|
+
|
|
133
|
+
`FileStats` is a discriminated union on `kind`. A file always reports a `size`
|
|
134
|
+
and a `lastModified`; a directory reports neither, because a directory has no
|
|
135
|
+
size and a modification time for one is unavailable on stores where a directory
|
|
136
|
+
is only a key prefix. Narrow on `kind` and the fields for that kind are then
|
|
137
|
+
known to be present:
|
|
84
138
|
|
|
85
139
|
```typescript
|
|
86
140
|
const info = await files.stats('/photo.jpg');
|
|
87
|
-
if (info) {
|
|
88
|
-
console.log(`Size: ${info.size} bytes`);
|
|
141
|
+
if (info?.kind === 'file') {
|
|
142
|
+
console.log(`Size: ${info.size} bytes`); // number, never undefined
|
|
89
143
|
console.log(`Modified: ${new Date(info.lastModified)}`);
|
|
90
|
-
|
|
144
|
+
} else if (info?.kind === 'directory') {
|
|
145
|
+
console.log('a directory'); // nothing else to report
|
|
91
146
|
}
|
|
92
147
|
```
|
|
93
148
|
|
|
94
|
-
|
|
149
|
+
A zero-byte file is the file variant with `size: 0`, so test the `kind` rather
|
|
150
|
+
than the truthiness of `size`:
|
|
95
151
|
|
|
96
152
|
```typescript
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
// WRONG - reads an empty file as though it had no size
|
|
154
|
+
if (info.size) { /* ... */ }
|
|
155
|
+
|
|
156
|
+
// RIGHT
|
|
157
|
+
if (info.kind === 'file') { /* info.size may legitimately be 0 */ }
|
|
100
158
|
```
|
|
101
159
|
|
|
102
|
-
|
|
160
|
+
### Listing Directories
|
|
161
|
+
|
|
162
|
+
`FileInfo` is the same union plus `name` and `path`, so a listing narrows per
|
|
163
|
+
entry exactly as `stats()` does:
|
|
103
164
|
|
|
104
165
|
```typescript
|
|
166
|
+
// List direct children
|
|
167
|
+
for await (const entry of files.list('/documents')) {
|
|
168
|
+
const size = entry.kind === 'file' ? `${entry.size} bytes` : '';
|
|
169
|
+
console.log(`${entry.name} (${entry.kind}) ${size}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// List recursively
|
|
105
173
|
for await (const entry of files.list('/project', { recursive: true })) {
|
|
106
174
|
console.log(entry.path);
|
|
107
175
|
}
|
|
108
176
|
```
|
|
109
177
|
|
|
110
|
-
|
|
178
|
+
### File Management
|
|
111
179
|
|
|
112
180
|
```typescript
|
|
113
181
|
await files.mkdir('/archive/2024');
|
|
@@ -116,83 +184,32 @@ await files.move('/temp/draft.txt', '/documents/final.txt');
|
|
|
116
184
|
await files.remove('/old-stuff'); // Recursively deletes directories
|
|
117
185
|
```
|
|
118
186
|
|
|
119
|
-
## Low-Level File Handle Operations
|
|
120
|
-
|
|
121
|
-
When you need fine-grained control over file operations, work directly with file handles. Opening a file gives you a handle that supports reading, writing, and appending at specific positions:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
const handle = await files.open('/data.bin');
|
|
125
|
-
|
|
126
|
-
// Get the current file size
|
|
127
|
-
console.log(`File is ${handle.size} bytes`);
|
|
128
|
-
|
|
129
|
-
// Read a specific range
|
|
130
|
-
for await (const chunk of handle.createReadStream({ start: 0, end: 100 })) {
|
|
131
|
-
// First 100 bytes
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Write at a specific position
|
|
135
|
-
await handle.createWriteStream([newData], { start: 50 });
|
|
136
|
-
|
|
137
|
-
// Append to the end
|
|
138
|
-
await handle.appendFile([moreData]);
|
|
139
|
-
|
|
140
|
-
// Always close when done
|
|
141
|
-
await handle.close();
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Building Your Own Backend
|
|
145
|
-
|
|
146
|
-
The `IFilesApi` interface defines the contract that all backends must implement. At minimum, you need four methods:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
interface IFilesApi {
|
|
150
|
-
list(file: FileRef, options?: ListOptions): AsyncGenerator<FileInfo>;
|
|
151
|
-
stats(file: FileRef): Promise<FileInfo | undefined>;
|
|
152
|
-
remove(file: FileRef): Promise<boolean>;
|
|
153
|
-
open(file: FileRef): Promise<FileHandle>;
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The `FilesApi` wrapper class builds convenience methods like `read`, `write`, `copy`, and `move` on top of these primitives. Backends can optionally implement native versions of `move`, `copy`, and `mkdir` for better performance.
|
|
158
|
-
|
|
159
|
-
Take a look at `MemFilesApi` or `NodeFilesApi` in the source code for reference implementations.
|
|
160
|
-
|
|
161
187
|
## Type Reference
|
|
162
188
|
|
|
163
|
-
The package exports all the TypeScript types you'll need:
|
|
164
|
-
|
|
165
189
|
```typescript
|
|
166
190
|
import type {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
FilesApi, // Core interface for backends
|
|
192
|
+
FileStats, // Metadata from stats(): FileEntryStats | DirectoryEntryStats
|
|
193
|
+
FileEntryStats, // { kind: "file", size: number, lastModified: number }
|
|
194
|
+
DirectoryEntryStats, // { kind: "directory" }
|
|
195
|
+
FileInfo, // Metadata from list(): FileEntryInfo | DirectoryEntryInfo
|
|
196
|
+
FileEntryInfo, // FileEntryStats & { name, path }
|
|
197
|
+
DirectoryEntryInfo, // DirectoryEntryStats & { name, path }
|
|
198
|
+
FileEntryLocation, // { name: string, path: string }
|
|
199
|
+
FileKind, // "file" | "directory"
|
|
200
|
+
ReadOptions, // { start?: number, length?: number, signal?: AbortSignal }
|
|
201
|
+
ListOptions, // { recursive?: boolean }
|
|
178
202
|
} from '@statewalker/webrun-files';
|
|
179
203
|
```
|
|
180
204
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
The package includes helpful utilities for working with paths and streams:
|
|
205
|
+
### Implementing a backend
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
basename, // Get filename portion
|
|
192
|
-
extname, // Get file extension
|
|
193
|
-
collectGenerator, // Collect async iterable into array
|
|
194
|
-
} from '@statewalker/webrun-files';
|
|
195
|
-
```
|
|
207
|
+
An implementation must return **exactly** one variant: a file with both numbers
|
|
208
|
+
present, a directory with nothing but its `kind`. An implementation that knows a
|
|
209
|
+
directory's modification time drops it rather than offering a value that would
|
|
210
|
+
be present on one backend and missing on the next. The parametrized suite in
|
|
211
|
+
`@statewalker/webrun-files-tests` checks this at runtime, and
|
|
212
|
+
`createFilesApiTests` runs it for you.
|
|
196
213
|
|
|
197
214
|
## License
|
|
198
215
|
|