@bunit/storage 0.0.0 → 0.0.1
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 +80 -0
- package/dist/drivers/fs.d.ts +2 -1
- package/dist/drivers/fs.mjs +35 -15
- package/dist/drivers/redis.d.ts +20 -0
- package/dist/drivers/redis.mjs +94 -0
- package/dist/drivers/s3.d.ts +10 -0
- package/dist/drivers/s3.mjs +96 -0
- package/package.json +13 -3
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @bunit/storage
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
> 🔌 Native Bun drivers for [**unstorage**](https://unstorage.unjs.io) - Filesystem, Redis, and S3.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ⚡️ **Native Bun APIs** - Zero abstraction overhead using Bun's built-in clients
|
|
12
|
+
- 🔌 **Unstorage Compatible** - Drop-in drivers for any unstorage setup
|
|
13
|
+
- 🎯 **Type-Safe** - Full TypeScript support with proper types
|
|
14
|
+
- 📦 **Zero Runtime Dependencies** - Uses only Bun's native capabilities
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @bunit/storage unstorage
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Filesystem Driver
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createStorage } from "unstorage";
|
|
28
|
+
import fsDriver from "@bunit/storage/drivers/fs";
|
|
29
|
+
|
|
30
|
+
const storage = createStorage({
|
|
31
|
+
driver: fsDriver({
|
|
32
|
+
base: "./data",
|
|
33
|
+
ignore: ["node_modules", ".git"],
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await storage.setItem("config:user", JSON.stringify({ name: "Alice" }));
|
|
38
|
+
const data = await storage.getItem("config:user");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Redis Driver
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createStorage } from "unstorage";
|
|
45
|
+
import redisDriver from "@bunit/storage/drivers/redis";
|
|
46
|
+
|
|
47
|
+
const storage = createStorage({
|
|
48
|
+
driver: redisDriver({
|
|
49
|
+
url: "redis://localhost:6379",
|
|
50
|
+
base: "app:cache",
|
|
51
|
+
ttl: 3600, // 1 hour default TTL
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await storage.setItem("session:123", "data");
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### S3 Driver
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createStorage } from "unstorage";
|
|
62
|
+
import s3Driver from "@bunit/storage/drivers/s3";
|
|
63
|
+
|
|
64
|
+
const storage = createStorage({
|
|
65
|
+
driver: s3Driver({
|
|
66
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
67
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
68
|
+
bucket: "my-bucket",
|
|
69
|
+
region: "us-east-1",
|
|
70
|
+
endpoint: "https://s3.us-east-1.amazonaws.com",
|
|
71
|
+
base: "prefix/",
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await storage.setItem("uploads/image.png", buffer);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[MIT](../../LICENSE) © [Demo Macro](https://www.demomacro.com/)
|
package/dist/drivers/fs.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Driver } from "unstorage";
|
|
1
2
|
export interface FSDriverOptions {
|
|
2
3
|
base?: string;
|
|
3
4
|
ignore?: string | string[];
|
|
4
5
|
}
|
|
5
|
-
declare const _default: (opts: FSDriverOptions) =>
|
|
6
|
+
declare const _default: (opts: FSDriverOptions) => Driver<FSDriverOptions, any>;
|
|
6
7
|
export default _default;
|
package/dist/drivers/fs.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineDriver, normalizeKey } from "unstorage";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Glob } from "bun";
|
|
5
5
|
export default defineDriver((options = {}) => {
|
|
@@ -7,29 +7,50 @@ export default defineDriver((options = {}) => {
|
|
|
7
7
|
return {
|
|
8
8
|
name: "fs",
|
|
9
9
|
options,
|
|
10
|
-
hasItem(key) {
|
|
10
|
+
hasItem(key, _opts) {
|
|
11
11
|
const path = join(base, key.replace(/:/g, "/"));
|
|
12
12
|
return Bun.file(path).exists();
|
|
13
13
|
},
|
|
14
|
-
async getItem(key) {
|
|
14
|
+
async getItem(key, _opts) {
|
|
15
15
|
const path = join(base, key.replace(/:/g, "/")), file = Bun.file(path);
|
|
16
16
|
if (!await file.exists())
|
|
17
17
|
return null;
|
|
18
18
|
return await file.text();
|
|
19
19
|
},
|
|
20
|
-
async
|
|
20
|
+
async getItemRaw(key, _opts) {
|
|
21
|
+
const path = join(base, key.replace(/:/g, "/")), file = Bun.file(path);
|
|
22
|
+
if (!await file.exists())
|
|
23
|
+
return null;
|
|
24
|
+
return await file.arrayBuffer();
|
|
25
|
+
},
|
|
26
|
+
async setItem(key, value, _opts) {
|
|
27
|
+
const path = join(base, key.replace(/:/g, "/"));
|
|
28
|
+
await mkdir(dirname(path), { recursive: !0 });
|
|
29
|
+
await Bun.write(path, value);
|
|
30
|
+
},
|
|
31
|
+
async setItemRaw(key, value, _opts) {
|
|
21
32
|
const path = join(base, key.replace(/:/g, "/"));
|
|
22
33
|
await mkdir(dirname(path), { recursive: !0 });
|
|
23
34
|
await Bun.write(path, value);
|
|
24
35
|
},
|
|
25
|
-
async removeItem(key) {
|
|
36
|
+
async removeItem(key, _opts) {
|
|
26
37
|
const path = join(base, key.replace(/:/g, "/"));
|
|
27
38
|
await Bun.file(path).delete();
|
|
28
39
|
},
|
|
29
|
-
async
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
async getMeta(key, _opts) {
|
|
41
|
+
const path = join(base, key.replace(/:/g, "/"));
|
|
42
|
+
if (!await Bun.file(path).exists())
|
|
43
|
+
return null;
|
|
44
|
+
const stat = await Bun.file(path).stat();
|
|
45
|
+
return {
|
|
46
|
+
mtime: stat.mtime,
|
|
47
|
+
size: stat.size
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
async getKeys(baseKey, _opts) {
|
|
51
|
+
const glob = new Glob("**/*"), keys = [], scanPath = baseKey ? join(base, baseKey.replace(/:/g, "/")) : base;
|
|
52
|
+
for await (const file of glob.scan(scanPath)) {
|
|
53
|
+
const relativePath = baseKey ? file.replace(baseKey.replace(/:/g, "/") + "/", "") : file, key = normalizeKey(relativePath);
|
|
33
54
|
if (ignore.length > 0) {
|
|
34
55
|
if (Array.isArray(ignore) ? ignore.some((pattern) => file.includes(pattern)) : file.includes(ignore))
|
|
35
56
|
continue;
|
|
@@ -38,12 +59,11 @@ export default defineDriver((options = {}) => {
|
|
|
38
59
|
}
|
|
39
60
|
return keys;
|
|
40
61
|
},
|
|
41
|
-
async clear() {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
62
|
+
async clear(baseKey, _opts) {
|
|
63
|
+
const targetPath = baseKey ? join(base, baseKey.replace(/:/g, "/")) : base;
|
|
64
|
+
try {
|
|
65
|
+
await rm(targetPath, { recursive: !0, force: !0 });
|
|
66
|
+
} catch {}
|
|
47
67
|
},
|
|
48
68
|
async dispose() {}
|
|
49
69
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Driver } from "unstorage";
|
|
2
|
+
import { RedisClient, type RedisOptions } from "bun";
|
|
3
|
+
export interface RedisDriverOptions extends RedisOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Connection URL for Redis.
|
|
6
|
+
* Format: `redis://username:password@localhost:6379`
|
|
7
|
+
* Defaults to `redis://localhost:6379`
|
|
8
|
+
*/
|
|
9
|
+
url?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional prefix to use for all keys.
|
|
12
|
+
*/
|
|
13
|
+
base?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Default TTL for all items in seconds.
|
|
16
|
+
*/
|
|
17
|
+
ttl?: number;
|
|
18
|
+
}
|
|
19
|
+
declare const _default: (opts: RedisDriverOptions) => Driver<RedisDriverOptions, RedisClient>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineDriver, normalizeKey } from "unstorage";
|
|
2
|
+
import { RedisClient } from "bun";
|
|
3
|
+
export default defineDriver((options = {}) => {
|
|
4
|
+
let client;
|
|
5
|
+
const getClient = () => {
|
|
6
|
+
if (!client)
|
|
7
|
+
client = new RedisClient(options.url, options);
|
|
8
|
+
return client;
|
|
9
|
+
}, base = (options.base || "").replace(/:$/, ""), p = (key) => base ? `${base}:${key}` : key, d = (key) => base ? key.replace(`${base}:`, "") : key;
|
|
10
|
+
return {
|
|
11
|
+
name: "redis",
|
|
12
|
+
options,
|
|
13
|
+
getInstance: getClient,
|
|
14
|
+
async hasItem(key, _opts) {
|
|
15
|
+
const exists = await getClient().exists(p(key));
|
|
16
|
+
return typeof exists === "number" ? exists > 0 : exists;
|
|
17
|
+
},
|
|
18
|
+
async getItem(key, _opts) {
|
|
19
|
+
return await getClient().get(p(key)) ?? null;
|
|
20
|
+
},
|
|
21
|
+
async getItems(items, _commonOpts) {
|
|
22
|
+
const keys = items.map((item) => p(item.key)), values = await getClient().mget(...keys);
|
|
23
|
+
return keys.map((key, index) => ({
|
|
24
|
+
key: d(key),
|
|
25
|
+
value: values[index] ?? null
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
async getItemRaw(key, _opts) {
|
|
29
|
+
return await getClient().getBuffer(p(key));
|
|
30
|
+
},
|
|
31
|
+
async setItem(key, value, _opts) {
|
|
32
|
+
const client = getClient();
|
|
33
|
+
if (options.ttl) {
|
|
34
|
+
await client.set(p(key), value);
|
|
35
|
+
await client.expire(p(key), options.ttl);
|
|
36
|
+
} else
|
|
37
|
+
await client.set(p(key), value);
|
|
38
|
+
},
|
|
39
|
+
async setItems(items, _opts) {
|
|
40
|
+
const client = getClient(), pipeline = [];
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
pipeline.push(client.set(p(item.key), item.value));
|
|
43
|
+
if (options.ttl)
|
|
44
|
+
pipeline.push(client.expire(p(item.key), options.ttl));
|
|
45
|
+
}
|
|
46
|
+
await Promise.allSettled(pipeline);
|
|
47
|
+
},
|
|
48
|
+
async setItemRaw(key, value, _opts) {
|
|
49
|
+
const client = getClient();
|
|
50
|
+
if (typeof value === "string")
|
|
51
|
+
await client.set(p(key), value);
|
|
52
|
+
else
|
|
53
|
+
await client.set(p(key), value);
|
|
54
|
+
if (options.ttl)
|
|
55
|
+
await client.expire(p(key), options.ttl);
|
|
56
|
+
},
|
|
57
|
+
async removeItem(key, _opts) {
|
|
58
|
+
await getClient().del(p(key));
|
|
59
|
+
},
|
|
60
|
+
async getMeta(key, _opts) {
|
|
61
|
+
const ttl = await getClient().ttl(p(key));
|
|
62
|
+
if (!await getClient().exists(p(key)))
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
ttl: ttl > 0 ? ttl : void 0
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
async getKeys(baseKey, _opts) {
|
|
69
|
+
const keys = [];
|
|
70
|
+
let cursor = 0;
|
|
71
|
+
const pattern = p(baseKey + "*");
|
|
72
|
+
do {
|
|
73
|
+
const result = await getClient().scan(cursor, "MATCH", pattern);
|
|
74
|
+
cursor = typeof result[0] === "number" ? result[0] : parseInt(result[0], 10);
|
|
75
|
+
const scanKeys = result[1];
|
|
76
|
+
keys.push(...scanKeys);
|
|
77
|
+
} while (cursor !== 0);
|
|
78
|
+
return keys.map((key) => normalizeKey(d(key)));
|
|
79
|
+
},
|
|
80
|
+
async clear(base, _opts) {
|
|
81
|
+
const keys = await this.getKeys(base, _opts);
|
|
82
|
+
if (keys.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const client = getClient(), keysToDelete = keys.map((key) => p(key));
|
|
85
|
+
if (keysToDelete.length === 1)
|
|
86
|
+
await client.del(keysToDelete[0]);
|
|
87
|
+
else
|
|
88
|
+
await client.del(...keysToDelete);
|
|
89
|
+
},
|
|
90
|
+
async dispose() {
|
|
91
|
+
client = void 0;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Driver } from "unstorage";
|
|
2
|
+
import { S3Client, type S3Options } from "bun";
|
|
3
|
+
export interface S3DriverOptions extends S3Options {
|
|
4
|
+
/**
|
|
5
|
+
* Optional prefix to use for all keys.
|
|
6
|
+
*/
|
|
7
|
+
base?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const _default: (opts: S3DriverOptions) => Driver<S3DriverOptions, S3Client>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineDriver, normalizeKey } from "unstorage";
|
|
2
|
+
import { S3Client } from "bun";
|
|
3
|
+
export default defineDriver((options) => {
|
|
4
|
+
let client;
|
|
5
|
+
const getClient = () => {
|
|
6
|
+
if (!client)
|
|
7
|
+
client = new S3Client(options);
|
|
8
|
+
return client;
|
|
9
|
+
}, base = (options.base || "").replace(/\/$/, ""), p = (key) => base ? `${base}/${normalizeKey(key)}` : normalizeKey(key), d = (key) => base ? key.replace(`${base}/`, "") : key;
|
|
10
|
+
return {
|
|
11
|
+
name: "s3",
|
|
12
|
+
options,
|
|
13
|
+
getInstance: getClient,
|
|
14
|
+
async hasItem(key, _opts) {
|
|
15
|
+
try {
|
|
16
|
+
await getClient().file(p(key)).exists();
|
|
17
|
+
return !0;
|
|
18
|
+
} catch {
|
|
19
|
+
return !1;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
async getItem(key, _opts) {
|
|
23
|
+
try {
|
|
24
|
+
return await getClient().file(p(key)).text();
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async getItemRaw(key, _opts) {
|
|
30
|
+
try {
|
|
31
|
+
return await getClient().file(p(key)).bytes();
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
async setItem(key, value, _opts) {
|
|
37
|
+
await getClient().file(p(key)).write(value);
|
|
38
|
+
},
|
|
39
|
+
async setItemRaw(key, value, _opts) {
|
|
40
|
+
await getClient().file(p(key)).write(value);
|
|
41
|
+
},
|
|
42
|
+
async removeItem(key, _opts) {
|
|
43
|
+
await getClient().file(p(key)).delete();
|
|
44
|
+
},
|
|
45
|
+
async getMeta(key, _opts) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await S3Client.list({ prefix: p(key) }, options);
|
|
48
|
+
if (result.contents && result.contents.length > 0) {
|
|
49
|
+
const object = result.contents[0];
|
|
50
|
+
return {
|
|
51
|
+
size: object.size,
|
|
52
|
+
mtime: object.lastModified ? new Date(object.lastModified) : void 0,
|
|
53
|
+
etag: object.eTag
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async getKeys(baseKey, _opts) {
|
|
62
|
+
const keys = [];
|
|
63
|
+
let startAfter = void 0;
|
|
64
|
+
const prefix = p(baseKey || "");
|
|
65
|
+
let hasMore = !0;
|
|
66
|
+
while (hasMore) {
|
|
67
|
+
const result = await S3Client.list({
|
|
68
|
+
prefix,
|
|
69
|
+
startAfter,
|
|
70
|
+
maxKeys: 1000
|
|
71
|
+
}, options);
|
|
72
|
+
if (result.contents)
|
|
73
|
+
for (const object of result.contents) {
|
|
74
|
+
const relativeKey = d(object.key);
|
|
75
|
+
keys.push(normalizeKey(relativeKey));
|
|
76
|
+
}
|
|
77
|
+
if (result.isTruncated && result.contents && result.contents.length > 0)
|
|
78
|
+
startAfter = result.contents[result.contents.length - 1].key;
|
|
79
|
+
else
|
|
80
|
+
hasMore = !1;
|
|
81
|
+
}
|
|
82
|
+
return keys;
|
|
83
|
+
},
|
|
84
|
+
async clear(baseKey, _opts) {
|
|
85
|
+
const keys = await this.getKeys(baseKey, _opts);
|
|
86
|
+
if (keys.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
await Promise.allSettled(keys.map(async (key) => {
|
|
89
|
+
await getClient().file(p(key)).delete();
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
92
|
+
async dispose() {
|
|
93
|
+
client = void 0;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunit/storage",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Universal storage abstraction with native Bun drivers for filesystem, Redis, and S3",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"cloudflare",
|
|
8
|
+
"driver",
|
|
9
|
+
"filesystem",
|
|
10
|
+
"r2",
|
|
11
|
+
"redis",
|
|
12
|
+
"s3",
|
|
13
|
+
"storage",
|
|
14
|
+
"unstorage"
|
|
15
|
+
],
|
|
6
16
|
"homepage": "https://github.com/DemoMacro/BunIt#readme",
|
|
7
17
|
"bugs": {
|
|
8
18
|
"url": "https://github.com/DemoMacro/BunIt/issues"
|