@stacksjs/storage 0.70.87 → 0.70.90
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/dist/adapters/bun.js +226 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/local.js +225 -0
- package/dist/adapters/memory.js +318 -0
- package/dist/adapters/s3.js +471 -0
- package/dist/adapters/scoped.js +142 -0
- package/dist/copy.js +30 -0
- package/dist/delete.js +103 -0
- package/dist/drivers/aws.js +94 -0
- package/dist/drivers/bun.js +88 -0
- package/dist/drivers/index.js +4 -0
- package/dist/drivers/local.js +88 -0
- package/dist/drivers/memory.js +67 -0
- package/dist/facade.js +226 -0
- package/dist/files.js +126 -0
- package/dist/folders.js +36 -0
- package/dist/fs.js +7 -0
- package/dist/glob.js +40 -0
- package/dist/hash.js +33 -0
- package/dist/helpers.js +28 -0
- package/dist/image.js +8 -22
- package/dist/index.js +25 -2808
- package/dist/mime-verify.js +47 -0
- package/dist/move.js +55 -0
- package/dist/path-sanitize.js +84 -0
- package/dist/put-file.js +85 -0
- package/dist/s3-presigned-post.js +68 -0
- package/dist/signed-url.js +86 -0
- package/dist/static-serve.js +110 -0
- package/dist/storage.js +9 -0
- package/dist/types/filesystem.js +40 -0
- package/dist/types.js +25 -0
- package/dist/uploaded-file.js +114 -0
- package/dist/visibility.js +3 -0
- package/dist/zip.js +41 -0
- package/package.json +6 -6
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { file, write as bunWrite } from "bun";
|
|
3
|
+
import { chmod, lstat } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
+
import { createDirectoryListing, normalizeExpiryToDate } from "../types";
|
|
6
|
+
import { createSignedStorageToken } from "../signed-url";
|
|
7
|
+
|
|
8
|
+
export class BunStorageAdapter {
|
|
9
|
+
root;
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.root = config.root || process.cwd();
|
|
12
|
+
}
|
|
13
|
+
resolvePath(path) {
|
|
14
|
+
const resolved = join(this.root, path), rel = relative(this.root, resolved);
|
|
15
|
+
if (rel.startsWith("..") || rel.startsWith("../") || rel.startsWith("..\\"))
|
|
16
|
+
throw Error(`Path traversal detected: '${path}' resolves outside storage root`);
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
async write(path, contents) {
|
|
20
|
+
const fullPath = this.resolvePath(path), dir = dirname(fullPath);
|
|
21
|
+
await this.createDirectory(relative(this.root, dir));
|
|
22
|
+
if (typeof contents === "string")
|
|
23
|
+
await bunWrite(fullPath, contents);
|
|
24
|
+
else if (contents instanceof Buffer)
|
|
25
|
+
await bunWrite(fullPath, contents);
|
|
26
|
+
else if (contents instanceof Uint8Array)
|
|
27
|
+
await bunWrite(fullPath, contents);
|
|
28
|
+
else {
|
|
29
|
+
if (typeof contents.getReader !== "function")
|
|
30
|
+
throw TypeError("[storage/bun] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");
|
|
31
|
+
const reader = contents.getReader(), chunks = [];
|
|
32
|
+
while (!0) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
if (value)
|
|
37
|
+
chunks.push(value);
|
|
38
|
+
}
|
|
39
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0), result = new Uint8Array(totalLength);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
result.set(chunk, offset);
|
|
43
|
+
offset += chunk.length;
|
|
44
|
+
}
|
|
45
|
+
await bunWrite(fullPath, result);
|
|
46
|
+
}
|
|
47
|
+
const written = file(fullPath);
|
|
48
|
+
return {
|
|
49
|
+
path,
|
|
50
|
+
size: written.size,
|
|
51
|
+
lastModified: written.lastModified
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async read(path) {
|
|
55
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
56
|
+
if (!await bunFile.exists())
|
|
57
|
+
throw Error(`File not found: ${path}`);
|
|
58
|
+
return await bunFile.arrayBuffer().then((buf) => new Uint8Array(buf));
|
|
59
|
+
}
|
|
60
|
+
async getStream(path, _options) {
|
|
61
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
62
|
+
if (!await bunFile.exists())
|
|
63
|
+
throw Error(`File not found: ${path}`);
|
|
64
|
+
return bunFile.stream();
|
|
65
|
+
}
|
|
66
|
+
async putStream(path, stream, options) {
|
|
67
|
+
const fullPath = this.resolvePath(path), dir = dirname(fullPath);
|
|
68
|
+
await this.createDirectory(relative(this.root, dir));
|
|
69
|
+
try {
|
|
70
|
+
const body = new Response(stream), writePromise = bunWrite(fullPath, body);
|
|
71
|
+
if (options?.signal) {
|
|
72
|
+
const abortHandler = () => {};
|
|
73
|
+
options.signal.addEventListener("abort", abortHandler, { once: !0 });
|
|
74
|
+
try {
|
|
75
|
+
await writePromise;
|
|
76
|
+
} finally {
|
|
77
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
78
|
+
}
|
|
79
|
+
} else
|
|
80
|
+
await writePromise;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
try {
|
|
83
|
+
await Bun.$.throws(!1)`rm -f ${fullPath}`;
|
|
84
|
+
} catch {}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
const written = file(fullPath);
|
|
88
|
+
return {
|
|
89
|
+
path,
|
|
90
|
+
size: written.size,
|
|
91
|
+
contentType: options?.contentType,
|
|
92
|
+
lastModified: written.lastModified
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async readToString(path) {
|
|
96
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
97
|
+
if (!await bunFile.exists())
|
|
98
|
+
throw Error(`File not found: ${path}`);
|
|
99
|
+
return await bunFile.text();
|
|
100
|
+
}
|
|
101
|
+
async readToBuffer(path) {
|
|
102
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
103
|
+
if (!await bunFile.exists())
|
|
104
|
+
throw Error(`File not found: ${path}`);
|
|
105
|
+
const arrayBuffer = await bunFile.arrayBuffer();
|
|
106
|
+
return Buffer.from(arrayBuffer);
|
|
107
|
+
}
|
|
108
|
+
async readToUint8Array(path) {
|
|
109
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
110
|
+
if (!await bunFile.exists())
|
|
111
|
+
throw Error(`File not found: ${path}`);
|
|
112
|
+
const arrayBuffer = await bunFile.arrayBuffer();
|
|
113
|
+
return new Uint8Array(arrayBuffer);
|
|
114
|
+
}
|
|
115
|
+
async deleteFile(path) {
|
|
116
|
+
const fullPath = this.resolvePath(path);
|
|
117
|
+
if (await file(fullPath).exists())
|
|
118
|
+
await Bun.$.throws(!1)`rm ${fullPath}`;
|
|
119
|
+
}
|
|
120
|
+
async deleteDirectory(path) {
|
|
121
|
+
const fullPath = this.resolvePath(path);
|
|
122
|
+
await Bun.$.throws(!1)`rm -rf ${fullPath}`;
|
|
123
|
+
}
|
|
124
|
+
async createDirectory(path) {
|
|
125
|
+
const fullPath = this.resolvePath(path);
|
|
126
|
+
await Bun.$.throws(!1)`mkdir -p ${fullPath}`;
|
|
127
|
+
}
|
|
128
|
+
async moveFile(from, to) {
|
|
129
|
+
const fromPath = this.resolvePath(from), toPath = this.resolvePath(to), toDir = dirname(toPath);
|
|
130
|
+
await this.createDirectory(relative(this.root, toDir));
|
|
131
|
+
await Bun.$.throws(!0)`mv ${fromPath} ${toPath}`;
|
|
132
|
+
}
|
|
133
|
+
async copyFile(from, to) {
|
|
134
|
+
const fromPath = this.resolvePath(from), toPath = this.resolvePath(to), toDir = dirname(toPath);
|
|
135
|
+
await this.createDirectory(relative(this.root, toDir));
|
|
136
|
+
await Bun.$.throws(!0)`cp ${fromPath} ${toPath}`;
|
|
137
|
+
}
|
|
138
|
+
async stat(path) {
|
|
139
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
140
|
+
if (!await bunFile.exists())
|
|
141
|
+
throw Error(`File not found: ${path}`);
|
|
142
|
+
const stats = await Bun.file(fullPath).stat(), isDir = stats.isDirectory();
|
|
143
|
+
return {
|
|
144
|
+
path,
|
|
145
|
+
type: isDir ? "directory" : "file",
|
|
146
|
+
visibility: "private",
|
|
147
|
+
size: isDir ? 0 : stats.size,
|
|
148
|
+
lastModified: stats.mtime?.getTime() || Date.now(),
|
|
149
|
+
mimeType: isDir ? void 0 : bunFile.type
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
list(path, options = {}) {
|
|
153
|
+
const fullPath = this.resolvePath(path);
|
|
154
|
+
return this.createAsyncIterator(fullPath, options.deep || !1);
|
|
155
|
+
}
|
|
156
|
+
async* createAsyncIterator(dirPath, deep) {
|
|
157
|
+
const entries = [];
|
|
158
|
+
try {
|
|
159
|
+
const glob = new Bun.Glob(deep ? "**/*" : "*");
|
|
160
|
+
for await (const entry of glob.scan({ cwd: dirPath, onlyFiles: !1 })) {
|
|
161
|
+
const fullEntryPath = join(dirPath, entry), stats = await Bun.file(fullEntryPath).stat();
|
|
162
|
+
entries.push({
|
|
163
|
+
path: relative(this.root, fullEntryPath),
|
|
164
|
+
type: stats.isDirectory() ? "directory" : "file"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
yield* createDirectoryListing(entries);
|
|
171
|
+
}
|
|
172
|
+
async changeVisibility(path, vis) {
|
|
173
|
+
const fullPath = this.resolvePath(path), isDir = (await lstat(fullPath)).isDirectory();
|
|
174
|
+
await chmod(fullPath, vis === "public" ? isDir ? 493 : 420 : isDir ? 448 : 384);
|
|
175
|
+
}
|
|
176
|
+
async visibility(path) {
|
|
177
|
+
const fullPath = this.resolvePath(path);
|
|
178
|
+
return (await lstat(fullPath)).mode & 511 & 4 ? "public" : "private";
|
|
179
|
+
}
|
|
180
|
+
async fileExists(path) {
|
|
181
|
+
const fullPath = this.resolvePath(path);
|
|
182
|
+
return await file(fullPath).exists();
|
|
183
|
+
}
|
|
184
|
+
async directoryExists(path) {
|
|
185
|
+
const fullPath = this.resolvePath(path);
|
|
186
|
+
try {
|
|
187
|
+
return (await Bun.file(fullPath).stat()).isDirectory();
|
|
188
|
+
} catch {
|
|
189
|
+
return !1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async publicUrl(path, options = {}) {
|
|
193
|
+
return `${options.domain || "http://localhost"}/${path}`;
|
|
194
|
+
}
|
|
195
|
+
async temporaryUrl(path, options) {
|
|
196
|
+
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
197
|
+
return `http://localhost/temp/${Buffer.from(`${path}:${expiry.getTime()}`).toString("base64url")}`;
|
|
198
|
+
}
|
|
199
|
+
async signedUrl(path, options) {
|
|
200
|
+
const token = createSignedStorageToken(path, options);
|
|
201
|
+
return `${(options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "")}/__storage/${encodeURIComponent(path)}?token=${token}`;
|
|
202
|
+
}
|
|
203
|
+
async checksum(path, options = {}) {
|
|
204
|
+
const algorithm = options.algorithm || "sha256", fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
205
|
+
if (!await bunFile.exists())
|
|
206
|
+
throw Error(`File not found: ${path}`);
|
|
207
|
+
const hasher = new Bun.CryptoHasher(algorithm), arrayBuffer = await bunFile.arrayBuffer();
|
|
208
|
+
hasher.update(new Uint8Array(arrayBuffer));
|
|
209
|
+
return hasher.digest("hex");
|
|
210
|
+
}
|
|
211
|
+
async mimeType(path, _options = {}) {
|
|
212
|
+
const fullPath = this.resolvePath(path), bunFile = file(fullPath);
|
|
213
|
+
if (!await bunFile.exists())
|
|
214
|
+
throw Error(`File not found: ${path}`);
|
|
215
|
+
return bunFile.type || "application/octet-stream";
|
|
216
|
+
}
|
|
217
|
+
async lastModified(path) {
|
|
218
|
+
return (await this.stat(path)).lastModified;
|
|
219
|
+
}
|
|
220
|
+
async fileSize(path) {
|
|
221
|
+
return (await this.stat(path)).size;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function createBunStorage(config = {}) {
|
|
225
|
+
return new BunStorageAdapter(config);
|
|
226
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHmac } from "node:crypto";
|
|
3
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
+
import { access, chmod, constants, copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { createDirectoryListing, normalizeExpiryToDate } from "../types";
|
|
9
|
+
import { createSignedStorageToken } from "../signed-url";
|
|
10
|
+
|
|
11
|
+
export class LocalStorageAdapter {
|
|
12
|
+
root;
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.root = config.root || process.cwd();
|
|
15
|
+
}
|
|
16
|
+
resolvePath(path) {
|
|
17
|
+
const resolved = join(this.root, path), rel = relative(this.root, resolved);
|
|
18
|
+
if (rel.startsWith("..") || rel.startsWith("../") || rel.startsWith("..\\"))
|
|
19
|
+
throw Error(`Path traversal detected: '${path}' resolves outside storage root`);
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
async write(path, contents) {
|
|
23
|
+
const fullPath = this.resolvePath(path), dir = dirname(fullPath);
|
|
24
|
+
await mkdir(dir, { recursive: !0 });
|
|
25
|
+
if (typeof contents === "string")
|
|
26
|
+
await writeFile(fullPath, contents, "utf8");
|
|
27
|
+
else if (contents instanceof Buffer)
|
|
28
|
+
await writeFile(fullPath, contents);
|
|
29
|
+
else if (contents instanceof Uint8Array)
|
|
30
|
+
await writeFile(fullPath, contents);
|
|
31
|
+
else {
|
|
32
|
+
const writeStream = createWriteStream(fullPath);
|
|
33
|
+
await pipeline(contents, writeStream);
|
|
34
|
+
}
|
|
35
|
+
const st = await stat(fullPath);
|
|
36
|
+
return {
|
|
37
|
+
path,
|
|
38
|
+
size: st.size,
|
|
39
|
+
lastModified: st.mtimeMs
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async read(path) {
|
|
43
|
+
const fullPath = this.resolvePath(path);
|
|
44
|
+
return await readFile(fullPath);
|
|
45
|
+
}
|
|
46
|
+
async getStream(path, options) {
|
|
47
|
+
const fullPath = this.resolvePath(path);
|
|
48
|
+
await access(fullPath, constants.R_OK);
|
|
49
|
+
const nodeStream = createReadStream(fullPath, { signal: options?.signal });
|
|
50
|
+
return Readable.toWeb(nodeStream);
|
|
51
|
+
}
|
|
52
|
+
async putStream(path, stream, options) {
|
|
53
|
+
const fullPath = this.resolvePath(path), dir = dirname(fullPath);
|
|
54
|
+
await mkdir(dir, { recursive: !0 });
|
|
55
|
+
const nodeReadable = Readable.fromWeb(stream), writeStream = createWriteStream(fullPath);
|
|
56
|
+
try {
|
|
57
|
+
await pipeline(nodeReadable, writeStream, { signal: options?.signal });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
try {
|
|
60
|
+
await unlink(fullPath);
|
|
61
|
+
} catch {}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
const st = await stat(fullPath);
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
size: st.size,
|
|
68
|
+
contentType: options?.contentType,
|
|
69
|
+
lastModified: st.mtimeMs
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async readToString(path) {
|
|
73
|
+
const fullPath = this.resolvePath(path);
|
|
74
|
+
return await readFile(fullPath, "utf8");
|
|
75
|
+
}
|
|
76
|
+
async readToBuffer(path) {
|
|
77
|
+
const fullPath = this.resolvePath(path);
|
|
78
|
+
return await readFile(fullPath);
|
|
79
|
+
}
|
|
80
|
+
async readToUint8Array(path) {
|
|
81
|
+
const fullPath = this.resolvePath(path), buffer = await readFile(fullPath);
|
|
82
|
+
return new Uint8Array(buffer);
|
|
83
|
+
}
|
|
84
|
+
async deleteFile(path) {
|
|
85
|
+
const fullPath = this.resolvePath(path);
|
|
86
|
+
await unlink(fullPath);
|
|
87
|
+
}
|
|
88
|
+
async deleteDirectory(path) {
|
|
89
|
+
const fullPath = this.resolvePath(path);
|
|
90
|
+
await rm(fullPath, { recursive: !0, force: !0 });
|
|
91
|
+
}
|
|
92
|
+
async createDirectory(path) {
|
|
93
|
+
const fullPath = this.resolvePath(path);
|
|
94
|
+
await mkdir(fullPath, { recursive: !0 });
|
|
95
|
+
}
|
|
96
|
+
async moveFile(from, to) {
|
|
97
|
+
const fromPath = this.resolvePath(from), toPath = this.resolvePath(to), toDir = dirname(toPath);
|
|
98
|
+
await mkdir(toDir, { recursive: !0 });
|
|
99
|
+
await rename(fromPath, toPath);
|
|
100
|
+
}
|
|
101
|
+
async copyFile(from, to) {
|
|
102
|
+
const fromPath = this.resolvePath(from), toPath = this.resolvePath(to), toDir = dirname(toPath);
|
|
103
|
+
await mkdir(toDir, { recursive: !0 });
|
|
104
|
+
await copyFile(fromPath, toPath);
|
|
105
|
+
}
|
|
106
|
+
async stat(path) {
|
|
107
|
+
const fullPath = this.resolvePath(path), stats = await lstat(fullPath);
|
|
108
|
+
return {
|
|
109
|
+
path,
|
|
110
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
111
|
+
visibility: await this.visibility(path),
|
|
112
|
+
size: stats.size,
|
|
113
|
+
lastModified: stats.mtimeMs,
|
|
114
|
+
mimeType: stats.isFile() ? await this.detectMimeType(fullPath) : void 0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
list(path, options = {}) {
|
|
118
|
+
return this.createAsyncIterator(path, options.deep || !1);
|
|
119
|
+
}
|
|
120
|
+
async* createAsyncIterator(path, deep) {
|
|
121
|
+
const fullPath = this.resolvePath(path);
|
|
122
|
+
try {
|
|
123
|
+
await access(fullPath, constants.R_OK);
|
|
124
|
+
} catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const entries = await this.readDirectoryRecursive(fullPath, deep);
|
|
128
|
+
yield* createDirectoryListing(entries);
|
|
129
|
+
}
|
|
130
|
+
async readDirectoryRecursive(dirPath, deep) {
|
|
131
|
+
const entries = [];
|
|
132
|
+
try {
|
|
133
|
+
const items = await readdir(dirPath, { withFileTypes: !0 });
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const itemPath = join(dirPath, item.name), relativePath = relative(this.root, itemPath);
|
|
136
|
+
entries.push({
|
|
137
|
+
path: relativePath,
|
|
138
|
+
type: item.isDirectory() ? "directory" : "file"
|
|
139
|
+
});
|
|
140
|
+
if (deep && item.isDirectory()) {
|
|
141
|
+
const subEntries = await this.readDirectoryRecursive(itemPath, !0);
|
|
142
|
+
entries.push(...subEntries);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error?.code !== "EACCES" && error?.code !== "EPERM")
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return entries;
|
|
150
|
+
}
|
|
151
|
+
async changeVisibility(path, vis) {
|
|
152
|
+
const fullPath = this.resolvePath(path), isDir = (await lstat(fullPath)).isDirectory();
|
|
153
|
+
await chmod(fullPath, vis === "public" ? isDir ? 493 : 420 : isDir ? 448 : 384);
|
|
154
|
+
}
|
|
155
|
+
async visibility(path) {
|
|
156
|
+
const fullPath = this.resolvePath(path);
|
|
157
|
+
return (await lstat(fullPath)).mode & 511 & 4 ? "public" : "private";
|
|
158
|
+
}
|
|
159
|
+
async fileExists(path) {
|
|
160
|
+
const fullPath = this.resolvePath(path);
|
|
161
|
+
try {
|
|
162
|
+
return (await lstat(fullPath)).isFile();
|
|
163
|
+
} catch {
|
|
164
|
+
return !1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async directoryExists(path) {
|
|
168
|
+
const fullPath = this.resolvePath(path);
|
|
169
|
+
try {
|
|
170
|
+
return (await lstat(fullPath)).isDirectory();
|
|
171
|
+
} catch {
|
|
172
|
+
return !1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async publicUrl(path, options = {}) {
|
|
176
|
+
return `${(options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "")}/${path}`;
|
|
177
|
+
}
|
|
178
|
+
async temporaryUrl(path, options) {
|
|
179
|
+
const expiry = normalizeExpiryToDate(options.expiresIn), payload = `${path}:${expiry.getTime()}`, appKey = process.env.APP_KEY || "stacks-default-key", signature = createHmac("sha256", appKey).update(payload).digest("hex");
|
|
180
|
+
return `http://localhost/temp/${Buffer.from(`${payload}:${signature}`).toString("base64url")}`;
|
|
181
|
+
}
|
|
182
|
+
async signedUrl(path, options) {
|
|
183
|
+
const token = createSignedStorageToken(path, options);
|
|
184
|
+
return `${(options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "")}/__storage/${encodeURIComponent(path)}?token=${token}`;
|
|
185
|
+
}
|
|
186
|
+
async checksum(path, options = {}) {
|
|
187
|
+
const algorithm = options.algorithm || "sha256", fullPath = this.resolvePath(path), content = await readFile(fullPath), hasher = new Bun.CryptoHasher(algorithm);
|
|
188
|
+
hasher.update(content);
|
|
189
|
+
return hasher.digest("hex");
|
|
190
|
+
}
|
|
191
|
+
async mimeType(path, options = {}) {
|
|
192
|
+
const fullPath = this.resolvePath(path);
|
|
193
|
+
return await this.detectMimeType(fullPath);
|
|
194
|
+
}
|
|
195
|
+
async detectMimeType(filePath) {
|
|
196
|
+
const ext = basename(filePath).split(".").pop()?.toLowerCase();
|
|
197
|
+
return {
|
|
198
|
+
txt: "text/plain",
|
|
199
|
+
html: "text/html",
|
|
200
|
+
css: "text/css",
|
|
201
|
+
js: "application/javascript",
|
|
202
|
+
json: "application/json",
|
|
203
|
+
xml: "application/xml",
|
|
204
|
+
pdf: "application/pdf",
|
|
205
|
+
zip: "application/zip",
|
|
206
|
+
jpg: "image/jpeg",
|
|
207
|
+
jpeg: "image/jpeg",
|
|
208
|
+
png: "image/png",
|
|
209
|
+
gif: "image/gif",
|
|
210
|
+
svg: "image/svg+xml",
|
|
211
|
+
mp4: "video/mp4",
|
|
212
|
+
mp3: "audio/mpeg",
|
|
213
|
+
wav: "audio/wav"
|
|
214
|
+
}[ext || ""] || "application/octet-stream";
|
|
215
|
+
}
|
|
216
|
+
async lastModified(path) {
|
|
217
|
+
return (await this.stat(path)).lastModified;
|
|
218
|
+
}
|
|
219
|
+
async fileSize(path) {
|
|
220
|
+
return (await this.stat(path)).size;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function createLocalStorage(config = {}) {
|
|
224
|
+
return new LocalStorageAdapter(config);
|
|
225
|
+
}
|