img-upload-kit 0.0.0-stage → 2.0.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/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +192 -0
- package/README.md +242 -2
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +41 -0
- package/dist/blur.cjs.map +1 -0
- package/dist/blur.d.cts +17 -0
- package/dist/blur.d.ts +17 -0
- package/dist/blur.js +41 -0
- package/dist/blur.js.map +1 -0
- 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-2OVNXHTN.cjs +514 -0
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/chunk-3ICG2SLL.cjs +135 -0
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/chunk-4ASFG3KB.js +514 -0
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/chunk-AI3JJLSA.js +2370 -0
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/chunk-CV23O4YU.js +145 -0
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/chunk-G7LLSKBV.js +158 -0
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/chunk-IB3TEDLR.js +294 -0
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/chunk-KJVYWKIG.cjs +158 -0
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/chunk-NEDBIIE3.cjs +226 -0
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-QQHZJKNA.js +135 -0
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TT5C7K3Q.js +226 -0
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/chunk-U2TWTEBY.cjs +2370 -0
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/chunk-WISG74VL.cjs +294 -0
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/chunk-YRXXQUDE.cjs +145 -0
- package/dist/chunk-YRXXQUDE.cjs.map +1 -0
- package/dist/cli.cjs +1241 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +99 -0
- package/dist/cli.d.ts +99 -0
- package/dist/cli.js +1194 -0
- package/dist/cli.js.map +1 -0
- package/dist/headless.cjs +27 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +4 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +27 -0
- package/dist/headless.js.map +1 -0
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/index-CPMlaQsC.d.ts +635 -0
- package/dist/index-DHINe-rA.d.cts +635 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/memoryAdapter-Ck0ccZbF.d.cts +25 -0
- package/dist/memoryAdapter-DxEKaG0b.d.ts +25 -0
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +3779 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +476 -0
- package/dist/react.d.ts +476 -0
- package/dist/react.js +3779 -0
- package/dist/react.js.map +1 -0
- 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 +1801 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +142 -0
- package/dist/server.d.ts +142 -0
- package/dist/server.js +1801 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.cjs +447 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +163 -0
- package/dist/storage.d.ts +163 -0
- package/dist/storage.js +447 -0
- package/dist/storage.js.map +1 -0
- 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 +1122 -0
- 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/upload-C5Trd6Dh.d.cts +154 -0
- package/dist/upload-C5Trd6Dh.d.ts +154 -0
- package/package.json +198 -4
package/dist/storage.cjs
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkYRXXQUDEcjs = require('./chunk-YRXXQUDE.cjs');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
var _chunkQHU7FUO4cjs = require('./chunk-QHU7FUO4.cjs');
|
|
11
|
+
|
|
12
|
+
// src/storage/localFsAdapter.ts
|
|
13
|
+
var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
|
|
14
|
+
var _path = require('path'); var _path2 = _interopRequireDefault(_path);
|
|
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
|
+
}
|
|
22
|
+
const normalized = inputPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
23
|
+
const rawSegments = normalized.split("/").filter(Boolean);
|
|
24
|
+
if (rawSegments.length === 0) {
|
|
25
|
+
throw new Error("Invalid storage path");
|
|
26
|
+
}
|
|
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 = _path2.default.relative(resolvedRoot, targetPath);
|
|
36
|
+
if (relative.startsWith("..") || _path2.default.isAbsolute(relative)) {
|
|
37
|
+
throw new Error("Path traversal blocked in LocalFsStorageAdapter");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createLocalFsStorageAdapter(options) {
|
|
41
|
+
const resolvedRoot = _path2.default.resolve(options.rootDir);
|
|
42
|
+
const baseUrl = (_nullishCoalesce(options.baseUrl, () => ( "/uploads"))).replace(/\/+$/, "");
|
|
43
|
+
const includeBucket = options.includeBucketInPath !== false;
|
|
44
|
+
const resolveDiskTarget = (bucket, relativePath) => {
|
|
45
|
+
const cleanBucket = sanitizeRelativePath(bucket);
|
|
46
|
+
const cleanRel = sanitizeRelativePath(relativePath);
|
|
47
|
+
const subPath = includeBucket ? `${cleanBucket}/${cleanRel}` : cleanRel;
|
|
48
|
+
const diskPath = _path2.default.resolve(resolvedRoot, subPath);
|
|
49
|
+
assertContainedInRoot(resolvedRoot, diskPath);
|
|
50
|
+
return { diskPath, subPath, cleanRel };
|
|
51
|
+
};
|
|
52
|
+
const getPublicUrl = (bucket, relativePath) => {
|
|
53
|
+
const { subPath } = resolveDiskTarget(bucket, relativePath);
|
|
54
|
+
return `${baseUrl}/${subPath}`;
|
|
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 = _path2.default.resolve(resolvedRoot, ...segments);
|
|
82
|
+
const relative = _path2.default.relative(resolvedRoot, diskPath);
|
|
83
|
+
if (relative.startsWith("..") || _path2.default.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 (e2) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
async upload(params) {
|
|
100
|
+
const { diskPath, cleanRel } = resolveDiskTarget(
|
|
101
|
+
params.bucket,
|
|
102
|
+
params.path
|
|
103
|
+
);
|
|
104
|
+
await _promises2.default.mkdir(_path2.default.dirname(diskPath), { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
await _promises2.default.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(
|
|
114
|
+
`File already exists at ${params.bucket}/${cleanRel}`
|
|
115
|
+
);
|
|
116
|
+
existsError.code = "EEXIST";
|
|
117
|
+
throw existsError;
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
bucket: params.bucket,
|
|
123
|
+
path: cleanRel,
|
|
124
|
+
url: getPublicUrl(params.bucket, cleanRel)
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
async delete(bucket, relativePath) {
|
|
128
|
+
const { diskPath } = resolveDiskTarget(bucket, relativePath);
|
|
129
|
+
try {
|
|
130
|
+
await _promises2.default.unlink(diskPath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err.code !== "ENOENT") {
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async exists(bucket, relativePath) {
|
|
138
|
+
try {
|
|
139
|
+
const { diskPath } = resolveDiskTarget(bucket, relativePath);
|
|
140
|
+
const stat = await _promises2.default.stat(diskPath);
|
|
141
|
+
return stat.isFile();
|
|
142
|
+
} catch (e3) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
getPublicUrl,
|
|
147
|
+
parseUrl,
|
|
148
|
+
async deleteByUrl(url) {
|
|
149
|
+
const parsed = parseUrl(url);
|
|
150
|
+
if (!parsed) return false;
|
|
151
|
+
try {
|
|
152
|
+
const subSegments = includeBucket && parsed.bucket ? [parsed.bucket, ...parsed.path.split("/")] : parsed.path.split("/");
|
|
153
|
+
const diskPath = _path2.default.resolve(resolvedRoot, ...subSegments);
|
|
154
|
+
assertContainedInRoot(resolvedRoot, diskPath);
|
|
155
|
+
await _promises2.default.unlink(diskPath);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (e4) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async list(bucket, folder, listOptions) {
|
|
162
|
+
const cleanBucket = sanitizeRelativePath(bucket);
|
|
163
|
+
const cleanFolder = folder ? sanitizeRelativePath(folder) : "";
|
|
164
|
+
const targetDir = _path2.default.resolve(
|
|
165
|
+
resolvedRoot,
|
|
166
|
+
includeBucket ? cleanBucket : "",
|
|
167
|
+
cleanFolder
|
|
168
|
+
);
|
|
169
|
+
assertContainedInRoot(resolvedRoot, targetDir);
|
|
170
|
+
try {
|
|
171
|
+
const dirEntries = await _promises2.default.readdir(targetDir, { withFileTypes: true });
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const entry of dirEntries) {
|
|
174
|
+
if (!entry.isFile()) continue;
|
|
175
|
+
const full = _path2.default.join(targetDir, entry.name);
|
|
176
|
+
const stat = await _promises2.default.stat(full);
|
|
177
|
+
results.push({
|
|
178
|
+
name: entry.name,
|
|
179
|
+
id: entry.name,
|
|
180
|
+
createdAt: stat.birthtime.toISOString(),
|
|
181
|
+
size: stat.size
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const offset = _optionalChain([listOptions, 'optionalAccess', _ => _.cursor]) ? Number(listOptions.cursor) || 0 : 0;
|
|
185
|
+
const limit = _nullishCoalesce(_optionalChain([listOptions, 'optionalAccess', _2 => _2.limit]), () => ( results.length));
|
|
186
|
+
const sliced = _optionalChain([listOptions, 'optionalAccess', _3 => _3.limit]) !== void 0 || offset > 0 ? results.slice(offset, offset + limit) : results;
|
|
187
|
+
const nextCursor = _optionalChain([listOptions, 'optionalAccess', _4 => _4.limit]) !== void 0 && offset + sliced.length < results.length ? String(offset + sliced.length) : void 0;
|
|
188
|
+
return _chunkYRXXQUDEcjs.toPaginatedListResult.call(void 0, sliced, nextCursor);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (err.code === "ENOENT") {
|
|
191
|
+
return _chunkYRXXQUDEcjs.toPaginatedListResult.call(void 0, []);
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/storage/s3Adapter.ts
|
|
200
|
+
function normalizeS3Key(rawPath) {
|
|
201
|
+
const normalized = rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
202
|
+
if (!normalized) throw new Error("Invalid S3 object key");
|
|
203
|
+
return normalized;
|
|
204
|
+
}
|
|
205
|
+
function resolveTransport(options) {
|
|
206
|
+
if ("client" in options && options.client) return options.client;
|
|
207
|
+
if ("transport" in options && options.transport) return options.transport;
|
|
208
|
+
if ("putObject" in options && typeof options.putObject === "function") return options;
|
|
209
|
+
throw new Error("S3StorageAdapter requires putObject/deleteObject or client/transport");
|
|
210
|
+
}
|
|
211
|
+
function isS3PreconditionError(err) {
|
|
212
|
+
if (!err || typeof err !== "object") return false;
|
|
213
|
+
const e = err;
|
|
214
|
+
const status = _nullishCoalesce(_optionalChain([e, 'access', _5 => _5.$metadata, 'optionalAccess', _6 => _6.httpStatusCode]), () => ( Number(e.statusCode)));
|
|
215
|
+
const code = String(_nullishCoalesce(_nullishCoalesce(e.code, () => ( e.name)), () => ( ""))).toLowerCase();
|
|
216
|
+
return status === 412 || code === "preconditionfailed" || code === "eexist";
|
|
217
|
+
}
|
|
218
|
+
function createS3StorageAdapter(options) {
|
|
219
|
+
const transport = resolveTransport(options);
|
|
220
|
+
const includeBucket = options.includeBucketInUrl !== false;
|
|
221
|
+
const getPublicUrl = (bucket, path2) => {
|
|
222
|
+
const key = normalizeS3Key(path2);
|
|
223
|
+
if (typeof options.publicBaseUrl === "function") return options.publicBaseUrl(bucket, key);
|
|
224
|
+
const base = options.publicBaseUrl.replace(/\/+$/, "");
|
|
225
|
+
return includeBucket ? `${base}/${bucket}/${key}` : `${base}/${key}`;
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
async upload(params) {
|
|
229
|
+
const key = normalizeS3Key(params.path);
|
|
230
|
+
_chunkYRXXQUDEcjs.validateStorageMetadata.call(void 0, params.metadata);
|
|
231
|
+
const isImmutable = params.upsert === false && params.contentType.includes("image/avif");
|
|
232
|
+
const cacheControl = _nullishCoalesce(_nullishCoalesce(params.cacheControl, () => ( options.cacheControl)), () => ( (isImmutable ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate")));
|
|
233
|
+
const metadata = params.metadata || params.contentHash ? {
|
|
234
|
+
...params.metadata,
|
|
235
|
+
...params.contentHash ? { contentHash: params.contentHash } : {}
|
|
236
|
+
} : void 0;
|
|
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
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
bucket: params.bucket,
|
|
260
|
+
path: key,
|
|
261
|
+
url: getPublicUrl(params.bucket, key)
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
async delete(bucket, path2) {
|
|
265
|
+
await transport.deleteObject({ Bucket: bucket, Key: normalizeS3Key(path2) });
|
|
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 (e5) {
|
|
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
|
+
},
|
|
283
|
+
getPublicUrl,
|
|
284
|
+
async list(bucket, folder, listOptions) {
|
|
285
|
+
const cleanFolder = folder ? normalizeS3Key(folder).replace(/\/+$/, "") : "";
|
|
286
|
+
const prefix = cleanFolder ? `${cleanFolder}/` : void 0;
|
|
287
|
+
const input = {
|
|
288
|
+
Bucket: bucket,
|
|
289
|
+
...prefix ? { Prefix: prefix } : {},
|
|
290
|
+
MaxKeys: _nullishCoalesce(_optionalChain([listOptions, 'optionalAccess', _7 => _7.limit]), () => ( 1e3)),
|
|
291
|
+
..._optionalChain([listOptions, 'optionalAccess', _8 => _8.cursor]) ? { ContinuationToken: listOptions.cursor } : {}
|
|
292
|
+
};
|
|
293
|
+
const output = transport.listObjects ? await transport.listObjects(input) : transport.listObjectsV2 ? await transport.listObjectsV2(input) : void 0;
|
|
294
|
+
const entries = (_nullishCoalesce(_optionalChain([output, 'optionalAccess', _9 => _9.Contents]), () => ( []))).filter((item) => Boolean(item.Key)).map((item) => {
|
|
295
|
+
const name = prefix && item.Key.startsWith(prefix) ? item.Key.slice(prefix.length) : item.Key;
|
|
296
|
+
const createdAt = item.LastModified instanceof Date ? item.LastModified.toISOString() : item.LastModified;
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
id: `${bucket}/${item.Key}`,
|
|
300
|
+
...createdAt !== void 0 ? { createdAt } : {},
|
|
301
|
+
...item.Size !== void 0 ? { size: item.Size } : {}
|
|
302
|
+
};
|
|
303
|
+
}).filter((entry) => entry.name.length > 0);
|
|
304
|
+
return _chunkYRXXQUDEcjs.toPaginatedListResult.call(void 0, entries, _optionalChain([output, 'optionalAccess', _10 => _10.NextContinuationToken]));
|
|
305
|
+
},
|
|
306
|
+
parseUrl(url) {
|
|
307
|
+
if (typeof options.publicBaseUrl !== "string" || !url) return null;
|
|
308
|
+
const prefix = `${options.publicBaseUrl.replace(/\/+$/, "")}/`;
|
|
309
|
+
const cleanUrl = url.split(/[?#]/)[0];
|
|
310
|
+
if (!cleanUrl.startsWith(prefix)) return null;
|
|
311
|
+
try {
|
|
312
|
+
const rest = decodeURIComponent(cleanUrl.slice(prefix.length)).replace(/^\/+/, "");
|
|
313
|
+
if (!rest) return null;
|
|
314
|
+
if (includeBucket) {
|
|
315
|
+
const slashIdx = rest.indexOf("/");
|
|
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
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (!options.defaultBucket) return null;
|
|
323
|
+
return { bucket: options.defaultBucket, path: rest };
|
|
324
|
+
} catch (e6) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
async deleteByUrl(url) {
|
|
329
|
+
const parsed = _optionalChain([this, 'access', _11 => _11.parseUrl, 'optionalCall', _12 => _12(url)]);
|
|
330
|
+
if (!parsed || !parsed.bucket) return false;
|
|
331
|
+
try {
|
|
332
|
+
await transport.deleteObject({
|
|
333
|
+
Bucket: parsed.bucket,
|
|
334
|
+
Key: parsed.path
|
|
335
|
+
});
|
|
336
|
+
return true;
|
|
337
|
+
} catch (e7) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/storage/supabaseAdapter.ts
|
|
345
|
+
function createSupabaseStorageAdapter(client, options = {}) {
|
|
346
|
+
const publicPrefix = options.publicUrlPrefix || _chunkQHU7FUO4cjs.DEFAULT_PUBLIC_STORAGE_PREFIX;
|
|
347
|
+
return {
|
|
348
|
+
async upload(params) {
|
|
349
|
+
_chunkYRXXQUDEcjs.validateStorageMetadata.call(void 0, 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");
|
|
352
|
+
const { data, error } = await client.storage.from(params.bucket).upload(params.path, params.data, {
|
|
353
|
+
contentType: params.contentType,
|
|
354
|
+
upsert: params.upsert === void 0 ? true : params.upsert,
|
|
355
|
+
cacheControl,
|
|
356
|
+
metadata: {
|
|
357
|
+
...params.metadata,
|
|
358
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
359
|
+
contentHash: params.contentHash || "unknown"
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
if (error || !data) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Failed to upload to Supabase storage (bucket: ${params.bucket}, path: ${params.path}): ${_optionalChain([error, 'optionalAccess', _13 => _13.message]) || "Unknown error"}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const {
|
|
368
|
+
data: { publicUrl }
|
|
369
|
+
} = client.storage.from(params.bucket).getPublicUrl(data.path);
|
|
370
|
+
return {
|
|
371
|
+
bucket: params.bucket,
|
|
372
|
+
path: data.path,
|
|
373
|
+
url: publicUrl
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
async delete(bucket, path2) {
|
|
377
|
+
const { error } = await client.storage.from(bucket).remove([path2]);
|
|
378
|
+
if (error) {
|
|
379
|
+
throw new Error(`Failed to delete from Supabase storage: ${error.message}`);
|
|
380
|
+
}
|
|
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
|
+
},
|
|
390
|
+
getPublicUrl(bucket, path2) {
|
|
391
|
+
const {
|
|
392
|
+
data: { publicUrl }
|
|
393
|
+
} = client.storage.from(bucket).getPublicUrl(path2);
|
|
394
|
+
return publicUrl;
|
|
395
|
+
},
|
|
396
|
+
async list(bucket, folder, listOptions) {
|
|
397
|
+
const bucketClient = client.storage.from(bucket);
|
|
398
|
+
if (!bucketClient.list) {
|
|
399
|
+
return _chunkYRXXQUDEcjs.toPaginatedListResult.call(void 0, []);
|
|
400
|
+
}
|
|
401
|
+
const limit = _nullishCoalesce(_optionalChain([listOptions, 'optionalAccess', _14 => _14.limit]), () => ( 1e3));
|
|
402
|
+
const offset = _optionalChain([listOptions, 'optionalAccess', _15 => _15.cursor]) ? Number(listOptions.cursor) || 0 : 0;
|
|
403
|
+
const { data, error } = await bucketClient.list(folder, {
|
|
404
|
+
limit,
|
|
405
|
+
...offset > 0 ? { offset } : {},
|
|
406
|
+
sortBy: { column: "created_at", order: "desc" }
|
|
407
|
+
});
|
|
408
|
+
if (error) {
|
|
409
|
+
throw new Error(`Failed to list Supabase bucket files: ${error.message}`);
|
|
410
|
+
}
|
|
411
|
+
const entries = (data || []).filter((f) => f.id !== null).map((f) => ({
|
|
412
|
+
name: f.name,
|
|
413
|
+
id: _nullishCoalesce(f.id, () => ( void 0)),
|
|
414
|
+
createdAt: _nullishCoalesce(f.created_at, () => ( void 0))
|
|
415
|
+
}));
|
|
416
|
+
const nextCursor = _optionalChain([listOptions, 'optionalAccess', _16 => _16.limit]) !== void 0 && entries.length >= limit ? String(offset + entries.length) : void 0;
|
|
417
|
+
return _chunkYRXXQUDEcjs.toPaginatedListResult.call(void 0, entries, nextCursor);
|
|
418
|
+
},
|
|
419
|
+
parseUrl(url) {
|
|
420
|
+
const info = _chunkQHU7FUO4cjs.parsePublicStorageUrl.call(void 0, url, publicPrefix);
|
|
421
|
+
if (!info) return null;
|
|
422
|
+
return { bucket: info.bucket, path: info.path };
|
|
423
|
+
},
|
|
424
|
+
async deleteByUrl(url) {
|
|
425
|
+
const info = _chunkQHU7FUO4cjs.parsePublicStorageUrl.call(void 0, url, publicPrefix);
|
|
426
|
+
if (!info) return false;
|
|
427
|
+
try {
|
|
428
|
+
const { error } = await client.storage.from(info.bucket).remove([info.path]);
|
|
429
|
+
return !error;
|
|
430
|
+
} catch (e8) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
exports.DEFAULT_PUBLIC_STORAGE_PREFIX = _chunkQHU7FUO4cjs.DEFAULT_PUBLIC_STORAGE_PREFIX; exports.createLocalFsStorageAdapter = createLocalFsStorageAdapter; exports.createMemoryStorageAdapter = _chunkYRXXQUDEcjs.createMemoryStorageAdapter; exports.createS3StorageAdapter = createS3StorageAdapter; exports.createStorageAdapter = _chunkYRXXQUDEcjs.createStorageAdapter; exports.createSupabaseStorageAdapter = createSupabaseStorageAdapter; exports.parsePublicStorageUrl = _chunkQHU7FUO4cjs.parsePublicStorageUrl; exports.toPaginatedListResult = _chunkYRXXQUDEcjs.toPaginatedListResult; exports.validateStorageMetadata = _chunkYRXXQUDEcjs.validateStorageMetadata;
|
|
447
|
+
//# sourceMappingURL=storage.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/image-upload-kit/image-upload-kit/dist/storage.cjs","../src/storage/localFsAdapter.ts","../src/storage/s3Adapter.ts","../src/storage/supabaseAdapter.ts"],"names":["path"],"mappings":"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","file":"/home/runner/work/image-upload-kit/image-upload-kit/dist/storage.cjs","sourcesContent":[null,"import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport {\n type StorageAdapter,\n type StorageFileEntry,\n type StorageListOptions,\n type StoragePaginatedListResult,\n type StorageUploadParams,\n type StorageUploadResult,\n toPaginatedListResult,\n} from './types';\n\nexport interface LocalFsStorageOptions {\n /** Absolute or relative root directory on disk (e.g. `./public/uploads`) */\n rootDir: string;\n /** Public URL prefix matching `rootDir` (e.g. `/uploads` or `https://cdn.example.com/uploads`) */\n baseUrl?: string;\n /** Include the bucket segment as a subdirectory under `rootDir`. Defaults to `true`. */\n includeBucketInPath?: boolean;\n}\n\nfunction sanitizeRelativePath(inputPath: string): string {\n if (!inputPath || typeof inputPath !== 'string') {\n throw new Error('Invalid storage path');\n }\n if (inputPath.includes(':') || inputPath.includes('\\0')) {\n throw new Error('Path traversal blocked in LocalFsStorageAdapter');\n }\n\n const normalized = inputPath.replace(/\\\\/g, '/').replace(/^\\/+/, '');\n const rawSegments = normalized.split('/').filter(Boolean);\n\n if (rawSegments.length === 0) {\n throw new Error('Invalid storage path');\n }\n\n for (const segment of rawSegments) {\n if (segment === '.' || segment === '..') {\n throw new Error('Invalid storage path: path traversal blocked');\n }\n }\n\n return rawSegments.join('/');\n}\n\nfunction assertContainedInRoot(resolvedRoot: string, targetPath: string): void {\n const relative = path.relative(resolvedRoot, targetPath);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n throw new Error('Path traversal blocked in LocalFsStorageAdapter');\n }\n}\n\nexport interface LocalFsStorageAdapter extends StorageAdapter {\n exists(bucket: string, relativePath: string): Promise<boolean>;\n getPublicUrl(bucket: string, relativePath: string): string;\n list(\n bucket: string,\n folder?: string,\n options?: StorageListOptions,\n ): Promise<StoragePaginatedListResult>;\n deleteByUrl(url: string): Promise<boolean>;\n parseUrl(url: string): { bucket?: string; path: string } | null;\n}\n\nexport function createLocalFsStorageAdapter(\n options: LocalFsStorageOptions,\n): LocalFsStorageAdapter {\n const resolvedRoot = path.resolve(options.rootDir);\n const baseUrl = (options.baseUrl ?? '/uploads').replace(/\\/+$/, '');\n const includeBucket = options.includeBucketInPath !== false;\n\n const resolveDiskTarget = (bucket: string, relativePath: string) => {\n const cleanBucket = sanitizeRelativePath(bucket);\n const cleanRel = sanitizeRelativePath(relativePath);\n const subPath = includeBucket ? `${cleanBucket}/${cleanRel}` : cleanRel;\n const diskPath = path.resolve(resolvedRoot, subPath);\n\n assertContainedInRoot(resolvedRoot, diskPath);\n\n return { diskPath, subPath, cleanRel };\n };\n\n const getPublicUrl = (bucket: string, relativePath: string): string => {\n const { subPath } = resolveDiskTarget(bucket, relativePath);\n return `${baseUrl}/${subPath}`;\n };\n\n const parseUrl = (url: string): { bucket?: string; path: string } | null => {\n if (!url || typeof url !== 'string') return null;\n\n try {\n const cleanUrl = url.split(/[?#]/)[0];\n const prefix = `${baseUrl}/`;\n let subPath: string | null = null;\n\n if (cleanUrl.startsWith(prefix)) {\n subPath = cleanUrl.slice(prefix.length);\n } else if (baseUrl.startsWith('/')) {\n const parsed = new URL(cleanUrl, 'http://localhost');\n const isLocal =\n parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';\n const isRelative =\n !cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://');\n if ((isLocal || isRelative) && parsed.pathname.startsWith(prefix)) {\n subPath = parsed.pathname.slice(prefix.length);\n }\n }\n\n if (!subPath) return null;\n\n const decoded = decodeURIComponent(subPath)\n .replace(/\\\\/g, '/')\n .replace(/^\\/+/, '');\n if (!decoded || decoded.includes(':') || decoded.includes('\\0')) {\n return null;\n }\n\n const segments = decoded.split('/').filter(Boolean);\n if (\n segments.length === 0 ||\n segments.includes('..') ||\n segments.includes('.')\n ) {\n return null;\n }\n\n const diskPath = path.resolve(resolvedRoot, ...segments);\n const relative = path.relative(resolvedRoot, diskPath);\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n return null;\n }\n\n if (includeBucket) {\n if (segments.length < 2) return null;\n return {\n bucket: segments[0],\n path: segments.slice(1).join('/'),\n };\n }\n\n return { path: segments.join('/') };\n } catch {\n return null;\n }\n };\n\n return {\n async upload(params: StorageUploadParams): Promise<StorageUploadResult> {\n const { diskPath, cleanRel } = resolveDiskTarget(\n params.bucket,\n params.path,\n );\n\n await fs.mkdir(path.dirname(diskPath), { recursive: true });\n\n try {\n await fs.writeFile(\n diskPath,\n params.data,\n params.upsert === false ? { flag: 'wx' } : undefined,\n );\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n const existsError = new Error(\n `File already exists at ${params.bucket}/${cleanRel}`,\n ) as NodeJS.ErrnoException;\n existsError.code = 'EEXIST';\n throw existsError;\n }\n throw err;\n }\n\n return {\n bucket: params.bucket,\n path: cleanRel,\n url: getPublicUrl(params.bucket, cleanRel),\n };\n },\n\n async delete(bucket: string, relativePath: string): Promise<void> {\n const { diskPath } = resolveDiskTarget(bucket, relativePath);\n try {\n await fs.unlink(diskPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n },\n\n async exists(bucket: string, relativePath: string): Promise<boolean> {\n try {\n const { diskPath } = resolveDiskTarget(bucket, relativePath);\n const stat = await fs.stat(diskPath);\n return stat.isFile();\n } catch {\n return false;\n }\n },\n\n getPublicUrl,\n parseUrl,\n\n async deleteByUrl(url: string): Promise<boolean> {\n const parsed = parseUrl(url);\n if (!parsed) return false;\n\n try {\n const subSegments =\n includeBucket && parsed.bucket\n ? [parsed.bucket, ...parsed.path.split('/')]\n : parsed.path.split('/');\n const diskPath = path.resolve(resolvedRoot, ...subSegments);\n assertContainedInRoot(resolvedRoot, diskPath);\n await fs.unlink(diskPath);\n return true;\n } catch {\n return false;\n }\n },\n\n async list(\n bucket: string,\n folder?: string,\n listOptions?: StorageListOptions,\n ): Promise<StoragePaginatedListResult> {\n const cleanBucket = sanitizeRelativePath(bucket);\n const cleanFolder = folder ? sanitizeRelativePath(folder) : '';\n const targetDir = path.resolve(\n resolvedRoot,\n includeBucket ? cleanBucket : '',\n cleanFolder,\n );\n\n assertContainedInRoot(resolvedRoot, targetDir);\n\n try {\n const dirEntries = await fs.readdir(targetDir, { withFileTypes: true });\n const results: StorageFileEntry[] = [];\n\n for (const entry of dirEntries) {\n if (!entry.isFile()) continue;\n const full = path.join(targetDir, entry.name);\n const stat = await fs.stat(full);\n results.push({\n name: entry.name,\n id: entry.name,\n createdAt: stat.birthtime.toISOString(),\n size: stat.size,\n });\n }\n\n const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;\n const limit = listOptions?.limit ?? results.length;\n const sliced =\n listOptions?.limit !== undefined || offset > 0\n ? results.slice(offset, offset + limit)\n : results;\n const nextCursor =\n listOptions?.limit !== undefined && offset + sliced.length < results.length\n ? String(offset + sliced.length)\n : undefined;\n\n return toPaginatedListResult(sliced, nextCursor);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return toPaginatedListResult([]);\n }\n throw err;\n }\n },\n };\n}\n","import {\n type StorageAdapter,\n type StorageFileEntry,\n type StorageListOptions,\n type StoragePaginatedListResult,\n type StorageUploadParams,\n type StorageUploadResult,\n toPaginatedListResult,\n validateStorageMetadata,\n} from './types';\n\nexport interface S3PutObjectInput {\n Bucket: string;\n Key: string;\n Body: Buffer | Uint8Array;\n ContentType: string;\n CacheControl?: string;\n Metadata?: Record<string, string>;\n IfNoneMatch?: string;\n}\n\nexport interface S3DeleteObjectInput {\n Bucket: string;\n Key: string;\n}\n\nexport interface S3HeadObjectInput {\n Bucket: string;\n Key: string;\n}\n\nexport interface S3ListObjectsInput {\n Bucket: string;\n Prefix?: string;\n MaxKeys?: number;\n ContinuationToken?: string;\n}\n\nexport interface S3ListedObject {\n Key?: string;\n Size?: number;\n LastModified?: Date | string;\n}\n\nexport interface S3ListObjectsOutput {\n Contents?: S3ListedObject[];\n NextContinuationToken?: string;\n IsTruncated?: boolean;\n}\n\n/** Structural S3 / Cloudflare R2 / MinIO transport with zero runtime SDK dependency. */\nexport interface S3TransportLike {\n putObject(input: S3PutObjectInput): Promise<unknown>;\n deleteObject(input: S3DeleteObjectInput): Promise<unknown>;\n headObject?(input: S3HeadObjectInput): Promise<unknown>;\n listObjects?(input: S3ListObjectsInput): Promise<S3ListObjectsOutput>;\n listObjectsV2?(input: S3ListObjectsInput): Promise<S3ListObjectsOutput>;\n}\n\nexport type S3PublicUrlBuilder = (bucket: string, path: string) => string;\n\nexport interface S3StorageBaseOptions {\n /** Public base URL prefix (e.g. `'https://cdn.example.com'`) or `(bucket, path) => string` builder. */\n publicBaseUrl: string | S3PublicUrlBuilder;\n /** Default `Cache-Control` header applied when `StorageUploadParams.cacheControl` is omitted. */\n cacheControl?: string;\n /** Include `bucket` segment in string-prefix URLs (`${publicBaseUrl}/${bucket}/${path}`). Defaults to `true`. */\n includeBucketInUrl?: boolean;\n /** Fallback bucket used by `deleteByUrl` when `includeBucketInUrl` is `false`. */\n defaultBucket?: string;\n}\n\nexport type S3StorageAdapterOptions = S3StorageBaseOptions &\n (\n | (S3TransportLike & { client?: never; transport?: never })\n | { client: S3TransportLike; transport?: never; putObject?: never }\n | { transport: S3TransportLike; client?: never; putObject?: never }\n );\n\nfunction normalizeS3Key(rawPath: string): string {\n const normalized = rawPath.replace(/\\\\/g, '/').replace(/^\\/+/, '');\n if (!normalized) throw new Error('Invalid S3 object key');\n return normalized;\n}\n\nfunction resolveTransport(options: S3StorageAdapterOptions): S3TransportLike {\n if ('client' in options && options.client) return options.client;\n if ('transport' in options && options.transport) return options.transport;\n if ('putObject' in options && typeof options.putObject === 'function') return options;\n throw new Error('S3StorageAdapter requires putObject/deleteObject or client/transport');\n}\n\nfunction isS3PreconditionError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as {\n code?: unknown;\n name?: unknown;\n statusCode?: unknown;\n $metadata?: { httpStatusCode?: number };\n };\n const status = e.$metadata?.httpStatusCode ?? Number(e.statusCode);\n const code = String(e.code ?? e.name ?? '').toLowerCase();\n return status === 412 || code === 'preconditionfailed' || code === 'eexist';\n}\n\n/** Zero-dependency S3 / Cloudflare R2 / MinIO `StorageAdapter`. */\nexport function createS3StorageAdapter(options: S3StorageAdapterOptions): StorageAdapter {\n const transport = resolveTransport(options);\n const includeBucket = options.includeBucketInUrl !== false;\n\n const getPublicUrl = (bucket: string, path: string): string => {\n const key = normalizeS3Key(path);\n if (typeof options.publicBaseUrl === 'function') return options.publicBaseUrl(bucket, key);\n const base = options.publicBaseUrl.replace(/\\/+$/, '');\n return includeBucket ? `${base}/${bucket}/${key}` : `${base}/${key}`;\n };\n\n return {\n async upload(params: StorageUploadParams): Promise<StorageUploadResult> {\n const key = normalizeS3Key(params.path);\n validateStorageMetadata(params.metadata);\n\n const isImmutable =\n params.upsert === false && params.contentType.includes('image/avif');\n const cacheControl =\n params.cacheControl ??\n options.cacheControl ??\n (isImmutable\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=2592000, must-revalidate');\n\n const metadata =\n params.metadata || params.contentHash\n ? {\n ...params.metadata,\n ...(params.contentHash ? { contentHash: params.contentHash } : {}),\n }\n : undefined;\n\n try {\n await transport.putObject({\n Bucket: params.bucket,\n Key: key,\n Body: params.data,\n ContentType: params.contentType,\n ...(cacheControl ? { CacheControl: cacheControl } : {}),\n ...(metadata ? { Metadata: metadata } : {}),\n ...(params.upsert === false ? { IfNoneMatch: '*' } : {}),\n });\n } catch (err) {\n if (isS3PreconditionError(err)) {\n const existsErr = new Error(\n `Object already exists at ${params.bucket}/${key}`,\n ) as Error & { code?: string; statusCode?: number };\n existsErr.code = 'EEXIST';\n existsErr.statusCode = 412;\n throw existsErr;\n }\n throw err;\n }\n\n return {\n bucket: params.bucket,\n path: key,\n url: getPublicUrl(params.bucket, key),\n };\n },\n\n async delete(bucket: string, path: string): Promise<void> {\n await transport.deleteObject({ Bucket: bucket, Key: normalizeS3Key(path) });\n },\n\n async exists(bucket: string, path: string): Promise<boolean> {\n const key = normalizeS3Key(path);\n if (transport.headObject) {\n try {\n await transport.headObject({ Bucket: bucket, Key: key });\n return true;\n } catch {\n return false;\n }\n }\n const lastSlash = key.lastIndexOf('/');\n const folder = lastSlash > 0 ? key.slice(0, lastSlash) : undefined;\n const targetName = lastSlash > 0 ? key.slice(lastSlash + 1) : key;\n const listed = await this.list!(bucket, folder);\n return listed.some((item) => item.name === targetName);\n },\n\n getPublicUrl,\n\n async list(\n bucket: string,\n folder?: string,\n listOptions?: StorageListOptions,\n ): Promise<StoragePaginatedListResult> {\n const cleanFolder = folder ? normalizeS3Key(folder).replace(/\\/+$/, '') : '';\n const prefix = cleanFolder ? `${cleanFolder}/` : undefined;\n const input: S3ListObjectsInput = {\n Bucket: bucket,\n ...(prefix ? { Prefix: prefix } : {}),\n MaxKeys: listOptions?.limit ?? 1000,\n ...(listOptions?.cursor ? { ContinuationToken: listOptions.cursor } : {}),\n };\n\n const output = transport.listObjects\n ? await transport.listObjects(input)\n : transport.listObjectsV2\n ? await transport.listObjectsV2(input)\n : undefined;\n\n const entries: StorageFileEntry[] = (output?.Contents ?? [])\n .filter((item): item is S3ListedObject & { Key: string } => Boolean(item.Key))\n .map((item) => {\n const name =\n prefix && item.Key.startsWith(prefix)\n ? item.Key.slice(prefix.length)\n : item.Key;\n const createdAt =\n item.LastModified instanceof Date\n ? item.LastModified.toISOString()\n : item.LastModified;\n return {\n name,\n id: `${bucket}/${item.Key}`,\n ...(createdAt !== undefined ? { createdAt } : {}),\n ...(item.Size !== undefined ? { size: item.Size } : {}),\n };\n })\n .filter((entry) => entry.name.length > 0);\n\n return toPaginatedListResult(entries, output?.NextContinuationToken);\n },\n\n parseUrl(url: string): { bucket?: string; path: string } | null {\n if (typeof options.publicBaseUrl !== 'string' || !url) return null;\n const prefix = `${options.publicBaseUrl.replace(/\\/+$/, '')}/`;\n const cleanUrl = url.split(/[?#]/)[0];\n if (!cleanUrl.startsWith(prefix)) return null;\n\n try {\n const rest = decodeURIComponent(cleanUrl.slice(prefix.length)).replace(/^\\/+/, '');\n if (!rest) return null;\n\n if (includeBucket) {\n const slashIdx = rest.indexOf('/');\n if (slashIdx <= 0 || slashIdx === rest.length - 1) return null;\n return {\n bucket: rest.slice(0, slashIdx),\n path: rest.slice(slashIdx + 1),\n };\n }\n\n if (!options.defaultBucket) return null;\n return { bucket: options.defaultBucket, path: rest };\n } catch {\n return null;\n }\n },\n\n async deleteByUrl(url: string): Promise<boolean> {\n const parsed = this.parseUrl?.(url);\n if (!parsed || !parsed.bucket) return false;\n\n try {\n await transport.deleteObject({\n Bucket: parsed.bucket,\n Key: parsed.path,\n });\n return true;\n } catch {\n return false;\n }\n },\n };\n}\n","import {\n type StorageAdapter,\n type StorageListOptions,\n type StoragePaginatedListResult,\n type StorageUploadParams,\n type StorageUploadResult,\n toPaginatedListResult,\n validateStorageMetadata,\n} from './types';\nimport {\n DEFAULT_PUBLIC_STORAGE_PREFIX,\n parsePublicStorageUrl,\n} from './urlParser';\n\n/**\n * Minimal structural interface matching `@supabase/supabase-js` Storage client,\n * avoiding a mandatory `@supabase/supabase-js` runtime dependency.\n */\nexport interface SupabaseBucketClientLike {\n upload(\n path: string,\n fileBody: Buffer | Uint8Array,\n fileOptions?: {\n contentType?: string;\n upsert?: boolean;\n cacheControl?: string;\n metadata?: Record<string, string>;\n },\n ): Promise<{\n data: { path: string } | null;\n error: { message: string } | null;\n }>;\n remove(paths: string[]): Promise<{\n data?: unknown;\n error: { message: string } | null;\n }>;\n getPublicUrl(path: string): {\n data: { publicUrl: string };\n };\n list?(\n path?: string,\n options?: {\n limit?: number;\n offset?: number;\n sortBy?: { column: string; order: string };\n },\n ): Promise<{\n data: Array<{\n name: string;\n id: string | null;\n created_at: string | null;\n }> | null;\n error: { message: string } | null;\n }>;\n}\n\nexport interface SupabaseStorageClientLike {\n storage: {\n from(bucket: string): SupabaseBucketClientLike;\n };\n}\n\nexport interface SupabaseStorageAdapterOptions {\n publicUrlPrefix?: string;\n}\n\n/**\n * Plug in any `@supabase/supabase-js` client instance (e.g., service-role client)\n * as a `StorageAdapter` with zero extra wrapper boilerplate.\n */\nexport function createSupabaseStorageAdapter(\n client: SupabaseStorageClientLike,\n options: SupabaseStorageAdapterOptions = {},\n): StorageAdapter {\n const publicPrefix =\n options.publicUrlPrefix || DEFAULT_PUBLIC_STORAGE_PREFIX;\n\n return {\n async upload(params: StorageUploadParams): Promise<StorageUploadResult> {\n validateStorageMetadata(params.metadata);\n\n const isImmutable =\n params.upsert === false && params.contentType.includes('image/avif');\n const cacheControl =\n params.cacheControl ||\n (isImmutable\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=2592000, must-revalidate');\n\n const { data, error } = await client.storage\n .from(params.bucket)\n .upload(params.path, params.data, {\n contentType: params.contentType,\n upsert: params.upsert === undefined ? true : params.upsert,\n cacheControl,\n metadata: {\n ...params.metadata,\n uploadedAt: new Date().toISOString(),\n contentHash: params.contentHash || 'unknown',\n },\n });\n\n if (error || !data) {\n throw new Error(\n `Failed to upload to Supabase storage (bucket: ${params.bucket}, path: ${params.path}): ${error?.message || 'Unknown error'}`,\n );\n }\n\n const {\n data: { publicUrl },\n } = client.storage.from(params.bucket).getPublicUrl(data.path);\n\n return {\n bucket: params.bucket,\n path: data.path,\n url: publicUrl,\n };\n },\n\n async delete(bucket: string, path: string): Promise<void> {\n const { error } = await client.storage.from(bucket).remove([path]);\n if (error) {\n throw new Error(`Failed to delete from Supabase storage: ${error.message}`);\n }\n },\n\n async exists(bucket: string, path: string): Promise<boolean> {\n const cleanPath = path.replace(/^\\/+/, '');\n const lastSlash = cleanPath.lastIndexOf('/');\n const folder = lastSlash > 0 ? cleanPath.slice(0, lastSlash) : undefined;\n const targetName = lastSlash > 0 ? cleanPath.slice(lastSlash + 1) : cleanPath;\n const entries = await this.list!(bucket, folder);\n return entries.some((entry) => entry.name === targetName);\n },\n\n getPublicUrl(bucket: string, path: string): string {\n const {\n data: { publicUrl },\n } = client.storage.from(bucket).getPublicUrl(path);\n return publicUrl;\n },\n\n async list(\n bucket: string,\n folder?: string,\n listOptions?: StorageListOptions,\n ): Promise<StoragePaginatedListResult> {\n const bucketClient = client.storage.from(bucket);\n if (!bucketClient.list) {\n return toPaginatedListResult([]);\n }\n\n const limit = listOptions?.limit ?? 1000;\n const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;\n\n const { data, error } = await bucketClient.list(folder, {\n limit,\n ...(offset > 0 ? { offset } : {}),\n sortBy: { column: 'created_at', order: 'desc' },\n });\n\n if (error) {\n throw new Error(`Failed to list Supabase bucket files: ${error.message}`);\n }\n\n const entries = (data || [])\n .filter((f) => f.id !== null)\n .map((f) => ({\n name: f.name,\n id: f.id ?? undefined,\n createdAt: f.created_at ?? undefined,\n }));\n\n const nextCursor =\n listOptions?.limit !== undefined && entries.length >= limit\n ? String(offset + entries.length)\n : undefined;\n\n return toPaginatedListResult(entries, nextCursor);\n },\n\n parseUrl(url: string): { bucket?: string; path: string } | null {\n const info = parsePublicStorageUrl(url, publicPrefix);\n if (!info) return null;\n return { bucket: info.bucket, path: info.path };\n },\n\n async deleteByUrl(url: string): Promise<boolean> {\n const info = parsePublicStorageUrl(url, publicPrefix);\n if (!info) return false;\n\n try {\n const { error } = await client.storage\n .from(info.bucket)\n .remove([info.path]);\n return !error;\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|