img-upload-kit 1.1.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -74
- package/MIGRATION.md +192 -0
- package/README.md +243 -259
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +40 -412
- package/dist/blur.cjs.map +1 -1
- package/dist/blur.d.cts +5 -5
- package/dist/blur.d.ts +5 -5
- package/dist/blur.js +39 -393
- package/dist/blur.js.map +1 -1
- package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
- package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
- package/dist/blurService-D5TpMNji.d.ts +83 -0
- package/dist/blurService-DacwylJx.d.cts +83 -0
- package/dist/chunk-25GYKQ6J.js +158 -0
- package/dist/chunk-25GYKQ6J.js.map +1 -0
- package/dist/chunk-34L4TEE3.cjs +226 -0
- package/dist/chunk-34L4TEE3.cjs.map +1 -0
- package/dist/chunk-DFCSN3X7.cjs +158 -0
- package/dist/chunk-DFCSN3X7.cjs.map +1 -0
- package/dist/chunk-EWVIL57J.js +294 -0
- package/dist/chunk-EWVIL57J.js.map +1 -0
- package/dist/chunk-FP5YIWG5.cjs +135 -0
- package/dist/chunk-FP5YIWG5.cjs.map +1 -0
- package/dist/chunk-HILELQGS.js +226 -0
- package/dist/chunk-HILELQGS.js.map +1 -0
- package/dist/chunk-IDLLCVGB.js +2370 -0
- package/dist/chunk-IDLLCVGB.js.map +1 -0
- package/dist/chunk-J6A44Y3W.js +514 -0
- package/dist/chunk-J6A44Y3W.js.map +1 -0
- package/dist/chunk-LOSVFO56.cjs +145 -0
- package/dist/chunk-LOSVFO56.cjs.map +1 -0
- package/dist/chunk-MBQJ6NVJ.cjs +294 -0
- package/dist/chunk-MBQJ6NVJ.cjs.map +1 -0
- package/dist/chunk-MHGQEBPW.js +145 -0
- package/dist/chunk-MHGQEBPW.js.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TA5Y64JM.cjs +2370 -0
- package/dist/chunk-TA5Y64JM.cjs.map +1 -0
- package/dist/chunk-VYVZVUNI.js +135 -0
- package/dist/chunk-VYVZVUNI.js.map +1 -0
- package/dist/chunk-XKQMCY5Z.cjs +514 -0
- package/dist/chunk-XKQMCY5Z.cjs.map +1 -0
- package/dist/cli.cjs +273 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +269 -73
- package/dist/cli.js.map +1 -1
- package/dist/headless.cjs +8 -1880
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +3 -1
- package/dist/headless.d.ts +3 -1
- package/dist/headless.js +25 -1886
- package/dist/headless.js.map +1 -1
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
- package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
- package/dist/index.cjs +64 -532
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +63 -504
- package/dist/index.js.map +1 -1
- package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
- package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +1952 -3257
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +188 -50
- package/dist/react.d.ts +188 -50
- package/dist/react.js +1763 -3013
- package/dist/react.js.map +1 -1
- package/dist/server-handler.cjs +14 -0
- package/dist/server-handler.cjs.map +1 -0
- package/dist/server-handler.d.cts +3 -0
- package/dist/server-handler.d.ts +3 -0
- package/dist/server-handler.js +14 -0
- package/dist/server-handler.js.map +1 -0
- package/dist/server.cjs +1096 -532
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +77 -154
- package/dist/server.d.ts +77 -154
- package/dist/server.js +1050 -448
- package/dist/server.js.map +1 -1
- package/dist/storage.cjs +239 -232
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.d.cts +21 -5
- package/dist/storage.d.ts +21 -5
- package/dist/storage.js +227 -207
- package/dist/storage.js.map +1 -1
- package/dist/styles/animated-image.css +112 -0
- package/dist/styles/dropzone-and-tiles.css +247 -0
- package/dist/styles/modals-and-dock.css +248 -0
- package/dist/styles/primitives.css +197 -0
- package/dist/styles/quality-and-inputs.css +184 -0
- package/dist/styles/tokens.css +123 -0
- package/dist/styles.css +235 -220
- package/dist/types-BrP-o9nd.d.cts +85 -0
- package/dist/types-sZ3KoFQY.d.ts +85 -0
- package/dist/types-yShGNQnW.d.cts +61 -0
- package/dist/types-yShGNQnW.d.ts +61 -0
- package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
- package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
- package/package.json +92 -32
- package/dist/blurService-DqsnfPTd.d.cts +0 -172
- package/dist/blurService-DqsnfPTd.d.ts +0 -172
package/dist/storage.js
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createMemoryStorageAdapter,
|
|
3
|
+
createStorageAdapter,
|
|
4
|
+
toPaginatedListResult,
|
|
5
|
+
validateStorageMetadata
|
|
6
|
+
} from "./chunk-MHGQEBPW.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_PUBLIC_STORAGE_PREFIX,
|
|
9
|
+
parsePublicStorageUrl
|
|
10
|
+
} from "./chunk-S2IFAWJK.js";
|
|
3
11
|
|
|
4
12
|
// src/storage/localFsAdapter.ts
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
5
15
|
function sanitizeRelativePath(inputPath) {
|
|
16
|
+
if (!inputPath || typeof inputPath !== "string") {
|
|
17
|
+
throw new Error("Invalid storage path");
|
|
18
|
+
}
|
|
19
|
+
if (inputPath.includes(":") || inputPath.includes("\0")) {
|
|
20
|
+
throw new Error("Path traversal blocked in LocalFsStorageAdapter");
|
|
21
|
+
}
|
|
6
22
|
const normalized = inputPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
23
|
+
const rawSegments = normalized.split("/").filter(Boolean);
|
|
24
|
+
if (rawSegments.length === 0) {
|
|
9
25
|
throw new Error("Invalid storage path");
|
|
10
26
|
}
|
|
11
|
-
|
|
27
|
+
for (const segment of rawSegments) {
|
|
28
|
+
if (segment === "." || segment === "..") {
|
|
29
|
+
throw new Error("Invalid storage path: path traversal blocked");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return rawSegments.join("/");
|
|
33
|
+
}
|
|
34
|
+
function assertContainedInRoot(resolvedRoot, targetPath) {
|
|
35
|
+
const relative = path.relative(resolvedRoot, targetPath);
|
|
36
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
37
|
+
throw new Error("Path traversal blocked in LocalFsStorageAdapter");
|
|
38
|
+
}
|
|
12
39
|
}
|
|
13
40
|
function createLocalFsStorageAdapter(options) {
|
|
14
41
|
const resolvedRoot = path.resolve(options.rootDir);
|
|
@@ -19,35 +46,78 @@ function createLocalFsStorageAdapter(options) {
|
|
|
19
46
|
const cleanRel = sanitizeRelativePath(relativePath);
|
|
20
47
|
const subPath = includeBucket ? `${cleanBucket}/${cleanRel}` : cleanRel;
|
|
21
48
|
const diskPath = path.resolve(resolvedRoot, subPath);
|
|
22
|
-
|
|
23
|
-
throw new Error("Path traversal blocked in LocalFsStorageAdapter");
|
|
24
|
-
}
|
|
49
|
+
assertContainedInRoot(resolvedRoot, diskPath);
|
|
25
50
|
return { diskPath, subPath, cleanRel };
|
|
26
51
|
};
|
|
27
52
|
const getPublicUrl = (bucket, relativePath) => {
|
|
28
53
|
const { subPath } = resolveDiskTarget(bucket, relativePath);
|
|
29
54
|
return `${baseUrl}/${subPath}`;
|
|
30
55
|
};
|
|
56
|
+
const parseUrl = (url) => {
|
|
57
|
+
if (!url || typeof url !== "string") return null;
|
|
58
|
+
try {
|
|
59
|
+
const cleanUrl = url.split(/[?#]/)[0];
|
|
60
|
+
const prefix = `${baseUrl}/`;
|
|
61
|
+
let subPath = null;
|
|
62
|
+
if (cleanUrl.startsWith(prefix)) {
|
|
63
|
+
subPath = cleanUrl.slice(prefix.length);
|
|
64
|
+
} else if (baseUrl.startsWith("/")) {
|
|
65
|
+
const parsed = new URL(cleanUrl, "http://localhost");
|
|
66
|
+
const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
67
|
+
const isRelative = !cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://");
|
|
68
|
+
if ((isLocal || isRelative) && parsed.pathname.startsWith(prefix)) {
|
|
69
|
+
subPath = parsed.pathname.slice(prefix.length);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!subPath) return null;
|
|
73
|
+
const decoded = decodeURIComponent(subPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
74
|
+
if (!decoded || decoded.includes(":") || decoded.includes("\0")) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const segments = decoded.split("/").filter(Boolean);
|
|
78
|
+
if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const diskPath = path.resolve(resolvedRoot, ...segments);
|
|
82
|
+
const relative = path.relative(resolvedRoot, diskPath);
|
|
83
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (includeBucket) {
|
|
87
|
+
if (segments.length < 2) return null;
|
|
88
|
+
return {
|
|
89
|
+
bucket: segments[0],
|
|
90
|
+
path: segments.slice(1).join("/")
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { path: segments.join("/") };
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
31
98
|
return {
|
|
32
99
|
async upload(params) {
|
|
33
100
|
const { diskPath, cleanRel } = resolveDiskTarget(
|
|
34
101
|
params.bucket,
|
|
35
102
|
params.path
|
|
36
103
|
);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
104
|
+
await fs.mkdir(path.dirname(diskPath), { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
await fs.writeFile(
|
|
107
|
+
diskPath,
|
|
108
|
+
params.data,
|
|
109
|
+
params.upsert === false ? { flag: "wx" } : void 0
|
|
110
|
+
);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err.code === "EEXIST") {
|
|
113
|
+
const existsError = new Error(
|
|
41
114
|
`File already exists at ${params.bucket}/${cleanRel}`
|
|
42
115
|
);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw err;
|
|
46
|
-
}
|
|
116
|
+
existsError.code = "EEXIST";
|
|
117
|
+
throw existsError;
|
|
47
118
|
}
|
|
119
|
+
throw err;
|
|
48
120
|
}
|
|
49
|
-
await fs.mkdir(path.dirname(diskPath), { recursive: true });
|
|
50
|
-
await fs.writeFile(diskPath, params.data);
|
|
51
121
|
return {
|
|
52
122
|
bucket: params.bucket,
|
|
53
123
|
path: cleanRel,
|
|
@@ -64,42 +134,31 @@ function createLocalFsStorageAdapter(options) {
|
|
|
64
134
|
}
|
|
65
135
|
}
|
|
66
136
|
},
|
|
137
|
+
async exists(bucket, relativePath) {
|
|
138
|
+
try {
|
|
139
|
+
const { diskPath } = resolveDiskTarget(bucket, relativePath);
|
|
140
|
+
const stat = await fs.stat(diskPath);
|
|
141
|
+
return stat.isFile();
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
67
146
|
getPublicUrl,
|
|
147
|
+
parseUrl,
|
|
68
148
|
async deleteByUrl(url) {
|
|
69
|
-
|
|
149
|
+
const parsed = parseUrl(url);
|
|
150
|
+
if (!parsed) return false;
|
|
70
151
|
try {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (cleanUrl.startsWith(prefix)) {
|
|
75
|
-
subPath = cleanUrl.slice(prefix.length);
|
|
76
|
-
} else if (baseUrl.startsWith("/")) {
|
|
77
|
-
const parsed = new URL(cleanUrl, "http://localhost");
|
|
78
|
-
const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
79
|
-
const isRelative = !cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://");
|
|
80
|
-
if ((isLocal || isRelative) && parsed.pathname.startsWith(prefix)) {
|
|
81
|
-
subPath = parsed.pathname.slice(prefix.length);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (!subPath) return false;
|
|
85
|
-
const decoded = decodeURIComponent(subPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
86
|
-
if (!decoded) return false;
|
|
87
|
-
const segments = decoded.split("/").filter(Boolean);
|
|
88
|
-
if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
const diskPath = path.resolve(resolvedRoot, ...segments);
|
|
92
|
-
const relative = path.relative(resolvedRoot, diskPath);
|
|
93
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
152
|
+
const subSegments = includeBucket && parsed.bucket ? [parsed.bucket, ...parsed.path.split("/")] : parsed.path.split("/");
|
|
153
|
+
const diskPath = path.resolve(resolvedRoot, ...subSegments);
|
|
154
|
+
assertContainedInRoot(resolvedRoot, diskPath);
|
|
96
155
|
await fs.unlink(diskPath);
|
|
97
156
|
return true;
|
|
98
157
|
} catch {
|
|
99
158
|
return false;
|
|
100
159
|
}
|
|
101
160
|
},
|
|
102
|
-
async list(bucket, folder) {
|
|
161
|
+
async list(bucket, folder, listOptions) {
|
|
103
162
|
const cleanBucket = sanitizeRelativePath(bucket);
|
|
104
163
|
const cleanFolder = folder ? sanitizeRelativePath(folder) : "";
|
|
105
164
|
const targetDir = path.resolve(
|
|
@@ -107,9 +166,7 @@ function createLocalFsStorageAdapter(options) {
|
|
|
107
166
|
includeBucket ? cleanBucket : "",
|
|
108
167
|
cleanFolder
|
|
109
168
|
);
|
|
110
|
-
|
|
111
|
-
throw new Error("Path traversal blocked in LocalFsStorageAdapter");
|
|
112
|
-
}
|
|
169
|
+
assertContainedInRoot(resolvedRoot, targetDir);
|
|
113
170
|
try {
|
|
114
171
|
const dirEntries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
115
172
|
const results = [];
|
|
@@ -124,10 +181,14 @@ function createLocalFsStorageAdapter(options) {
|
|
|
124
181
|
size: stat.size
|
|
125
182
|
});
|
|
126
183
|
}
|
|
127
|
-
|
|
184
|
+
const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;
|
|
185
|
+
const limit = listOptions?.limit ?? results.length;
|
|
186
|
+
const sliced = listOptions?.limit !== void 0 || offset > 0 ? results.slice(offset, offset + limit) : results;
|
|
187
|
+
const nextCursor = listOptions?.limit !== void 0 && offset + sliced.length < results.length ? String(offset + sliced.length) : void 0;
|
|
188
|
+
return toPaginatedListResult(sliced, nextCursor);
|
|
128
189
|
} catch (err) {
|
|
129
190
|
if (err.code === "ENOENT") {
|
|
130
|
-
return [];
|
|
191
|
+
return toPaginatedListResult([]);
|
|
131
192
|
}
|
|
132
193
|
throw err;
|
|
133
194
|
}
|
|
@@ -135,83 +196,6 @@ function createLocalFsStorageAdapter(options) {
|
|
|
135
196
|
};
|
|
136
197
|
}
|
|
137
198
|
|
|
138
|
-
// src/storage/memoryAdapter.ts
|
|
139
|
-
function createMemoryStorageAdapter(options = {}) {
|
|
140
|
-
const baseUrl = (options.baseUrl || "https://memory.local/uploads").replace(
|
|
141
|
-
/\/+$/,
|
|
142
|
-
""
|
|
143
|
-
);
|
|
144
|
-
const store = /* @__PURE__ */ new Map();
|
|
145
|
-
const toKey = (bucket, path2) => `${bucket}::${path2.replace(/^\/+/, "")}`;
|
|
146
|
-
const getPublicUrl = (bucket, path2) => {
|
|
147
|
-
const cleanPath = path2.replace(/^\/+/, "");
|
|
148
|
-
return `${baseUrl}/${bucket}/${cleanPath}`;
|
|
149
|
-
};
|
|
150
|
-
return {
|
|
151
|
-
async upload(params) {
|
|
152
|
-
const cleanPath = params.path.replace(/^\/+/, "");
|
|
153
|
-
const key = toKey(params.bucket, cleanPath);
|
|
154
|
-
if (params.upsert === false && store.has(key)) {
|
|
155
|
-
throw new Error(`Object already exists at ${params.bucket}/${cleanPath}`);
|
|
156
|
-
}
|
|
157
|
-
store.set(key, {
|
|
158
|
-
bucket: params.bucket,
|
|
159
|
-
path: cleanPath,
|
|
160
|
-
data: new Uint8Array(params.data),
|
|
161
|
-
contentType: params.contentType,
|
|
162
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
163
|
-
metadata: params.metadata
|
|
164
|
-
});
|
|
165
|
-
return {
|
|
166
|
-
bucket: params.bucket,
|
|
167
|
-
path: cleanPath,
|
|
168
|
-
url: getPublicUrl(params.bucket, cleanPath)
|
|
169
|
-
};
|
|
170
|
-
},
|
|
171
|
-
async delete(bucket, path2) {
|
|
172
|
-
const key = toKey(bucket, path2);
|
|
173
|
-
store.delete(key);
|
|
174
|
-
},
|
|
175
|
-
getPublicUrl,
|
|
176
|
-
async list(bucket, folder) {
|
|
177
|
-
const cleanFolder = folder ? folder.replace(/^\/+|\/+$/g, "") : "";
|
|
178
|
-
const entries = [];
|
|
179
|
-
for (const obj of store.values()) {
|
|
180
|
-
if (obj.bucket !== bucket) continue;
|
|
181
|
-
if (cleanFolder && !obj.path.startsWith(`${cleanFolder}/`)) continue;
|
|
182
|
-
const name = cleanFolder ? obj.path.slice(cleanFolder.length + 1) : obj.path;
|
|
183
|
-
entries.push({
|
|
184
|
-
name,
|
|
185
|
-
id: `${obj.bucket}/${obj.path}`,
|
|
186
|
-
createdAt: obj.createdAt,
|
|
187
|
-
size: obj.data.byteLength
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return entries;
|
|
191
|
-
},
|
|
192
|
-
async deleteByUrl(url) {
|
|
193
|
-
const prefix = `${baseUrl}/`;
|
|
194
|
-
if (!url.startsWith(prefix)) return false;
|
|
195
|
-
const rest = url.slice(prefix.length);
|
|
196
|
-
const slashIdx = rest.indexOf("/");
|
|
197
|
-
if (slashIdx === -1) return false;
|
|
198
|
-
const bucket = rest.slice(0, slashIdx);
|
|
199
|
-
const filePath = rest.slice(slashIdx + 1);
|
|
200
|
-
const key = toKey(bucket, filePath);
|
|
201
|
-
return store.delete(key);
|
|
202
|
-
},
|
|
203
|
-
getObject(bucket, path2) {
|
|
204
|
-
return store.get(toKey(bucket, path2));
|
|
205
|
-
},
|
|
206
|
-
clear() {
|
|
207
|
-
store.clear();
|
|
208
|
-
},
|
|
209
|
-
get size() {
|
|
210
|
-
return store.size;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
199
|
// src/storage/s3Adapter.ts
|
|
216
200
|
function normalizeS3Key(rawPath) {
|
|
217
201
|
const normalized = rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
@@ -224,6 +208,13 @@ function resolveTransport(options) {
|
|
|
224
208
|
if ("putObject" in options && typeof options.putObject === "function") return options;
|
|
225
209
|
throw new Error("S3StorageAdapter requires putObject/deleteObject or client/transport");
|
|
226
210
|
}
|
|
211
|
+
function isS3PreconditionError(err) {
|
|
212
|
+
if (!err || typeof err !== "object") return false;
|
|
213
|
+
const e = err;
|
|
214
|
+
const status = e.$metadata?.httpStatusCode ?? Number(e.statusCode);
|
|
215
|
+
const code = String(e.code ?? e.name ?? "").toLowerCase();
|
|
216
|
+
return status === 412 || code === "preconditionfailed" || code === "eexist";
|
|
217
|
+
}
|
|
227
218
|
function createS3StorageAdapter(options) {
|
|
228
219
|
const transport = resolveTransport(options);
|
|
229
220
|
const includeBucket = options.includeBucketInUrl !== false;
|
|
@@ -236,19 +227,34 @@ function createS3StorageAdapter(options) {
|
|
|
236
227
|
return {
|
|
237
228
|
async upload(params) {
|
|
238
229
|
const key = normalizeS3Key(params.path);
|
|
239
|
-
|
|
230
|
+
validateStorageMetadata(params.metadata);
|
|
231
|
+
const isImmutable = params.upsert === false && params.contentType.includes("image/avif");
|
|
232
|
+
const cacheControl = params.cacheControl ?? options.cacheControl ?? (isImmutable ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
|
|
240
233
|
const metadata = params.metadata || params.contentHash ? {
|
|
241
|
-
...params.
|
|
242
|
-
...params.
|
|
234
|
+
...params.metadata,
|
|
235
|
+
...params.contentHash ? { contentHash: params.contentHash } : {}
|
|
243
236
|
} : void 0;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
237
|
+
try {
|
|
238
|
+
await transport.putObject({
|
|
239
|
+
Bucket: params.bucket,
|
|
240
|
+
Key: key,
|
|
241
|
+
Body: params.data,
|
|
242
|
+
ContentType: params.contentType,
|
|
243
|
+
...cacheControl ? { CacheControl: cacheControl } : {},
|
|
244
|
+
...metadata ? { Metadata: metadata } : {},
|
|
245
|
+
...params.upsert === false ? { IfNoneMatch: "*" } : {}
|
|
246
|
+
});
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (isS3PreconditionError(err)) {
|
|
249
|
+
const existsErr = new Error(
|
|
250
|
+
`Object already exists at ${params.bucket}/${key}`
|
|
251
|
+
);
|
|
252
|
+
existsErr.code = "EEXIST";
|
|
253
|
+
existsErr.statusCode = 412;
|
|
254
|
+
throw existsErr;
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
252
258
|
return {
|
|
253
259
|
bucket: params.bucket,
|
|
254
260
|
path: key,
|
|
@@ -258,17 +264,34 @@ function createS3StorageAdapter(options) {
|
|
|
258
264
|
async delete(bucket, path2) {
|
|
259
265
|
await transport.deleteObject({ Bucket: bucket, Key: normalizeS3Key(path2) });
|
|
260
266
|
},
|
|
267
|
+
async exists(bucket, path2) {
|
|
268
|
+
const key = normalizeS3Key(path2);
|
|
269
|
+
if (transport.headObject) {
|
|
270
|
+
try {
|
|
271
|
+
await transport.headObject({ Bucket: bucket, Key: key });
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const lastSlash = key.lastIndexOf("/");
|
|
278
|
+
const folder = lastSlash > 0 ? key.slice(0, lastSlash) : void 0;
|
|
279
|
+
const targetName = lastSlash > 0 ? key.slice(lastSlash + 1) : key;
|
|
280
|
+
const listed = await this.list(bucket, folder);
|
|
281
|
+
return listed.some((item) => item.name === targetName);
|
|
282
|
+
},
|
|
261
283
|
getPublicUrl,
|
|
262
|
-
async list(bucket, folder) {
|
|
284
|
+
async list(bucket, folder, listOptions) {
|
|
263
285
|
const cleanFolder = folder ? normalizeS3Key(folder).replace(/\/+$/, "") : "";
|
|
264
286
|
const prefix = cleanFolder ? `${cleanFolder}/` : void 0;
|
|
265
287
|
const input = {
|
|
266
288
|
Bucket: bucket,
|
|
267
289
|
...prefix ? { Prefix: prefix } : {},
|
|
268
|
-
MaxKeys: 1e3
|
|
290
|
+
MaxKeys: listOptions?.limit ?? 1e3,
|
|
291
|
+
...listOptions?.cursor ? { ContinuationToken: listOptions.cursor } : {}
|
|
269
292
|
};
|
|
270
293
|
const output = transport.listObjects ? await transport.listObjects(input) : transport.listObjectsV2 ? await transport.listObjectsV2(input) : void 0;
|
|
271
|
-
|
|
294
|
+
const entries = (output?.Contents ?? []).filter((item) => Boolean(item.Key)).map((item) => {
|
|
272
295
|
const name = prefix && item.Key.startsWith(prefix) ? item.Key.slice(prefix.length) : item.Key;
|
|
273
296
|
const createdAt = item.LastModified instanceof Date ? item.LastModified.toISOString() : item.LastModified;
|
|
274
297
|
return {
|
|
@@ -278,26 +301,38 @@ function createS3StorageAdapter(options) {
|
|
|
278
301
|
...item.Size !== void 0 ? { size: item.Size } : {}
|
|
279
302
|
};
|
|
280
303
|
}).filter((entry) => entry.name.length > 0);
|
|
304
|
+
return toPaginatedListResult(entries, output?.NextContinuationToken);
|
|
281
305
|
},
|
|
282
|
-
|
|
283
|
-
if (typeof options.publicBaseUrl !== "string" || !url) return
|
|
306
|
+
parseUrl(url) {
|
|
307
|
+
if (typeof options.publicBaseUrl !== "string" || !url) return null;
|
|
284
308
|
const prefix = `${options.publicBaseUrl.replace(/\/+$/, "")}/`;
|
|
285
309
|
const cleanUrl = url.split(/[?#]/)[0];
|
|
286
|
-
if (!cleanUrl.startsWith(prefix)) return
|
|
310
|
+
if (!cleanUrl.startsWith(prefix)) return null;
|
|
287
311
|
try {
|
|
288
312
|
const rest = decodeURIComponent(cleanUrl.slice(prefix.length)).replace(/^\/+/, "");
|
|
289
|
-
if (!rest) return
|
|
313
|
+
if (!rest) return null;
|
|
290
314
|
if (includeBucket) {
|
|
291
315
|
const slashIdx = rest.indexOf("/");
|
|
292
|
-
if (slashIdx <= 0 || slashIdx === rest.length - 1) return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
return true;
|
|
316
|
+
if (slashIdx <= 0 || slashIdx === rest.length - 1) return null;
|
|
317
|
+
return {
|
|
318
|
+
bucket: rest.slice(0, slashIdx),
|
|
319
|
+
path: rest.slice(slashIdx + 1)
|
|
320
|
+
};
|
|
298
321
|
}
|
|
299
|
-
if (!options.defaultBucket) return
|
|
300
|
-
|
|
322
|
+
if (!options.defaultBucket) return null;
|
|
323
|
+
return { bucket: options.defaultBucket, path: rest };
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
async deleteByUrl(url) {
|
|
329
|
+
const parsed = this.parseUrl?.(url);
|
|
330
|
+
if (!parsed || !parsed.bucket) return false;
|
|
331
|
+
try {
|
|
332
|
+
await transport.deleteObject({
|
|
333
|
+
Bucket: parsed.bucket,
|
|
334
|
+
Key: parsed.path
|
|
335
|
+
});
|
|
301
336
|
return true;
|
|
302
337
|
} catch {
|
|
303
338
|
return false;
|
|
@@ -306,52 +341,22 @@ function createS3StorageAdapter(options) {
|
|
|
306
341
|
};
|
|
307
342
|
}
|
|
308
343
|
|
|
309
|
-
// src/storage/urlParser.ts
|
|
310
|
-
var DEFAULT_PUBLIC_STORAGE_PREFIX = "/storage/v1/object/public/";
|
|
311
|
-
function parsePublicStorageUrl(url, publicPrefix = DEFAULT_PUBLIC_STORAGE_PREFIX) {
|
|
312
|
-
if (!url) return null;
|
|
313
|
-
try {
|
|
314
|
-
const parsed = new URL(url, "http://localhost");
|
|
315
|
-
const pathname = parsed.pathname;
|
|
316
|
-
const prefixIndex = pathname.indexOf(publicPrefix);
|
|
317
|
-
if (prefixIndex === -1) {
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
const relativePath = pathname.slice(prefixIndex + publicPrefix.length).replace(/^\/+/, "");
|
|
321
|
-
const segments = relativePath.split("/").filter(Boolean);
|
|
322
|
-
if (segments.length < 2) {
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
const [bucket, ...pathParts] = segments;
|
|
326
|
-
const filePath = pathParts.join("/");
|
|
327
|
-
const filename = pathParts[pathParts.length - 1];
|
|
328
|
-
const lastDotIndex = filename.lastIndexOf(".");
|
|
329
|
-
const extension = lastDotIndex >= 0 ? filename.slice(lastDotIndex) : null;
|
|
330
|
-
return {
|
|
331
|
-
bucket,
|
|
332
|
-
path: filePath,
|
|
333
|
-
filename,
|
|
334
|
-
extension
|
|
335
|
-
};
|
|
336
|
-
} catch {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
344
|
// src/storage/supabaseAdapter.ts
|
|
342
345
|
function createSupabaseStorageAdapter(client, options = {}) {
|
|
343
346
|
const publicPrefix = options.publicUrlPrefix || DEFAULT_PUBLIC_STORAGE_PREFIX;
|
|
344
347
|
return {
|
|
345
348
|
async upload(params) {
|
|
346
|
-
|
|
349
|
+
validateStorageMetadata(params.metadata);
|
|
350
|
+
const isImmutable = params.upsert === false && params.contentType.includes("image/avif");
|
|
351
|
+
const cacheControl = params.cacheControl || (isImmutable ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
|
|
347
352
|
const { data, error } = await client.storage.from(params.bucket).upload(params.path, params.data, {
|
|
348
353
|
contentType: params.contentType,
|
|
349
354
|
upsert: params.upsert === void 0 ? true : params.upsert,
|
|
350
355
|
cacheControl,
|
|
351
356
|
metadata: {
|
|
357
|
+
...params.metadata,
|
|
352
358
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
353
|
-
contentHash: params.contentHash || "unknown"
|
|
354
|
-
...params.metadata
|
|
359
|
+
contentHash: params.contentHash || "unknown"
|
|
355
360
|
}
|
|
356
361
|
});
|
|
357
362
|
if (error || !data) {
|
|
@@ -374,41 +379,53 @@ function createSupabaseStorageAdapter(client, options = {}) {
|
|
|
374
379
|
throw new Error(`Failed to delete from Supabase storage: ${error.message}`);
|
|
375
380
|
}
|
|
376
381
|
},
|
|
382
|
+
async exists(bucket, path2) {
|
|
383
|
+
const cleanPath = path2.replace(/^\/+/, "");
|
|
384
|
+
const lastSlash = cleanPath.lastIndexOf("/");
|
|
385
|
+
const folder = lastSlash > 0 ? cleanPath.slice(0, lastSlash) : void 0;
|
|
386
|
+
const targetName = lastSlash > 0 ? cleanPath.slice(lastSlash + 1) : cleanPath;
|
|
387
|
+
const entries = await this.list(bucket, folder);
|
|
388
|
+
return entries.some((entry) => entry.name === targetName);
|
|
389
|
+
},
|
|
377
390
|
getPublicUrl(bucket, path2) {
|
|
378
391
|
const {
|
|
379
392
|
data: { publicUrl }
|
|
380
393
|
} = client.storage.from(bucket).getPublicUrl(path2);
|
|
381
394
|
return publicUrl;
|
|
382
395
|
},
|
|
383
|
-
async list(bucket, folder) {
|
|
396
|
+
async list(bucket, folder, listOptions) {
|
|
384
397
|
const bucketClient = client.storage.from(bucket);
|
|
385
398
|
if (!bucketClient.list) {
|
|
386
|
-
return [];
|
|
399
|
+
return toPaginatedListResult([]);
|
|
387
400
|
}
|
|
401
|
+
const limit = listOptions?.limit ?? 1e3;
|
|
402
|
+
const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;
|
|
388
403
|
const { data, error } = await bucketClient.list(folder, {
|
|
389
|
-
limit
|
|
404
|
+
limit,
|
|
405
|
+
...offset > 0 ? { offset } : {},
|
|
390
406
|
sortBy: { column: "created_at", order: "desc" }
|
|
391
407
|
});
|
|
392
408
|
if (error) {
|
|
393
409
|
throw new Error(`Failed to list Supabase bucket files: ${error.message}`);
|
|
394
410
|
}
|
|
395
|
-
|
|
411
|
+
const entries = (data || []).filter((f) => f.id !== null).map((f) => ({
|
|
396
412
|
name: f.name,
|
|
397
413
|
id: f.id ?? void 0,
|
|
398
414
|
createdAt: f.created_at ?? void 0
|
|
399
415
|
}));
|
|
416
|
+
const nextCursor = listOptions?.limit !== void 0 && entries.length >= limit ? String(offset + entries.length) : void 0;
|
|
417
|
+
return toPaginatedListResult(entries, nextCursor);
|
|
418
|
+
},
|
|
419
|
+
parseUrl(url) {
|
|
420
|
+
const info = parsePublicStorageUrl(url, publicPrefix);
|
|
421
|
+
if (!info) return null;
|
|
422
|
+
return { bucket: info.bucket, path: info.path };
|
|
400
423
|
},
|
|
401
424
|
async deleteByUrl(url) {
|
|
402
425
|
const info = parsePublicStorageUrl(url, publicPrefix);
|
|
403
426
|
if (!info) return false;
|
|
404
427
|
try {
|
|
405
|
-
const
|
|
406
|
-
if (info.extension?.toLowerCase() === ".avif") {
|
|
407
|
-
const basePath = info.path.slice(0, -info.extension.length);
|
|
408
|
-
targets.add(`${basePath}-blur${info.extension}`);
|
|
409
|
-
targets.add(`${basePath}-thumb${info.extension}`);
|
|
410
|
-
}
|
|
411
|
-
const { error } = await client.storage.from(info.bucket).remove([...targets]);
|
|
428
|
+
const { error } = await client.storage.from(info.bucket).remove([info.path]);
|
|
412
429
|
return !error;
|
|
413
430
|
} catch {
|
|
414
431
|
return false;
|
|
@@ -416,12 +433,15 @@ function createSupabaseStorageAdapter(client, options = {}) {
|
|
|
416
433
|
}
|
|
417
434
|
};
|
|
418
435
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
436
|
+
export {
|
|
437
|
+
DEFAULT_PUBLIC_STORAGE_PREFIX,
|
|
438
|
+
createLocalFsStorageAdapter,
|
|
439
|
+
createMemoryStorageAdapter,
|
|
440
|
+
createS3StorageAdapter,
|
|
441
|
+
createStorageAdapter,
|
|
442
|
+
createSupabaseStorageAdapter,
|
|
443
|
+
parsePublicStorageUrl,
|
|
444
|
+
toPaginatedListResult,
|
|
445
|
+
validateStorageMetadata
|
|
446
|
+
};
|
|
427
447
|
//# sourceMappingURL=storage.js.map
|