Skip to content

Commit babcc16

Browse files
committed
feat: add opfs driver
1 parent 0a32d4d commit babcc16

File tree

6 files changed

+608
-0
lines changed

6 files changed

+608
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
navigation.title: Origin Private File System
3+
---
4+
5+
# Origin Private File System
6+
7+
Maps data to the [origin private file system (OPFS)](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) using directory structure for nested keys.
8+
9+
This driver implements meta for each key including `mtime` (last modified time), `type` (mime type) and `size` (file size) of the underlying [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) object.
10+
11+
The origin private file system cannot be watched.
12+
13+
```js
14+
import { createStorage } from "unstorage";
15+
import opfsDriver from "unstorage/drivers/opfs";
16+
17+
const storage = createStorage({
18+
driver: opfsDriver({ base: "tmp" }),
19+
});
20+
```
21+
22+
**Options:**
23+
24+
- `base`: Base directory to isolate operations on this directory
25+
- `ignore`: Ignore patterns for key listing
26+
- `readOnly`: Whether to ignore any write operations
27+
- `noClear`: Whether to disallow clearing the storage
28+
- `fs`: An alternative file system handle using the [`FileSystemDirectoryHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) interface (e.g. the user's native file system using `window.showDirectoryPicker()`)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"eslint": "^8.51.0",
8080
"eslint-config-unjs": "^0.2.1",
8181
"fake-indexeddb": "^4.0.2",
82+
"file-system-access": "^1.0.4",
8283
"idb-keyval": "^6.2.1",
8384
"ioredis-mock": "^8.9.0",
8485
"jiti": "^1.20.0",

pnpm-lock.yaml

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/drivers/opfs.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { defineDriver } from "./utils";
2+
import {
3+
DRIVER_NAME,
4+
exists,
5+
getFileObject,
6+
joinPaths,
7+
normalizePath,
8+
readFile,
9+
readdirRecursive,
10+
remove,
11+
removeChildren,
12+
unlink,
13+
writeFile,
14+
} from "./utils/opfs-utils";
15+
16+
export interface OPFSStorageOptions {
17+
/**
18+
* The filesystem root to use
19+
* Defaults to the OPFS root at `navigator.storage.getDirectory()`
20+
*/
21+
fs?: FileSystemDirectoryHandle | Promise<FileSystemDirectoryHandle>;
22+
23+
/**
24+
* The base path to use for all operations
25+
* Defaults to the root directory (empty string)
26+
*/
27+
base?: string;
28+
29+
/**
30+
* A callback to ignore certain files in getKeys()
31+
*/
32+
ignore?: (path: string) => boolean;
33+
34+
/**
35+
* Whether to ignore any write operations
36+
*/
37+
readOnly?: boolean;
38+
39+
/**
40+
* Whether to disallow clearing the storage
41+
*/
42+
noClear?: boolean;
43+
}
44+
45+
export default defineDriver<OPFSStorageOptions | undefined>(
46+
(opts: OPFSStorageOptions = {}) => {
47+
const fsPromise = Promise.resolve(
48+
opts.fs ?? navigator.storage.getDirectory()
49+
);
50+
51+
opts.base = normalizePath(opts.base ?? "");
52+
53+
const resolve = (path: string) => joinPaths(opts.base!, path);
54+
55+
return {
56+
name: DRIVER_NAME,
57+
options: opts,
58+
async hasItem(key) {
59+
return exists(await fsPromise, resolve(key), "file");
60+
},
61+
async getItem(key) {
62+
if (!(await exists(await fsPromise, resolve(key), "file"))) return null;
63+
return readFile(await fsPromise, resolve(key), "utf8");
64+
},
65+
async getItemRaw(key) {
66+
if (!(await exists(await fsPromise, resolve(key), "file"))) return null;
67+
return readFile(await fsPromise, resolve(key));
68+
},
69+
async getMeta(key) {
70+
const file = await getFileObject(await fsPromise, resolve(key));
71+
if (!file) return null;
72+
73+
return {
74+
mtime: new Date(file.lastModified),
75+
size: file.size,
76+
type: file.type,
77+
};
78+
},
79+
async setItem(key, value) {
80+
if (opts.readOnly) return;
81+
82+
return writeFile(await fsPromise, resolve(key), value);
83+
},
84+
async setItemRaw(key, value) {
85+
if (opts.readOnly) return;
86+
87+
return writeFile(await fsPromise, resolve(key), value);
88+
},
89+
async removeItem(key) {
90+
if (opts.readOnly) return;
91+
92+
return unlink(await fsPromise, resolve(key));
93+
},
94+
async getKeys() {
95+
return readdirRecursive(await fsPromise, resolve(""), opts.ignore);
96+
},
97+
async clear() {
98+
if (opts.readOnly || opts.noClear) return;
99+
100+
if (opts.base!.length === 0) {
101+
// We cannot delete an OPFS root, so we just empty it
102+
await removeChildren(await fsPromise, resolve(""));
103+
} else {
104+
await remove(await fsPromise, resolve(""));
105+
}
106+
},
107+
};
108+
}
109+
);

0 commit comments

Comments
 (0)