s3kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -0
- package/dist/adapters/express.cjs +305 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +10 -0
- package/dist/adapters/express.d.ts +10 -0
- package/dist/adapters/express.js +278 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fetch.cjs +298 -0
- package/dist/adapters/fetch.cjs.map +1 -0
- package/dist/adapters/fetch.d.cts +9 -0
- package/dist/adapters/fetch.d.ts +9 -0
- package/dist/adapters/fetch.js +271 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/next.cjs +796 -0
- package/dist/adapters/next.cjs.map +1 -0
- package/dist/adapters/next.d.cts +28 -0
- package/dist/adapters/next.d.ts +28 -0
- package/dist/adapters/next.js +775 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/client/index.cjs +153 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +59 -0
- package/dist/client/index.d.ts +59 -0
- package/dist/client/index.js +126 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.cjs +452 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +11 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +430 -0
- package/dist/core/index.js.map +1 -0
- package/dist/http/index.cjs +270 -0
- package/dist/http/index.cjs.map +1 -0
- package/dist/http/index.d.cts +49 -0
- package/dist/http/index.d.ts +49 -0
- package/dist/http/index.js +243 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.cjs +808 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/manager-BbmXpgXN.d.ts +29 -0
- package/dist/manager-gIjo-t8h.d.cts +29 -0
- package/dist/react/index.cjs +4320 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.css +155 -0
- package/dist/react/index.css.map +1 -0
- package/dist/react/index.d.cts +79 -0
- package/dist/react/index.d.ts +79 -0
- package/dist/react/index.js +4315 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-g2IYvH3O.d.cts +123 -0
- package/dist/types-g2IYvH3O.d.ts +123 -0
- package/package.json +100 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/next.ts
|
|
21
|
+
var next_exports = {};
|
|
22
|
+
__export(next_exports, {
|
|
23
|
+
createNextApiHandler: () => createNextApiHandler,
|
|
24
|
+
createNextRouteHandler: () => createNextRouteHandler,
|
|
25
|
+
createNextRouteHandlerFromEnv: () => createNextRouteHandlerFromEnv
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(next_exports);
|
|
28
|
+
var import_client_s33 = require("@aws-sdk/client-s3");
|
|
29
|
+
|
|
30
|
+
// src/core/manager.ts
|
|
31
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
32
|
+
var import_client_s32 = require("@aws-sdk/client-s3");
|
|
33
|
+
var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
34
|
+
|
|
35
|
+
// src/core/errors.ts
|
|
36
|
+
var S3FileManagerAuthorizationError = class extends Error {
|
|
37
|
+
status;
|
|
38
|
+
code;
|
|
39
|
+
constructor(message, status, code) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.code = code;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/core/manager.ts
|
|
47
|
+
var DEFAULT_DELIMITER = "/";
|
|
48
|
+
function trimSlashes(input) {
|
|
49
|
+
return input.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
50
|
+
}
|
|
51
|
+
function normalizePath(input) {
|
|
52
|
+
const raw = input.replace(/\\/g, "/");
|
|
53
|
+
const noLeading = raw.replace(/^\/+/, "");
|
|
54
|
+
const segments = noLeading.split("/").filter((s) => s.length > 0);
|
|
55
|
+
for (const seg of segments) {
|
|
56
|
+
if (seg === "..") {
|
|
57
|
+
throw new Error("Invalid path");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return segments.join("/");
|
|
61
|
+
}
|
|
62
|
+
function ensureTrailingDelimiter(prefix, delimiter) {
|
|
63
|
+
if (prefix === "") return "";
|
|
64
|
+
return prefix.endsWith(delimiter) ? prefix : `${prefix}${delimiter}`;
|
|
65
|
+
}
|
|
66
|
+
function encodeS3CopySource(bucket, key) {
|
|
67
|
+
return encodeURIComponent(`${bucket}/${key}`).replace(/%2F/g, "/");
|
|
68
|
+
}
|
|
69
|
+
function isNoSuchKeyError(err) {
|
|
70
|
+
if (!err || typeof err !== "object") return false;
|
|
71
|
+
if ("name" in err && err.name === "NoSuchKey") return true;
|
|
72
|
+
if ("message" in err && typeof err.message === "string") {
|
|
73
|
+
return err.message.includes("The specified key does not exist");
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
async function* listAllKeys(s3, bucket, prefix) {
|
|
78
|
+
let cursor;
|
|
79
|
+
while (true) {
|
|
80
|
+
const out = await s3.send(
|
|
81
|
+
new import_client_s3.ListObjectsV2Command({
|
|
82
|
+
Bucket: bucket,
|
|
83
|
+
Prefix: prefix,
|
|
84
|
+
ContinuationToken: cursor
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
for (const obj of out.Contents ?? []) {
|
|
88
|
+
if (obj.Key) yield obj.Key;
|
|
89
|
+
}
|
|
90
|
+
if (!out.IsTruncated) break;
|
|
91
|
+
cursor = out.NextContinuationToken;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function deleteKeysInBatches(s3, bucket, keys) {
|
|
95
|
+
const batchSize = 1e3;
|
|
96
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
97
|
+
const batch = keys.slice(i, i + batchSize);
|
|
98
|
+
await s3.send(
|
|
99
|
+
new import_client_s3.DeleteObjectsCommand({
|
|
100
|
+
Bucket: bucket,
|
|
101
|
+
Delete: {
|
|
102
|
+
Objects: batch.map((Key) => ({ Key })),
|
|
103
|
+
Quiet: true
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
var S3FileManager = class {
|
|
110
|
+
s3;
|
|
111
|
+
bucket;
|
|
112
|
+
rootPrefix;
|
|
113
|
+
delimiter;
|
|
114
|
+
hooks;
|
|
115
|
+
authorizationMode;
|
|
116
|
+
constructor(s3, options) {
|
|
117
|
+
this.s3 = s3;
|
|
118
|
+
this.bucket = options.bucket;
|
|
119
|
+
this.delimiter = options.delimiter ?? DEFAULT_DELIMITER;
|
|
120
|
+
this.rootPrefix = ensureTrailingDelimiter(trimSlashes(options.rootPrefix ?? ""), this.delimiter);
|
|
121
|
+
this.hooks = options.hooks;
|
|
122
|
+
this.authorizationMode = options.authorizationMode ?? "deny-by-default";
|
|
123
|
+
}
|
|
124
|
+
async authorize(args) {
|
|
125
|
+
const hasAuthHooks = Boolean(this.hooks?.authorize || this.hooks?.allowAction);
|
|
126
|
+
if (this.hooks?.authorize) {
|
|
127
|
+
const result = await this.hooks.authorize(args);
|
|
128
|
+
if (result === false) {
|
|
129
|
+
throw new S3FileManagerAuthorizationError("Unauthorized", 401, "unauthorized");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (this.hooks?.allowAction) {
|
|
133
|
+
const allowed = await this.hooks.allowAction(args);
|
|
134
|
+
if (allowed === false) {
|
|
135
|
+
throw new S3FileManagerAuthorizationError("Forbidden", 403, "forbidden");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!hasAuthHooks && this.authorizationMode === "deny-by-default") {
|
|
139
|
+
throw new S3FileManagerAuthorizationError("Unauthorized", 401, "unauthorized");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
pathToKey(path) {
|
|
143
|
+
const p = normalizePath(path);
|
|
144
|
+
return `${this.rootPrefix}${p}`;
|
|
145
|
+
}
|
|
146
|
+
pathToFolderPrefix(path) {
|
|
147
|
+
const key = this.pathToKey(path);
|
|
148
|
+
return ensureTrailingDelimiter(key, this.delimiter);
|
|
149
|
+
}
|
|
150
|
+
keyToPath(key) {
|
|
151
|
+
if (!key.startsWith(this.rootPrefix)) {
|
|
152
|
+
throw new Error("Key is outside of rootPrefix");
|
|
153
|
+
}
|
|
154
|
+
return key.slice(this.rootPrefix.length);
|
|
155
|
+
}
|
|
156
|
+
makeFolderEntry(path) {
|
|
157
|
+
const p = normalizePath(path);
|
|
158
|
+
const name = p === "" ? "" : p.split("/").at(-1) ?? "";
|
|
159
|
+
return {
|
|
160
|
+
type: "folder",
|
|
161
|
+
path: p === "" ? "" : ensureTrailingDelimiter(p, this.delimiter),
|
|
162
|
+
name
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
makeFileEntryFromKey(key, obj) {
|
|
166
|
+
const path = this.keyToPath(key);
|
|
167
|
+
const name = path.split("/").at(-1) ?? "";
|
|
168
|
+
return {
|
|
169
|
+
type: "file",
|
|
170
|
+
path,
|
|
171
|
+
name,
|
|
172
|
+
...obj.Size !== void 0 ? { size: obj.Size } : {},
|
|
173
|
+
...obj.LastModified ? { lastModified: obj.LastModified.toISOString() } : {},
|
|
174
|
+
...obj.ETag !== void 0 ? { etag: obj.ETag } : {}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async list(options, ctx = {}) {
|
|
178
|
+
const path = normalizePath(options.path);
|
|
179
|
+
await this.authorize({ action: "list", path, ctx });
|
|
180
|
+
const prefix = path === "" ? this.rootPrefix : this.pathToFolderPrefix(path);
|
|
181
|
+
const out = await this.s3.send(
|
|
182
|
+
new import_client_s3.ListObjectsV2Command({
|
|
183
|
+
Bucket: this.bucket,
|
|
184
|
+
Prefix: prefix,
|
|
185
|
+
Delimiter: this.delimiter,
|
|
186
|
+
ContinuationToken: options.cursor,
|
|
187
|
+
MaxKeys: options.limit
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
const folders = (out.CommonPrefixes ?? []).map((cp) => cp.Prefix).filter((p) => typeof p === "string").map((p) => {
|
|
191
|
+
const rel = this.keyToPath(p);
|
|
192
|
+
const folderPath = ensureTrailingDelimiter(trimSlashes(rel), this.delimiter);
|
|
193
|
+
return this.makeFolderEntry(folderPath);
|
|
194
|
+
});
|
|
195
|
+
const files = (out.Contents ?? []).filter((obj) => typeof obj.Key === "string").filter((obj) => obj.Key !== prefix).map((obj) => this.makeFileEntryFromKey(obj.Key, obj));
|
|
196
|
+
for (const folder of folders) {
|
|
197
|
+
await this.hooks?.decorateFolder?.(folder, {
|
|
198
|
+
path: folder.path,
|
|
199
|
+
prefix: this.pathToFolderPrefix(folder.path)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
await this.hooks?.decorateFile?.(file, {
|
|
204
|
+
path: file.path,
|
|
205
|
+
key: this.pathToKey(file.path)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const entries = [...folders, ...files].sort((a, b) => {
|
|
209
|
+
if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
|
|
210
|
+
return a.name.localeCompare(b.name);
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
path,
|
|
214
|
+
entries,
|
|
215
|
+
...out.IsTruncated && out.NextContinuationToken ? { nextCursor: out.NextContinuationToken } : {}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async createFolder(options, ctx = {}) {
|
|
219
|
+
const path = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
|
|
220
|
+
await this.authorize({ action: "folder.create", path, ctx });
|
|
221
|
+
const key = this.pathToFolderPrefix(path);
|
|
222
|
+
await this.s3.send(
|
|
223
|
+
new import_client_s3.PutObjectCommand({
|
|
224
|
+
Bucket: this.bucket,
|
|
225
|
+
Key: key,
|
|
226
|
+
Body: ""
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
async deleteFolder(options, ctx = {}) {
|
|
231
|
+
const path = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
|
|
232
|
+
await this.authorize({ action: "folder.delete", path, ctx });
|
|
233
|
+
const prefix = this.pathToFolderPrefix(path);
|
|
234
|
+
if (!options.recursive) {
|
|
235
|
+
const out = await this.s3.send(
|
|
236
|
+
new import_client_s3.ListObjectsV2Command({
|
|
237
|
+
Bucket: this.bucket,
|
|
238
|
+
Prefix: prefix,
|
|
239
|
+
MaxKeys: 2
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
const keys2 = (out.Contents ?? []).map((o) => o.Key).filter((k) => typeof k === "string" && k !== prefix);
|
|
243
|
+
if (keys2.length > 0) {
|
|
244
|
+
throw new Error("Folder is not empty");
|
|
245
|
+
}
|
|
246
|
+
await this.s3.send(new import_client_s3.DeleteObjectCommand({ Bucket: this.bucket, Key: prefix }));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const keys = [];
|
|
250
|
+
for await (const key of listAllKeys(this.s3, this.bucket, prefix)) {
|
|
251
|
+
keys.push(key);
|
|
252
|
+
}
|
|
253
|
+
if (keys.length === 0) return;
|
|
254
|
+
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
255
|
+
}
|
|
256
|
+
async deleteFiles(options, ctx = {}) {
|
|
257
|
+
const paths = options.paths.map((p) => normalizePath(p));
|
|
258
|
+
for (const path of paths) {
|
|
259
|
+
await this.authorize({ action: "file.delete", path, ctx });
|
|
260
|
+
}
|
|
261
|
+
const keys = paths.map((p) => this.pathToKey(p));
|
|
262
|
+
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
263
|
+
}
|
|
264
|
+
async copy(options, ctx = {}) {
|
|
265
|
+
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
266
|
+
const fromPath = normalizePath(options.fromPath);
|
|
267
|
+
const toPath = normalizePath(options.toPath);
|
|
268
|
+
if (fromPath === toPath) return;
|
|
269
|
+
if (isFolder) {
|
|
270
|
+
const fromPathWithSlash = ensureTrailingDelimiter(fromPath, this.delimiter);
|
|
271
|
+
const toPathWithSlash = ensureTrailingDelimiter(toPath, this.delimiter);
|
|
272
|
+
await this.authorize({
|
|
273
|
+
action: "folder.copy",
|
|
274
|
+
fromPath: fromPathWithSlash,
|
|
275
|
+
toPath: toPathWithSlash,
|
|
276
|
+
ctx
|
|
277
|
+
});
|
|
278
|
+
const fromPrefix = this.pathToFolderPrefix(fromPathWithSlash);
|
|
279
|
+
const toPrefix = this.pathToFolderPrefix(toPathWithSlash);
|
|
280
|
+
try {
|
|
281
|
+
await this.s3.send(
|
|
282
|
+
new import_client_s3.CopyObjectCommand({
|
|
283
|
+
Bucket: this.bucket,
|
|
284
|
+
Key: toPrefix,
|
|
285
|
+
CopySource: encodeS3CopySource(this.bucket, fromPrefix)
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
for await (const sourceKey of listAllKeys(this.s3, this.bucket, fromPrefix)) {
|
|
291
|
+
if (sourceKey === fromPrefix) continue;
|
|
292
|
+
const relKey = sourceKey.slice(fromPrefix.length);
|
|
293
|
+
const destKey = toPrefix + relKey;
|
|
294
|
+
try {
|
|
295
|
+
await this.s3.send(
|
|
296
|
+
new import_client_s3.CopyObjectCommand({
|
|
297
|
+
Bucket: this.bucket,
|
|
298
|
+
Key: destKey,
|
|
299
|
+
CopySource: encodeS3CopySource(this.bucket, sourceKey)
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (isNoSuchKeyError(err)) continue;
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await this.authorize({ action: "file.copy", fromPath, toPath, ctx });
|
|
310
|
+
const fromKey = this.pathToKey(fromPath);
|
|
311
|
+
const toKey = this.pathToKey(toPath);
|
|
312
|
+
await this.s3.send(
|
|
313
|
+
new import_client_s3.CopyObjectCommand({
|
|
314
|
+
Bucket: this.bucket,
|
|
315
|
+
Key: toKey,
|
|
316
|
+
CopySource: encodeS3CopySource(this.bucket, fromKey)
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
async move(options, ctx = {}) {
|
|
321
|
+
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
322
|
+
const fromPath = normalizePath(options.fromPath);
|
|
323
|
+
const toPath = normalizePath(options.toPath);
|
|
324
|
+
if (fromPath === toPath) return;
|
|
325
|
+
if (isFolder) {
|
|
326
|
+
const fromPathWithSlash = ensureTrailingDelimiter(fromPath, this.delimiter);
|
|
327
|
+
const toPathWithSlash = ensureTrailingDelimiter(toPath, this.delimiter);
|
|
328
|
+
await this.authorize({
|
|
329
|
+
action: "folder.move",
|
|
330
|
+
fromPath: fromPathWithSlash,
|
|
331
|
+
toPath: toPathWithSlash,
|
|
332
|
+
ctx
|
|
333
|
+
});
|
|
334
|
+
await this.copy(options, ctx);
|
|
335
|
+
await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await this.authorize({ action: "file.move", fromPath, toPath, ctx });
|
|
339
|
+
await this.copy(options, ctx);
|
|
340
|
+
await this.deleteFiles({ paths: [fromPath] }, ctx);
|
|
341
|
+
}
|
|
342
|
+
async prepareUploads(options, ctx = {}) {
|
|
343
|
+
const expiresIn = options.expiresInSeconds ?? 60 * 5;
|
|
344
|
+
const result = [];
|
|
345
|
+
for (const item of options.items) {
|
|
346
|
+
const path = normalizePath(item.path);
|
|
347
|
+
await this.authorize({ action: "upload.prepare", path, ctx });
|
|
348
|
+
const key = this.pathToKey(path);
|
|
349
|
+
const cmd = new import_client_s3.PutObjectCommand({
|
|
350
|
+
Bucket: this.bucket,
|
|
351
|
+
Key: key,
|
|
352
|
+
ContentType: item.contentType,
|
|
353
|
+
CacheControl: item.cacheControl,
|
|
354
|
+
ContentDisposition: item.contentDisposition,
|
|
355
|
+
Metadata: item.metadata
|
|
356
|
+
});
|
|
357
|
+
const url = await (0, import_s3_request_presigner.getSignedUrl)(this.s3, cmd, { expiresIn });
|
|
358
|
+
const headers = {};
|
|
359
|
+
if (item.contentType) headers["Content-Type"] = item.contentType;
|
|
360
|
+
if (item.cacheControl) headers["Cache-Control"] = item.cacheControl;
|
|
361
|
+
if (item.contentDisposition) headers["Content-Disposition"] = item.contentDisposition;
|
|
362
|
+
if (item.metadata) {
|
|
363
|
+
for (const [k, v] of Object.entries(item.metadata)) {
|
|
364
|
+
headers[`x-amz-meta-${k}`] = v;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
result.push({
|
|
368
|
+
path,
|
|
369
|
+
url,
|
|
370
|
+
method: "PUT",
|
|
371
|
+
headers
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
async search(options, ctx = {}) {
|
|
377
|
+
const query = options.query.toLowerCase().trim();
|
|
378
|
+
if (!query) {
|
|
379
|
+
return { query: options.query, entries: [] };
|
|
380
|
+
}
|
|
381
|
+
await this.authorize({ action: "search", ctx });
|
|
382
|
+
const searchPrefix = options.path ? this.pathToFolderPrefix(normalizePath(options.path)) : this.rootPrefix;
|
|
383
|
+
const entries = [];
|
|
384
|
+
const limit = options.limit ?? 500;
|
|
385
|
+
const recursive = options.recursive !== false;
|
|
386
|
+
let cursor = options.cursor;
|
|
387
|
+
let nextCursor;
|
|
388
|
+
while (entries.length < limit) {
|
|
389
|
+
const out = await this.s3.send(
|
|
390
|
+
new import_client_s3.ListObjectsV2Command({
|
|
391
|
+
Bucket: this.bucket,
|
|
392
|
+
Prefix: searchPrefix,
|
|
393
|
+
ContinuationToken: cursor,
|
|
394
|
+
MaxKeys: 1e3
|
|
395
|
+
// Fetch more to filter
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
nextCursor = out.IsTruncated && out.NextContinuationToken ? out.NextContinuationToken : void 0;
|
|
399
|
+
for (const obj of out.Contents ?? []) {
|
|
400
|
+
if (!obj.Key || obj.Key === searchPrefix) continue;
|
|
401
|
+
const path = this.keyToPath(obj.Key);
|
|
402
|
+
const name = path.split("/").at(-1) ?? "";
|
|
403
|
+
if (!recursive && options.path) {
|
|
404
|
+
const base = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
|
|
405
|
+
const rel = path.startsWith(base) ? path.slice(base.length) : path;
|
|
406
|
+
if (rel.includes(this.delimiter)) continue;
|
|
407
|
+
}
|
|
408
|
+
if (!recursive && !options.path) {
|
|
409
|
+
if (path.includes(this.delimiter)) continue;
|
|
410
|
+
}
|
|
411
|
+
if (name.toLowerCase().includes(query)) {
|
|
412
|
+
const fileEntry = this.makeFileEntryFromKey(obj.Key, {
|
|
413
|
+
...obj.Size !== void 0 ? { Size: obj.Size } : {},
|
|
414
|
+
...obj.LastModified !== void 0 ? { LastModified: obj.LastModified } : {},
|
|
415
|
+
...obj.ETag !== void 0 ? { ETag: obj.ETag } : {}
|
|
416
|
+
});
|
|
417
|
+
await this.hooks?.decorateFile?.(fileEntry, {
|
|
418
|
+
path: fileEntry.path,
|
|
419
|
+
key: this.pathToKey(fileEntry.path)
|
|
420
|
+
});
|
|
421
|
+
entries.push(fileEntry);
|
|
422
|
+
}
|
|
423
|
+
if (entries.length >= limit) break;
|
|
424
|
+
}
|
|
425
|
+
if (!out.IsTruncated || !out.NextContinuationToken) break;
|
|
426
|
+
cursor = out.NextContinuationToken;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
query: options.query,
|
|
430
|
+
entries,
|
|
431
|
+
...nextCursor ? { nextCursor } : {}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async getPreviewUrl(options, ctx = {}) {
|
|
435
|
+
const path = normalizePath(options.path);
|
|
436
|
+
await this.authorize({ action: "preview.get", path, ctx });
|
|
437
|
+
const expiresIn = options.expiresInSeconds ?? 60 * 5;
|
|
438
|
+
const key = this.pathToKey(path);
|
|
439
|
+
const cmd = new import_client_s32.GetObjectCommand({
|
|
440
|
+
Bucket: this.bucket,
|
|
441
|
+
Key: key,
|
|
442
|
+
ResponseContentDisposition: options.inline ? "inline" : "attachment"
|
|
443
|
+
});
|
|
444
|
+
const url = await (0, import_s3_request_presigner.getSignedUrl)(this.s3, cmd, { expiresIn });
|
|
445
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
|
|
446
|
+
return { path, url, expiresAt };
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/http/handler.ts
|
|
451
|
+
var S3FileManagerHttpError = class extends Error {
|
|
452
|
+
status;
|
|
453
|
+
code;
|
|
454
|
+
constructor(status, code, message) {
|
|
455
|
+
super(message);
|
|
456
|
+
this.status = status;
|
|
457
|
+
this.code = code;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function normalizeBasePath(basePath) {
|
|
461
|
+
if (!basePath) return "";
|
|
462
|
+
if (basePath === "/") return "";
|
|
463
|
+
return basePath.startsWith("/") ? basePath.replace(/\/+$/, "") : `/${basePath.replace(/\/+$/, "")}`;
|
|
464
|
+
}
|
|
465
|
+
function jsonError(status, code, message) {
|
|
466
|
+
return {
|
|
467
|
+
status,
|
|
468
|
+
headers: { "content-type": "application/json" },
|
|
469
|
+
body: {
|
|
470
|
+
error: {
|
|
471
|
+
code,
|
|
472
|
+
message
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function ensureObject(body) {
|
|
478
|
+
if (body && typeof body === "object" && !Array.isArray(body))
|
|
479
|
+
return body;
|
|
480
|
+
throw new S3FileManagerHttpError(400, "invalid_body", "Expected JSON object body");
|
|
481
|
+
}
|
|
482
|
+
function optionalString(value, key) {
|
|
483
|
+
if (value === void 0) return void 0;
|
|
484
|
+
if (typeof value === "string") return value;
|
|
485
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
486
|
+
}
|
|
487
|
+
function requiredString(value, key) {
|
|
488
|
+
if (typeof value === "string") return value;
|
|
489
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
490
|
+
}
|
|
491
|
+
function optionalNumber(value, key) {
|
|
492
|
+
if (value === void 0) return void 0;
|
|
493
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
494
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a finite number`);
|
|
495
|
+
}
|
|
496
|
+
function optionalBoolean(value, key) {
|
|
497
|
+
if (value === void 0) return void 0;
|
|
498
|
+
if (typeof value === "boolean") return value;
|
|
499
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a boolean`);
|
|
500
|
+
}
|
|
501
|
+
function requiredStringArray(value, key) {
|
|
502
|
+
if (!Array.isArray(value)) {
|
|
503
|
+
throw new S3FileManagerHttpError(
|
|
504
|
+
400,
|
|
505
|
+
"invalid_body",
|
|
506
|
+
`Expected '${key}' to be an array of strings`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
for (const item of value) {
|
|
510
|
+
if (typeof item !== "string") {
|
|
511
|
+
throw new S3FileManagerHttpError(
|
|
512
|
+
400,
|
|
513
|
+
"invalid_body",
|
|
514
|
+
`Expected '${key}' to be an array of strings`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
function optionalStringRecord(value, key) {
|
|
521
|
+
if (value === void 0) return void 0;
|
|
522
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
523
|
+
throw new S3FileManagerHttpError(
|
|
524
|
+
400,
|
|
525
|
+
"invalid_body",
|
|
526
|
+
`Expected '${key}' to be an object of strings`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const out = {};
|
|
530
|
+
for (const [k, v] of Object.entries(value)) {
|
|
531
|
+
if (typeof v !== "string") {
|
|
532
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}.${k}' to be a string`);
|
|
533
|
+
}
|
|
534
|
+
out[k] = v;
|
|
535
|
+
}
|
|
536
|
+
return out;
|
|
537
|
+
}
|
|
538
|
+
function parseListOptions(body) {
|
|
539
|
+
const obj = ensureObject(body);
|
|
540
|
+
return {
|
|
541
|
+
path: requiredString(obj.path, "path"),
|
|
542
|
+
cursor: optionalString(obj.cursor, "cursor"),
|
|
543
|
+
limit: optionalNumber(obj.limit, "limit")
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function parseSearchOptions(body) {
|
|
547
|
+
const obj = ensureObject(body);
|
|
548
|
+
return {
|
|
549
|
+
query: requiredString(obj.query, "query"),
|
|
550
|
+
path: optionalString(obj.path, "path"),
|
|
551
|
+
recursive: optionalBoolean(obj.recursive, "recursive"),
|
|
552
|
+
limit: optionalNumber(obj.limit, "limit"),
|
|
553
|
+
cursor: optionalString(obj.cursor, "cursor")
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function parseCreateFolderOptions(body) {
|
|
557
|
+
const obj = ensureObject(body);
|
|
558
|
+
return { path: requiredString(obj.path, "path") };
|
|
559
|
+
}
|
|
560
|
+
function parseDeleteFolderOptions(body) {
|
|
561
|
+
const obj = ensureObject(body);
|
|
562
|
+
return {
|
|
563
|
+
path: requiredString(obj.path, "path"),
|
|
564
|
+
recursive: optionalBoolean(obj.recursive, "recursive")
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function parseDeleteFilesOptions(body) {
|
|
568
|
+
const obj = ensureObject(body);
|
|
569
|
+
return { paths: requiredStringArray(obj.paths, "paths") };
|
|
570
|
+
}
|
|
571
|
+
function parseCopyMoveOptions(body) {
|
|
572
|
+
const obj = ensureObject(body);
|
|
573
|
+
return {
|
|
574
|
+
fromPath: requiredString(obj.fromPath, "fromPath"),
|
|
575
|
+
toPath: requiredString(obj.toPath, "toPath")
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function parsePrepareUploadsOptions(body) {
|
|
579
|
+
const obj = ensureObject(body);
|
|
580
|
+
const itemsValue = obj.items;
|
|
581
|
+
if (!Array.isArray(itemsValue)) {
|
|
582
|
+
throw new S3FileManagerHttpError(400, "invalid_body", "Expected 'items' to be an array");
|
|
583
|
+
}
|
|
584
|
+
const items = itemsValue.map((raw, idx) => {
|
|
585
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
586
|
+
throw new S3FileManagerHttpError(
|
|
587
|
+
400,
|
|
588
|
+
"invalid_body",
|
|
589
|
+
`Expected 'items[${idx}]' to be an object`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const item = raw;
|
|
593
|
+
return {
|
|
594
|
+
path: requiredString(item.path, `items[${idx}].path`),
|
|
595
|
+
contentType: optionalString(item.contentType, `items[${idx}].contentType`),
|
|
596
|
+
cacheControl: optionalString(item.cacheControl, `items[${idx}].cacheControl`),
|
|
597
|
+
contentDisposition: optionalString(
|
|
598
|
+
item.contentDisposition,
|
|
599
|
+
`items[${idx}].contentDisposition`
|
|
600
|
+
),
|
|
601
|
+
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
items,
|
|
606
|
+
expiresInSeconds: optionalNumber(obj.expiresInSeconds, "expiresInSeconds")
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function parsePreviewOptions(body) {
|
|
610
|
+
const obj = ensureObject(body);
|
|
611
|
+
return {
|
|
612
|
+
path: requiredString(obj.path, "path"),
|
|
613
|
+
expiresInSeconds: optionalNumber(obj.expiresInSeconds, "expiresInSeconds"),
|
|
614
|
+
inline: optionalBoolean(obj.inline, "inline")
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function createS3FileManagerHttpHandler(options) {
|
|
618
|
+
const basePath = normalizeBasePath(options.api?.basePath);
|
|
619
|
+
if (!options.manager && !options.getManager) {
|
|
620
|
+
throw new Error("createS3FileManagerHttpHandler requires either manager or getManager");
|
|
621
|
+
}
|
|
622
|
+
return async (req) => {
|
|
623
|
+
try {
|
|
624
|
+
const ctx = await options.getContext?.(req) ?? {};
|
|
625
|
+
const manager = options.getManager ? await options.getManager(req, ctx) : options.manager;
|
|
626
|
+
const method = req.method.toUpperCase();
|
|
627
|
+
const path = req.path.startsWith(basePath) ? req.path.slice(basePath.length) || "/" : req.path;
|
|
628
|
+
if (method === "POST" && path === "/list") {
|
|
629
|
+
const out = await manager.list(parseListOptions(req.body), ctx);
|
|
630
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
631
|
+
}
|
|
632
|
+
if (method === "POST" && path === "/search") {
|
|
633
|
+
const out = await manager.search(parseSearchOptions(req.body), ctx);
|
|
634
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
635
|
+
}
|
|
636
|
+
if (method === "POST" && path === "/folder/create") {
|
|
637
|
+
await manager.createFolder(parseCreateFolderOptions(req.body), ctx);
|
|
638
|
+
return { status: 204 };
|
|
639
|
+
}
|
|
640
|
+
if (method === "POST" && path === "/folder/delete") {
|
|
641
|
+
await manager.deleteFolder(parseDeleteFolderOptions(req.body), ctx);
|
|
642
|
+
return { status: 204 };
|
|
643
|
+
}
|
|
644
|
+
if (method === "POST" && path === "/files/delete") {
|
|
645
|
+
await manager.deleteFiles(parseDeleteFilesOptions(req.body), ctx);
|
|
646
|
+
return { status: 204 };
|
|
647
|
+
}
|
|
648
|
+
if (method === "POST" && path === "/files/copy") {
|
|
649
|
+
await manager.copy(parseCopyMoveOptions(req.body), ctx);
|
|
650
|
+
return { status: 204 };
|
|
651
|
+
}
|
|
652
|
+
if (method === "POST" && path === "/files/move") {
|
|
653
|
+
await manager.move(parseCopyMoveOptions(req.body), ctx);
|
|
654
|
+
return { status: 204 };
|
|
655
|
+
}
|
|
656
|
+
if (method === "POST" && path === "/upload/prepare") {
|
|
657
|
+
const out = await manager.prepareUploads(parsePrepareUploadsOptions(req.body), ctx);
|
|
658
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
659
|
+
}
|
|
660
|
+
if (method === "POST" && path === "/preview") {
|
|
661
|
+
const out = await manager.getPreviewUrl(parsePreviewOptions(req.body), ctx);
|
|
662
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
663
|
+
}
|
|
664
|
+
return jsonError(404, "not_found", "Route not found");
|
|
665
|
+
} catch (err) {
|
|
666
|
+
if (err instanceof S3FileManagerHttpError) {
|
|
667
|
+
return jsonError(err.status, err.code, err.message);
|
|
668
|
+
}
|
|
669
|
+
if (err instanceof S3FileManagerAuthorizationError) {
|
|
670
|
+
return jsonError(err.status, err.code, err.message);
|
|
671
|
+
}
|
|
672
|
+
console.error("[S3FileManager Error]", err);
|
|
673
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
674
|
+
return jsonError(500, "internal_error", message);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/adapters/next.ts
|
|
680
|
+
async function readJsonBody(req) {
|
|
681
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
682
|
+
if (!ct.includes("application/json")) return void 0;
|
|
683
|
+
return await req.json();
|
|
684
|
+
}
|
|
685
|
+
function createNextRouteHandler(options) {
|
|
686
|
+
const handler = createS3FileManagerHttpHandler(options);
|
|
687
|
+
return async (req) => {
|
|
688
|
+
const url = new URL(req.url);
|
|
689
|
+
const body = await readJsonBody(req);
|
|
690
|
+
const httpReq = {
|
|
691
|
+
method: req.method,
|
|
692
|
+
path: url.pathname,
|
|
693
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
694
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
695
|
+
body
|
|
696
|
+
};
|
|
697
|
+
const out = await handler(httpReq);
|
|
698
|
+
const init = { status: out.status };
|
|
699
|
+
if (out.headers) init.headers = out.headers;
|
|
700
|
+
return new Response(out.body ? JSON.stringify(out.body) : null, init);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
var cachedEnvManager;
|
|
704
|
+
var cachedEnvSignature;
|
|
705
|
+
function requiredEnv(name) {
|
|
706
|
+
const v = process.env[name];
|
|
707
|
+
if (!v) throw new Error(`Missing env var: ${name}`);
|
|
708
|
+
return v;
|
|
709
|
+
}
|
|
710
|
+
function parseBool(v) {
|
|
711
|
+
if (!v) return false;
|
|
712
|
+
return v === "1" || v.toLowerCase() === "true";
|
|
713
|
+
}
|
|
714
|
+
function getEnvManager(options) {
|
|
715
|
+
const envMap = options.env;
|
|
716
|
+
const envSignature = JSON.stringify(envMap ?? {});
|
|
717
|
+
if (cachedEnvManager && cachedEnvSignature === envSignature) return cachedEnvManager;
|
|
718
|
+
const cfg = {
|
|
719
|
+
region: requiredEnv(envMap?.region ?? "AWS_REGION"),
|
|
720
|
+
bucket: requiredEnv(envMap?.bucket ?? "S3_BUCKET"),
|
|
721
|
+
rootPrefix: process.env[envMap?.rootPrefix ?? "S3_ROOT_PREFIX"] ?? "",
|
|
722
|
+
endpoint: process.env[envMap?.endpoint ?? "S3_ENDPOINT"],
|
|
723
|
+
forcePathStyle: parseBool(process.env[envMap?.forcePathStyle ?? "S3_FORCE_PATH_STYLE"]),
|
|
724
|
+
requireUserId: parseBool(process.env[envMap?.requireUserId ?? "REQUIRE_USER_ID"])
|
|
725
|
+
};
|
|
726
|
+
const s3 = new import_client_s33.S3Client({
|
|
727
|
+
region: cfg.region,
|
|
728
|
+
...cfg.endpoint ? { endpoint: cfg.endpoint } : {},
|
|
729
|
+
...cfg.forcePathStyle ? { forcePathStyle: true } : {}
|
|
730
|
+
});
|
|
731
|
+
const authorizeHook = cfg.requireUserId || options.authorization?.authorize ? async (args) => {
|
|
732
|
+
if (cfg.requireUserId && !args.ctx.userId) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
return options.authorization?.authorize?.(args);
|
|
736
|
+
} : void 0;
|
|
737
|
+
const allowActionHook = options.authorization?.allowAction;
|
|
738
|
+
const hooks = authorizeHook || allowActionHook ? {
|
|
739
|
+
...authorizeHook ? { authorize: authorizeHook } : {},
|
|
740
|
+
...allowActionHook ? { allowAction: allowActionHook } : {}
|
|
741
|
+
} : void 0;
|
|
742
|
+
cachedEnvManager = new S3FileManager(s3, {
|
|
743
|
+
bucket: cfg.bucket,
|
|
744
|
+
rootPrefix: cfg.rootPrefix,
|
|
745
|
+
...options.authorization?.mode ? { authorizationMode: options.authorization.mode } : {},
|
|
746
|
+
...hooks ? { hooks } : {}
|
|
747
|
+
});
|
|
748
|
+
cachedEnvSignature = envSignature;
|
|
749
|
+
return cachedEnvManager;
|
|
750
|
+
}
|
|
751
|
+
function getContextFromHeaders(headers, headerName) {
|
|
752
|
+
const raw = headers[headerName];
|
|
753
|
+
const userId = Array.isArray(raw) ? raw[0] : raw;
|
|
754
|
+
return userId ? { userId } : {};
|
|
755
|
+
}
|
|
756
|
+
function createNextRouteHandlerFromEnv(options = {}) {
|
|
757
|
+
const api = options.basePath ? { basePath: options.basePath } : void 0;
|
|
758
|
+
const handlerOptions = {
|
|
759
|
+
getManager: () => getEnvManager(options),
|
|
760
|
+
getContext: (req) => getContextFromHeaders(req.headers, options.userIdHeader ?? "x-user-id")
|
|
761
|
+
};
|
|
762
|
+
if (api) handlerOptions.api = api;
|
|
763
|
+
return createNextRouteHandler(handlerOptions);
|
|
764
|
+
}
|
|
765
|
+
function createNextApiHandler(options) {
|
|
766
|
+
const handler = createS3FileManagerHttpHandler(options);
|
|
767
|
+
return async (req, res) => {
|
|
768
|
+
const httpReq = {
|
|
769
|
+
method: req.method,
|
|
770
|
+
path: req.url?.split("?")[0] ?? "/",
|
|
771
|
+
query: req.query ?? {},
|
|
772
|
+
headers: req.headers ?? {},
|
|
773
|
+
body: req.body
|
|
774
|
+
};
|
|
775
|
+
const out = await handler(httpReq);
|
|
776
|
+
if (out.headers) {
|
|
777
|
+
for (const [k, v] of Object.entries(out.headers)) {
|
|
778
|
+
res.setHeader(k, v);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
res.statusCode = out.status;
|
|
782
|
+
if (out.status === 204) {
|
|
783
|
+
res.end();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
res.setHeader("content-type", out.headers?.["content-type"] ?? "application/json");
|
|
787
|
+
res.end(JSON.stringify(out.body ?? null));
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
791
|
+
0 && (module.exports = {
|
|
792
|
+
createNextApiHandler,
|
|
793
|
+
createNextRouteHandler,
|
|
794
|
+
createNextRouteHandlerFromEnv
|
|
795
|
+
});
|
|
796
|
+
//# sourceMappingURL=next.cjs.map
|