@stacksjs/storage 0.70.44 → 0.70.53
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.md +21 -0
- package/dist/image.js +58 -0
- package/dist/index.js +2386 -1278
- package/dist/src/adapters/bun.d.ts +6 -4
- package/dist/src/adapters/index.d.ts +2 -0
- package/dist/src/adapters/local.d.ts +6 -4
- package/dist/src/adapters/memory.d.ts +4 -2
- package/dist/src/adapters/s3.d.ts +24 -7
- package/dist/src/adapters/scoped.d.ts +68 -0
- package/dist/src/facade.d.ts +14 -4
- package/dist/src/image.d.ts +55 -0
- package/dist/src/index.d.ts +34 -1
- package/dist/src/mime-verify.d.ts +65 -0
- package/dist/src/path-sanitize.d.ts +92 -0
- package/dist/src/put-file.d.ts +53 -0
- package/dist/src/s3-presigned-post.d.ts +52 -0
- package/dist/src/signed-url.d.ts +33 -1
- package/dist/src/types/filesystem.d.ts +31 -0
- package/dist/src/types.d.ts +90 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -13,916 +13,2104 @@ var __export = (target, all) => {
|
|
|
13
13
|
set: __exportSetter.bind(all, name)
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
16
17
|
var __require = import.meta.require;
|
|
17
18
|
|
|
18
|
-
// src/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// src/fs.ts
|
|
23
|
-
import * as fs from "fs";
|
|
24
|
-
import { existsSync, watch as fsWatch, mkdirSync, readFileSync, watchFile, writeFileSync } from "fs";
|
|
25
|
-
function exists(path) {
|
|
26
|
-
return existsSync(path);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// src/copy.ts
|
|
30
|
-
function copy(src, dest, exclude = []) {
|
|
31
|
-
if (Array.isArray(src)) {
|
|
32
|
-
src.forEach((file) => {
|
|
33
|
-
copy(file, dest, exclude);
|
|
34
|
-
});
|
|
35
|
-
} else {
|
|
36
|
-
if (fs.statSync(src).isDirectory())
|
|
37
|
-
copyFolder(src, dest, exclude);
|
|
38
|
-
else
|
|
39
|
-
copyFile(src, dest);
|
|
19
|
+
// src/types.ts
|
|
20
|
+
async function* createDirectoryListing(entries) {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
yield entry;
|
|
40
23
|
}
|
|
41
24
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function copyFolder(src, dest, exclude = []) {
|
|
46
|
-
if (!fs.existsSync(dest))
|
|
47
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
48
|
-
if (fs.existsSync(src)) {
|
|
49
|
-
fs.readdirSync(src).forEach((file) => {
|
|
50
|
-
if (!contains(join(src, file), exclude)) {
|
|
51
|
-
const srcPath = join(src, file);
|
|
52
|
-
const destPath = join(dest, file);
|
|
53
|
-
if (fs.statSync(srcPath).isDirectory())
|
|
54
|
-
copyFolder(srcPath, destPath, exclude);
|
|
55
|
-
else
|
|
56
|
-
fs.copyFileSync(srcPath, destPath);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
25
|
+
function normalizeExpiryToMilliseconds(expiry) {
|
|
26
|
+
if (expiry instanceof Date) {
|
|
27
|
+
return expiry.getTime() - Date.now();
|
|
59
28
|
}
|
|
29
|
+
return expiry * 1000;
|
|
60
30
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
import { join as join3 } from "@stacksjs/path";
|
|
65
|
-
|
|
66
|
-
// src/folders.ts
|
|
67
|
-
import { join as join2 } from "@stacksjs/path";
|
|
68
|
-
function isFolder(path) {
|
|
69
|
-
try {
|
|
70
|
-
return fs.statSync(path).isDirectory();
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
31
|
+
function normalizeExpiryToDate(expiry) {
|
|
32
|
+
if (expiry instanceof Date) {
|
|
33
|
+
return expiry;
|
|
73
34
|
}
|
|
35
|
+
return new Date(Date.now() + expiry * 1000);
|
|
74
36
|
}
|
|
75
|
-
function
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
function doesFolderExist(path) {
|
|
79
|
-
return fs.existsSync(path);
|
|
80
|
-
}
|
|
81
|
-
function createFolder(dir) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
try {
|
|
84
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
-
resolve();
|
|
86
|
-
} catch (err) {
|
|
87
|
-
reject(err);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
37
|
+
function isFile2(entry) {
|
|
38
|
+
return entry.type === "file";
|
|
90
39
|
}
|
|
91
|
-
function
|
|
92
|
-
return
|
|
93
|
-
return fs.statSync(join2(dir, file)).isDirectory();
|
|
94
|
-
});
|
|
40
|
+
function isDirectory(entry) {
|
|
41
|
+
return entry.type === "directory";
|
|
95
42
|
}
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
43
|
+
var Visibility;
|
|
44
|
+
var init_types = __esm(() => {
|
|
45
|
+
((Visibility2) => {
|
|
46
|
+
Visibility2["PUBLIC"] = "public";
|
|
47
|
+
Visibility2["PRIVATE"] = "private";
|
|
48
|
+
})(Visibility ||= {});
|
|
49
|
+
});
|
|
102
50
|
|
|
103
|
-
// src/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
absolute: options?.absolute,
|
|
113
|
-
dot: options?.dot,
|
|
114
|
-
onlyFiles: options?.onlyFiles
|
|
115
|
-
});
|
|
116
|
-
for (const match of matches) {
|
|
117
|
-
results.push(match);
|
|
51
|
+
// src/signed-url.ts
|
|
52
|
+
import { Buffer } from "buffer";
|
|
53
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
54
|
+
import process2 from "process";
|
|
55
|
+
function getAppKey() {
|
|
56
|
+
const k = process2.env.APP_KEY;
|
|
57
|
+
if (!k || k.length < 16) {
|
|
58
|
+
if (process2.env.APP_ENV === "production" || process2.env.NODE_ENV === "production") {
|
|
59
|
+
throw new Error("[storage/signed-url] APP_KEY is missing or too short (need \u226516 chars). Cannot sign URL.");
|
|
118
60
|
}
|
|
119
61
|
}
|
|
120
|
-
return
|
|
62
|
+
return k || "stacks-default-key-dev-only-do-not-use-prod";
|
|
121
63
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const results = [];
|
|
125
|
-
for (const pattern of patternArray) {
|
|
126
|
-
const globInstance = new BunGlob(pattern);
|
|
127
|
-
const matches = globInstance.scan({
|
|
128
|
-
cwd: options?.cwd,
|
|
129
|
-
absolute: options?.absolute,
|
|
130
|
-
dot: options?.dot,
|
|
131
|
-
onlyFiles: options?.onlyFiles
|
|
132
|
-
});
|
|
133
|
-
for await (const match of matches) {
|
|
134
|
-
results.push(match);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return results;
|
|
64
|
+
function base64UrlEncode(buf) {
|
|
65
|
+
return buf.toString("base64url");
|
|
138
66
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
function deleteFolder(path) {
|
|
142
|
-
return new Promise((resolve, reject) => {
|
|
143
|
-
try {
|
|
144
|
-
if (isFolder(path)) {
|
|
145
|
-
fs.rmSync(path, { recursive: true, force: true });
|
|
146
|
-
return resolve(ok(`Deleted ${path}`));
|
|
147
|
-
}
|
|
148
|
-
return resolve(ok(`Path ${path} was not a directory`));
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return reject(err(error));
|
|
151
|
-
}
|
|
152
|
-
});
|
|
67
|
+
function base64UrlDecode(str) {
|
|
68
|
+
return Buffer.from(str, "base64url");
|
|
153
69
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (fs.readdirSync(path).length === 0)
|
|
159
|
-
return resolve(ok(true));
|
|
160
|
-
return resolve(ok(false));
|
|
161
|
-
}
|
|
162
|
-
return resolve(ok(false));
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return reject(err(error));
|
|
165
|
-
}
|
|
166
|
-
});
|
|
70
|
+
function normalizeExpiry(expiresIn) {
|
|
71
|
+
if (expiresIn instanceof Date)
|
|
72
|
+
return Math.floor(expiresIn.getTime() / 1000);
|
|
73
|
+
return Math.floor(Date.now() / 1000) + Math.floor(expiresIn);
|
|
167
74
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
75
|
+
function createSignedStorageToken(path2, options) {
|
|
76
|
+
const exp = normalizeExpiry(options.expiresIn);
|
|
77
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
78
|
+
const header = { alg: ALG, typ: "JWT" };
|
|
79
|
+
const payload = {
|
|
80
|
+
iss: options.issuer || "stacks",
|
|
81
|
+
iat,
|
|
82
|
+
exp,
|
|
83
|
+
path: path2
|
|
84
|
+
};
|
|
85
|
+
const headerPart = base64UrlEncode(Buffer.from(JSON.stringify(header)));
|
|
86
|
+
const payloadPart = base64UrlEncode(Buffer.from(JSON.stringify(payload)));
|
|
87
|
+
const signingInput = `${headerPart}.${payloadPart}`;
|
|
88
|
+
const sig = base64UrlEncode(createHmac("sha256", getAppKey()).update(signingInput).digest());
|
|
89
|
+
return `${signingInput}.${sig}`;
|
|
183
90
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (isFolder(p)) {
|
|
192
|
-
if (fs.readdirSync(p).length === 0)
|
|
193
|
-
fs.rmSync(p, { recursive: true, force: true });
|
|
194
|
-
else
|
|
195
|
-
await deleteEmptyFolders(p);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return ok(`Deleted empty folders located in ${dir}`);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
return err(error);
|
|
91
|
+
function rememberRevoked(sigPart) {
|
|
92
|
+
if (revokedSignatures.has(sigPart))
|
|
93
|
+
return;
|
|
94
|
+
if (revokedSignatures.size >= REVOCATION_LIMIT) {
|
|
95
|
+
const oldest = revokedSignatures.values().next().value;
|
|
96
|
+
if (oldest !== undefined)
|
|
97
|
+
revokedSignatures.delete(oldest);
|
|
201
98
|
}
|
|
99
|
+
revokedSignatures.add(sigPart);
|
|
202
100
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return resolve(ok(`Path ${path} was not a file`));
|
|
211
|
-
} catch (error) {
|
|
212
|
-
return reject(err(error));
|
|
213
|
-
}
|
|
214
|
-
});
|
|
101
|
+
function revokeSignedStorageToken(token) {
|
|
102
|
+
if (typeof token !== "string" || token.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
const parts = token.split(".");
|
|
105
|
+
const sig = parts.length === 3 ? parts[2] : token;
|
|
106
|
+
if (sig)
|
|
107
|
+
rememberRevoked(sig);
|
|
215
108
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return err(handleError(`Path ${path} does not contain a glob`));
|
|
219
|
-
const directories = await glob([path], { onlyDirectories: true });
|
|
220
|
-
for (const directory of directories) {
|
|
221
|
-
const result = await deleteFolder(directory);
|
|
222
|
-
if (result.isErr) {
|
|
223
|
-
log.error(result.error);
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
log.info(`Deleted ${italic(directory)}`);
|
|
227
|
-
}
|
|
228
|
-
return ok(`Deleted ${directories.length} directories`);
|
|
109
|
+
function isSignedStorageTokenRevoked(sigPart) {
|
|
110
|
+
return revokedSignatures.has(sigPart);
|
|
229
111
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return await deleteFile(path);
|
|
233
|
-
if (isFolder(path))
|
|
234
|
-
return await deleteFolder(path);
|
|
235
|
-
if (path.includes("*"))
|
|
236
|
-
return await deleteGlob(path);
|
|
237
|
-
return err(handleError(`Path ${path} cannot be deleted due to an unhandled condition. Please report this issue.`));
|
|
112
|
+
function clearRevokedSignedStorageTokens() {
|
|
113
|
+
revokedSignatures.clear();
|
|
238
114
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
import { dirname, join as join4, path as p } from "@stacksjs/path";
|
|
243
|
-
import { detectIndent, detectNewline } from "@stacksjs/strings";
|
|
244
|
-
async function readJsonFile(name, cwd) {
|
|
245
|
-
const file = await readTextFile(name, cwd);
|
|
246
|
-
let data;
|
|
247
|
-
try {
|
|
248
|
-
data = JSON.parse(file.data);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
throw new Error(`Failed to parse JSON file "${name}": ${error.message}`);
|
|
115
|
+
function verifySignedStorageToken(token, requestedPath) {
|
|
116
|
+
if (typeof token !== "string") {
|
|
117
|
+
return { valid: false, reason: "malformed" };
|
|
251
118
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
119
|
+
const parts = token.split(".");
|
|
120
|
+
if (parts.length !== 3) {
|
|
121
|
+
return { valid: false, reason: "malformed" };
|
|
122
|
+
}
|
|
123
|
+
const headerPart = parts[0];
|
|
124
|
+
const payloadPart = parts[1];
|
|
125
|
+
const sigPart = parts[2];
|
|
126
|
+
const signingInput = `${headerPart}.${payloadPart}`;
|
|
127
|
+
const expectedSig = createHmac("sha256", getAppKey()).update(signingInput).digest();
|
|
128
|
+
let providedSig;
|
|
129
|
+
try {
|
|
130
|
+
providedSig = base64UrlDecode(sigPart);
|
|
131
|
+
} catch {
|
|
132
|
+
return { valid: false, reason: "malformed" };
|
|
133
|
+
}
|
|
134
|
+
if (providedSig.length !== expectedSig.length || !timingSafeEqual(providedSig, expectedSig)) {
|
|
135
|
+
return { valid: false, reason: "bad_signature" };
|
|
136
|
+
}
|
|
137
|
+
if (revokedSignatures.has(sigPart)) {
|
|
138
|
+
return { valid: false, reason: "revoked" };
|
|
139
|
+
}
|
|
140
|
+
let claims;
|
|
141
|
+
try {
|
|
142
|
+
claims = JSON.parse(base64UrlDecode(payloadPart).toString("utf8"));
|
|
143
|
+
} catch {
|
|
144
|
+
return { valid: false, reason: "malformed" };
|
|
145
|
+
}
|
|
146
|
+
const now = Math.floor(Date.now() / 1000);
|
|
147
|
+
if (typeof claims.exp !== "number" || now >= claims.exp) {
|
|
148
|
+
return { valid: false, reason: "expired" };
|
|
149
|
+
}
|
|
150
|
+
if (claims.path !== requestedPath) {
|
|
151
|
+
return { valid: false, reason: "path_mismatch" };
|
|
152
|
+
}
|
|
153
|
+
return { valid: true, claims };
|
|
255
154
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
155
|
+
var ALG = "HS256", REVOCATION_LIMIT = 1e5, revokedSignatures;
|
|
156
|
+
var init_signed_url = __esm(() => {
|
|
157
|
+
revokedSignatures = new Set;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/adapters/local.ts
|
|
161
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
162
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
163
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
164
|
+
import { access, chmod, constants, copyFile as copyFile2, lstat, mkdir, readdir, readFile, rename as rename2, rm, stat, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
165
|
+
import { basename, dirname as dirname3, join as join5, relative } from "path";
|
|
166
|
+
import { Readable } from "stream";
|
|
167
|
+
import { pipeline } from "stream/promises";
|
|
168
|
+
|
|
169
|
+
class LocalStorageAdapter {
|
|
170
|
+
root;
|
|
171
|
+
constructor(config = {}) {
|
|
172
|
+
this.root = config.root || process.cwd();
|
|
173
|
+
}
|
|
174
|
+
resolvePath(path2) {
|
|
175
|
+
const resolved = join5(this.root, path2);
|
|
176
|
+
const rel = relative(this.root, resolved);
|
|
177
|
+
if (rel.startsWith("..") || rel.startsWith("../") || rel.startsWith("..\\")) {
|
|
178
|
+
throw new Error(`Path traversal detected: '${path2}' resolves outside storage root`);
|
|
179
|
+
}
|
|
180
|
+
return resolved;
|
|
181
|
+
}
|
|
182
|
+
async write(path2, contents) {
|
|
183
|
+
const fullPath = this.resolvePath(path2);
|
|
184
|
+
const dir = dirname3(fullPath);
|
|
185
|
+
await mkdir(dir, { recursive: true });
|
|
186
|
+
if (typeof contents === "string") {
|
|
187
|
+
await writeFile2(fullPath, contents, "utf8");
|
|
188
|
+
} else if (contents instanceof Buffer2) {
|
|
189
|
+
await writeFile2(fullPath, contents);
|
|
190
|
+
} else if (contents instanceof Uint8Array) {
|
|
191
|
+
await writeFile2(fullPath, contents);
|
|
192
|
+
} else {
|
|
193
|
+
const writeStream = createWriteStream(fullPath);
|
|
194
|
+
await pipeline(contents, writeStream);
|
|
195
|
+
}
|
|
196
|
+
const st = await stat(fullPath);
|
|
197
|
+
return {
|
|
198
|
+
path: path2,
|
|
199
|
+
size: st.size,
|
|
200
|
+
lastModified: st.mtimeMs
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async read(path2) {
|
|
204
|
+
const fullPath = this.resolvePath(path2);
|
|
205
|
+
return await readFile(fullPath);
|
|
206
|
+
}
|
|
207
|
+
async getStream(path2, options) {
|
|
208
|
+
const fullPath = this.resolvePath(path2);
|
|
209
|
+
await access(fullPath, constants.R_OK);
|
|
210
|
+
const nodeStream = createReadStream(fullPath, { signal: options?.signal });
|
|
211
|
+
return Readable.toWeb(nodeStream);
|
|
212
|
+
}
|
|
213
|
+
async putStream(path2, stream, options) {
|
|
214
|
+
const fullPath = this.resolvePath(path2);
|
|
215
|
+
const dir = dirname3(fullPath);
|
|
216
|
+
await mkdir(dir, { recursive: true });
|
|
217
|
+
const nodeReadable = Readable.fromWeb(stream);
|
|
218
|
+
const writeStream = createWriteStream(fullPath);
|
|
219
|
+
try {
|
|
220
|
+
await pipeline(nodeReadable, writeStream, { signal: options?.signal });
|
|
221
|
+
} catch (err3) {
|
|
222
|
+
try {
|
|
223
|
+
await unlink(fullPath);
|
|
224
|
+
} catch {}
|
|
225
|
+
throw err3;
|
|
226
|
+
}
|
|
227
|
+
const st = await stat(fullPath);
|
|
228
|
+
return {
|
|
229
|
+
path: path2,
|
|
230
|
+
size: st.size,
|
|
231
|
+
contentType: options?.contentType,
|
|
232
|
+
lastModified: st.mtimeMs
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async readToString(path2) {
|
|
236
|
+
const fullPath = this.resolvePath(path2);
|
|
237
|
+
return await readFile(fullPath, "utf8");
|
|
238
|
+
}
|
|
239
|
+
async readToBuffer(path2) {
|
|
240
|
+
const fullPath = this.resolvePath(path2);
|
|
241
|
+
return await readFile(fullPath);
|
|
242
|
+
}
|
|
243
|
+
async readToUint8Array(path2) {
|
|
244
|
+
const fullPath = this.resolvePath(path2);
|
|
245
|
+
const buffer = await readFile(fullPath);
|
|
246
|
+
return new Uint8Array(buffer);
|
|
247
|
+
}
|
|
248
|
+
async deleteFile(path2) {
|
|
249
|
+
const fullPath = this.resolvePath(path2);
|
|
250
|
+
await unlink(fullPath);
|
|
251
|
+
}
|
|
252
|
+
async deleteDirectory(path2) {
|
|
253
|
+
const fullPath = this.resolvePath(path2);
|
|
254
|
+
await rm(fullPath, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
async createDirectory(path2) {
|
|
257
|
+
const fullPath = this.resolvePath(path2);
|
|
258
|
+
await mkdir(fullPath, { recursive: true });
|
|
259
|
+
}
|
|
260
|
+
async moveFile(from, to) {
|
|
261
|
+
const fromPath = this.resolvePath(from);
|
|
262
|
+
const toPath = this.resolvePath(to);
|
|
263
|
+
const toDir = dirname3(toPath);
|
|
264
|
+
await mkdir(toDir, { recursive: true });
|
|
265
|
+
await rename2(fromPath, toPath);
|
|
266
|
+
}
|
|
267
|
+
async copyFile(from, to) {
|
|
268
|
+
const fromPath = this.resolvePath(from);
|
|
269
|
+
const toPath = this.resolvePath(to);
|
|
270
|
+
const toDir = dirname3(toPath);
|
|
271
|
+
await mkdir(toDir, { recursive: true });
|
|
272
|
+
await copyFile2(fromPath, toPath);
|
|
273
|
+
}
|
|
274
|
+
async stat(path2) {
|
|
275
|
+
const fullPath = this.resolvePath(path2);
|
|
276
|
+
const stats = await lstat(fullPath);
|
|
277
|
+
return {
|
|
278
|
+
path: path2,
|
|
279
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
280
|
+
visibility: await this.visibility(path2),
|
|
281
|
+
size: stats.size,
|
|
282
|
+
lastModified: stats.mtimeMs,
|
|
283
|
+
mimeType: stats.isFile() ? await this.detectMimeType(fullPath) : undefined
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
list(path2, options = {}) {
|
|
287
|
+
return this.createAsyncIterator(path2, options.deep || false);
|
|
288
|
+
}
|
|
289
|
+
async* createAsyncIterator(path2, deep) {
|
|
290
|
+
const fullPath = this.resolvePath(path2);
|
|
291
|
+
try {
|
|
292
|
+
await access(fullPath, constants.R_OK);
|
|
293
|
+
} catch {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const entries = await this.readDirectoryRecursive(fullPath, deep);
|
|
297
|
+
yield* createDirectoryListing(entries);
|
|
298
|
+
}
|
|
299
|
+
async readDirectoryRecursive(dirPath, deep) {
|
|
300
|
+
const entries = [];
|
|
301
|
+
try {
|
|
302
|
+
const items = await readdir(dirPath, { withFileTypes: true });
|
|
303
|
+
for (const item of items) {
|
|
304
|
+
const itemPath = join5(dirPath, item.name);
|
|
305
|
+
const relativePath = relative(this.root, itemPath);
|
|
306
|
+
entries.push({
|
|
307
|
+
path: relativePath,
|
|
308
|
+
type: item.isDirectory() ? "directory" : "file"
|
|
309
|
+
});
|
|
310
|
+
if (deep && item.isDirectory()) {
|
|
311
|
+
const subEntries = await this.readDirectoryRecursive(itemPath, true);
|
|
312
|
+
entries.push(...subEntries);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error?.code !== "EACCES" && error?.code !== "EPERM") {
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return entries;
|
|
321
|
+
}
|
|
322
|
+
async changeVisibility(path2, vis) {
|
|
323
|
+
const fullPath = this.resolvePath(path2);
|
|
324
|
+
const stats = await lstat(fullPath);
|
|
325
|
+
const isDir2 = stats.isDirectory();
|
|
326
|
+
const mode = vis === "public" ? isDir2 ? 493 : 420 : isDir2 ? 448 : 384;
|
|
327
|
+
await chmod(fullPath, mode);
|
|
328
|
+
}
|
|
329
|
+
async visibility(path2) {
|
|
330
|
+
const fullPath = this.resolvePath(path2);
|
|
331
|
+
const stats = await lstat(fullPath);
|
|
332
|
+
const perms = stats.mode & 511;
|
|
333
|
+
return perms & 4 ? "public" : "private";
|
|
334
|
+
}
|
|
335
|
+
async fileExists(path2) {
|
|
336
|
+
const fullPath = this.resolvePath(path2);
|
|
337
|
+
try {
|
|
338
|
+
const stats = await lstat(fullPath);
|
|
339
|
+
return stats.isFile();
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async directoryExists(path2) {
|
|
345
|
+
const fullPath = this.resolvePath(path2);
|
|
346
|
+
try {
|
|
347
|
+
const stats = await lstat(fullPath);
|
|
348
|
+
return stats.isDirectory();
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async publicUrl(path2, options = {}) {
|
|
354
|
+
const base = (options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
|
|
355
|
+
return `${base}/${path2}`;
|
|
356
|
+
}
|
|
357
|
+
async temporaryUrl(path2, options) {
|
|
358
|
+
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
359
|
+
const payload = `${path2}:${expiry.getTime()}`;
|
|
360
|
+
const appKey = process.env.APP_KEY || "stacks-default-key";
|
|
361
|
+
const signature = createHmac2("sha256", appKey).update(payload).digest("hex");
|
|
362
|
+
const token = Buffer2.from(`${payload}:${signature}`).toString("base64url");
|
|
363
|
+
return `http://localhost/temp/${token}`;
|
|
364
|
+
}
|
|
365
|
+
async signedUrl(path2, options) {
|
|
366
|
+
const token = createSignedStorageToken(path2, options);
|
|
367
|
+
const baseUrl = (options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
|
|
368
|
+
return `${baseUrl}/__storage/${encodeURIComponent(path2)}?token=${token}`;
|
|
369
|
+
}
|
|
370
|
+
async checksum(path2, options = {}) {
|
|
371
|
+
const algorithm = options.algorithm || "sha256";
|
|
372
|
+
const fullPath = this.resolvePath(path2);
|
|
373
|
+
const content = await readFile(fullPath);
|
|
374
|
+
const hasher = new Bun.CryptoHasher(algorithm);
|
|
375
|
+
hasher.update(content);
|
|
376
|
+
return hasher.digest("hex");
|
|
377
|
+
}
|
|
378
|
+
async mimeType(path2, options = {}) {
|
|
379
|
+
const fullPath = this.resolvePath(path2);
|
|
380
|
+
return await this.detectMimeType(fullPath);
|
|
381
|
+
}
|
|
382
|
+
async detectMimeType(filePath) {
|
|
383
|
+
const ext = basename(filePath).split(".").pop()?.toLowerCase();
|
|
384
|
+
const mimeTypes = {
|
|
385
|
+
txt: "text/plain",
|
|
386
|
+
html: "text/html",
|
|
387
|
+
css: "text/css",
|
|
388
|
+
js: "application/javascript",
|
|
389
|
+
json: "application/json",
|
|
390
|
+
xml: "application/xml",
|
|
391
|
+
pdf: "application/pdf",
|
|
392
|
+
zip: "application/zip",
|
|
393
|
+
jpg: "image/jpeg",
|
|
394
|
+
jpeg: "image/jpeg",
|
|
395
|
+
png: "image/png",
|
|
396
|
+
gif: "image/gif",
|
|
397
|
+
svg: "image/svg+xml",
|
|
398
|
+
mp4: "video/mp4",
|
|
399
|
+
mp3: "audio/mpeg",
|
|
400
|
+
wav: "audio/wav"
|
|
401
|
+
};
|
|
402
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
403
|
+
}
|
|
404
|
+
async lastModified(path2) {
|
|
405
|
+
const stats = await this.stat(path2);
|
|
406
|
+
return stats.lastModified;
|
|
407
|
+
}
|
|
408
|
+
async fileSize(path2) {
|
|
409
|
+
const stats = await this.stat(path2);
|
|
410
|
+
return stats.size;
|
|
411
|
+
}
|
|
259
412
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
413
|
+
function createLocalStorage(config = {}) {
|
|
414
|
+
return new LocalStorageAdapter(config);
|
|
415
|
+
}
|
|
416
|
+
var init_local = __esm(() => {
|
|
417
|
+
init_types();
|
|
418
|
+
init_signed_url();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// src/path-sanitize.ts
|
|
422
|
+
function sanitizePresignedDir(dir) {
|
|
423
|
+
if (dir === undefined || dir === "")
|
|
424
|
+
return "";
|
|
425
|
+
if (typeof dir !== "string")
|
|
426
|
+
throw new PathSanitizeError(`dir must be a string, got ${typeof dir}`, "not-string");
|
|
427
|
+
const trimmed = dir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
428
|
+
if (dir.startsWith("/"))
|
|
429
|
+
throw new PathSanitizeError(`dir must not be absolute: '${dir}'`, "absolute-path");
|
|
430
|
+
if (trimmed === "")
|
|
431
|
+
return "";
|
|
432
|
+
if (trimmed.includes("\x00"))
|
|
433
|
+
throw new PathSanitizeError(`dir contains null byte`, "null-byte");
|
|
434
|
+
if (/[\x00-\x1F\x7F]/.test(trimmed))
|
|
435
|
+
throw new PathSanitizeError(`dir contains control character`, "control-char");
|
|
436
|
+
const segments = trimmed.split("/");
|
|
437
|
+
for (const segment of segments) {
|
|
438
|
+
if (segment === "" || segment === "." || segment === "..")
|
|
439
|
+
throw new PathSanitizeError(`dir contains traversal or empty segment: '${dir}'`, "traversal");
|
|
440
|
+
if (segment.length > MAX_COMPONENT_LENGTH)
|
|
441
|
+
throw new PathSanitizeError(`dir segment exceeds ${MAX_COMPONENT_LENGTH} chars`, "too-long");
|
|
442
|
+
if (!ALLOWED_DIR_CHAR.test(segment))
|
|
443
|
+
throw new PathSanitizeError(`dir segment contains disallowed character: '${segment}'`, "invalid-char");
|
|
444
|
+
}
|
|
445
|
+
return segments.join("/");
|
|
446
|
+
}
|
|
447
|
+
function sanitizePresignedFilename(filename) {
|
|
448
|
+
if (typeof filename !== "string")
|
|
449
|
+
throw new PathSanitizeError(`filename must be a string, got ${typeof filename}`, "not-string");
|
|
450
|
+
if (filename === "")
|
|
451
|
+
throw new PathSanitizeError(`filename must not be empty`, "empty");
|
|
452
|
+
if (filename.length > MAX_COMPONENT_LENGTH)
|
|
453
|
+
throw new PathSanitizeError(`filename exceeds ${MAX_COMPONENT_LENGTH} chars`, "too-long");
|
|
454
|
+
if (filename.includes("\x00"))
|
|
455
|
+
throw new PathSanitizeError(`filename contains null byte`, "null-byte");
|
|
456
|
+
if (/[\x00-\x1F\x7F]/.test(filename))
|
|
457
|
+
throw new PathSanitizeError(`filename contains control character`, "control-char");
|
|
458
|
+
if (filename.includes("/") || filename.includes("\\"))
|
|
459
|
+
throw new PathSanitizeError(`filename must not contain path separators: '${filename}'`, "traversal");
|
|
460
|
+
if (filename === "." || filename === ".." || filename.startsWith("../") || filename.includes("/.."))
|
|
461
|
+
throw new PathSanitizeError(`filename contains traversal token: '${filename}'`, "traversal");
|
|
462
|
+
if (!ALLOWED_FILENAME_CHAR.test(filename))
|
|
463
|
+
throw new PathSanitizeError(`filename contains disallowed character: '${filename}'`, "invalid-char");
|
|
464
|
+
const dotIdx = filename.lastIndexOf(".");
|
|
465
|
+
if (dotIdx > 0 && dotIdx < filename.length - 1) {
|
|
466
|
+
const ext = filename.slice(dotIdx + 1).toLowerCase();
|
|
467
|
+
if (!ALLOWED_EXTENSION.test(ext))
|
|
468
|
+
throw new PathSanitizeError(`filename has invalid extension: '.${ext}'`, "invalid-extension");
|
|
469
|
+
}
|
|
470
|
+
return filename;
|
|
471
|
+
}
|
|
472
|
+
function parseDiskPath(input) {
|
|
473
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
474
|
+
throw new PathSanitizeError("disk-path reference is empty", "empty");
|
|
475
|
+
}
|
|
476
|
+
if (input.includes("\x00")) {
|
|
477
|
+
throw new PathSanitizeError("disk-path reference contains a null byte", "null-byte");
|
|
478
|
+
}
|
|
479
|
+
const colonIdx = input.indexOf(":");
|
|
480
|
+
if (colonIdx <= 0 || colonIdx === input.length - 1) {
|
|
481
|
+
throw new PathSanitizeError(`disk-path reference must use '<disk>:<path>' format, got '${input}'`, "invalid-char");
|
|
482
|
+
}
|
|
483
|
+
const disk = input.slice(0, colonIdx);
|
|
484
|
+
const path2 = input.slice(colonIdx + 1);
|
|
485
|
+
if (!DISK_NAME_RE.test(disk)) {
|
|
486
|
+
throw new PathSanitizeError(`disk name '${disk}' is invalid (alphanumeric + '-' / '_' only)`, "invalid-char");
|
|
487
|
+
}
|
|
488
|
+
if (path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2)) {
|
|
489
|
+
throw new PathSanitizeError(`disk-path '${path2}' is absolute`, "absolute-path");
|
|
490
|
+
}
|
|
491
|
+
if (path2.includes("\x00")) {
|
|
492
|
+
throw new PathSanitizeError("disk-path contains a null byte", "null-byte");
|
|
493
|
+
}
|
|
494
|
+
for (let i = 0;i < path2.length; i++) {
|
|
495
|
+
const code = path2.charCodeAt(i);
|
|
496
|
+
if (code < 32 || code === 127) {
|
|
497
|
+
throw new PathSanitizeError(`disk-path contains a control character at index ${i}`, "control-char");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const segments = path2.split(/[/\\]/);
|
|
501
|
+
if (segments.some((seg) => seg === "..")) {
|
|
502
|
+
throw new PathSanitizeError(`disk-path '${path2}' contains a '..' segment`, "traversal");
|
|
503
|
+
}
|
|
504
|
+
return { disk, path: path2 };
|
|
505
|
+
}
|
|
506
|
+
var PathSanitizeError, MAX_COMPONENT_LENGTH = 255, ALLOWED_DIR_CHAR, ALLOWED_FILENAME_CHAR, ALLOWED_EXTENSION, DISK_NAME_RE;
|
|
507
|
+
var init_path_sanitize = __esm(() => {
|
|
508
|
+
PathSanitizeError = class PathSanitizeError extends Error {
|
|
509
|
+
reason;
|
|
510
|
+
constructor(message, reason) {
|
|
511
|
+
super(message);
|
|
512
|
+
this.name = "PathSanitizeError";
|
|
513
|
+
this.reason = reason;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
ALLOWED_DIR_CHAR = /^[A-Za-z0-9._-]+$/;
|
|
517
|
+
ALLOWED_FILENAME_CHAR = /^[A-Za-z0-9._-]+$/;
|
|
518
|
+
ALLOWED_EXTENSION = /^[a-z0-9]+$/;
|
|
519
|
+
DISK_NAME_RE = /^[a-z0-9_-]+$/i;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// src/s3-presigned-post.ts
|
|
523
|
+
import { createHmac as createHmac3 } from "crypto";
|
|
524
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
525
|
+
function hmac(key, data) {
|
|
526
|
+
return createHmac3("sha256", key).update(data, "utf8").digest();
|
|
527
|
+
}
|
|
528
|
+
function deriveSigningKey(secretAccessKey, dateStamp, region) {
|
|
529
|
+
const kDate = hmac(`AWS4${secretAccessKey}`, dateStamp);
|
|
530
|
+
const kRegion = hmac(kDate, region);
|
|
531
|
+
const kService = hmac(kRegion, "s3");
|
|
532
|
+
const kSigning = hmac(kService, "aws4_request");
|
|
533
|
+
return kSigning;
|
|
534
|
+
}
|
|
535
|
+
function isoDate(now) {
|
|
536
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
537
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
538
|
+
return { amzDate, dateStamp };
|
|
539
|
+
}
|
|
540
|
+
function signS3PresignedPost(input) {
|
|
541
|
+
const expiresIn = Math.floor(input.expiresIn);
|
|
542
|
+
if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY || expiresIn > MAX_EXPIRY) {
|
|
543
|
+
throw new RangeError(`[storage/s3-post] expiresIn must be between ${MIN_EXPIRY}s and ${MAX_EXPIRY}s (got ${expiresIn}s)`);
|
|
544
|
+
}
|
|
545
|
+
if (!input.bucket)
|
|
546
|
+
throw new Error("[storage/s3-post] bucket is required");
|
|
547
|
+
if (!input.credentials?.accessKeyId || !input.credentials?.secretAccessKey) {
|
|
548
|
+
throw new Error("[storage/s3-post] credentials.accessKeyId and credentials.secretAccessKey are required");
|
|
549
|
+
}
|
|
550
|
+
const now = new Date;
|
|
551
|
+
const { amzDate, dateStamp } = isoDate(now);
|
|
552
|
+
const credentialScope = `${dateStamp}/${input.region}/s3/aws4_request`;
|
|
553
|
+
const credentialField = `${input.credentials.accessKeyId}/${credentialScope}`;
|
|
554
|
+
const expirationDate = new Date(now.getTime() + expiresIn * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
555
|
+
const conditions = [];
|
|
556
|
+
conditions.push({ bucket: input.bucket });
|
|
557
|
+
if (typeof input.key === "string") {
|
|
558
|
+
conditions.push({ key: input.key });
|
|
559
|
+
} else {
|
|
560
|
+
conditions.push(["starts-with", "$key", input.key.startsWith]);
|
|
561
|
+
}
|
|
562
|
+
const acl = input.acl ?? "private";
|
|
563
|
+
conditions.push({ acl });
|
|
564
|
+
if (typeof input.contentType === "string") {
|
|
565
|
+
conditions.push({ "Content-Type": input.contentType });
|
|
566
|
+
} else {
|
|
567
|
+
conditions.push(["starts-with", "$Content-Type", input.contentType.startsWith]);
|
|
568
|
+
}
|
|
569
|
+
if (input.contentLengthRange) {
|
|
570
|
+
if (!Number.isFinite(input.contentLengthRange.min) || input.contentLengthRange.min < 0 || !Number.isFinite(input.contentLengthRange.max) || input.contentLengthRange.max < input.contentLengthRange.min) {
|
|
571
|
+
throw new RangeError("[storage/s3-post] contentLengthRange must satisfy 0 <= min <= max");
|
|
572
|
+
}
|
|
573
|
+
conditions.push(["content-length-range", input.contentLengthRange.min, input.contentLengthRange.max]);
|
|
574
|
+
}
|
|
575
|
+
if (input.fields) {
|
|
576
|
+
for (const [k, v] of Object.entries(input.fields)) {
|
|
577
|
+
conditions.push({ [k]: v });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
conditions.push({ "x-amz-credential": credentialField });
|
|
581
|
+
conditions.push({ "x-amz-algorithm": ALGORITHM });
|
|
582
|
+
conditions.push({ "x-amz-date": amzDate });
|
|
583
|
+
if (input.credentials.sessionToken) {
|
|
584
|
+
conditions.push({ "x-amz-security-token": input.credentials.sessionToken });
|
|
585
|
+
}
|
|
586
|
+
const policy = {
|
|
587
|
+
expiration: expirationDate,
|
|
588
|
+
conditions
|
|
589
|
+
};
|
|
590
|
+
const policyBase64 = Buffer4.from(JSON.stringify(policy), "utf8").toString("base64");
|
|
591
|
+
const signingKey = deriveSigningKey(input.credentials.secretAccessKey, dateStamp, input.region);
|
|
592
|
+
const signature = createHmac3("sha256", signingKey).update(policyBase64, "utf8").digest("hex");
|
|
593
|
+
const fields = {
|
|
594
|
+
key: typeof input.key === "string" ? input.key : `${input.key.startsWith}\${filename}`,
|
|
595
|
+
acl,
|
|
596
|
+
"Content-Type": typeof input.contentType === "string" ? input.contentType : input.contentType.startsWith,
|
|
597
|
+
"x-amz-credential": credentialField,
|
|
598
|
+
"x-amz-algorithm": ALGORITHM,
|
|
599
|
+
"x-amz-date": amzDate,
|
|
600
|
+
policy: policyBase64,
|
|
601
|
+
"x-amz-signature": signature,
|
|
602
|
+
...input.credentials.sessionToken ? { "x-amz-security-token": input.credentials.sessionToken } : {},
|
|
603
|
+
...input.fields ?? {}
|
|
604
|
+
};
|
|
605
|
+
return {
|
|
606
|
+
url: `https://${input.bucket}.s3.${input.region}.amazonaws.com/`,
|
|
607
|
+
fields,
|
|
608
|
+
key: typeof input.key === "string" ? input.key : input.key.startsWith
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
var ALGORITHM = "AWS4-HMAC-SHA256", MIN_EXPIRY = 60, MAX_EXPIRY;
|
|
612
|
+
var init_s3_presigned_post = __esm(() => {
|
|
613
|
+
MAX_EXPIRY = 7 * 24 * 60 * 60;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// src/adapters/s3.ts
|
|
617
|
+
import { Buffer as Buffer5 } from "buffer";
|
|
618
|
+
import { basename as basename3 } from "path";
|
|
619
|
+
import process3 from "process";
|
|
620
|
+
function clampPartSize(requested) {
|
|
621
|
+
if (!Number.isFinite(requested))
|
|
622
|
+
return S3_MIN_PART_SIZE;
|
|
623
|
+
return Math.max(S3_MIN_PART_SIZE, Math.min(Math.floor(requested), S3_MAX_PART_SIZE));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
class ChunkBuffer {
|
|
627
|
+
chunks = [];
|
|
628
|
+
total = 0;
|
|
629
|
+
constructor(_partSize) {}
|
|
630
|
+
get length() {
|
|
631
|
+
return this.total;
|
|
632
|
+
}
|
|
633
|
+
push(c) {
|
|
634
|
+
this.chunks.push(c);
|
|
635
|
+
this.total += c.length;
|
|
636
|
+
}
|
|
637
|
+
take(n) {
|
|
638
|
+
const out = new Uint8Array(n);
|
|
639
|
+
let written = 0;
|
|
640
|
+
while (written < n && this.chunks.length > 0) {
|
|
641
|
+
const head = this.chunks[0];
|
|
642
|
+
const need = n - written;
|
|
643
|
+
if (head.length <= need) {
|
|
644
|
+
out.set(head, written);
|
|
645
|
+
written += head.length;
|
|
646
|
+
this.chunks.shift();
|
|
647
|
+
} else {
|
|
648
|
+
out.set(head.subarray(0, need), written);
|
|
649
|
+
this.chunks[0] = head.subarray(need);
|
|
650
|
+
written += need;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
this.total -= n;
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
flush() {
|
|
657
|
+
const out = new Uint8Array(this.total);
|
|
658
|
+
let off = 0;
|
|
659
|
+
for (const c of this.chunks) {
|
|
660
|
+
out.set(c, off);
|
|
661
|
+
off += c.length;
|
|
662
|
+
}
|
|
663
|
+
this.chunks = [];
|
|
664
|
+
this.total = 0;
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function isSettled(p3) {
|
|
669
|
+
const sentinel = Symbol("pending");
|
|
670
|
+
const result = await Promise.race([
|
|
671
|
+
p3.then(() => "settled", () => "settled"),
|
|
672
|
+
Promise.resolve(sentinel)
|
|
673
|
+
]);
|
|
674
|
+
return result !== sentinel;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
class S3StorageAdapter {
|
|
678
|
+
_client;
|
|
679
|
+
_clientPromise = null;
|
|
680
|
+
bucket;
|
|
681
|
+
prefix;
|
|
682
|
+
region;
|
|
683
|
+
credentials;
|
|
684
|
+
constructor(client, config) {
|
|
685
|
+
this._client = client;
|
|
686
|
+
this.bucket = config.bucket || "";
|
|
687
|
+
this.prefix = config.prefix || "";
|
|
688
|
+
this.region = config.region || "us-east-1";
|
|
689
|
+
this.credentials = config.credentials;
|
|
690
|
+
if (!this.bucket) {
|
|
691
|
+
throw new Error("S3 bucket name is required");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async getClient() {
|
|
695
|
+
if (this._client)
|
|
696
|
+
return this._client;
|
|
697
|
+
if (!this._clientPromise) {
|
|
698
|
+
this._clientPromise = import("@stacksjs/ts-cloud").then((cloud) => {
|
|
699
|
+
this._client = new cloud.S3Client(this.region);
|
|
700
|
+
return this._client;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return this._clientPromise;
|
|
704
|
+
}
|
|
705
|
+
resolveCredentials() {
|
|
706
|
+
if (this.credentials?.accessKeyId && this.credentials.secretAccessKey)
|
|
707
|
+
return this.credentials;
|
|
708
|
+
const accessKeyId = process3.env.AWS_ACCESS_KEY_ID;
|
|
709
|
+
const secretAccessKey = process3.env.AWS_SECRET_ACCESS_KEY;
|
|
710
|
+
const sessionToken = process3.env.AWS_SESSION_TOKEN;
|
|
711
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
712
|
+
throw new Error("[storage/s3] presignedUploadPolicy requires AWS credentials \u2014 " + "pass them via S3DiskConfig.credentials or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY.");
|
|
713
|
+
}
|
|
714
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
715
|
+
}
|
|
716
|
+
prefixPath(path2) {
|
|
717
|
+
if (!this.prefix)
|
|
718
|
+
return path2;
|
|
719
|
+
return `${this.prefix}/${path2}`.replace(/\/+/g, "/");
|
|
720
|
+
}
|
|
721
|
+
stripPrefix(path2) {
|
|
722
|
+
if (!this.prefix)
|
|
723
|
+
return path2;
|
|
724
|
+
const prefixWithSlash = `${this.prefix}/`;
|
|
725
|
+
return path2.startsWith(prefixWithSlash) ? path2.slice(prefixWithSlash.length) : path2;
|
|
726
|
+
}
|
|
727
|
+
async contentsToBuffer(contents) {
|
|
728
|
+
if (typeof contents === "string") {
|
|
729
|
+
return Buffer5.from(contents, "utf8");
|
|
730
|
+
} else if (contents instanceof Buffer5) {
|
|
731
|
+
return contents;
|
|
732
|
+
} else if (contents instanceof Uint8Array) {
|
|
733
|
+
return Buffer5.from(contents);
|
|
734
|
+
} else {
|
|
735
|
+
const stream = contents;
|
|
736
|
+
if (typeof stream.getReader !== "function") {
|
|
737
|
+
throw new TypeError("[storage/s3] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");
|
|
738
|
+
}
|
|
739
|
+
const reader = stream.getReader.call(contents);
|
|
740
|
+
const chunks = [];
|
|
741
|
+
while (true) {
|
|
742
|
+
const { done, value } = await reader.read();
|
|
743
|
+
if (done)
|
|
744
|
+
break;
|
|
745
|
+
if (value)
|
|
746
|
+
chunks.push(value);
|
|
747
|
+
}
|
|
748
|
+
return Buffer5.concat(chunks.map((c) => Buffer5.from(c)));
|
|
749
|
+
}
|
|
266
750
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
751
|
+
async write(path2, contents) {
|
|
752
|
+
const key = this.prefixPath(path2);
|
|
753
|
+
const body = await this.contentsToBuffer(contents);
|
|
754
|
+
const contentType = this.detectMimeType(path2);
|
|
755
|
+
await (await this.getClient()).putObject({
|
|
756
|
+
bucket: this.bucket,
|
|
757
|
+
key,
|
|
758
|
+
body,
|
|
759
|
+
contentType
|
|
760
|
+
});
|
|
761
|
+
return {
|
|
762
|
+
path: path2,
|
|
763
|
+
size: body.length,
|
|
764
|
+
contentType,
|
|
765
|
+
lastModified: Date.now()
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async read(path2) {
|
|
769
|
+
const key = this.prefixPath(path2);
|
|
770
|
+
const response = await (await this.getClient()).getObject(this.bucket, key);
|
|
771
|
+
if (!response) {
|
|
772
|
+
throw new Error(`Failed to read file: ${path2}`);
|
|
773
|
+
}
|
|
774
|
+
return Buffer5.from(response);
|
|
775
|
+
}
|
|
776
|
+
async getStream(path2, _options) {
|
|
777
|
+
const key = this.prefixPath(path2);
|
|
778
|
+
const buf = await (await this.getClient()).getObjectBuffer(this.bucket, key);
|
|
779
|
+
if (!buf)
|
|
780
|
+
throw new Error(`Failed to read file: ${path2}`);
|
|
781
|
+
const bytes = new Uint8Array(buf);
|
|
782
|
+
return new ReadableStream({
|
|
783
|
+
start(controller) {
|
|
784
|
+
controller.enqueue(bytes);
|
|
785
|
+
controller.close();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async putStream(path2, stream, options) {
|
|
790
|
+
const key = this.prefixPath(path2);
|
|
791
|
+
const contentType = options?.contentType ?? this.detectMimeType(path2);
|
|
792
|
+
const partSize = clampPartSize(options?.partSize ?? 5242880);
|
|
793
|
+
const concurrency = Math.max(1, Math.min(options?.concurrency ?? 4, 100));
|
|
794
|
+
const maxRetries = Math.max(0, options?.maxRetries ?? 3);
|
|
795
|
+
const signal = options?.signal;
|
|
796
|
+
const reader = stream.getReader();
|
|
797
|
+
let firstChunk = null;
|
|
798
|
+
let firstDone = false;
|
|
799
|
+
{
|
|
800
|
+
const buf = new ChunkBuffer(partSize);
|
|
801
|
+
while (!firstDone && buf.length < partSize) {
|
|
802
|
+
if (signal?.aborted) {
|
|
803
|
+
try {
|
|
804
|
+
reader.releaseLock();
|
|
805
|
+
} catch {}
|
|
806
|
+
throw new Error("aborted");
|
|
807
|
+
}
|
|
808
|
+
const { value, done } = await reader.read();
|
|
809
|
+
if (done) {
|
|
810
|
+
firstDone = true;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
if (value)
|
|
814
|
+
buf.push(value);
|
|
815
|
+
}
|
|
816
|
+
firstChunk = buf.flush();
|
|
817
|
+
}
|
|
818
|
+
if (firstDone) {
|
|
819
|
+
try {
|
|
820
|
+
reader.releaseLock();
|
|
821
|
+
} catch {}
|
|
822
|
+
await (await this.getClient()).putObject({
|
|
823
|
+
bucket: this.bucket,
|
|
824
|
+
key,
|
|
825
|
+
body: Buffer5.from(firstChunk),
|
|
826
|
+
contentType
|
|
827
|
+
});
|
|
828
|
+
return { path: path2, size: firstChunk.length, contentType, lastModified: Date.now() };
|
|
829
|
+
}
|
|
830
|
+
const { UploadId: uploadId } = await (await this.getClient()).createMultipartUpload(this.bucket, key, { contentType });
|
|
831
|
+
const completedParts = [];
|
|
832
|
+
let totalBytes = 0;
|
|
833
|
+
let partNumber = 1;
|
|
834
|
+
const inflight = [];
|
|
835
|
+
const uploadOne = async (body, n) => {
|
|
836
|
+
let attempt = 0;
|
|
837
|
+
while (true) {
|
|
838
|
+
if (signal?.aborted)
|
|
839
|
+
throw new Error("aborted");
|
|
840
|
+
try {
|
|
841
|
+
const { ETag } = await (await this.getClient()).uploadPart(this.bucket, key, uploadId, n, Buffer5.from(body));
|
|
842
|
+
completedParts.push({ PartNumber: n, ETag });
|
|
843
|
+
totalBytes += body.length;
|
|
844
|
+
return;
|
|
845
|
+
} catch (err3) {
|
|
846
|
+
if (attempt >= maxRetries)
|
|
847
|
+
throw err3;
|
|
848
|
+
attempt += 1;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
try {
|
|
853
|
+
inflight.push(uploadOne(firstChunk, partNumber++));
|
|
854
|
+
firstChunk = null;
|
|
855
|
+
const buf = new ChunkBuffer(partSize);
|
|
856
|
+
while (true) {
|
|
857
|
+
if (signal?.aborted)
|
|
858
|
+
throw new Error("aborted");
|
|
859
|
+
const { value, done } = await reader.read();
|
|
860
|
+
if (done)
|
|
861
|
+
break;
|
|
862
|
+
if (value)
|
|
863
|
+
buf.push(value);
|
|
864
|
+
while (buf.length >= partSize) {
|
|
865
|
+
const part = buf.take(partSize);
|
|
866
|
+
if (inflight.length >= concurrency) {
|
|
867
|
+
await Promise.race(inflight.map((p3, i) => p3.then(() => i)));
|
|
868
|
+
for (let i = inflight.length - 1;i >= 0; i--) {
|
|
869
|
+
if (await isSettled(inflight[i]))
|
|
870
|
+
inflight.splice(i, 1);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
inflight.push(uploadOne(part, partNumber++));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
reader.releaseLock();
|
|
878
|
+
} catch {}
|
|
879
|
+
const tail = buf.flush();
|
|
880
|
+
if (tail.length > 0)
|
|
881
|
+
inflight.push(uploadOne(tail, partNumber++));
|
|
882
|
+
await Promise.all(inflight);
|
|
883
|
+
completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
|
|
884
|
+
await (await this.getClient()).completeMultipartUpload(this.bucket, key, uploadId, completedParts);
|
|
885
|
+
return { path: path2, size: totalBytes, contentType, lastModified: Date.now() };
|
|
886
|
+
} catch (err3) {
|
|
887
|
+
try {
|
|
888
|
+
await (await this.getClient()).abortMultipartUpload(this.bucket, key, uploadId);
|
|
889
|
+
} catch {}
|
|
890
|
+
throw err3;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async readToString(path2) {
|
|
894
|
+
const key = this.prefixPath(path2);
|
|
895
|
+
const response = await (await this.getClient()).getObject(this.bucket, key);
|
|
896
|
+
if (!response) {
|
|
897
|
+
throw new Error(`Failed to read file: ${path2}`);
|
|
898
|
+
}
|
|
899
|
+
return response;
|
|
900
|
+
}
|
|
901
|
+
async readToBuffer(path2) {
|
|
902
|
+
const contents = await this.read(path2);
|
|
903
|
+
return contents;
|
|
904
|
+
}
|
|
905
|
+
async readToUint8Array(path2) {
|
|
906
|
+
const buffer = await this.readToBuffer(path2);
|
|
907
|
+
return new Uint8Array(buffer);
|
|
908
|
+
}
|
|
909
|
+
async deleteFile(path2) {
|
|
910
|
+
const key = this.prefixPath(path2);
|
|
911
|
+
await (await this.getClient()).deleteObject(this.bucket, key);
|
|
912
|
+
}
|
|
913
|
+
async deleteDirectory(path2) {
|
|
914
|
+
const prefix = this.prefixPath(path2);
|
|
915
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
916
|
+
const objects = await (await this.getClient()).listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
|
|
917
|
+
const keys = objects.map((obj) => obj.Key).filter((k) => typeof k === "string");
|
|
918
|
+
if (keys.length === 0) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
await (await this.getClient()).deleteObjects(this.bucket, keys);
|
|
922
|
+
}
|
|
923
|
+
async createDirectory(_path) {}
|
|
924
|
+
async moveFile(from, to) {
|
|
925
|
+
await this.copyFile(from, to);
|
|
926
|
+
await this.deleteFile(from);
|
|
927
|
+
}
|
|
928
|
+
async copyFile(from, to) {
|
|
929
|
+
const fromKey = this.prefixPath(from);
|
|
930
|
+
const toKey = this.prefixPath(to);
|
|
931
|
+
await (await this.getClient()).copyObject({
|
|
932
|
+
sourceBucket: this.bucket,
|
|
933
|
+
sourceKey: fromKey,
|
|
934
|
+
destinationBucket: this.bucket,
|
|
935
|
+
destinationKey: toKey
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
async stat(path2) {
|
|
939
|
+
const key = this.prefixPath(path2);
|
|
940
|
+
const result = await (await this.getClient()).headObject(this.bucket, key);
|
|
941
|
+
if (!result) {
|
|
942
|
+
throw new Error(`File not found: ${path2}`);
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
path: path2,
|
|
946
|
+
type: "file",
|
|
947
|
+
visibility: "private",
|
|
948
|
+
size: result.ContentLength || 0,
|
|
949
|
+
lastModified: result.LastModified ? new Date(result.LastModified).getTime() : Date.now(),
|
|
950
|
+
mimeType: result.ContentType
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
list(path2, options = {}) {
|
|
954
|
+
return this.createAsyncIterator(path2, options.deep || false);
|
|
955
|
+
}
|
|
956
|
+
async* createAsyncIterator(path2, deep) {
|
|
957
|
+
const prefix = this.prefixPath(path2);
|
|
958
|
+
const normalizedPrefix = prefix ? `${prefix}/` : undefined;
|
|
959
|
+
if (deep) {
|
|
960
|
+
const objects = await (await this.getClient()).listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
|
|
961
|
+
for (const obj of objects) {
|
|
962
|
+
yield {
|
|
963
|
+
path: this.stripPrefix(obj.Key),
|
|
964
|
+
type: "file"
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
let continuationToken;
|
|
969
|
+
do {
|
|
970
|
+
const result = await (await this.getClient()).listObjects({
|
|
971
|
+
bucket: this.bucket,
|
|
972
|
+
prefix: normalizedPrefix,
|
|
973
|
+
continuationToken
|
|
289
974
|
});
|
|
975
|
+
for (const obj of result.objects || []) {
|
|
976
|
+
yield {
|
|
977
|
+
path: this.stripPrefix(obj.Key),
|
|
978
|
+
type: "file"
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
continuationToken = result.nextContinuationToken;
|
|
982
|
+
} while (continuationToken);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async changeVisibility(path2, vis) {
|
|
986
|
+
const key = this.prefixPath(path2);
|
|
987
|
+
const acl = vis === "public" ? "public-read" : "private";
|
|
988
|
+
await (await this.getClient()).putObjectAcl(this.bucket, key, acl);
|
|
989
|
+
}
|
|
990
|
+
async visibility(path2) {
|
|
991
|
+
const key = this.prefixPath(path2);
|
|
992
|
+
const acl = await (await this.getClient()).getObjectAcl(this.bucket, key);
|
|
993
|
+
const grants = acl?.Grants ?? [];
|
|
994
|
+
const isPublic = grants.some((g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL"));
|
|
995
|
+
return isPublic ? "public" : "private";
|
|
996
|
+
}
|
|
997
|
+
async fileExists(path2) {
|
|
998
|
+
const key = this.prefixPath(path2);
|
|
999
|
+
try {
|
|
1000
|
+
const result = await (await this.getClient()).headObject(this.bucket, key);
|
|
1001
|
+
return !!result;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (!error.message?.includes("404") && !error.message?.includes("NoSuchKey") && !error.message?.includes("NotFound")) {
|
|
1004
|
+
console.debug(`[s3] Unexpected error checking file existence for ${path2}: ${error.message}`);
|
|
290
1005
|
}
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async directoryExists(path2) {
|
|
1010
|
+
const prefix = this.prefixPath(path2);
|
|
1011
|
+
const result = await (await this.getClient()).listObjects({
|
|
1012
|
+
bucket: this.bucket,
|
|
1013
|
+
prefix: `${prefix}/`,
|
|
1014
|
+
maxKeys: 1
|
|
291
1015
|
});
|
|
292
|
-
|
|
1016
|
+
return (result.objects || []).length > 0;
|
|
1017
|
+
}
|
|
1018
|
+
async publicUrl(path2, options = {}) {
|
|
1019
|
+
const key = this.prefixPath(path2);
|
|
1020
|
+
const domain = options.domain || `https://${this.bucket}.s3.${this.region}.amazonaws.com`;
|
|
1021
|
+
return `${domain}/${key}`;
|
|
1022
|
+
}
|
|
1023
|
+
async temporaryUrl(path2, options) {
|
|
1024
|
+
const key = this.prefixPath(path2);
|
|
1025
|
+
const expiresIn = Math.floor(normalizeExpiryToMilliseconds(options.expiresIn) / 1000);
|
|
1026
|
+
const MIN_EXPIRY2 = 60;
|
|
1027
|
+
const MAX_EXPIRY2 = 604800;
|
|
1028
|
+
if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY2 || expiresIn > MAX_EXPIRY2) {
|
|
1029
|
+
throw new RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
|
|
1030
|
+
}
|
|
1031
|
+
return await (await this.getClient()).getSignedUrl({
|
|
1032
|
+
bucket: this.bucket,
|
|
1033
|
+
key,
|
|
1034
|
+
expiresIn,
|
|
1035
|
+
operation: "getObject"
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async signedUrl(path2, options) {
|
|
1039
|
+
return this.temporaryUrl(path2, { expiresIn: options.expiresIn });
|
|
1040
|
+
}
|
|
1041
|
+
async presignedUploadUrl(options) {
|
|
1042
|
+
if (!options.contentType)
|
|
1043
|
+
throw new Error("[storage/s3] presignedUploadUrl requires `contentType` \u2014 S3 signs against the exact header.");
|
|
1044
|
+
const expiresIn = Math.floor(options.expiresIn);
|
|
1045
|
+
const MIN_EXPIRY2 = 60;
|
|
1046
|
+
const MAX_EXPIRY2 = 604800;
|
|
1047
|
+
if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY2 || expiresIn > MAX_EXPIRY2) {
|
|
1048
|
+
throw new RangeError(`[storage/s3] presignedUploadUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
|
|
1049
|
+
}
|
|
1050
|
+
const safeDir = sanitizePresignedDir(options.dir);
|
|
1051
|
+
const safeFilename = options.filename !== undefined ? sanitizePresignedFilename(options.filename) : `${crypto.randomUUID().replace(/-/g, "")}${this.extensionForContentType(options.contentType)}`;
|
|
1052
|
+
const path2 = safeDir ? `${safeDir}/${safeFilename}` : safeFilename;
|
|
1053
|
+
const key = this.prefixPath(path2);
|
|
1054
|
+
const url = await (await this.getClient()).getSignedUrl({
|
|
1055
|
+
bucket: this.bucket,
|
|
1056
|
+
key,
|
|
1057
|
+
expiresIn,
|
|
1058
|
+
operation: "putObject"
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
url,
|
|
1062
|
+
path: path2,
|
|
1063
|
+
key,
|
|
1064
|
+
contentType: options.contentType,
|
|
1065
|
+
maxBytes: options.maxBytes
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async presignedUploadPolicy(options) {
|
|
1069
|
+
const credentials = this.resolveCredentials();
|
|
1070
|
+
const scopedKey = typeof options.key === "string" ? this.prefixPath(options.key) : { startsWith: this.prefixPath(options.key.startsWith) };
|
|
1071
|
+
return signS3PresignedPost({
|
|
1072
|
+
bucket: this.bucket,
|
|
1073
|
+
region: this.region,
|
|
1074
|
+
credentials,
|
|
1075
|
+
key: scopedKey,
|
|
1076
|
+
contentType: options.contentType,
|
|
1077
|
+
contentLengthRange: options.contentLengthRange,
|
|
1078
|
+
acl: options.acl,
|
|
1079
|
+
expiresIn: options.expiresIn,
|
|
1080
|
+
fields: options.fields
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
extensionForContentType(contentType) {
|
|
1084
|
+
const mime = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
1085
|
+
const map = {
|
|
1086
|
+
"image/jpeg": ".jpg",
|
|
1087
|
+
"image/jpg": ".jpg",
|
|
1088
|
+
"image/png": ".png",
|
|
1089
|
+
"image/webp": ".webp",
|
|
1090
|
+
"image/gif": ".gif",
|
|
1091
|
+
"image/avif": ".avif",
|
|
1092
|
+
"image/svg+xml": ".svg",
|
|
1093
|
+
"application/pdf": ".pdf",
|
|
1094
|
+
"application/json": ".json",
|
|
1095
|
+
"application/zip": ".zip",
|
|
1096
|
+
"text/plain": ".txt",
|
|
1097
|
+
"text/csv": ".csv",
|
|
1098
|
+
"video/mp4": ".mp4",
|
|
1099
|
+
"video/webm": ".webm",
|
|
1100
|
+
"audio/mpeg": ".mp3",
|
|
1101
|
+
"audio/wav": ".wav"
|
|
1102
|
+
};
|
|
1103
|
+
return map[mime] ?? "";
|
|
1104
|
+
}
|
|
1105
|
+
async checksum(path2, options = {}) {
|
|
1106
|
+
const algorithm = options.algorithm || "sha256";
|
|
1107
|
+
const content = await this.readToUint8Array(path2);
|
|
1108
|
+
const hasher = new Bun.CryptoHasher(algorithm);
|
|
1109
|
+
hasher.update(content);
|
|
1110
|
+
return hasher.digest("hex");
|
|
1111
|
+
}
|
|
1112
|
+
async mimeType(path2, _options = {}) {
|
|
1113
|
+
const stats = await this.stat(path2);
|
|
1114
|
+
return stats.mimeType || this.detectMimeType(path2);
|
|
1115
|
+
}
|
|
1116
|
+
detectMimeType(path2) {
|
|
1117
|
+
const ext = basename3(path2).split(".").pop()?.toLowerCase();
|
|
1118
|
+
const mimeTypes = {
|
|
1119
|
+
txt: "text/plain",
|
|
1120
|
+
html: "text/html",
|
|
1121
|
+
css: "text/css",
|
|
1122
|
+
js: "application/javascript",
|
|
1123
|
+
json: "application/json",
|
|
1124
|
+
xml: "application/xml",
|
|
1125
|
+
pdf: "application/pdf",
|
|
1126
|
+
zip: "application/zip",
|
|
1127
|
+
jpg: "image/jpeg",
|
|
1128
|
+
jpeg: "image/jpeg",
|
|
1129
|
+
png: "image/png",
|
|
1130
|
+
gif: "image/gif",
|
|
1131
|
+
svg: "image/svg+xml",
|
|
1132
|
+
mp4: "video/mp4",
|
|
1133
|
+
mp3: "audio/mpeg",
|
|
1134
|
+
wav: "audio/wav"
|
|
1135
|
+
};
|
|
1136
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
1137
|
+
}
|
|
1138
|
+
async lastModified(path2) {
|
|
1139
|
+
const stats = await this.stat(path2);
|
|
1140
|
+
return stats.lastModified;
|
|
1141
|
+
}
|
|
1142
|
+
async fileSize(path2) {
|
|
1143
|
+
const stats = await this.stat(path2);
|
|
1144
|
+
return stats.size;
|
|
1145
|
+
}
|
|
293
1146
|
}
|
|
294
|
-
|
|
295
|
-
return
|
|
1147
|
+
function createS3Storage(client, config) {
|
|
1148
|
+
return new S3StorageAdapter(client, config);
|
|
296
1149
|
}
|
|
297
|
-
|
|
298
|
-
|
|
1150
|
+
var S3_MIN_PART_SIZE, S3_MAX_PART_SIZE;
|
|
1151
|
+
var init_s3 = __esm(() => {
|
|
1152
|
+
init_types();
|
|
1153
|
+
init_path_sanitize();
|
|
1154
|
+
init_s3_presigned_post();
|
|
1155
|
+
S3_MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
1156
|
+
S3_MAX_PART_SIZE = 5 * 1024 * 1024 * 1024;
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
// src/put-file.ts
|
|
1160
|
+
function extFromOriginalName(name) {
|
|
1161
|
+
if (!name)
|
|
1162
|
+
return null;
|
|
1163
|
+
const idx = name.lastIndexOf(".");
|
|
1164
|
+
if (idx <= 0 || idx === name.length - 1)
|
|
1165
|
+
return null;
|
|
1166
|
+
const ext = name.slice(idx + 1).toLowerCase();
|
|
1167
|
+
if (!/^[a-z0-9]+$/.test(ext))
|
|
1168
|
+
return null;
|
|
1169
|
+
return ext;
|
|
1170
|
+
}
|
|
1171
|
+
function originalNameOf(file2) {
|
|
1172
|
+
return file2.originalName ?? file2.name;
|
|
1173
|
+
}
|
|
1174
|
+
function mimetypeOf(file2) {
|
|
1175
|
+
return file2.mimetype ?? file2.mimeType;
|
|
1176
|
+
}
|
|
1177
|
+
function deriveExtension(file2) {
|
|
1178
|
+
const mime = mimetypeOf(file2);
|
|
1179
|
+
return extFromOriginalName(originalNameOf(file2)) ?? (mime && MIME_TO_EXT[mime.toLowerCase()]) ?? null;
|
|
1180
|
+
}
|
|
1181
|
+
async function readBytes(file2) {
|
|
1182
|
+
if (file2.buffer !== undefined)
|
|
1183
|
+
return file2.buffer;
|
|
1184
|
+
if (typeof file2.bytes === "function")
|
|
1185
|
+
return await file2.bytes();
|
|
1186
|
+
if (typeof file2.arrayBuffer === "function")
|
|
1187
|
+
return await file2.arrayBuffer();
|
|
1188
|
+
throw new Error("UploadedFile is missing both `buffer` and `bytes()`/`arrayBuffer()` accessors \u2014 cannot read file contents.");
|
|
1189
|
+
}
|
|
1190
|
+
function bufferLikeToHash(buffer) {
|
|
1191
|
+
const view = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
|
|
1192
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
1193
|
+
hasher.update(view);
|
|
1194
|
+
return hasher.digest("hex").slice(0, 32);
|
|
1195
|
+
}
|
|
1196
|
+
function sanitizeOriginalName(name) {
|
|
1197
|
+
const stripped = name.replace(/[/\\]/g, "_").replace(/\.{2,}/g, "_");
|
|
1198
|
+
return stripped.replace(/[^A-Za-z0-9._-]/g, "_").replace(/_+/g, "_");
|
|
1199
|
+
}
|
|
1200
|
+
async function resolveFilename(file2, strategy) {
|
|
1201
|
+
if (typeof strategy === "function")
|
|
1202
|
+
return strategy(file2);
|
|
1203
|
+
switch (strategy) {
|
|
1204
|
+
case "uuid":
|
|
1205
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
1206
|
+
case "hash": {
|
|
1207
|
+
const bytes = await readBytes(file2);
|
|
1208
|
+
return bufferLikeToHash(bytes);
|
|
1209
|
+
}
|
|
1210
|
+
case "original": {
|
|
1211
|
+
const name = originalNameOf(file2);
|
|
1212
|
+
return name ? sanitizeOriginalName(name) : crypto.randomUUID().replace(/-/g, "");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function joinPath(...parts) {
|
|
1217
|
+
return parts.filter(Boolean).map((p3, i) => i === 0 ? p3.replace(/\/+$/, "") : p3.replace(/^\/+/, "").replace(/\/+$/, "")).filter(Boolean).join("/");
|
|
1218
|
+
}
|
|
1219
|
+
async function putUploadedFile(manager, file2, opts) {
|
|
1220
|
+
const disk = manager.disk(opts.disk);
|
|
1221
|
+
const baseName = await resolveFilename(file2, opts.filename ?? "uuid");
|
|
1222
|
+
const wantExt = opts.preserveExtension !== false;
|
|
1223
|
+
const baseHasExt = /\.[A-Za-z0-9]+$/.test(baseName);
|
|
1224
|
+
const ext = wantExt && !baseHasExt ? deriveExtension(file2) : null;
|
|
1225
|
+
const finalName = ext ? `${baseName}.${ext}` : baseName;
|
|
1226
|
+
const fullPath = joinPath(opts.dir ?? "", finalName);
|
|
1227
|
+
const raw = await readBytes(file2);
|
|
1228
|
+
let contents = raw instanceof ArrayBuffer ? new Uint8Array(raw) : raw;
|
|
1229
|
+
if (opts.transform) {
|
|
1230
|
+
contents = await opts.transform(contents);
|
|
1231
|
+
}
|
|
1232
|
+
const written = await disk.write(fullPath, contents);
|
|
1233
|
+
const url = await disk.publicUrl(fullPath);
|
|
1234
|
+
return { ...written, path: fullPath, url };
|
|
1235
|
+
}
|
|
1236
|
+
var MIME_TO_EXT;
|
|
1237
|
+
var init_put_file = __esm(() => {
|
|
1238
|
+
MIME_TO_EXT = {
|
|
1239
|
+
"image/jpeg": "jpg",
|
|
1240
|
+
"image/jpg": "jpg",
|
|
1241
|
+
"image/png": "png",
|
|
1242
|
+
"image/webp": "webp",
|
|
1243
|
+
"image/gif": "gif",
|
|
1244
|
+
"image/svg+xml": "svg",
|
|
1245
|
+
"image/avif": "avif",
|
|
1246
|
+
"application/pdf": "pdf",
|
|
1247
|
+
"application/json": "json",
|
|
1248
|
+
"application/zip": "zip",
|
|
1249
|
+
"application/octet-stream": "bin",
|
|
1250
|
+
"text/plain": "txt",
|
|
1251
|
+
"text/csv": "csv",
|
|
1252
|
+
"text/html": "html",
|
|
1253
|
+
"video/mp4": "mp4",
|
|
1254
|
+
"video/webm": "webm",
|
|
1255
|
+
"audio/mpeg": "mp3",
|
|
1256
|
+
"audio/wav": "wav"
|
|
1257
|
+
};
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// src/facade.ts
|
|
1261
|
+
var exports_facade = {};
|
|
1262
|
+
__export(exports_facade, {
|
|
1263
|
+
StorageManager: () => StorageManager,
|
|
1264
|
+
Storage: () => Storage
|
|
1265
|
+
});
|
|
1266
|
+
import { resolve as resolve3 } from "path";
|
|
1267
|
+
import process6 from "process";
|
|
1268
|
+
import { filesystems, app as appConfig } from "@stacksjs/config";
|
|
1269
|
+
function buildConfig() {
|
|
1270
|
+
const cwd = process6.cwd();
|
|
1271
|
+
const rootDir = filesystems.root || cwd;
|
|
1272
|
+
const s3Config = filesystems.s3;
|
|
1273
|
+
const appUrl = appConfig?.url || "";
|
|
1274
|
+
const config = {
|
|
1275
|
+
default: filesystems.driver || "local",
|
|
1276
|
+
disks: {
|
|
1277
|
+
local: {
|
|
1278
|
+
driver: "local",
|
|
1279
|
+
root: resolve3(rootDir, "storage/app"),
|
|
1280
|
+
visibility: filesystems.defaultVisibility || "private"
|
|
1281
|
+
},
|
|
1282
|
+
public: {
|
|
1283
|
+
driver: "local",
|
|
1284
|
+
root: resolve3(rootDir, "public"),
|
|
1285
|
+
url: appUrl ? `${appUrl}/storage` : "/storage",
|
|
1286
|
+
visibility: "public"
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
if (s3Config?.bucket) {
|
|
1291
|
+
config.disks.s3 = {
|
|
1292
|
+
driver: "s3",
|
|
1293
|
+
bucket: s3Config.bucket,
|
|
1294
|
+
region: s3Config.region || "us-east-1",
|
|
1295
|
+
prefix: s3Config.prefix,
|
|
1296
|
+
endpoint: s3Config.endpoint,
|
|
1297
|
+
url: filesystems.publicUrl?.domain,
|
|
1298
|
+
usePathStyleEndpoint: !!s3Config.endpoint,
|
|
1299
|
+
visibility: filesystems.defaultVisibility || "private",
|
|
1300
|
+
credentials: s3Config.credentials ? { key: s3Config.credentials.accessKeyId, secret: s3Config.credentials.secretAccessKey } : undefined
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
return config;
|
|
299
1304
|
}
|
|
300
|
-
|
|
301
|
-
|
|
1305
|
+
|
|
1306
|
+
class StorageManager {
|
|
1307
|
+
_config = null;
|
|
1308
|
+
disks = new Map;
|
|
1309
|
+
customConfig = null;
|
|
1310
|
+
get config() {
|
|
1311
|
+
if (!this._config) {
|
|
1312
|
+
const builtConfig = buildConfig();
|
|
1313
|
+
this._config = this.customConfig ? {
|
|
1314
|
+
default: this.customConfig.default || builtConfig.default,
|
|
1315
|
+
disks: { ...builtConfig.disks, ...this.customConfig.disks }
|
|
1316
|
+
} : builtConfig;
|
|
1317
|
+
}
|
|
1318
|
+
return this._config;
|
|
1319
|
+
}
|
|
1320
|
+
init(config) {
|
|
1321
|
+
this.customConfig = config;
|
|
1322
|
+
this._config = null;
|
|
1323
|
+
this.disks.clear();
|
|
1324
|
+
return this;
|
|
1325
|
+
}
|
|
1326
|
+
disk(name) {
|
|
1327
|
+
const diskName = name || this.config.default;
|
|
1328
|
+
if (this.disks.has(diskName)) {
|
|
1329
|
+
return this.disks.get(diskName);
|
|
1330
|
+
}
|
|
1331
|
+
const diskConfig = this.config.disks[diskName];
|
|
1332
|
+
if (!diskConfig) {
|
|
1333
|
+
const available = Object.keys(this.config.disks).join(", ");
|
|
1334
|
+
throw new Error(`Disk [${diskName}] is not configured. Available: ${available}`);
|
|
1335
|
+
}
|
|
1336
|
+
const adapter2 = this.createAdapter(diskName, diskConfig);
|
|
1337
|
+
this.disks.set(diskName, adapter2);
|
|
1338
|
+
return adapter2;
|
|
1339
|
+
}
|
|
1340
|
+
createAdapter(name, config) {
|
|
1341
|
+
switch (config.driver) {
|
|
1342
|
+
case "local":
|
|
1343
|
+
return this.createLocalAdapter(config);
|
|
1344
|
+
case "s3":
|
|
1345
|
+
return this.createS3Adapter(name, config);
|
|
1346
|
+
default:
|
|
1347
|
+
throw new Error(`Unsupported driver: ${config.driver}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
createLocalAdapter(config) {
|
|
1351
|
+
return createLocalStorage({ root: config.root });
|
|
1352
|
+
}
|
|
1353
|
+
createS3Adapter(_name, config) {
|
|
1354
|
+
return new S3StorageAdapter(null, {
|
|
1355
|
+
bucket: config.bucket,
|
|
1356
|
+
region: config.region,
|
|
1357
|
+
prefix: config.prefix
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
async put(pathOrFile, contentsOrOpts) {
|
|
1361
|
+
if (typeof pathOrFile === "string") {
|
|
1362
|
+
return this.disk().write(pathOrFile, contentsOrOpts);
|
|
1363
|
+
}
|
|
1364
|
+
const opts = contentsOrOpts ?? {};
|
|
1365
|
+
return putUploadedFile(this, pathOrFile, opts);
|
|
1366
|
+
}
|
|
1367
|
+
async stat(path2) {
|
|
1368
|
+
return this.disk().stat(path2);
|
|
1369
|
+
}
|
|
1370
|
+
async getStream(path2, options) {
|
|
1371
|
+
const adapter2 = this.disk();
|
|
1372
|
+
if (typeof adapter2.getStream !== "function") {
|
|
1373
|
+
throw new Error(`[storage] disk '${this.config.default}' does not support getStream \u2014 adapter is missing the optional method`);
|
|
1374
|
+
}
|
|
1375
|
+
return adapter2.getStream(path2, options);
|
|
1376
|
+
}
|
|
1377
|
+
async putStream(path2, stream, options) {
|
|
1378
|
+
const adapter2 = this.disk();
|
|
1379
|
+
if (typeof adapter2.putStream !== "function") {
|
|
1380
|
+
throw new Error(`[storage] disk '${this.config.default}' does not support putStream \u2014 adapter is missing the optional method`);
|
|
1381
|
+
}
|
|
1382
|
+
return adapter2.putStream(path2, stream, options);
|
|
1383
|
+
}
|
|
1384
|
+
async copyAcross(source, dest) {
|
|
1385
|
+
const src = parseDiskPath(source);
|
|
1386
|
+
const dst = parseDiskPath(dest);
|
|
1387
|
+
if (src.disk === dst.disk) {
|
|
1388
|
+
const adapter2 = this.disk(src.disk);
|
|
1389
|
+
await adapter2.copyFile(src.path, dst.path);
|
|
1390
|
+
return adapter2.stat(dst.path).then((entry) => ({
|
|
1391
|
+
path: dst.path,
|
|
1392
|
+
size: entry.size,
|
|
1393
|
+
contentType: entry.mimeType,
|
|
1394
|
+
lastModified: entry.lastModified
|
|
1395
|
+
}));
|
|
1396
|
+
}
|
|
1397
|
+
const contents = await this.disk(src.disk).read(src.path);
|
|
1398
|
+
return this.disk(dst.disk).write(dst.path, contents);
|
|
1399
|
+
}
|
|
1400
|
+
async moveAcross(source, dest) {
|
|
1401
|
+
const src = parseDiskPath(source);
|
|
1402
|
+
const result = await this.copyAcross(source, dest);
|
|
1403
|
+
await this.disk(src.disk).deleteFile(src.path);
|
|
1404
|
+
return result;
|
|
1405
|
+
}
|
|
1406
|
+
async get(path2) {
|
|
1407
|
+
return this.disk().readToString(path2);
|
|
1408
|
+
}
|
|
1409
|
+
async exists(path2) {
|
|
1410
|
+
return this.disk().fileExists(path2);
|
|
1411
|
+
}
|
|
1412
|
+
async missing(path2) {
|
|
1413
|
+
return !await this.exists(path2);
|
|
1414
|
+
}
|
|
1415
|
+
async delete(path2) {
|
|
1416
|
+
return this.disk().deleteFile(path2);
|
|
1417
|
+
}
|
|
1418
|
+
async copy(from, to) {
|
|
1419
|
+
return this.disk().copyFile(from, to);
|
|
1420
|
+
}
|
|
1421
|
+
async move(from, to) {
|
|
1422
|
+
return this.disk().moveFile(from, to);
|
|
1423
|
+
}
|
|
1424
|
+
async url(path2) {
|
|
1425
|
+
return this.disk().publicUrl(path2);
|
|
1426
|
+
}
|
|
1427
|
+
async signedUrl(path2, options) {
|
|
1428
|
+
const adapter2 = this.disk();
|
|
1429
|
+
if (typeof adapter2.signedUrl !== "function") {
|
|
1430
|
+
throw new Error(`[storage] disk '${this.config.default}' does not support signedUrl`);
|
|
1431
|
+
}
|
|
1432
|
+
return adapter2.signedUrl(path2, options);
|
|
1433
|
+
}
|
|
1434
|
+
async presignedUploadUrl(options) {
|
|
1435
|
+
const adapter2 = this.disk();
|
|
1436
|
+
if (typeof adapter2.presignedUploadUrl !== "function") {
|
|
1437
|
+
throw new Error(`[storage] disk '${this.config.default}' does not support presignedUploadUrl \u2014 only S3-style adapters do. Use \`Storage.put(file, opts)\` for local/proxied uploads.`);
|
|
1438
|
+
}
|
|
1439
|
+
return adapter2.presignedUploadUrl(options);
|
|
1440
|
+
}
|
|
1441
|
+
async presignedUploadPolicy(options) {
|
|
1442
|
+
const adapter2 = this.disk();
|
|
1443
|
+
if (typeof adapter2.presignedUploadPolicy !== "function") {
|
|
1444
|
+
throw new Error(`[storage] disk '${this.config.default}' does not support presignedUploadPolicy \u2014 S3-only. Use \`presignedUploadUrl\` for the PUT-form, or \`Storage.put(file, opts)\` for server-proxied uploads.`);
|
|
1445
|
+
}
|
|
1446
|
+
return adapter2.presignedUploadPolicy(options);
|
|
1447
|
+
}
|
|
1448
|
+
async size(path2) {
|
|
1449
|
+
return this.disk().fileSize(path2);
|
|
1450
|
+
}
|
|
1451
|
+
async lastModified(path2) {
|
|
1452
|
+
return this.disk().lastModified(path2);
|
|
1453
|
+
}
|
|
1454
|
+
async mimeType(path2) {
|
|
1455
|
+
return this.disk().mimeType(path2);
|
|
1456
|
+
}
|
|
1457
|
+
async checksum(path2, algorithm) {
|
|
1458
|
+
return this.disk().checksum(path2, { algorithm });
|
|
1459
|
+
}
|
|
1460
|
+
async makeDirectory(path2) {
|
|
1461
|
+
return this.disk().createDirectory(path2);
|
|
1462
|
+
}
|
|
1463
|
+
async deleteDirectory(path2) {
|
|
1464
|
+
return this.disk().deleteDirectory(path2);
|
|
1465
|
+
}
|
|
1466
|
+
files(path2 = "") {
|
|
1467
|
+
return this.disk().list(path2);
|
|
1468
|
+
}
|
|
1469
|
+
allFiles(path2 = "") {
|
|
1470
|
+
return this.disk().list(path2, { deep: true });
|
|
1471
|
+
}
|
|
1472
|
+
configure(name, config) {
|
|
1473
|
+
const currentConfig = this.config;
|
|
1474
|
+
currentConfig.disks[name] = config;
|
|
1475
|
+
this.disks.delete(name);
|
|
1476
|
+
return this;
|
|
1477
|
+
}
|
|
1478
|
+
setDefaultDisk(name) {
|
|
1479
|
+
if (!this.config.disks[name]) {
|
|
1480
|
+
throw new Error(`Disk [${name}] is not configured`);
|
|
1481
|
+
}
|
|
1482
|
+
this.config.default = name;
|
|
1483
|
+
return this;
|
|
1484
|
+
}
|
|
1485
|
+
getDiskConfig(name) {
|
|
1486
|
+
return this.config.disks[name || this.config.default];
|
|
1487
|
+
}
|
|
1488
|
+
getConfiguredDisks() {
|
|
1489
|
+
return Object.keys(this.config.disks);
|
|
1490
|
+
}
|
|
1491
|
+
getDefaultDisk() {
|
|
1492
|
+
return this.config.default;
|
|
1493
|
+
}
|
|
1494
|
+
reset() {
|
|
1495
|
+
this._config = null;
|
|
1496
|
+
this.customConfig = null;
|
|
1497
|
+
this.disks.clear();
|
|
1498
|
+
return this;
|
|
1499
|
+
}
|
|
302
1500
|
}
|
|
303
|
-
|
|
304
|
-
|
|
1501
|
+
var Storage;
|
|
1502
|
+
var init_facade = __esm(() => {
|
|
1503
|
+
init_local();
|
|
1504
|
+
init_s3();
|
|
1505
|
+
init_path_sanitize();
|
|
1506
|
+
init_put_file();
|
|
1507
|
+
Storage = new StorageManager;
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// src/copy.ts
|
|
1511
|
+
import { contains } from "@stacksjs/arrays";
|
|
1512
|
+
import { join } from "@stacksjs/path";
|
|
1513
|
+
|
|
1514
|
+
// src/fs.ts
|
|
1515
|
+
import * as fs from "fs";
|
|
1516
|
+
import { existsSync, watch as fsWatch, mkdirSync, readFileSync, watchFile, writeFileSync } from "fs";
|
|
1517
|
+
function exists(path) {
|
|
1518
|
+
return existsSync(path);
|
|
305
1519
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1520
|
+
|
|
1521
|
+
// src/copy.ts
|
|
1522
|
+
function copy(src, dest, exclude = []) {
|
|
1523
|
+
if (Array.isArray(src)) {
|
|
1524
|
+
src.forEach((file) => {
|
|
1525
|
+
copy(file, dest, exclude);
|
|
1526
|
+
});
|
|
1527
|
+
} else {
|
|
1528
|
+
if (fs.statSync(src).isDirectory())
|
|
1529
|
+
copyFolder(src, dest, exclude);
|
|
1530
|
+
else
|
|
1531
|
+
copyFile(src, dest);
|
|
312
1532
|
}
|
|
313
1533
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
function hasFunctions() {
|
|
318
|
-
return hasFiles(p.functionsPath());
|
|
1534
|
+
function copyFile(src, dest) {
|
|
1535
|
+
fs.copyFileSync(src, dest);
|
|
319
1536
|
}
|
|
320
|
-
function
|
|
321
|
-
if (fs.existsSync(
|
|
322
|
-
fs.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
1537
|
+
function copyFolder(src, dest, exclude = []) {
|
|
1538
|
+
if (!fs.existsSync(dest))
|
|
1539
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1540
|
+
if (fs.existsSync(src)) {
|
|
1541
|
+
fs.readdirSync(src).forEach((file) => {
|
|
1542
|
+
if (!contains(join(src, file), exclude)) {
|
|
1543
|
+
const srcPath = join(src, file);
|
|
1544
|
+
const destPath = join(dest, file);
|
|
1545
|
+
if (fs.statSync(srcPath).isDirectory())
|
|
1546
|
+
copyFolder(srcPath, destPath, exclude);
|
|
327
1547
|
else
|
|
328
|
-
|
|
329
|
-
} else if (!contains2(p2, exclude)) {
|
|
330
|
-
fs.rmSync(p2);
|
|
1548
|
+
fs.copyFileSync(srcPath, destPath);
|
|
331
1549
|
}
|
|
332
1550
|
});
|
|
333
1551
|
}
|
|
334
1552
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
1553
|
+
// src/delete.ts
|
|
1554
|
+
import { italic, log } from "@stacksjs/cli";
|
|
1555
|
+
import { err, handleError, ok } from "@stacksjs/error-handling";
|
|
1556
|
+
import { join as join3 } from "@stacksjs/path";
|
|
1557
|
+
|
|
1558
|
+
// src/folders.ts
|
|
1559
|
+
import { join as join2 } from "@stacksjs/path";
|
|
1560
|
+
function isFolder(path) {
|
|
1561
|
+
try {
|
|
1562
|
+
return fs.statSync(path).isDirectory();
|
|
1563
|
+
} catch {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
347
1566
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
if (!fs.existsSync(dirPath))
|
|
351
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
352
|
-
fs.writeFileSync(path, contents, "utf-8");
|
|
1567
|
+
function isDir(path) {
|
|
1568
|
+
return isFolder(path);
|
|
353
1569
|
}
|
|
354
|
-
|
|
355
|
-
return
|
|
1570
|
+
function doesFolderExist(path) {
|
|
1571
|
+
return fs.existsSync(path);
|
|
356
1572
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
hasComponents,
|
|
365
|
-
hasFunctions,
|
|
366
|
-
deleteFiles,
|
|
367
|
-
getFiles,
|
|
368
|
-
put,
|
|
369
|
-
get
|
|
370
|
-
};
|
|
371
|
-
// src/hash.ts
|
|
372
|
-
import { createHash } from "crypto";
|
|
373
|
-
import { path as p2 } from "@stacksjs/path";
|
|
374
|
-
function hashFileOrDirectory(path, hash) {
|
|
375
|
-
if (!fs.existsSync(path)) {
|
|
376
|
-
console.error(`Path does not exist: ${path}`);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (fs.statSync(path).isDirectory()) {
|
|
380
|
-
const files2 = fs.readdirSync(path);
|
|
381
|
-
for (const file of files2) {
|
|
382
|
-
const filePath = p2.join(path, file);
|
|
383
|
-
hashFileOrDirectory(filePath, hash);
|
|
1573
|
+
function createFolder(dir) {
|
|
1574
|
+
return new Promise((resolve, reject) => {
|
|
1575
|
+
try {
|
|
1576
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1577
|
+
resolve();
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
reject(err);
|
|
384
1580
|
}
|
|
385
|
-
}
|
|
386
|
-
hash.update(fs.readFileSync(path));
|
|
387
|
-
}
|
|
1581
|
+
});
|
|
388
1582
|
}
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1583
|
+
function getFolders(dir) {
|
|
1584
|
+
return fs.readdirSync(dir).filter((file) => {
|
|
1585
|
+
return fs.statSync(join2(dir, file)).isDirectory();
|
|
1586
|
+
});
|
|
393
1587
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1588
|
+
var folders = {
|
|
1589
|
+
isFolder,
|
|
1590
|
+
doesFolderExist,
|
|
1591
|
+
createFolder,
|
|
1592
|
+
getFolders
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/glob.ts
|
|
1596
|
+
var {Glob: BunGlob } = globalThis.Bun;
|
|
1597
|
+
function isEnoent(err) {
|
|
1598
|
+
return !!err && typeof err === "object" && err.code === "ENOENT";
|
|
398
1599
|
}
|
|
399
|
-
function
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
for (const
|
|
403
|
-
|
|
404
|
-
|
|
1600
|
+
function globSync(patterns, options) {
|
|
1601
|
+
const patternArray = typeof patterns === "string" ? [patterns] : patterns;
|
|
1602
|
+
const results = [];
|
|
1603
|
+
for (const pattern of patternArray) {
|
|
1604
|
+
try {
|
|
1605
|
+
const globInstance = new BunGlob(pattern);
|
|
1606
|
+
const matches = globInstance.scanSync({
|
|
1607
|
+
cwd: options?.cwd,
|
|
1608
|
+
absolute: options?.absolute,
|
|
1609
|
+
dot: options?.dot,
|
|
1610
|
+
onlyFiles: options?.onlyFiles
|
|
1611
|
+
});
|
|
1612
|
+
for (const match of matches) {
|
|
1613
|
+
results.push(match);
|
|
1614
|
+
}
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
if (!isEnoent(err))
|
|
1617
|
+
throw err;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return results;
|
|
405
1621
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1622
|
+
async function glob(patterns, options) {
|
|
1623
|
+
const patternArray = typeof patterns === "string" ? [patterns] : patterns;
|
|
1624
|
+
const results = [];
|
|
1625
|
+
for (const pattern of patternArray) {
|
|
1626
|
+
try {
|
|
1627
|
+
const globInstance = new BunGlob(pattern);
|
|
1628
|
+
const matches = globInstance.scan({
|
|
1629
|
+
cwd: options?.cwd,
|
|
1630
|
+
absolute: options?.absolute,
|
|
1631
|
+
dot: options?.dot,
|
|
1632
|
+
onlyFiles: options?.onlyFiles
|
|
1633
|
+
});
|
|
1634
|
+
for await (const match of matches) {
|
|
1635
|
+
results.push(match);
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
if (!isEnoent(err))
|
|
1639
|
+
throw err;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return results;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/delete.ts
|
|
1646
|
+
function deleteFolder(path) {
|
|
412
1647
|
return new Promise((resolve, reject) => {
|
|
413
|
-
let config;
|
|
414
1648
|
try {
|
|
415
|
-
|
|
1649
|
+
if (isFolder(path)) {
|
|
1650
|
+
fs.rmSync(path, { recursive: true, force: true });
|
|
1651
|
+
return resolve(ok(`Deleted ${path}`));
|
|
1652
|
+
}
|
|
1653
|
+
return resolve(ok(`Path ${path} was not a directory`));
|
|
416
1654
|
} catch (error) {
|
|
417
|
-
reject(
|
|
418
|
-
return;
|
|
1655
|
+
return reject(err(error));
|
|
419
1656
|
}
|
|
420
|
-
|
|
421
|
-
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
async function isDirectoryEmpty(path) {
|
|
1660
|
+
return new Promise((resolve, reject) => {
|
|
422
1661
|
try {
|
|
423
|
-
fs.
|
|
424
|
-
|
|
1662
|
+
if (fs.statSync(path).isDirectory()) {
|
|
1663
|
+
if (fs.readdirSync(path).length === 0)
|
|
1664
|
+
return resolve(ok(true));
|
|
1665
|
+
return resolve(ok(false));
|
|
1666
|
+
}
|
|
1667
|
+
return resolve(ok(false));
|
|
425
1668
|
} catch (error) {
|
|
426
|
-
reject(error);
|
|
1669
|
+
return reject(err(error));
|
|
427
1670
|
}
|
|
428
1671
|
});
|
|
429
1672
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
updateConfigFile: () => updateConfigFile,
|
|
444
|
-
unzip: () => unzip,
|
|
445
|
-
unarchive: () => unarchive,
|
|
446
|
-
setVisibility: () => setVisibility,
|
|
447
|
-
rename: () => rename,
|
|
448
|
-
readTextFile: () => readTextFile,
|
|
449
|
-
readPackageJson: () => readPackageJson,
|
|
450
|
-
readJsonFile: () => readJsonFile,
|
|
451
|
-
readFileSync: () => readFileSync,
|
|
452
|
-
put: () => put,
|
|
453
|
-
move: () => move,
|
|
454
|
-
mkdirSync: () => mkdirSync,
|
|
455
|
-
isFolder: () => isFolder,
|
|
456
|
-
isDirectoryEmpty: () => isDirectoryEmpty,
|
|
457
|
-
isDir: () => isDir,
|
|
458
|
-
inflateSync: () => inflateSync,
|
|
459
|
-
helpers: () => helpers,
|
|
460
|
-
hasFunctions: () => hasFunctions,
|
|
461
|
-
hasFiles: () => hasFiles,
|
|
462
|
-
hasComponents: () => hasComponents,
|
|
463
|
-
gzipSync: () => gzipSync,
|
|
464
|
-
gunzipSync: () => gunzipSync,
|
|
465
|
-
getFolders: () => getFolders,
|
|
466
|
-
getFiles: () => getFiles,
|
|
467
|
-
get: () => get,
|
|
468
|
-
fsWatch: () => fsWatch,
|
|
469
|
-
fs: () => fs,
|
|
470
|
-
folders: () => folders,
|
|
471
|
-
files: () => files,
|
|
472
|
-
existsSync: () => existsSync,
|
|
473
|
-
exists: () => exists,
|
|
474
|
-
doesNotExist: () => doesNotExist,
|
|
475
|
-
doesFolderExist: () => doesFolderExist,
|
|
476
|
-
doesExist: () => doesExist,
|
|
477
|
-
deleteGlob: () => deleteGlob,
|
|
478
|
-
deleteFolder: () => deleteFolder,
|
|
479
|
-
deleteFiles: () => deleteFiles,
|
|
480
|
-
deleteFile: () => deleteFile,
|
|
481
|
-
deleteEmptyFolders: () => deleteEmptyFolders,
|
|
482
|
-
deleteEmptyFolder: () => deleteEmptyFolder,
|
|
483
|
-
del: () => del,
|
|
484
|
-
deflateSync: () => deflateSync,
|
|
485
|
-
decompress: () => decompress,
|
|
486
|
-
createFolder: () => createFolder,
|
|
487
|
-
copyFolder: () => copyFolder,
|
|
488
|
-
copyFile: () => copyFile,
|
|
489
|
-
copy: () => copy,
|
|
490
|
-
compress: () => compress,
|
|
491
|
-
archive: () => archive,
|
|
492
|
-
_dirname: () => _dirname
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// src/move.ts
|
|
496
|
-
import { err as err2, handleError as handleError2, ok as ok2 } from "@stacksjs/error-handling";
|
|
497
|
-
import { log as log3 } from "@stacksjs/logging";
|
|
498
|
-
import { path } from "@stacksjs/path";
|
|
499
|
-
async function move(src, dest, options) {
|
|
500
|
-
try {
|
|
501
|
-
if (Array.isArray(src)) {
|
|
502
|
-
const errors = [];
|
|
503
|
-
const operations = src.map(async (file) => {
|
|
504
|
-
const from2 = file;
|
|
505
|
-
const to2 = path.resolve(dest, path.basename(file));
|
|
506
|
-
const result2 = await rename(from2, to2, options);
|
|
507
|
-
if (result2.isErr) {
|
|
508
|
-
log3.error(result2.error);
|
|
509
|
-
errors.push(result2.error);
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
await Promise.all(operations);
|
|
513
|
-
if (errors.length > 0)
|
|
514
|
-
return err2(handleError2(errors[0]));
|
|
515
|
-
return ok2({ message: "Files moved successfully" });
|
|
1673
|
+
async function deleteEmptyFolder(path) {
|
|
1674
|
+
return new Promise((resolve, reject) => {
|
|
1675
|
+
try {
|
|
1676
|
+
if (fs.statSync(path).isDirectory()) {
|
|
1677
|
+
if (fs.readdirSync(path).length === 0) {
|
|
1678
|
+
fs.rmSync(path, { recursive: true, force: true });
|
|
1679
|
+
return resolve(ok(`Deleted ${path}`));
|
|
1680
|
+
}
|
|
1681
|
+
return resolve(ok(`Path ${path} was not empty`));
|
|
1682
|
+
}
|
|
1683
|
+
return resolve(ok(`Path ${path} was not a directory`));
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
return reject(err(error));
|
|
516
1686
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
async function deleteEmptyFolders(dir) {
|
|
1690
|
+
try {
|
|
1691
|
+
if (!fs.existsSync(dir))
|
|
1692
|
+
return ok(`Path ${dir} does not exist`);
|
|
1693
|
+
const files = fs.readdirSync(dir);
|
|
1694
|
+
for (const file of files) {
|
|
1695
|
+
const p = join3(dir, file);
|
|
1696
|
+
if (isFolder(p)) {
|
|
1697
|
+
if (fs.readdirSync(p).length === 0)
|
|
1698
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
1699
|
+
else
|
|
1700
|
+
await deleteEmptyFolders(p);
|
|
1701
|
+
}
|
|
523
1702
|
}
|
|
524
|
-
return
|
|
1703
|
+
return ok(`Deleted empty folders located in ${dir}`);
|
|
525
1704
|
} catch (error) {
|
|
526
|
-
return
|
|
1705
|
+
return err(error);
|
|
527
1706
|
}
|
|
528
1707
|
}
|
|
529
|
-
|
|
1708
|
+
function deleteFile(path) {
|
|
530
1709
|
return new Promise((resolve, reject) => {
|
|
531
1710
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (!fs.existsSync(from))
|
|
536
|
-
return reject(err2(new Error(`File or directory does not exist: ${from}`)));
|
|
537
|
-
if (fs.existsSync(to)) {
|
|
538
|
-
if (!options?.overwrite)
|
|
539
|
-
return reject(err2(new Error(`File or directory already exists: ${to}`)));
|
|
540
|
-
fs.rmSync(to, { recursive: true, force: true });
|
|
1711
|
+
if (fs.statSync(path).isFile()) {
|
|
1712
|
+
fs.rmSync(path, { recursive: true, force: true });
|
|
1713
|
+
return resolve(ok(`Deleted ${path}`));
|
|
541
1714
|
}
|
|
542
|
-
|
|
543
|
-
return resolve(ok2({ message: "File moved successfully" }));
|
|
1715
|
+
return resolve(ok(`Path ${path} was not a file`));
|
|
544
1716
|
} catch (error) {
|
|
545
|
-
|
|
546
|
-
log3.error(`File or directory does not exist
|
|
547
|
-
|
|
548
|
-
`, error);
|
|
549
|
-
else
|
|
550
|
-
log3.error(error);
|
|
551
|
-
return reject(err2(new Error(error)));
|
|
1717
|
+
return reject(err(error));
|
|
552
1718
|
}
|
|
553
1719
|
});
|
|
554
1720
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (Array.isArray(from)) {
|
|
567
|
-
const fromPath = from.map((f) => shellEscape(f)).join(" ");
|
|
568
|
-
return runCommand(`zip -r ${shellEscape(toPath)} ${fromPath}`, options);
|
|
569
|
-
}
|
|
570
|
-
return runCommand(`zip -r ${shellEscape(toPath)} ${shellEscape(from)}`, options);
|
|
571
|
-
}
|
|
572
|
-
async function unzip(paths) {
|
|
573
|
-
if (Array.isArray(paths))
|
|
574
|
-
return runCommand(`unzip ${paths.map((p3) => shellEscape(p3)).join(" ")}`);
|
|
575
|
-
return runCommand(`unzip ${shellEscape(paths)}`);
|
|
576
|
-
}
|
|
577
|
-
function archive(paths) {
|
|
578
|
-
return zip(paths);
|
|
579
|
-
}
|
|
580
|
-
function unarchive(paths) {
|
|
581
|
-
return unzip(paths);
|
|
582
|
-
}
|
|
583
|
-
function compress(paths) {
|
|
584
|
-
return zip(paths);
|
|
585
|
-
}
|
|
586
|
-
function decompress(paths) {
|
|
587
|
-
return unzip(paths);
|
|
588
|
-
}
|
|
589
|
-
function gzipSync(data, options) {
|
|
590
|
-
return Bun.gzipSync(data, options);
|
|
591
|
-
}
|
|
592
|
-
function gunzipSync(data) {
|
|
593
|
-
return Bun.gunzipSync(data);
|
|
594
|
-
}
|
|
595
|
-
function deflateSync(data, options) {
|
|
596
|
-
return Bun.deflateSync(data, options);
|
|
597
|
-
}
|
|
598
|
-
function inflateSync(data) {
|
|
599
|
-
return Bun.inflateSync(data);
|
|
600
|
-
}
|
|
601
|
-
// src/adapters/local.ts
|
|
602
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
603
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
604
|
-
import { createWriteStream } from "fs";
|
|
605
|
-
import { access, constants, copyFile as copyFile2, lstat, mkdir, readdir, readFile, rename as rename2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
606
|
-
import { basename, dirname as dirname3, join as join5, relative } from "path";
|
|
607
|
-
import { pipeline } from "stream/promises";
|
|
608
|
-
|
|
609
|
-
// src/types.ts
|
|
610
|
-
var Visibility;
|
|
611
|
-
((Visibility2) => {
|
|
612
|
-
Visibility2["PUBLIC"] = "public";
|
|
613
|
-
Visibility2["PRIVATE"] = "private";
|
|
614
|
-
})(Visibility ||= {});
|
|
615
|
-
async function* createDirectoryListing(entries) {
|
|
616
|
-
for (const entry of entries) {
|
|
617
|
-
yield entry;
|
|
1721
|
+
async function deleteGlob(path) {
|
|
1722
|
+
if (!path.includes("*"))
|
|
1723
|
+
return err(handleError(`Path ${path} does not contain a glob`));
|
|
1724
|
+
const directories = await glob([path], { onlyDirectories: true });
|
|
1725
|
+
for (const directory of directories) {
|
|
1726
|
+
const result = await deleteFolder(directory);
|
|
1727
|
+
if (result.isErr) {
|
|
1728
|
+
log.error(result.error);
|
|
1729
|
+
return result;
|
|
1730
|
+
}
|
|
1731
|
+
log.info(`Deleted ${italic(directory)}`);
|
|
618
1732
|
}
|
|
1733
|
+
return ok(`Deleted ${directories.length} directories`);
|
|
619
1734
|
}
|
|
620
|
-
function
|
|
621
|
-
if (
|
|
622
|
-
return
|
|
623
|
-
|
|
624
|
-
|
|
1735
|
+
async function del(path) {
|
|
1736
|
+
if (fs.existsSync(path) && fs.statSync(path).isFile())
|
|
1737
|
+
return await deleteFile(path);
|
|
1738
|
+
if (isFolder(path))
|
|
1739
|
+
return await deleteFolder(path);
|
|
1740
|
+
if (path.includes("*"))
|
|
1741
|
+
return await deleteGlob(path);
|
|
1742
|
+
return err(handleError(`Path ${path} cannot be deleted due to an unhandled condition. Please report this issue.`));
|
|
625
1743
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1744
|
+
// src/files.ts
|
|
1745
|
+
import { contains as contains2 } from "@stacksjs/arrays";
|
|
1746
|
+
import { log as log2 } from "@stacksjs/logging";
|
|
1747
|
+
import { dirname, join as join4, path as p } from "@stacksjs/path";
|
|
1748
|
+
import { detectIndent, detectNewline } from "@stacksjs/strings";
|
|
1749
|
+
async function readJsonFile(name, cwd) {
|
|
1750
|
+
const file = await readTextFile(name, cwd);
|
|
1751
|
+
let data;
|
|
1752
|
+
try {
|
|
1753
|
+
data = JSON.parse(file.data);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
throw new Error(`Failed to parse JSON file "${name}": ${error.message}`);
|
|
629
1756
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return entry.type === "file";
|
|
1757
|
+
const indent = detectIndent(file.data).indent;
|
|
1758
|
+
const newline = detectNewline(file.data);
|
|
1759
|
+
return { ...file, data, indent, newline };
|
|
634
1760
|
}
|
|
635
|
-
function
|
|
636
|
-
|
|
1761
|
+
async function readPackageJson(name, cwd) {
|
|
1762
|
+
const file = await readJsonFile(name, cwd);
|
|
1763
|
+
return file.data;
|
|
637
1764
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
function getAppKey() {
|
|
645
|
-
const k = process2.env.APP_KEY;
|
|
646
|
-
if (!k || k.length < 16) {
|
|
647
|
-
if (process2.env.APP_ENV === "production" || process2.env.NODE_ENV === "production") {
|
|
648
|
-
throw new Error("[storage/signed-url] APP_KEY is missing or too short (need \u226516 chars). Cannot sign URL.");
|
|
649
|
-
}
|
|
1765
|
+
async function writeFile(path, data) {
|
|
1766
|
+
if (typeof path === "string") {
|
|
1767
|
+
const dirPath = dirname(path);
|
|
1768
|
+
if (!await existsSync(dirPath))
|
|
1769
|
+
await createFolder(dirPath);
|
|
1770
|
+
return await Bun.write(Bun.file(path), data);
|
|
650
1771
|
}
|
|
651
|
-
return
|
|
652
|
-
}
|
|
653
|
-
function base64UrlEncode(buf) {
|
|
654
|
-
return buf.toString("base64url");
|
|
655
|
-
}
|
|
656
|
-
function base64UrlDecode(str) {
|
|
657
|
-
return Buffer.from(str, "base64url");
|
|
1772
|
+
return await Bun.write(path, data);
|
|
658
1773
|
}
|
|
659
|
-
function
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1774
|
+
async function writeJsonFile(file) {
|
|
1775
|
+
let json = JSON.stringify(file.data, undefined, file.indent);
|
|
1776
|
+
if (file.newline)
|
|
1777
|
+
json += file.newline;
|
|
1778
|
+
return writeTextFile({ ...file, data: json });
|
|
663
1779
|
}
|
|
664
|
-
function
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1780
|
+
function readTextFile(name, cwd) {
|
|
1781
|
+
return new Promise((resolve, reject) => {
|
|
1782
|
+
let filePath;
|
|
1783
|
+
if (cwd)
|
|
1784
|
+
filePath = join4(cwd, name);
|
|
1785
|
+
else
|
|
1786
|
+
filePath = name;
|
|
1787
|
+
fs.readFile(filePath, "utf8", (err2, text) => {
|
|
1788
|
+
if (err2) {
|
|
1789
|
+
reject(err2);
|
|
1790
|
+
} else {
|
|
1791
|
+
resolve({
|
|
1792
|
+
path: filePath,
|
|
1793
|
+
data: text
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
});
|
|
679
1798
|
}
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
let providedSig;
|
|
694
|
-
try {
|
|
695
|
-
providedSig = base64UrlDecode(sigPart);
|
|
696
|
-
} catch {
|
|
697
|
-
return { valid: false, reason: "malformed" };
|
|
698
|
-
}
|
|
699
|
-
if (providedSig.length !== expectedSig.length || !timingSafeEqual(providedSig, expectedSig)) {
|
|
700
|
-
return { valid: false, reason: "bad_signature" };
|
|
701
|
-
}
|
|
702
|
-
let claims;
|
|
1799
|
+
async function writeTextFile(file) {
|
|
1800
|
+
return await Bun.write(file.path, file.data);
|
|
1801
|
+
}
|
|
1802
|
+
function isFile(path) {
|
|
1803
|
+
return fs.existsSync(path);
|
|
1804
|
+
}
|
|
1805
|
+
function doesExist(path) {
|
|
1806
|
+
return isFile(path) || isFolder(path);
|
|
1807
|
+
}
|
|
1808
|
+
function doesNotExist(path) {
|
|
1809
|
+
return !isFile(path) && !isFolder(path);
|
|
1810
|
+
}
|
|
1811
|
+
function hasFiles(folder) {
|
|
703
1812
|
try {
|
|
704
|
-
|
|
705
|
-
} catch {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const now = Math.floor(Date.now() / 1000);
|
|
709
|
-
if (typeof claims.exp !== "number" || now >= claims.exp) {
|
|
710
|
-
return { valid: false, reason: "expired" };
|
|
711
|
-
}
|
|
712
|
-
if (claims.path !== requestedPath) {
|
|
713
|
-
return { valid: false, reason: "path_mismatch" };
|
|
1813
|
+
return fs.readdirSync(folder).length > 0;
|
|
1814
|
+
} catch (err2) {
|
|
1815
|
+
log2.debug(`Error reading folder: ${folder}`, err2);
|
|
1816
|
+
return false;
|
|
714
1817
|
}
|
|
715
|
-
return { valid: true, claims };
|
|
716
1818
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1819
|
+
function hasComponents() {
|
|
1820
|
+
return hasFiles(p.componentsPath());
|
|
1821
|
+
}
|
|
1822
|
+
function hasFunctions() {
|
|
1823
|
+
return hasFiles(p.functionsPath());
|
|
1824
|
+
}
|
|
1825
|
+
function deleteFiles(dir, exclude = []) {
|
|
1826
|
+
if (fs.existsSync(dir)) {
|
|
1827
|
+
fs.readdirSync(dir).forEach((file) => {
|
|
1828
|
+
const p2 = join4(dir, file);
|
|
1829
|
+
if (fs.statSync(p2).isDirectory()) {
|
|
1830
|
+
if (fs.readdirSync(p2).length === 0)
|
|
1831
|
+
fs.rmSync(p2, { recursive: true, force: true });
|
|
1832
|
+
else
|
|
1833
|
+
deleteFiles(p2, exclude);
|
|
1834
|
+
} else if (!contains2(p2, exclude)) {
|
|
1835
|
+
fs.rmSync(p2);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
723
1838
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1839
|
+
}
|
|
1840
|
+
function getFiles(dir, exclude = []) {
|
|
1841
|
+
let results = [];
|
|
1842
|
+
const list = fs.readdirSync(dir);
|
|
1843
|
+
list.forEach((file) => {
|
|
1844
|
+
file = join4(dir, file);
|
|
1845
|
+
const stat = fs.statSync(file);
|
|
1846
|
+
if (stat.isDirectory())
|
|
1847
|
+
results = results.concat(getFiles(file, exclude));
|
|
1848
|
+
else if (!contains2(file, exclude))
|
|
1849
|
+
results.push(file);
|
|
1850
|
+
});
|
|
1851
|
+
return results;
|
|
1852
|
+
}
|
|
1853
|
+
function put(path, contents) {
|
|
1854
|
+
const dirPath = dirname(path);
|
|
1855
|
+
if (!fs.existsSync(dirPath))
|
|
1856
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1857
|
+
fs.writeFileSync(path, contents, "utf-8");
|
|
1858
|
+
}
|
|
1859
|
+
async function get(path) {
|
|
1860
|
+
return Bun.file(path).text();
|
|
1861
|
+
}
|
|
1862
|
+
var files = {
|
|
1863
|
+
readJsonFile,
|
|
1864
|
+
readPackageJson,
|
|
1865
|
+
readTextFile,
|
|
1866
|
+
writeJsonFile,
|
|
1867
|
+
writeTextFile,
|
|
1868
|
+
hasFiles,
|
|
1869
|
+
hasComponents,
|
|
1870
|
+
hasFunctions,
|
|
1871
|
+
deleteFiles,
|
|
1872
|
+
getFiles,
|
|
1873
|
+
put,
|
|
1874
|
+
get
|
|
1875
|
+
};
|
|
1876
|
+
// src/hash.ts
|
|
1877
|
+
import { createHash } from "crypto";
|
|
1878
|
+
import { path as p2 } from "@stacksjs/path";
|
|
1879
|
+
function hashFileOrDirectory(path, hash) {
|
|
1880
|
+
if (!fs.existsSync(path)) {
|
|
1881
|
+
console.error(`Path does not exist: ${path}`);
|
|
1882
|
+
return;
|
|
731
1883
|
}
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
await writeFile2(fullPath, contents, "utf8");
|
|
738
|
-
} else if (contents instanceof Buffer2) {
|
|
739
|
-
await writeFile2(fullPath, contents);
|
|
740
|
-
} else if (contents instanceof Uint8Array) {
|
|
741
|
-
await writeFile2(fullPath, contents);
|
|
742
|
-
} else {
|
|
743
|
-
const writeStream = createWriteStream(fullPath);
|
|
744
|
-
await pipeline(contents, writeStream);
|
|
1884
|
+
if (fs.statSync(path).isDirectory()) {
|
|
1885
|
+
const files2 = fs.readdirSync(path);
|
|
1886
|
+
for (const file of files2) {
|
|
1887
|
+
const filePath = p2.join(path, file);
|
|
1888
|
+
hashFileOrDirectory(filePath, hash);
|
|
745
1889
|
}
|
|
1890
|
+
} else {
|
|
1891
|
+
hash.update(fs.readFileSync(path));
|
|
746
1892
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const fullPath = this.resolvePath(path2);
|
|
774
|
-
await mkdir(fullPath, { recursive: true });
|
|
775
|
-
}
|
|
776
|
-
async moveFile(from, to) {
|
|
777
|
-
const fromPath = this.resolvePath(from);
|
|
778
|
-
const toPath = this.resolvePath(to);
|
|
779
|
-
const toDir = dirname3(toPath);
|
|
780
|
-
await mkdir(toDir, { recursive: true });
|
|
781
|
-
await rename2(fromPath, toPath);
|
|
782
|
-
}
|
|
783
|
-
async copyFile(from, to) {
|
|
784
|
-
const fromPath = this.resolvePath(from);
|
|
785
|
-
const toPath = this.resolvePath(to);
|
|
786
|
-
const toDir = dirname3(toPath);
|
|
787
|
-
await mkdir(toDir, { recursive: true });
|
|
788
|
-
await copyFile2(fromPath, toPath);
|
|
789
|
-
}
|
|
790
|
-
async stat(path2) {
|
|
791
|
-
const fullPath = this.resolvePath(path2);
|
|
792
|
-
const stats = await lstat(fullPath);
|
|
793
|
-
return {
|
|
794
|
-
path: path2,
|
|
795
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
796
|
-
visibility: await this.visibility(path2),
|
|
797
|
-
size: stats.size,
|
|
798
|
-
lastModified: stats.mtimeMs,
|
|
799
|
-
mimeType: stats.isFile() ? await this.detectMimeType(fullPath) : undefined
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
list(path2, options = {}) {
|
|
803
|
-
return this.createAsyncIterator(path2, options.deep || false);
|
|
804
|
-
}
|
|
805
|
-
async* createAsyncIterator(path2, deep) {
|
|
806
|
-
const fullPath = this.resolvePath(path2);
|
|
1893
|
+
}
|
|
1894
|
+
function hashDirectory(directory) {
|
|
1895
|
+
const hash = createHash("sha256");
|
|
1896
|
+
hashFileOrDirectory(directory, hash);
|
|
1897
|
+
return hash.digest("hex");
|
|
1898
|
+
}
|
|
1899
|
+
function hashPath(path) {
|
|
1900
|
+
const hash = createHash("sha256");
|
|
1901
|
+
hashFileOrDirectory(path, hash);
|
|
1902
|
+
return hash.digest("hex");
|
|
1903
|
+
}
|
|
1904
|
+
function hashPaths(paths) {
|
|
1905
|
+
const hash = createHash("sha256");
|
|
1906
|
+
const pathsArray = Array.isArray(paths) ? paths : [paths];
|
|
1907
|
+
for (const path of pathsArray)
|
|
1908
|
+
hashFileOrDirectory(path, hash);
|
|
1909
|
+
return hash.digest("hex");
|
|
1910
|
+
}
|
|
1911
|
+
// src/helpers.ts
|
|
1912
|
+
import { fileURLToPath } from "url";
|
|
1913
|
+
import { dirname as dirname2 } from "@stacksjs/path";
|
|
1914
|
+
var __dirname = "/home/runner/work/stacks/stacks/storage/framework/core/storage/src";
|
|
1915
|
+
var _dirname = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
|
|
1916
|
+
function updateConfigFile(filePath, newConfig) {
|
|
1917
|
+
return new Promise((resolve, reject) => {
|
|
1918
|
+
let config;
|
|
807
1919
|
try {
|
|
808
|
-
|
|
809
|
-
} catch {
|
|
1920
|
+
config = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
reject(new Error(`Failed to parse config file "${filePath}": ${error.message}`));
|
|
810
1923
|
return;
|
|
811
1924
|
}
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
async readDirectoryRecursive(dirPath, deep) {
|
|
816
|
-
const entries = [];
|
|
1925
|
+
for (const key in newConfig)
|
|
1926
|
+
config[key] = newConfig[key];
|
|
817
1927
|
try {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1928
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
1929
|
+
resolve();
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
reject(error);
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
var helpers = {
|
|
1936
|
+
_dirname,
|
|
1937
|
+
updateConfigFile
|
|
1938
|
+
};
|
|
1939
|
+
// src/storage.ts
|
|
1940
|
+
var exports_storage = {};
|
|
1941
|
+
__export(exports_storage, {
|
|
1942
|
+
zip: () => zip,
|
|
1943
|
+
writeTextFile: () => writeTextFile,
|
|
1944
|
+
writeJsonFile: () => writeJsonFile,
|
|
1945
|
+
writeFileSync: () => writeFileSync,
|
|
1946
|
+
writeFile: () => writeFile,
|
|
1947
|
+
watchFile: () => watchFile,
|
|
1948
|
+
updateConfigFile: () => updateConfigFile,
|
|
1949
|
+
unzip: () => unzip,
|
|
1950
|
+
unarchive: () => unarchive,
|
|
1951
|
+
setVisibility: () => setVisibility,
|
|
1952
|
+
rename: () => rename,
|
|
1953
|
+
readTextFile: () => readTextFile,
|
|
1954
|
+
readPackageJson: () => readPackageJson,
|
|
1955
|
+
readJsonFile: () => readJsonFile,
|
|
1956
|
+
readFileSync: () => readFileSync,
|
|
1957
|
+
put: () => put,
|
|
1958
|
+
move: () => move,
|
|
1959
|
+
mkdirSync: () => mkdirSync,
|
|
1960
|
+
isFolder: () => isFolder,
|
|
1961
|
+
isDirectoryEmpty: () => isDirectoryEmpty,
|
|
1962
|
+
isDir: () => isDir,
|
|
1963
|
+
inflateSync: () => inflateSync,
|
|
1964
|
+
helpers: () => helpers,
|
|
1965
|
+
hasFunctions: () => hasFunctions,
|
|
1966
|
+
hasFiles: () => hasFiles,
|
|
1967
|
+
hasComponents: () => hasComponents,
|
|
1968
|
+
gzipSync: () => gzipSync,
|
|
1969
|
+
gunzipSync: () => gunzipSync,
|
|
1970
|
+
getFolders: () => getFolders,
|
|
1971
|
+
getFiles: () => getFiles,
|
|
1972
|
+
get: () => get,
|
|
1973
|
+
fsWatch: () => fsWatch,
|
|
1974
|
+
fs: () => fs,
|
|
1975
|
+
folders: () => folders,
|
|
1976
|
+
files: () => files,
|
|
1977
|
+
existsSync: () => existsSync,
|
|
1978
|
+
exists: () => exists,
|
|
1979
|
+
doesNotExist: () => doesNotExist,
|
|
1980
|
+
doesFolderExist: () => doesFolderExist,
|
|
1981
|
+
doesExist: () => doesExist,
|
|
1982
|
+
deleteGlob: () => deleteGlob,
|
|
1983
|
+
deleteFolder: () => deleteFolder,
|
|
1984
|
+
deleteFiles: () => deleteFiles,
|
|
1985
|
+
deleteFile: () => deleteFile,
|
|
1986
|
+
deleteEmptyFolders: () => deleteEmptyFolders,
|
|
1987
|
+
deleteEmptyFolder: () => deleteEmptyFolder,
|
|
1988
|
+
del: () => del,
|
|
1989
|
+
deflateSync: () => deflateSync,
|
|
1990
|
+
decompress: () => decompress,
|
|
1991
|
+
createFolder: () => createFolder,
|
|
1992
|
+
copyFolder: () => copyFolder,
|
|
1993
|
+
copyFile: () => copyFile,
|
|
1994
|
+
copy: () => copy,
|
|
1995
|
+
compress: () => compress,
|
|
1996
|
+
archive: () => archive,
|
|
1997
|
+
_dirname: () => _dirname
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// src/move.ts
|
|
2001
|
+
import { err as err2, handleError as handleError2, ok as ok2 } from "@stacksjs/error-handling";
|
|
2002
|
+
import { log as log3 } from "@stacksjs/logging";
|
|
2003
|
+
import { path } from "@stacksjs/path";
|
|
2004
|
+
async function move(src, dest, options) {
|
|
2005
|
+
try {
|
|
2006
|
+
if (Array.isArray(src)) {
|
|
2007
|
+
const errors = [];
|
|
2008
|
+
const operations = src.map(async (file) => {
|
|
2009
|
+
const from2 = file;
|
|
2010
|
+
const to2 = path.resolve(dest, path.basename(file));
|
|
2011
|
+
const result2 = await rename(from2, to2, options);
|
|
2012
|
+
if (result2.isErr) {
|
|
2013
|
+
log3.error(result2.error);
|
|
2014
|
+
errors.push(result2.error);
|
|
829
2015
|
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
}
|
|
2016
|
+
});
|
|
2017
|
+
await Promise.all(operations);
|
|
2018
|
+
if (errors.length > 0)
|
|
2019
|
+
return err2(handleError2(errors[0]));
|
|
2020
|
+
return ok2({ message: "Files moved successfully" });
|
|
835
2021
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
async fileExists(path2) {
|
|
843
|
-
const fullPath = this.resolvePath(path2);
|
|
844
|
-
try {
|
|
845
|
-
const stats = await lstat(fullPath);
|
|
846
|
-
return stats.isFile();
|
|
847
|
-
} catch {
|
|
848
|
-
return false;
|
|
2022
|
+
const from = src;
|
|
2023
|
+
const to = dest;
|
|
2024
|
+
const result = await rename(from, to, options);
|
|
2025
|
+
if (result.isErr) {
|
|
2026
|
+
log3.error(result.error);
|
|
2027
|
+
return err2(handleError2(result.error));
|
|
849
2028
|
}
|
|
2029
|
+
return ok2({ message: "File moved successfully" });
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
return err2(handleError2(error));
|
|
850
2032
|
}
|
|
851
|
-
|
|
852
|
-
|
|
2033
|
+
}
|
|
2034
|
+
async function rename(from, to, options) {
|
|
2035
|
+
return new Promise((resolve, reject) => {
|
|
853
2036
|
try {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
2037
|
+
const dir = path.dirname(to);
|
|
2038
|
+
if (!fs.existsSync(dir))
|
|
2039
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2040
|
+
if (!fs.existsSync(from))
|
|
2041
|
+
return reject(err2(new Error(`File or directory does not exist: ${from}`)));
|
|
2042
|
+
if (fs.existsSync(to)) {
|
|
2043
|
+
if (!options?.overwrite)
|
|
2044
|
+
return reject(err2(new Error(`File or directory already exists: ${to}`)));
|
|
2045
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
2046
|
+
}
|
|
2047
|
+
fs.renameSync(from, to);
|
|
2048
|
+
return resolve(ok2({ message: "File moved successfully" }));
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
if (error.code === "ENOENT")
|
|
2051
|
+
log3.error(`File or directory does not exist
|
|
2052
|
+
|
|
2053
|
+
`, error);
|
|
2054
|
+
else
|
|
2055
|
+
log3.error(error);
|
|
2056
|
+
return reject(err2(new Error(error)));
|
|
858
2057
|
}
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
// src/visibility.ts
|
|
2061
|
+
function setVisibility() {
|
|
2062
|
+
return "wip";
|
|
2063
|
+
}
|
|
2064
|
+
// src/zip.ts
|
|
2065
|
+
import { runCommand } from "@stacksjs/cli";
|
|
2066
|
+
function shellEscape(_arg) {
|
|
2067
|
+
return `'${_arg.replace(/'/g, "'\\''")}'`;
|
|
2068
|
+
}
|
|
2069
|
+
async function zip(from, to, options) {
|
|
2070
|
+
const toPath = to || "archive.zip";
|
|
2071
|
+
if (Array.isArray(from)) {
|
|
2072
|
+
const fromPath = from.map((f) => shellEscape(f)).join(" ");
|
|
2073
|
+
return runCommand(`zip -r ${shellEscape(toPath)} ${fromPath}`, options);
|
|
859
2074
|
}
|
|
860
|
-
|
|
861
|
-
const domain = options.domain || "http://localhost";
|
|
862
|
-
return `${domain}/${path2}`;
|
|
863
|
-
}
|
|
864
|
-
async temporaryUrl(path2, options) {
|
|
865
|
-
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
866
|
-
const payload = `${path2}:${expiry.getTime()}`;
|
|
867
|
-
const appKey = process.env.APP_KEY || "stacks-default-key";
|
|
868
|
-
const signature = createHmac2("sha256", appKey).update(payload).digest("hex");
|
|
869
|
-
const token = Buffer2.from(`${payload}:${signature}`).toString("base64url");
|
|
870
|
-
return `http://localhost/temp/${token}`;
|
|
871
|
-
}
|
|
872
|
-
async signedUrl(path2, options) {
|
|
873
|
-
const token = createSignedStorageToken(path2, options);
|
|
874
|
-
const baseUrl = (options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
|
|
875
|
-
return `${baseUrl}/__storage/${encodeURIComponent(path2)}?token=${token}`;
|
|
876
|
-
}
|
|
877
|
-
async checksum(path2, options = {}) {
|
|
878
|
-
const algorithm = options.algorithm || "sha256";
|
|
879
|
-
const fullPath = this.resolvePath(path2);
|
|
880
|
-
const content = await readFile(fullPath);
|
|
881
|
-
const hasher = new Bun.CryptoHasher(algorithm);
|
|
882
|
-
hasher.update(content);
|
|
883
|
-
return hasher.digest("hex");
|
|
884
|
-
}
|
|
885
|
-
async mimeType(path2, options = {}) {
|
|
886
|
-
const fullPath = this.resolvePath(path2);
|
|
887
|
-
return await this.detectMimeType(fullPath);
|
|
888
|
-
}
|
|
889
|
-
async detectMimeType(filePath) {
|
|
890
|
-
const ext = basename(filePath).split(".").pop()?.toLowerCase();
|
|
891
|
-
const mimeTypes = {
|
|
892
|
-
txt: "text/plain",
|
|
893
|
-
html: "text/html",
|
|
894
|
-
css: "text/css",
|
|
895
|
-
js: "application/javascript",
|
|
896
|
-
json: "application/json",
|
|
897
|
-
xml: "application/xml",
|
|
898
|
-
pdf: "application/pdf",
|
|
899
|
-
zip: "application/zip",
|
|
900
|
-
jpg: "image/jpeg",
|
|
901
|
-
jpeg: "image/jpeg",
|
|
902
|
-
png: "image/png",
|
|
903
|
-
gif: "image/gif",
|
|
904
|
-
svg: "image/svg+xml",
|
|
905
|
-
mp4: "video/mp4",
|
|
906
|
-
mp3: "audio/mpeg",
|
|
907
|
-
wav: "audio/wav"
|
|
908
|
-
};
|
|
909
|
-
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
910
|
-
}
|
|
911
|
-
async lastModified(path2) {
|
|
912
|
-
const stats = await this.stat(path2);
|
|
913
|
-
return stats.lastModified;
|
|
914
|
-
}
|
|
915
|
-
async fileSize(path2) {
|
|
916
|
-
const stats = await this.stat(path2);
|
|
917
|
-
return stats.size;
|
|
918
|
-
}
|
|
2075
|
+
return runCommand(`zip -r ${shellEscape(toPath)} ${shellEscape(from)}`, options);
|
|
919
2076
|
}
|
|
920
|
-
function
|
|
921
|
-
|
|
2077
|
+
async function unzip(paths) {
|
|
2078
|
+
if (Array.isArray(paths))
|
|
2079
|
+
return runCommand(`unzip ${paths.map((p3) => shellEscape(p3)).join(" ")}`);
|
|
2080
|
+
return runCommand(`unzip ${shellEscape(paths)}`);
|
|
2081
|
+
}
|
|
2082
|
+
function archive(paths) {
|
|
2083
|
+
return zip(paths);
|
|
2084
|
+
}
|
|
2085
|
+
function unarchive(paths) {
|
|
2086
|
+
return unzip(paths);
|
|
2087
|
+
}
|
|
2088
|
+
function compress(paths) {
|
|
2089
|
+
return zip(paths);
|
|
2090
|
+
}
|
|
2091
|
+
function decompress(paths) {
|
|
2092
|
+
return unzip(paths);
|
|
2093
|
+
}
|
|
2094
|
+
function gzipSync(data, options) {
|
|
2095
|
+
return Bun.gzipSync(data, options);
|
|
2096
|
+
}
|
|
2097
|
+
function gunzipSync(data) {
|
|
2098
|
+
return Bun.gunzipSync(data);
|
|
2099
|
+
}
|
|
2100
|
+
function deflateSync(data, options) {
|
|
2101
|
+
return Bun.deflateSync(data, options);
|
|
922
2102
|
}
|
|
2103
|
+
function inflateSync(data) {
|
|
2104
|
+
return Bun.inflateSync(data);
|
|
2105
|
+
}
|
|
2106
|
+
// src/adapters/index.ts
|
|
2107
|
+
init_local();
|
|
2108
|
+
|
|
923
2109
|
// src/adapters/memory.ts
|
|
2110
|
+
init_types();
|
|
924
2111
|
import { Buffer as Buffer3 } from "buffer";
|
|
925
2112
|
import { basename as basename2 } from "path";
|
|
2113
|
+
|
|
926
2114
|
class InMemoryStorageAdapter {
|
|
927
2115
|
files;
|
|
928
2116
|
directories;
|
|
@@ -947,7 +2135,11 @@ class InMemoryStorageAdapter {
|
|
|
947
2135
|
} else if (contents instanceof Uint8Array) {
|
|
948
2136
|
return contents;
|
|
949
2137
|
} else {
|
|
950
|
-
const
|
|
2138
|
+
const stream = contents;
|
|
2139
|
+
if (typeof stream.getReader !== "function") {
|
|
2140
|
+
throw new TypeError("[storage/memory] contents must be a web-standard ReadableStream " + "(with .getReader()), not a Node stream.Readable. " + "Convert via Readable.toWeb(nodeStream) before passing.");
|
|
2141
|
+
}
|
|
2142
|
+
const reader = stream.getReader.call(contents);
|
|
951
2143
|
const chunks = [];
|
|
952
2144
|
while (true) {
|
|
953
2145
|
const { done, value } = await reader.read();
|
|
@@ -973,20 +2165,81 @@ class InMemoryStorageAdapter {
|
|
|
973
2165
|
await this.createDirectory(dirPath);
|
|
974
2166
|
}
|
|
975
2167
|
const data = await this.contentsToUint8Array(contents);
|
|
2168
|
+
const lastModified = Date.now();
|
|
2169
|
+
const mimeType = this.detectMimeType(normalized);
|
|
2170
|
+
this.files.set(normalized, {
|
|
2171
|
+
contents: data,
|
|
2172
|
+
visibility: "private",
|
|
2173
|
+
mimeType,
|
|
2174
|
+
lastModified
|
|
2175
|
+
});
|
|
2176
|
+
return {
|
|
2177
|
+
path: normalized,
|
|
2178
|
+
size: data.length,
|
|
2179
|
+
contentType: mimeType,
|
|
2180
|
+
lastModified
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
async read(path2) {
|
|
2184
|
+
const normalized = this.normalizePath(path2);
|
|
2185
|
+
const file = this.files.get(normalized);
|
|
2186
|
+
if (!file) {
|
|
2187
|
+
throw new Error(`File not found: ${path2}`);
|
|
2188
|
+
}
|
|
2189
|
+
return file.contents;
|
|
2190
|
+
}
|
|
2191
|
+
async getStream(path2, _options) {
|
|
2192
|
+
const normalized = this.normalizePath(path2);
|
|
2193
|
+
const file = this.files.get(normalized);
|
|
2194
|
+
if (!file)
|
|
2195
|
+
throw new Error(`File not found: ${path2}`);
|
|
2196
|
+
const bytes = file.contents;
|
|
2197
|
+
return new ReadableStream({
|
|
2198
|
+
start(controller) {
|
|
2199
|
+
controller.enqueue(bytes);
|
|
2200
|
+
controller.close();
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
async putStream(path2, stream, options) {
|
|
2205
|
+
const normalized = this.normalizePath(path2);
|
|
2206
|
+
const dirPath = this.getDirectoryPath(normalized);
|
|
2207
|
+
if (dirPath)
|
|
2208
|
+
await this.createDirectory(dirPath);
|
|
2209
|
+
const chunks = [];
|
|
2210
|
+
const reader = stream.getReader();
|
|
2211
|
+
const abort = options?.signal;
|
|
2212
|
+
try {
|
|
2213
|
+
while (true) {
|
|
2214
|
+
if (abort?.aborted)
|
|
2215
|
+
throw new Error("aborted");
|
|
2216
|
+
const { done, value } = await reader.read();
|
|
2217
|
+
if (done)
|
|
2218
|
+
break;
|
|
2219
|
+
if (value)
|
|
2220
|
+
chunks.push(value);
|
|
2221
|
+
}
|
|
2222
|
+
} finally {
|
|
2223
|
+
try {
|
|
2224
|
+
reader.releaseLock();
|
|
2225
|
+
} catch {}
|
|
2226
|
+
}
|
|
2227
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
2228
|
+
const data = new Uint8Array(totalLength);
|
|
2229
|
+
let offset = 0;
|
|
2230
|
+
for (const c of chunks) {
|
|
2231
|
+
data.set(c, offset);
|
|
2232
|
+
offset += c.length;
|
|
2233
|
+
}
|
|
2234
|
+
const lastModified = Date.now();
|
|
2235
|
+
const mimeType = options?.contentType ?? this.detectMimeType(normalized);
|
|
976
2236
|
this.files.set(normalized, {
|
|
977
2237
|
contents: data,
|
|
978
2238
|
visibility: "private",
|
|
979
|
-
mimeType
|
|
980
|
-
lastModified
|
|
2239
|
+
mimeType,
|
|
2240
|
+
lastModified
|
|
981
2241
|
});
|
|
982
|
-
|
|
983
|
-
async read(path2) {
|
|
984
|
-
const normalized = this.normalizePath(path2);
|
|
985
|
-
const file = this.files.get(normalized);
|
|
986
|
-
if (!file) {
|
|
987
|
-
throw new Error(`File not found: ${path2}`);
|
|
988
|
-
}
|
|
989
|
-
return file.contents;
|
|
2242
|
+
return { path: normalized, size: data.length, contentType: mimeType, lastModified };
|
|
990
2243
|
}
|
|
991
2244
|
async readToString(path2) {
|
|
992
2245
|
const data = await this.read(path2);
|
|
@@ -1163,8 +2416,8 @@ class InMemoryStorageAdapter {
|
|
|
1163
2416
|
return this.directories.has(normalized) || normalized === "";
|
|
1164
2417
|
}
|
|
1165
2418
|
async publicUrl(path2, options = {}) {
|
|
1166
|
-
const
|
|
1167
|
-
return `${
|
|
2419
|
+
const base = (options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
|
|
2420
|
+
return `${base}/${this.normalizePath(path2)}`;
|
|
1168
2421
|
}
|
|
1169
2422
|
async temporaryUrl(path2, options) {
|
|
1170
2423
|
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
@@ -1228,261 +2481,18 @@ class InMemoryStorageAdapter {
|
|
|
1228
2481
|
function createMemoryStorage() {
|
|
1229
2482
|
return new InMemoryStorageAdapter;
|
|
1230
2483
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
client;
|
|
1236
|
-
bucket;
|
|
1237
|
-
prefix;
|
|
1238
|
-
region;
|
|
1239
|
-
constructor(client, config) {
|
|
1240
|
-
this.client = client;
|
|
1241
|
-
this.bucket = config.bucket || "";
|
|
1242
|
-
this.prefix = config.prefix || "";
|
|
1243
|
-
this.region = config.region || "us-east-1";
|
|
1244
|
-
if (!this.bucket) {
|
|
1245
|
-
throw new Error("S3 bucket name is required");
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
prefixPath(path2) {
|
|
1249
|
-
if (!this.prefix)
|
|
1250
|
-
return path2;
|
|
1251
|
-
return `${this.prefix}/${path2}`.replace(/\/+/g, "/");
|
|
1252
|
-
}
|
|
1253
|
-
stripPrefix(path2) {
|
|
1254
|
-
if (!this.prefix)
|
|
1255
|
-
return path2;
|
|
1256
|
-
const prefixWithSlash = `${this.prefix}/`;
|
|
1257
|
-
return path2.startsWith(prefixWithSlash) ? path2.slice(prefixWithSlash.length) : path2;
|
|
1258
|
-
}
|
|
1259
|
-
async contentsToBuffer(contents) {
|
|
1260
|
-
if (typeof contents === "string") {
|
|
1261
|
-
return Buffer4.from(contents, "utf8");
|
|
1262
|
-
} else if (contents instanceof Buffer4) {
|
|
1263
|
-
return contents;
|
|
1264
|
-
} else if (contents instanceof Uint8Array) {
|
|
1265
|
-
return Buffer4.from(contents);
|
|
1266
|
-
} else {
|
|
1267
|
-
const reader = contents.getReader();
|
|
1268
|
-
const chunks = [];
|
|
1269
|
-
while (true) {
|
|
1270
|
-
const { done, value } = await reader.read();
|
|
1271
|
-
if (done)
|
|
1272
|
-
break;
|
|
1273
|
-
if (value)
|
|
1274
|
-
chunks.push(value);
|
|
1275
|
-
}
|
|
1276
|
-
return Buffer4.concat(chunks.map((c) => Buffer4.from(c)));
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async write(path2, contents) {
|
|
1280
|
-
const key = this.prefixPath(path2);
|
|
1281
|
-
const body = await this.contentsToBuffer(contents);
|
|
1282
|
-
await this.client.putObject({
|
|
1283
|
-
bucket: this.bucket,
|
|
1284
|
-
key,
|
|
1285
|
-
body,
|
|
1286
|
-
contentType: this.detectMimeType(path2)
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
async read(path2) {
|
|
1290
|
-
const key = this.prefixPath(path2);
|
|
1291
|
-
const response = await this.client.getObject(this.bucket, key);
|
|
1292
|
-
if (!response) {
|
|
1293
|
-
throw new Error(`Failed to read file: ${path2}`);
|
|
1294
|
-
}
|
|
1295
|
-
return Buffer4.from(response);
|
|
1296
|
-
}
|
|
1297
|
-
async readToString(path2) {
|
|
1298
|
-
const key = this.prefixPath(path2);
|
|
1299
|
-
const response = await this.client.getObject(this.bucket, key);
|
|
1300
|
-
if (!response) {
|
|
1301
|
-
throw new Error(`Failed to read file: ${path2}`);
|
|
1302
|
-
}
|
|
1303
|
-
return response;
|
|
1304
|
-
}
|
|
1305
|
-
async readToBuffer(path2) {
|
|
1306
|
-
const contents = await this.read(path2);
|
|
1307
|
-
return contents;
|
|
1308
|
-
}
|
|
1309
|
-
async readToUint8Array(path2) {
|
|
1310
|
-
const buffer = await this.readToBuffer(path2);
|
|
1311
|
-
return new Uint8Array(buffer);
|
|
1312
|
-
}
|
|
1313
|
-
async deleteFile(path2) {
|
|
1314
|
-
const key = this.prefixPath(path2);
|
|
1315
|
-
await this.client.deleteObject(this.bucket, key);
|
|
1316
|
-
}
|
|
1317
|
-
async deleteDirectory(path2) {
|
|
1318
|
-
const prefix = this.prefixPath(path2);
|
|
1319
|
-
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
1320
|
-
const objects = await this.client.listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
|
|
1321
|
-
const keys = objects.map((obj) => obj.Key).filter((k) => typeof k === "string");
|
|
1322
|
-
if (keys.length === 0) {
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
await this.client.deleteObjects(this.bucket, keys);
|
|
1326
|
-
}
|
|
1327
|
-
async createDirectory(_path) {}
|
|
1328
|
-
async moveFile(from, to) {
|
|
1329
|
-
await this.copyFile(from, to);
|
|
1330
|
-
await this.deleteFile(from);
|
|
1331
|
-
}
|
|
1332
|
-
async copyFile(from, to) {
|
|
1333
|
-
const fromKey = this.prefixPath(from);
|
|
1334
|
-
const toKey = this.prefixPath(to);
|
|
1335
|
-
await this.client.copyObject({
|
|
1336
|
-
sourceBucket: this.bucket,
|
|
1337
|
-
sourceKey: fromKey,
|
|
1338
|
-
destinationBucket: this.bucket,
|
|
1339
|
-
destinationKey: toKey
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
async stat(path2) {
|
|
1343
|
-
const key = this.prefixPath(path2);
|
|
1344
|
-
const result = await this.client.headObject(this.bucket, key);
|
|
1345
|
-
if (!result) {
|
|
1346
|
-
throw new Error(`File not found: ${path2}`);
|
|
1347
|
-
}
|
|
1348
|
-
return {
|
|
1349
|
-
path: path2,
|
|
1350
|
-
type: "file",
|
|
1351
|
-
visibility: "private",
|
|
1352
|
-
size: result.ContentLength || 0,
|
|
1353
|
-
lastModified: result.LastModified ? new Date(result.LastModified).getTime() : Date.now(),
|
|
1354
|
-
mimeType: result.ContentType
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
list(path2, options = {}) {
|
|
1358
|
-
return this.createAsyncIterator(path2, options.deep || false);
|
|
1359
|
-
}
|
|
1360
|
-
async* createAsyncIterator(path2, deep) {
|
|
1361
|
-
const prefix = this.prefixPath(path2);
|
|
1362
|
-
const normalizedPrefix = prefix ? `${prefix}/` : undefined;
|
|
1363
|
-
if (deep) {
|
|
1364
|
-
const objects = await this.client.listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
|
|
1365
|
-
for (const obj of objects) {
|
|
1366
|
-
yield {
|
|
1367
|
-
path: this.stripPrefix(obj.Key),
|
|
1368
|
-
type: "file"
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
} else {
|
|
1372
|
-
let continuationToken;
|
|
1373
|
-
do {
|
|
1374
|
-
const result = await this.client.listObjects({
|
|
1375
|
-
bucket: this.bucket,
|
|
1376
|
-
prefix: normalizedPrefix,
|
|
1377
|
-
continuationToken
|
|
1378
|
-
});
|
|
1379
|
-
for (const obj of result.objects || []) {
|
|
1380
|
-
yield {
|
|
1381
|
-
path: this.stripPrefix(obj.Key),
|
|
1382
|
-
type: "file"
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
continuationToken = result.nextContinuationToken;
|
|
1386
|
-
} while (continuationToken);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
async changeVisibility(_path, _visibility) {}
|
|
1390
|
-
async visibility(_path) {
|
|
1391
|
-
return "private";
|
|
1392
|
-
}
|
|
1393
|
-
async fileExists(path2) {
|
|
1394
|
-
const key = this.prefixPath(path2);
|
|
1395
|
-
try {
|
|
1396
|
-
const result = await this.client.headObject(this.bucket, key);
|
|
1397
|
-
return !!result;
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
if (!error.message?.includes("404") && !error.message?.includes("NoSuchKey") && !error.message?.includes("NotFound")) {
|
|
1400
|
-
console.debug(`[s3] Unexpected error checking file existence for ${path2}: ${error.message}`);
|
|
1401
|
-
}
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
async directoryExists(path2) {
|
|
1406
|
-
const prefix = this.prefixPath(path2);
|
|
1407
|
-
const result = await this.client.listObjects({
|
|
1408
|
-
bucket: this.bucket,
|
|
1409
|
-
prefix: `${prefix}/`,
|
|
1410
|
-
maxKeys: 1
|
|
1411
|
-
});
|
|
1412
|
-
return (result.objects || []).length > 0;
|
|
1413
|
-
}
|
|
1414
|
-
async publicUrl(path2, options = {}) {
|
|
1415
|
-
const key = this.prefixPath(path2);
|
|
1416
|
-
const domain = options.domain || `https://${this.bucket}.s3.${this.region}.amazonaws.com`;
|
|
1417
|
-
return `${domain}/${key}`;
|
|
1418
|
-
}
|
|
1419
|
-
async temporaryUrl(path2, options) {
|
|
1420
|
-
const key = this.prefixPath(path2);
|
|
1421
|
-
const expiresIn = Math.floor(normalizeExpiryToMilliseconds(options.expiresIn) / 1000);
|
|
1422
|
-
const MIN_EXPIRY = 60;
|
|
1423
|
-
const MAX_EXPIRY = 7 * 24 * 60 * 60;
|
|
1424
|
-
if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY || expiresIn > MAX_EXPIRY) {
|
|
1425
|
-
throw new RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
|
|
1426
|
-
}
|
|
1427
|
-
return await this.client.getSignedUrl({
|
|
1428
|
-
bucket: this.bucket,
|
|
1429
|
-
key,
|
|
1430
|
-
expiresIn,
|
|
1431
|
-
operation: "getObject"
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
async signedUrl(path2, options) {
|
|
1435
|
-
return this.temporaryUrl(path2, { expiresIn: options.expiresIn });
|
|
1436
|
-
}
|
|
1437
|
-
async checksum(path2, options = {}) {
|
|
1438
|
-
const algorithm = options.algorithm || "sha256";
|
|
1439
|
-
const content = await this.readToUint8Array(path2);
|
|
1440
|
-
const hasher = new Bun.CryptoHasher(algorithm);
|
|
1441
|
-
hasher.update(content);
|
|
1442
|
-
return hasher.digest("hex");
|
|
1443
|
-
}
|
|
1444
|
-
async mimeType(path2, _options = {}) {
|
|
1445
|
-
const stats = await this.stat(path2);
|
|
1446
|
-
return stats.mimeType || this.detectMimeType(path2);
|
|
1447
|
-
}
|
|
1448
|
-
detectMimeType(path2) {
|
|
1449
|
-
const ext = basename3(path2).split(".").pop()?.toLowerCase();
|
|
1450
|
-
const mimeTypes = {
|
|
1451
|
-
txt: "text/plain",
|
|
1452
|
-
html: "text/html",
|
|
1453
|
-
css: "text/css",
|
|
1454
|
-
js: "application/javascript",
|
|
1455
|
-
json: "application/json",
|
|
1456
|
-
xml: "application/xml",
|
|
1457
|
-
pdf: "application/pdf",
|
|
1458
|
-
zip: "application/zip",
|
|
1459
|
-
jpg: "image/jpeg",
|
|
1460
|
-
jpeg: "image/jpeg",
|
|
1461
|
-
png: "image/png",
|
|
1462
|
-
gif: "image/gif",
|
|
1463
|
-
svg: "image/svg+xml",
|
|
1464
|
-
mp4: "video/mp4",
|
|
1465
|
-
mp3: "audio/mpeg",
|
|
1466
|
-
wav: "audio/wav"
|
|
1467
|
-
};
|
|
1468
|
-
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
1469
|
-
}
|
|
1470
|
-
async lastModified(path2) {
|
|
1471
|
-
const stats = await this.stat(path2);
|
|
1472
|
-
return stats.lastModified;
|
|
1473
|
-
}
|
|
1474
|
-
async fileSize(path2) {
|
|
1475
|
-
const stats = await this.stat(path2);
|
|
1476
|
-
return stats.size;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function createS3Storage(client, config) {
|
|
1480
|
-
return new S3StorageAdapter(client, config);
|
|
1481
|
-
}
|
|
2484
|
+
|
|
2485
|
+
// src/adapters/index.ts
|
|
2486
|
+
init_s3();
|
|
2487
|
+
|
|
1482
2488
|
// src/adapters/bun.ts
|
|
1483
|
-
|
|
2489
|
+
init_types();
|
|
2490
|
+
init_signed_url();
|
|
2491
|
+
import { Buffer as Buffer6 } from "buffer";
|
|
1484
2492
|
var {file, write: bunWrite } = globalThis.Bun;
|
|
2493
|
+
import { chmod as chmod2, lstat as lstat2 } from "fs/promises";
|
|
1485
2494
|
import { dirname as dirname4, join as join6, relative as relative2 } from "path";
|
|
2495
|
+
|
|
1486
2496
|
class BunStorageAdapter {
|
|
1487
2497
|
root;
|
|
1488
2498
|
constructor(config = {}) {
|
|
@@ -1502,12 +2512,16 @@ class BunStorageAdapter {
|
|
|
1502
2512
|
await this.createDirectory(relative2(this.root, dir));
|
|
1503
2513
|
if (typeof contents === "string") {
|
|
1504
2514
|
await bunWrite(fullPath, contents);
|
|
1505
|
-
} else if (contents instanceof
|
|
2515
|
+
} else if (contents instanceof Buffer6) {
|
|
1506
2516
|
await bunWrite(fullPath, contents);
|
|
1507
2517
|
} else if (contents instanceof Uint8Array) {
|
|
1508
2518
|
await bunWrite(fullPath, contents);
|
|
1509
2519
|
} else {
|
|
1510
|
-
const
|
|
2520
|
+
const stream = contents;
|
|
2521
|
+
if (typeof stream.getReader !== "function") {
|
|
2522
|
+
throw new TypeError("[storage/bun] contents must be a web-standard ReadableStream " + "(with .getReader()), not a Node stream.Readable. " + "Convert via Readable.toWeb(nodeStream) before passing.");
|
|
2523
|
+
}
|
|
2524
|
+
const reader = stream.getReader.call(contents);
|
|
1511
2525
|
const chunks = [];
|
|
1512
2526
|
while (true) {
|
|
1513
2527
|
const { done, value } = await reader.read();
|
|
@@ -1525,6 +2539,12 @@ class BunStorageAdapter {
|
|
|
1525
2539
|
}
|
|
1526
2540
|
await bunWrite(fullPath, result);
|
|
1527
2541
|
}
|
|
2542
|
+
const written = file(fullPath);
|
|
2543
|
+
return {
|
|
2544
|
+
path: path2,
|
|
2545
|
+
size: written.size,
|
|
2546
|
+
lastModified: written.lastModified
|
|
2547
|
+
};
|
|
1528
2548
|
}
|
|
1529
2549
|
async read(path2) {
|
|
1530
2550
|
const fullPath = this.resolvePath(path2);
|
|
@@ -1532,7 +2552,46 @@ class BunStorageAdapter {
|
|
|
1532
2552
|
if (!await bunFile.exists()) {
|
|
1533
2553
|
throw new Error(`File not found: ${path2}`);
|
|
1534
2554
|
}
|
|
1535
|
-
return await bunFile.arrayBuffer().then((buf) => new Uint8Array(buf));
|
|
2555
|
+
return await bunFile.arrayBuffer().then((buf) => new Uint8Array(buf));
|
|
2556
|
+
}
|
|
2557
|
+
async getStream(path2, _options) {
|
|
2558
|
+
const fullPath = this.resolvePath(path2);
|
|
2559
|
+
const bunFile = file(fullPath);
|
|
2560
|
+
if (!await bunFile.exists())
|
|
2561
|
+
throw new Error(`File not found: ${path2}`);
|
|
2562
|
+
return bunFile.stream();
|
|
2563
|
+
}
|
|
2564
|
+
async putStream(path2, stream, options) {
|
|
2565
|
+
const fullPath = this.resolvePath(path2);
|
|
2566
|
+
const dir = dirname4(fullPath);
|
|
2567
|
+
await this.createDirectory(relative2(this.root, dir));
|
|
2568
|
+
try {
|
|
2569
|
+
const body = new Response(stream);
|
|
2570
|
+
const writePromise = bunWrite(fullPath, body);
|
|
2571
|
+
if (options?.signal) {
|
|
2572
|
+
const abortHandler = () => {};
|
|
2573
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
2574
|
+
try {
|
|
2575
|
+
await writePromise;
|
|
2576
|
+
} finally {
|
|
2577
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
await writePromise;
|
|
2581
|
+
}
|
|
2582
|
+
} catch (err3) {
|
|
2583
|
+
try {
|
|
2584
|
+
await Bun.$.throws(false)`rm -f ${fullPath}`;
|
|
2585
|
+
} catch {}
|
|
2586
|
+
throw err3;
|
|
2587
|
+
}
|
|
2588
|
+
const written = file(fullPath);
|
|
2589
|
+
return {
|
|
2590
|
+
path: path2,
|
|
2591
|
+
size: written.size,
|
|
2592
|
+
contentType: options?.contentType,
|
|
2593
|
+
lastModified: written.lastModified
|
|
2594
|
+
};
|
|
1536
2595
|
}
|
|
1537
2596
|
async readToString(path2) {
|
|
1538
2597
|
const fullPath = this.resolvePath(path2);
|
|
@@ -1549,7 +2608,7 @@ class BunStorageAdapter {
|
|
|
1549
2608
|
throw new Error(`File not found: ${path2}`);
|
|
1550
2609
|
}
|
|
1551
2610
|
const arrayBuffer = await bunFile.arrayBuffer();
|
|
1552
|
-
return
|
|
2611
|
+
return Buffer6.from(arrayBuffer);
|
|
1553
2612
|
}
|
|
1554
2613
|
async readToUint8Array(path2) {
|
|
1555
2614
|
const fullPath = this.resolvePath(path2);
|
|
@@ -1627,9 +2686,18 @@ class BunStorageAdapter {
|
|
|
1627
2686
|
}
|
|
1628
2687
|
yield* createDirectoryListing(entries);
|
|
1629
2688
|
}
|
|
1630
|
-
async changeVisibility(
|
|
1631
|
-
|
|
1632
|
-
|
|
2689
|
+
async changeVisibility(path2, vis) {
|
|
2690
|
+
const fullPath = this.resolvePath(path2);
|
|
2691
|
+
const stats = await lstat2(fullPath);
|
|
2692
|
+
const isDir2 = stats.isDirectory();
|
|
2693
|
+
const mode = vis === "public" ? isDir2 ? 493 : 420 : isDir2 ? 448 : 384;
|
|
2694
|
+
await chmod2(fullPath, mode);
|
|
2695
|
+
}
|
|
2696
|
+
async visibility(path2) {
|
|
2697
|
+
const fullPath = this.resolvePath(path2);
|
|
2698
|
+
const stats = await lstat2(fullPath);
|
|
2699
|
+
const perms = stats.mode & 511;
|
|
2700
|
+
return perms & 4 ? "public" : "private";
|
|
1633
2701
|
}
|
|
1634
2702
|
async fileExists(path2) {
|
|
1635
2703
|
const fullPath = this.resolvePath(path2);
|
|
@@ -1651,7 +2719,7 @@ class BunStorageAdapter {
|
|
|
1651
2719
|
}
|
|
1652
2720
|
async temporaryUrl(path2, options) {
|
|
1653
2721
|
const expiry = normalizeExpiryToDate(options.expiresIn);
|
|
1654
|
-
const token =
|
|
2722
|
+
const token = Buffer6.from(`${path2}:${expiry.getTime()}`).toString("base64url");
|
|
1655
2723
|
return `http://localhost/temp/${token}`;
|
|
1656
2724
|
}
|
|
1657
2725
|
async signedUrl(path2, options) {
|
|
@@ -1691,23 +2759,173 @@ class BunStorageAdapter {
|
|
|
1691
2759
|
function createBunStorage(config = {}) {
|
|
1692
2760
|
return new BunStorageAdapter(config);
|
|
1693
2761
|
}
|
|
2762
|
+
// src/adapters/scoped.ts
|
|
2763
|
+
var DEFAULT_SCOPE_PATTERN = /^[a-z0-9_-]+$/i;
|
|
2764
|
+
|
|
2765
|
+
class ScopedStorageAdapter {
|
|
2766
|
+
inner;
|
|
2767
|
+
scope;
|
|
2768
|
+
scopeWithSlash;
|
|
2769
|
+
constructor(inner, options) {
|
|
2770
|
+
const pattern = options.scopePattern ?? DEFAULT_SCOPE_PATTERN;
|
|
2771
|
+
const cleaned = String(options.scope).replace(/^\/+|\/+$/g, "");
|
|
2772
|
+
if (!cleaned)
|
|
2773
|
+
throw new Error("[storage/scoped] scope is required");
|
|
2774
|
+
if (!pattern.test(cleaned))
|
|
2775
|
+
throw new Error(`[storage/scoped] scope '${cleaned}' contains disallowed characters`);
|
|
2776
|
+
if (cleaned.includes("..") || cleaned.includes("/"))
|
|
2777
|
+
throw new Error(`[storage/scoped] scope '${cleaned}' cannot contain path separators or traversal`);
|
|
2778
|
+
this.inner = inner;
|
|
2779
|
+
this.scope = cleaned;
|
|
2780
|
+
this.scopeWithSlash = `${cleaned}/`;
|
|
2781
|
+
}
|
|
2782
|
+
scopePath(path2) {
|
|
2783
|
+
if (typeof path2 !== "string")
|
|
2784
|
+
throw new Error("[storage/scoped] path must be a string");
|
|
2785
|
+
if (path2.length === 0)
|
|
2786
|
+
return this.scope;
|
|
2787
|
+
if (path2.includes("\x00"))
|
|
2788
|
+
throw new Error("[storage/scoped] path contains a null byte");
|
|
2789
|
+
if (path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2))
|
|
2790
|
+
throw new Error(`[storage/scoped] path '${path2}' is absolute \u2014 refusing to escape scope`);
|
|
2791
|
+
const segments = path2.split(/[/\\]/);
|
|
2792
|
+
if (segments.some((s) => s === ".."))
|
|
2793
|
+
throw new Error(`[storage/scoped] path '${path2}' contains a '..' segment \u2014 refusing to escape scope`);
|
|
2794
|
+
return `${this.scope}/${path2.replace(/^\/+/, "")}`;
|
|
2795
|
+
}
|
|
2796
|
+
unscopePath(path2) {
|
|
2797
|
+
if (path2 === this.scope)
|
|
2798
|
+
return "";
|
|
2799
|
+
if (path2.startsWith(this.scopeWithSlash))
|
|
2800
|
+
return path2.slice(this.scopeWithSlash.length);
|
|
2801
|
+
return path2;
|
|
2802
|
+
}
|
|
2803
|
+
async write(path2, contents) {
|
|
2804
|
+
const result = await this.inner.write(this.scopePath(path2), contents);
|
|
2805
|
+
return { ...result, path: this.unscopePath(result.path) };
|
|
2806
|
+
}
|
|
2807
|
+
async read(path2) {
|
|
2808
|
+
return this.inner.read(this.scopePath(path2));
|
|
2809
|
+
}
|
|
2810
|
+
async readToString(path2) {
|
|
2811
|
+
return this.inner.readToString(this.scopePath(path2));
|
|
2812
|
+
}
|
|
2813
|
+
async readToBuffer(path2) {
|
|
2814
|
+
return this.inner.readToBuffer(this.scopePath(path2));
|
|
2815
|
+
}
|
|
2816
|
+
async readToUint8Array(path2) {
|
|
2817
|
+
return this.inner.readToUint8Array(this.scopePath(path2));
|
|
2818
|
+
}
|
|
2819
|
+
async deleteFile(path2) {
|
|
2820
|
+
return this.inner.deleteFile(this.scopePath(path2));
|
|
2821
|
+
}
|
|
2822
|
+
async deleteDirectory(path2) {
|
|
2823
|
+
return this.inner.deleteDirectory(this.scopePath(path2));
|
|
2824
|
+
}
|
|
2825
|
+
async createDirectory(path2) {
|
|
2826
|
+
return this.inner.createDirectory(this.scopePath(path2));
|
|
2827
|
+
}
|
|
2828
|
+
async moveFile(from, to) {
|
|
2829
|
+
return this.inner.moveFile(this.scopePath(from), this.scopePath(to));
|
|
2830
|
+
}
|
|
2831
|
+
async copyFile(from, to) {
|
|
2832
|
+
return this.inner.copyFile(this.scopePath(from), this.scopePath(to));
|
|
2833
|
+
}
|
|
2834
|
+
async stat(path2) {
|
|
2835
|
+
const entry = await this.inner.stat(this.scopePath(path2));
|
|
2836
|
+
return { ...entry, path: this.unscopePath(entry.path) };
|
|
2837
|
+
}
|
|
2838
|
+
list(path2, options) {
|
|
2839
|
+
const inner = this.inner.list(this.scopePath(path2), options);
|
|
2840
|
+
const unscope = this.unscopePath.bind(this);
|
|
2841
|
+
return async function* () {
|
|
2842
|
+
for await (const entry of inner) {
|
|
2843
|
+
yield { ...entry, path: unscope(entry.path) };
|
|
2844
|
+
}
|
|
2845
|
+
}();
|
|
2846
|
+
}
|
|
2847
|
+
async changeVisibility(path2, visibility2) {
|
|
2848
|
+
return this.inner.changeVisibility(this.scopePath(path2), visibility2);
|
|
2849
|
+
}
|
|
2850
|
+
async visibility(path2) {
|
|
2851
|
+
return this.inner.visibility(this.scopePath(path2));
|
|
2852
|
+
}
|
|
2853
|
+
async fileExists(path2) {
|
|
2854
|
+
return this.inner.fileExists(this.scopePath(path2));
|
|
2855
|
+
}
|
|
2856
|
+
async directoryExists(path2) {
|
|
2857
|
+
return this.inner.directoryExists(this.scopePath(path2));
|
|
2858
|
+
}
|
|
2859
|
+
async publicUrl(path2, options) {
|
|
2860
|
+
return this.inner.publicUrl(this.scopePath(path2), options);
|
|
2861
|
+
}
|
|
2862
|
+
async temporaryUrl(path2, options) {
|
|
2863
|
+
return this.inner.temporaryUrl(this.scopePath(path2), options);
|
|
2864
|
+
}
|
|
2865
|
+
async signedUrl(path2, options) {
|
|
2866
|
+
if (typeof this.inner.signedUrl !== "function")
|
|
2867
|
+
throw new Error("[storage/scoped] wrapped adapter does not support signedUrl");
|
|
2868
|
+
return this.inner.signedUrl(this.scopePath(path2), options);
|
|
2869
|
+
}
|
|
2870
|
+
async presignedUploadUrl(options) {
|
|
2871
|
+
if (typeof this.inner.presignedUploadUrl !== "function")
|
|
2872
|
+
throw new Error("[storage/scoped] wrapped adapter does not support presignedUploadUrl");
|
|
2873
|
+
const scopedDir = options.dir ? `${this.scope}/${options.dir.replace(/^\/+/, "")}` : this.scope;
|
|
2874
|
+
const result = await this.inner.presignedUploadUrl({ ...options, dir: scopedDir });
|
|
2875
|
+
return { ...result, path: this.unscopePath(result.path), key: this.unscopePath(result.key) };
|
|
2876
|
+
}
|
|
2877
|
+
async presignedUploadPolicy(options) {
|
|
2878
|
+
if (typeof this.inner.presignedUploadPolicy !== "function")
|
|
2879
|
+
throw new Error("[storage/scoped] wrapped adapter does not support presignedUploadPolicy");
|
|
2880
|
+
const scopedKey = typeof options.key === "string" ? this.scopePath(options.key) : { startsWith: this.scopePath(options.key.startsWith) };
|
|
2881
|
+
const result = await this.inner.presignedUploadPolicy({ ...options, key: scopedKey });
|
|
2882
|
+
return { ...result, key: this.unscopePath(result.key) };
|
|
2883
|
+
}
|
|
2884
|
+
async getStream(path2, options) {
|
|
2885
|
+
if (typeof this.inner.getStream !== "function")
|
|
2886
|
+
throw new Error("[storage/scoped] wrapped adapter does not support getStream");
|
|
2887
|
+
return this.inner.getStream(this.scopePath(path2), options);
|
|
2888
|
+
}
|
|
2889
|
+
async putStream(path2, stream, options) {
|
|
2890
|
+
if (typeof this.inner.putStream !== "function")
|
|
2891
|
+
throw new Error("[storage/scoped] wrapped adapter does not support putStream");
|
|
2892
|
+
const result = await this.inner.putStream(this.scopePath(path2), stream, options);
|
|
2893
|
+
return { ...result, path: this.unscopePath(result.path) };
|
|
2894
|
+
}
|
|
2895
|
+
async checksum(path2, options) {
|
|
2896
|
+
return this.inner.checksum(this.scopePath(path2), options);
|
|
2897
|
+
}
|
|
2898
|
+
async mimeType(path2, options) {
|
|
2899
|
+
return this.inner.mimeType(this.scopePath(path2), options);
|
|
2900
|
+
}
|
|
2901
|
+
async lastModified(path2) {
|
|
2902
|
+
return this.inner.lastModified(this.scopePath(path2));
|
|
2903
|
+
}
|
|
2904
|
+
async fileSize(path2) {
|
|
2905
|
+
return this.inner.fileSize(this.scopePath(path2));
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
function scoped(inner, options) {
|
|
2909
|
+
return new ScopedStorageAdapter(inner, options);
|
|
2910
|
+
}
|
|
2911
|
+
// src/index.ts
|
|
2912
|
+
init_types();
|
|
2913
|
+
|
|
1694
2914
|
// src/drivers/aws.ts
|
|
1695
|
-
|
|
2915
|
+
init_s3();
|
|
1696
2916
|
var _adapterPromise = null;
|
|
1697
2917
|
async function loadConfig() {
|
|
1698
2918
|
try {
|
|
1699
2919
|
const { filesystems } = await import("@stacksjs/config");
|
|
1700
2920
|
const s3Config = filesystems.s3;
|
|
1701
|
-
|
|
1702
|
-
return createS3Storage(client, {
|
|
2921
|
+
return createS3Storage(null, {
|
|
1703
2922
|
bucket: s3Config?.bucket || "stacks",
|
|
1704
2923
|
prefix: s3Config?.prefix || "stx",
|
|
1705
2924
|
region: s3Config?.region || "us-east-1"
|
|
1706
2925
|
});
|
|
1707
2926
|
} catch {
|
|
1708
2927
|
const { env } = await import("@stacksjs/env");
|
|
1709
|
-
|
|
1710
|
-
return createS3Storage(client, {
|
|
2928
|
+
return createS3Storage(null, {
|
|
1711
2929
|
bucket: env.AWS_S3_BUCKET || "stacks",
|
|
1712
2930
|
prefix: env.AWS_S3_PREFIX || "stx",
|
|
1713
2931
|
region: env.AWS_REGION || "us-east-1"
|
|
@@ -1812,16 +3030,17 @@ var aws = {
|
|
|
1812
3030
|
}
|
|
1813
3031
|
};
|
|
1814
3032
|
// src/drivers/local.ts
|
|
3033
|
+
init_local();
|
|
1815
3034
|
import { resolve } from "path";
|
|
1816
|
-
import
|
|
3035
|
+
import process4 from "process";
|
|
1817
3036
|
var _adapterPromise2 = null;
|
|
1818
3037
|
async function loadConfig2() {
|
|
1819
3038
|
try {
|
|
1820
3039
|
const { filesystems } = await import("@stacksjs/config");
|
|
1821
|
-
const rootDirectory = resolve(filesystems.root ||
|
|
3040
|
+
const rootDirectory = resolve(filesystems.root || process4.cwd());
|
|
1822
3041
|
return createLocalStorage({ root: rootDirectory });
|
|
1823
3042
|
} catch {
|
|
1824
|
-
const rootDirectory = resolve(
|
|
3043
|
+
const rootDirectory = resolve(process4.cwd());
|
|
1825
3044
|
return createLocalStorage({ root: rootDirectory });
|
|
1826
3045
|
}
|
|
1827
3046
|
}
|
|
@@ -1992,15 +3211,15 @@ var memory2 = {
|
|
|
1992
3211
|
};
|
|
1993
3212
|
// src/drivers/bun.ts
|
|
1994
3213
|
import { resolve as resolve2 } from "path";
|
|
1995
|
-
import
|
|
3214
|
+
import process5 from "process";
|
|
1996
3215
|
var _adapterPromise3 = null;
|
|
1997
3216
|
async function loadConfig3() {
|
|
1998
3217
|
try {
|
|
1999
3218
|
const { filesystems } = await import("@stacksjs/config");
|
|
2000
|
-
const rootDirectory = resolve2(filesystems.root ||
|
|
3219
|
+
const rootDirectory = resolve2(filesystems.root || process5.cwd());
|
|
2001
3220
|
return createBunStorage({ root: rootDirectory });
|
|
2002
3221
|
} catch {
|
|
2003
|
-
const rootDirectory = resolve2(
|
|
3222
|
+
const rootDirectory = resolve2(process5.cwd());
|
|
2004
3223
|
return createBunStorage({ root: rootDirectory });
|
|
2005
3224
|
}
|
|
2006
3225
|
}
|
|
@@ -2224,198 +3443,14 @@ function guessMime(filePath) {
|
|
|
2224
3443
|
return "application/octet-stream";
|
|
2225
3444
|
}
|
|
2226
3445
|
}
|
|
2227
|
-
// src/facade.ts
|
|
2228
|
-
import { resolve as resolve3 } from "path";
|
|
2229
|
-
import process5 from "process";
|
|
2230
|
-
import { filesystems, app as appConfig } from "@stacksjs/config";
|
|
2231
|
-
import { S3Client as S3Client2 } from "@stacksjs/ts-cloud";
|
|
2232
|
-
function buildConfig() {
|
|
2233
|
-
const cwd = process5.cwd();
|
|
2234
|
-
const rootDir = filesystems.root || cwd;
|
|
2235
|
-
const s3Config = filesystems.s3;
|
|
2236
|
-
const appUrl = appConfig?.url || "";
|
|
2237
|
-
const config = {
|
|
2238
|
-
default: filesystems.driver || "local",
|
|
2239
|
-
disks: {
|
|
2240
|
-
local: {
|
|
2241
|
-
driver: "local",
|
|
2242
|
-
root: resolve3(rootDir, "storage/app"),
|
|
2243
|
-
visibility: filesystems.defaultVisibility || "private"
|
|
2244
|
-
},
|
|
2245
|
-
public: {
|
|
2246
|
-
driver: "local",
|
|
2247
|
-
root: resolve3(rootDir, "public"),
|
|
2248
|
-
url: appUrl ? `${appUrl}/storage` : "/storage",
|
|
2249
|
-
visibility: "public"
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
};
|
|
2253
|
-
if (s3Config?.bucket) {
|
|
2254
|
-
config.disks.s3 = {
|
|
2255
|
-
driver: "s3",
|
|
2256
|
-
bucket: s3Config.bucket,
|
|
2257
|
-
region: s3Config.region || "us-east-1",
|
|
2258
|
-
prefix: s3Config.prefix,
|
|
2259
|
-
endpoint: s3Config.endpoint,
|
|
2260
|
-
url: filesystems.publicUrl?.domain,
|
|
2261
|
-
usePathStyleEndpoint: !!s3Config.endpoint,
|
|
2262
|
-
visibility: filesystems.defaultVisibility || "private",
|
|
2263
|
-
credentials: s3Config.credentials ? { key: s3Config.credentials.accessKeyId, secret: s3Config.credentials.secretAccessKey } : undefined
|
|
2264
|
-
};
|
|
2265
|
-
}
|
|
2266
|
-
return config;
|
|
2267
|
-
}
|
|
2268
3446
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
s3Clients = new Map;
|
|
2273
|
-
customConfig = null;
|
|
2274
|
-
get config() {
|
|
2275
|
-
if (!this._config) {
|
|
2276
|
-
const builtConfig = buildConfig();
|
|
2277
|
-
this._config = this.customConfig ? {
|
|
2278
|
-
default: this.customConfig.default || builtConfig.default,
|
|
2279
|
-
disks: { ...builtConfig.disks, ...this.customConfig.disks }
|
|
2280
|
-
} : builtConfig;
|
|
2281
|
-
}
|
|
2282
|
-
return this._config;
|
|
2283
|
-
}
|
|
2284
|
-
init(config) {
|
|
2285
|
-
this.customConfig = config;
|
|
2286
|
-
this._config = null;
|
|
2287
|
-
this.disks.clear();
|
|
2288
|
-
this.s3Clients.clear();
|
|
2289
|
-
return this;
|
|
2290
|
-
}
|
|
2291
|
-
disk(name) {
|
|
2292
|
-
const diskName = name || this.config.default;
|
|
2293
|
-
if (this.disks.has(diskName)) {
|
|
2294
|
-
return this.disks.get(diskName);
|
|
2295
|
-
}
|
|
2296
|
-
const diskConfig = this.config.disks[diskName];
|
|
2297
|
-
if (!diskConfig) {
|
|
2298
|
-
const available = Object.keys(this.config.disks).join(", ");
|
|
2299
|
-
throw new Error(`Disk [${diskName}] is not configured. Available: ${available}`);
|
|
2300
|
-
}
|
|
2301
|
-
const adapter2 = this.createAdapter(diskName, diskConfig);
|
|
2302
|
-
this.disks.set(diskName, adapter2);
|
|
2303
|
-
return adapter2;
|
|
2304
|
-
}
|
|
2305
|
-
createAdapter(name, config) {
|
|
2306
|
-
switch (config.driver) {
|
|
2307
|
-
case "local":
|
|
2308
|
-
return this.createLocalAdapter(config);
|
|
2309
|
-
case "s3":
|
|
2310
|
-
return this.createS3Adapter(name, config);
|
|
2311
|
-
default:
|
|
2312
|
-
throw new Error(`Unsupported driver: ${config.driver}`);
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
createLocalAdapter(config) {
|
|
2316
|
-
return createLocalStorage({ root: config.root });
|
|
2317
|
-
}
|
|
2318
|
-
createS3Adapter(name, config) {
|
|
2319
|
-
let client = this.s3Clients.get(name);
|
|
2320
|
-
if (!client) {
|
|
2321
|
-
client = new S3Client2(config.region || "us-east-1");
|
|
2322
|
-
this.s3Clients.set(name, client);
|
|
2323
|
-
}
|
|
2324
|
-
return new S3StorageAdapter(client, {
|
|
2325
|
-
bucket: config.bucket,
|
|
2326
|
-
region: config.region,
|
|
2327
|
-
prefix: config.prefix
|
|
2328
|
-
});
|
|
2329
|
-
}
|
|
2330
|
-
async put(path2, contents) {
|
|
2331
|
-
return this.disk().write(path2, contents);
|
|
2332
|
-
}
|
|
2333
|
-
async get(path2) {
|
|
2334
|
-
return this.disk().readToString(path2);
|
|
2335
|
-
}
|
|
2336
|
-
async exists(path2) {
|
|
2337
|
-
return this.disk().fileExists(path2);
|
|
2338
|
-
}
|
|
2339
|
-
async missing(path2) {
|
|
2340
|
-
return !await this.exists(path2);
|
|
2341
|
-
}
|
|
2342
|
-
async delete(path2) {
|
|
2343
|
-
return this.disk().deleteFile(path2);
|
|
2344
|
-
}
|
|
2345
|
-
async copy(from, to) {
|
|
2346
|
-
return this.disk().copyFile(from, to);
|
|
2347
|
-
}
|
|
2348
|
-
async move(from, to) {
|
|
2349
|
-
return this.disk().moveFile(from, to);
|
|
2350
|
-
}
|
|
2351
|
-
async url(path2) {
|
|
2352
|
-
return this.disk().publicUrl(path2);
|
|
2353
|
-
}
|
|
2354
|
-
async signedUrl(path2, options) {
|
|
2355
|
-
const adapter2 = this.disk();
|
|
2356
|
-
if (typeof adapter2.signedUrl !== "function") {
|
|
2357
|
-
throw new Error(`[storage] disk '${this.config.default}' does not support signedUrl`);
|
|
2358
|
-
}
|
|
2359
|
-
return adapter2.signedUrl(path2, options);
|
|
2360
|
-
}
|
|
2361
|
-
async size(path2) {
|
|
2362
|
-
return this.disk().fileSize(path2);
|
|
2363
|
-
}
|
|
2364
|
-
async lastModified(path2) {
|
|
2365
|
-
return this.disk().lastModified(path2);
|
|
2366
|
-
}
|
|
2367
|
-
async mimeType(path2) {
|
|
2368
|
-
return this.disk().mimeType(path2);
|
|
2369
|
-
}
|
|
2370
|
-
async checksum(path2, algorithm) {
|
|
2371
|
-
return this.disk().checksum(path2, { algorithm });
|
|
2372
|
-
}
|
|
2373
|
-
async makeDirectory(path2) {
|
|
2374
|
-
return this.disk().createDirectory(path2);
|
|
2375
|
-
}
|
|
2376
|
-
async deleteDirectory(path2) {
|
|
2377
|
-
return this.disk().deleteDirectory(path2);
|
|
2378
|
-
}
|
|
2379
|
-
files(path2 = "") {
|
|
2380
|
-
return this.disk().list(path2);
|
|
2381
|
-
}
|
|
2382
|
-
allFiles(path2 = "") {
|
|
2383
|
-
return this.disk().list(path2, { deep: true });
|
|
2384
|
-
}
|
|
2385
|
-
configure(name, config) {
|
|
2386
|
-
const currentConfig = this.config;
|
|
2387
|
-
currentConfig.disks[name] = config;
|
|
2388
|
-
this.disks.delete(name);
|
|
2389
|
-
this.s3Clients.delete(name);
|
|
2390
|
-
return this;
|
|
2391
|
-
}
|
|
2392
|
-
setDefaultDisk(name) {
|
|
2393
|
-
if (!this.config.disks[name]) {
|
|
2394
|
-
throw new Error(`Disk [${name}] is not configured`);
|
|
2395
|
-
}
|
|
2396
|
-
this.config.default = name;
|
|
2397
|
-
return this;
|
|
2398
|
-
}
|
|
2399
|
-
getDiskConfig(name) {
|
|
2400
|
-
return this.config.disks[name || this.config.default];
|
|
2401
|
-
}
|
|
2402
|
-
getConfiguredDisks() {
|
|
2403
|
-
return Object.keys(this.config.disks);
|
|
2404
|
-
}
|
|
2405
|
-
getDefaultDisk() {
|
|
2406
|
-
return this.config.default;
|
|
2407
|
-
}
|
|
2408
|
-
reset() {
|
|
2409
|
-
this._config = null;
|
|
2410
|
-
this.customConfig = null;
|
|
2411
|
-
this.disks.clear();
|
|
2412
|
-
this.s3Clients.clear();
|
|
2413
|
-
return this;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
var Storage = new StorageManager;
|
|
3447
|
+
// src/index.ts
|
|
3448
|
+
init_facade();
|
|
3449
|
+
|
|
2417
3450
|
// src/uploaded-file.ts
|
|
3451
|
+
init_facade();
|
|
2418
3452
|
import { basename as basename5, extname as extname2, join as join7 } from "path";
|
|
3453
|
+
|
|
2419
3454
|
class UploadedFile {
|
|
2420
3455
|
_file;
|
|
2421
3456
|
_hashName = null;
|
|
@@ -2574,6 +3609,67 @@ function configFromEnv(base = {}) {
|
|
|
2574
3609
|
}
|
|
2575
3610
|
};
|
|
2576
3611
|
}
|
|
3612
|
+
|
|
3613
|
+
// src/index.ts
|
|
3614
|
+
init_signed_url();
|
|
3615
|
+
init_path_sanitize();
|
|
3616
|
+
|
|
3617
|
+
// src/mime-verify.ts
|
|
3618
|
+
function detectMimeFromMagicBytes(bytes) {
|
|
3619
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
3620
|
+
if (view.length < 4)
|
|
3621
|
+
return null;
|
|
3622
|
+
if (view[0] === 137 && view[1] === 80 && view[2] === 78 && view[3] === 71 && view[4] === 13 && view[5] === 10 && view[6] === 26 && view[7] === 10)
|
|
3623
|
+
return "image/png";
|
|
3624
|
+
if (view[0] === 255 && view[1] === 216 && view[2] === 255)
|
|
3625
|
+
return "image/jpeg";
|
|
3626
|
+
if (view[0] === 71 && view[1] === 73 && view[2] === 70 && view[3] === 56)
|
|
3627
|
+
return "image/gif";
|
|
3628
|
+
if (view[0] === 82 && view[1] === 73 && view[2] === 70 && view[3] === 70 && view.length >= 12) {
|
|
3629
|
+
if (view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80)
|
|
3630
|
+
return "image/webp";
|
|
3631
|
+
if (view[8] === 87 && view[9] === 65 && view[10] === 86 && view[11] === 69)
|
|
3632
|
+
return "audio/wav";
|
|
3633
|
+
}
|
|
3634
|
+
if (view[0] === 37 && view[1] === 80 && view[2] === 68 && view[3] === 70)
|
|
3635
|
+
return "application/pdf";
|
|
3636
|
+
if (view[0] === 80 && view[1] === 75 && (view[2] === 3 || view[2] === 5) && (view[3] === 4 || view[3] === 6))
|
|
3637
|
+
return "application/zip";
|
|
3638
|
+
if (view.length >= 12 && view[4] === 102 && view[5] === 116 && view[6] === 121 && view[7] === 112) {
|
|
3639
|
+
const brand = String.fromCharCode(view[8] ?? 0, view[9] ?? 0, view[10] ?? 0, view[11] ?? 0);
|
|
3640
|
+
if (brand === "avif" || brand === "avis")
|
|
3641
|
+
return "image/avif";
|
|
3642
|
+
if (brand === "heic" || brand === "heix" || brand === "mif1")
|
|
3643
|
+
return "image/heic";
|
|
3644
|
+
return "video/mp4";
|
|
3645
|
+
}
|
|
3646
|
+
if (view[0] === 26 && view[1] === 69 && view[2] === 223 && view[3] === 163)
|
|
3647
|
+
return "video/webm";
|
|
3648
|
+
if (view[0] === 73 && view[1] === 68 && view[2] === 51)
|
|
3649
|
+
return "audio/mpeg";
|
|
3650
|
+
if (view[0] === 255 && (view[1] ?? 0) >= 224)
|
|
3651
|
+
return "audio/mpeg";
|
|
3652
|
+
return null;
|
|
3653
|
+
}
|
|
3654
|
+
function normalizeContentType(contentType) {
|
|
3655
|
+
return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
3656
|
+
}
|
|
3657
|
+
async function verifyUploadedMime(path2, expectedContentType, options = {}) {
|
|
3658
|
+
const { Storage: Storage2 } = await Promise.resolve().then(() => (init_facade(), exports_facade));
|
|
3659
|
+
const disk = Storage2.disk(options.disk);
|
|
3660
|
+
const bytes = await disk.readToUint8Array(path2);
|
|
3661
|
+
const expected = normalizeContentType(expectedContentType);
|
|
3662
|
+
const detected = detectMimeFromMagicBytes(bytes.slice(0, 32));
|
|
3663
|
+
const matches = detected === expected || detected === "image/jpeg" && (expected === "image/jpg" || expected === "image/pjpeg");
|
|
3664
|
+
return {
|
|
3665
|
+
ok: matches,
|
|
3666
|
+
expected,
|
|
3667
|
+
detected
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// src/index.ts
|
|
3672
|
+
init_s3_presigned_post();
|
|
2577
3673
|
export {
|
|
2578
3674
|
zip,
|
|
2579
3675
|
writeTextFile,
|
|
@@ -2581,6 +3677,7 @@ export {
|
|
|
2581
3677
|
writeFileSync,
|
|
2582
3678
|
writeFile,
|
|
2583
3679
|
watchFile,
|
|
3680
|
+
verifyUploadedMime,
|
|
2584
3681
|
verifySignedStorageToken,
|
|
2585
3682
|
uploadedFiles,
|
|
2586
3683
|
uploadedFile,
|
|
@@ -2588,13 +3685,19 @@ export {
|
|
|
2588
3685
|
unzip,
|
|
2589
3686
|
unarchive,
|
|
2590
3687
|
exports_storage as storage,
|
|
3688
|
+
signS3PresignedPost,
|
|
2591
3689
|
serveFile,
|
|
3690
|
+
scoped,
|
|
3691
|
+
sanitizePresignedFilename,
|
|
3692
|
+
sanitizePresignedDir,
|
|
2592
3693
|
s3Disk,
|
|
3694
|
+
revokeSignedStorageToken,
|
|
2593
3695
|
readTextFile,
|
|
2594
3696
|
readPackageJson,
|
|
2595
3697
|
readJsonFile,
|
|
2596
3698
|
readFileSync,
|
|
2597
3699
|
put,
|
|
3700
|
+
parseDiskPath,
|
|
2598
3701
|
normalizeExpiryToMilliseconds,
|
|
2599
3702
|
normalizeExpiryToDate,
|
|
2600
3703
|
mkdirSync,
|
|
@@ -2602,6 +3705,7 @@ export {
|
|
|
2602
3705
|
memory2 as memory,
|
|
2603
3706
|
localDisk,
|
|
2604
3707
|
local2 as local,
|
|
3708
|
+
isSignedStorageTokenRevoked,
|
|
2605
3709
|
isFolder,
|
|
2606
3710
|
isFile2 as isFile,
|
|
2607
3711
|
isDirectoryEmpty,
|
|
@@ -2635,6 +3739,7 @@ export {
|
|
|
2635
3739
|
doesNotExist,
|
|
2636
3740
|
doesFolderExist,
|
|
2637
3741
|
doesExist,
|
|
3742
|
+
detectMimeFromMagicBytes,
|
|
2638
3743
|
deleteGlob,
|
|
2639
3744
|
deleteFolder,
|
|
2640
3745
|
deleteFiles,
|
|
@@ -2656,6 +3761,7 @@ export {
|
|
|
2656
3761
|
copy,
|
|
2657
3762
|
configFromEnv,
|
|
2658
3763
|
compress,
|
|
3764
|
+
clearRevokedSignedStorageTokens,
|
|
2659
3765
|
bun2 as bun,
|
|
2660
3766
|
aws,
|
|
2661
3767
|
archive,
|
|
@@ -2664,7 +3770,9 @@ export {
|
|
|
2664
3770
|
UploadedFile,
|
|
2665
3771
|
StorageManager,
|
|
2666
3772
|
Storage,
|
|
3773
|
+
ScopedStorageAdapter,
|
|
2667
3774
|
S3StorageAdapter,
|
|
3775
|
+
PathSanitizeError,
|
|
2668
3776
|
LocalStorageAdapter,
|
|
2669
3777
|
InMemoryStorageAdapter,
|
|
2670
3778
|
BunStorageAdapter
|