@nextlyhq/storage-uploadthing 0.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/LICENSE +22 -0
- package/README.md +75 -0
- package/dist/chunk-3GDQP6AS.mjs +14 -0
- package/dist/chunk-3GDQP6AS.mjs.map +1 -0
- package/dist/chunk-CV4XIHXE.mjs +255 -0
- package/dist/chunk-CV4XIHXE.mjs.map +1 -0
- package/dist/index.cjs +242 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +475 -0
- package/dist/index.d.ts +475 -0
- package/dist/index.mjs +142 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib-D34FQA4J.mjs +6285 -0
- package/dist/lib-D34FQA4J.mjs.map +1 -0
- package/dist/local-plugin-PTET4NAT-P6OHYYH6.mjs +4 -0
- package/dist/local-plugin-PTET4NAT-P6OHYYH6.mjs.map +1 -0
- package/dist/main-GMP6CIN5.mjs +400 -0
- package/dist/main-GMP6CIN5.mjs.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextlyHQ <info@nextlyhq.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @nextlyhq/storage-uploadthing
|
|
2
|
+
|
|
3
|
+
[UploadThing](https://uploadthing.com?utm_source=nextly&utm_medium=readme) storage adapter for Nextly. Stores media on UploadThing's CDN and serves them from `utfs.io`.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@nextlyhq/storage-uploadthing"><img alt="npm" src="https://img.shields.io/npm/v/@nextlyhq/storage-uploadthing?style=flat-square&label=npm&color=cb3837" /></a>
|
|
7
|
+
<a href="https://github.com/nextlyhq/nextly/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/github/license/nextlyhq/nextly?style=flat-square&color=blue" /></a>
|
|
8
|
+
<a href="https://nextlyhq.com/docs"><img alt="Status" src="https://img.shields.io/badge/status-alpha-orange?style=flat-square" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> Nextly is in alpha. APIs may change before 1.0. Pin exact versions in production.
|
|
13
|
+
|
|
14
|
+
## What it is
|
|
15
|
+
|
|
16
|
+
Stores Nextly media uploads on UploadThing. Useful if you already use UploadThing for the rest of your Next.js stack, or want a managed CDN with no AWS account setup.
|
|
17
|
+
|
|
18
|
+
> **You do not need this for development.** Nextly's default storage is local disk under `./public/uploads/`. Install this when you are ready to move uploads to UploadThing, typically for production.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @nextlyhq/storage-uploadthing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick usage
|
|
27
|
+
|
|
28
|
+
Register the storage adapter in `nextly.config.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { defineConfig } from "nextly/config";
|
|
32
|
+
import { uploadthingStorage } from "@nextlyhq/storage-uploadthing";
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
storage: [
|
|
36
|
+
uploadthingStorage({
|
|
37
|
+
token: process.env.UPLOADTHING_TOKEN!,
|
|
38
|
+
collections: { media: true },
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Required environment variables
|
|
45
|
+
|
|
46
|
+
| Variable | Required? | Default | Notes |
|
|
47
|
+
| ------------------- | --------- | ------- | ------------------------------------------------- |
|
|
48
|
+
| `UPLOADTHING_TOKEN` | yes | (none) | Find in the UploadThing dashboard under API keys. |
|
|
49
|
+
|
|
50
|
+
## Main exports
|
|
51
|
+
|
|
52
|
+
- `uploadthingStorage`: plugin factory for `defineConfig.storage`
|
|
53
|
+
- `UploadthingStorageAdapter`: the adapter class (advanced)
|
|
54
|
+
- Type exports: `UploadthingStorageConfig`
|
|
55
|
+
|
|
56
|
+
## Compatibility
|
|
57
|
+
|
|
58
|
+
| Tool | Version |
|
|
59
|
+
| ------------- | ------- |
|
|
60
|
+
| Node.js | 20+ |
|
|
61
|
+
| `uploadthing` | peer |
|
|
62
|
+
| `nextly` | 0.0.x |
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [**Media and storage docs**](https://nextlyhq.com/docs/guides/media-storage)
|
|
67
|
+
|
|
68
|
+
## Related packages
|
|
69
|
+
|
|
70
|
+
- [`@nextlyhq/storage-s3`](../storage-s3)
|
|
71
|
+
- [`@nextlyhq/storage-vercel-blob`](../storage-vercel-blob)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
[MIT](../../LICENSE.md)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export { __commonJS, __require };
|
|
13
|
+
//# sourceMappingURL=chunk-3GDQP6AS.mjs.map
|
|
14
|
+
//# sourceMappingURL=chunk-3GDQP6AS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-3GDQP6AS.mjs"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
// ../nextly/dist/chunk-G2AA4QLC.mjs
|
|
5
|
+
var BaseStorageAdapter = class {
|
|
6
|
+
/**
|
|
7
|
+
* Get adapter info including capabilities.
|
|
8
|
+
*
|
|
9
|
+
* Default implementation that auto-detects capabilities by checking
|
|
10
|
+
* if getSignedUrl and getPresignedUploadUrl methods are implemented.
|
|
11
|
+
* Override in subclasses for more accurate capability reporting.
|
|
12
|
+
*
|
|
13
|
+
* @returns Adapter info with type, name, and capability flags
|
|
14
|
+
*/
|
|
15
|
+
getInfo() {
|
|
16
|
+
const hasSignedUrls = "getSignedUrl" in this && typeof this.getSignedUrl === "function";
|
|
17
|
+
const hasClientUploads = "getPresignedUploadUrl" in this && typeof this.getPresignedUploadUrl === "function";
|
|
18
|
+
return {
|
|
19
|
+
type: this.getType(),
|
|
20
|
+
name: this.constructor.name,
|
|
21
|
+
supportsSignedUrls: hasSignedUrls,
|
|
22
|
+
supportsClientUploads: hasClientUploads
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize filename to prevent directory traversal and storage issues.
|
|
27
|
+
*
|
|
28
|
+
* Security measures:
|
|
29
|
+
* - Remove path separators (/, \)
|
|
30
|
+
* - Keep only basename (no directories)
|
|
31
|
+
* - Replace problematic characters with hyphens
|
|
32
|
+
* - Preserve alphanumeric, dots, underscores, hyphens
|
|
33
|
+
*
|
|
34
|
+
* @param filename - Original filename to sanitize
|
|
35
|
+
* @returns Sanitized filename safe for storage
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* this.sanitizeFilename('../../../etc/passwd') // 'passwd'
|
|
40
|
+
* this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'
|
|
41
|
+
* this.sanitizeFilename('photo.jpg') // 'photo.jpg'
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
sanitizeFilename(filename) {
|
|
45
|
+
const basename = filename.split(/[/\\]/).pop() || filename;
|
|
46
|
+
return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a unique storage key with date-based prefix.
|
|
50
|
+
*
|
|
51
|
+
* Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}
|
|
52
|
+
* This provides:
|
|
53
|
+
* - Unique keys via UUID to prevent collisions
|
|
54
|
+
* - Date-based organization for easier management
|
|
55
|
+
* - Readable filenames for debugging
|
|
56
|
+
*
|
|
57
|
+
* @param filename - Original filename (will be sanitized)
|
|
58
|
+
* @param folder - Optional folder/prefix for organizing uploads
|
|
59
|
+
* @returns Generated storage key
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* this.generateKey('photo.jpg')
|
|
64
|
+
* // 'uploads/2026/01/abc-123-...-photo.jpg'
|
|
65
|
+
*
|
|
66
|
+
* this.generateKey('doc.pdf', 'documents')
|
|
67
|
+
* // 'documents/2026/01/abc-123-...-doc.pdf'
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
generateKey(filename, folder) {
|
|
71
|
+
const sanitized = this.sanitizeFilename(filename);
|
|
72
|
+
const uuid = crypto.randomUUID();
|
|
73
|
+
const date = /* @__PURE__ */ new Date();
|
|
74
|
+
const year = date.getFullYear();
|
|
75
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
76
|
+
const prefix = folder ? `${folder}/${year}/${month}` : `uploads/${year}/${month}`;
|
|
77
|
+
return `${prefix}/${uuid}-${sanitized}`;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var gitignoreUpdated = false;
|
|
81
|
+
var LocalStorageAdapter = class extends BaseStorageAdapter {
|
|
82
|
+
basePath;
|
|
83
|
+
baseUrl;
|
|
84
|
+
constructor(config) {
|
|
85
|
+
super();
|
|
86
|
+
this.basePath = path.resolve(config.basePath);
|
|
87
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Upload file to local disk.
|
|
91
|
+
* Creates directories as needed and writes the file buffer.
|
|
92
|
+
*/
|
|
93
|
+
async upload(buffer, options) {
|
|
94
|
+
const key = this.generateKey(options.filename, options.folder);
|
|
95
|
+
const fullPath = this.resolveAndValidate(key);
|
|
96
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
97
|
+
await fs.writeFile(fullPath, buffer);
|
|
98
|
+
await this.ensureGitignore();
|
|
99
|
+
return {
|
|
100
|
+
url: this.getPublicUrl(key),
|
|
101
|
+
path: key
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Delete file from local disk.
|
|
106
|
+
* Silently succeeds if the file doesn't exist.
|
|
107
|
+
*/
|
|
108
|
+
async delete(filePath) {
|
|
109
|
+
let fullPath;
|
|
110
|
+
try {
|
|
111
|
+
fullPath = this.resolveAndValidate(filePath);
|
|
112
|
+
} catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await fs.unlink(fullPath);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.code !== "ENOENT") {
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Bulk delete files from local disk.
|
|
125
|
+
* Uses parallel unlinks with Promise.allSettled for best performance.
|
|
126
|
+
*/
|
|
127
|
+
async bulkDelete(filePaths) {
|
|
128
|
+
const results = await Promise.allSettled(
|
|
129
|
+
filePaths.map(async (filePath) => {
|
|
130
|
+
await this.delete(filePath);
|
|
131
|
+
return filePath;
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
const successful = [];
|
|
135
|
+
const failed = [];
|
|
136
|
+
results.forEach((result, index) => {
|
|
137
|
+
if (result.status === "fulfilled") {
|
|
138
|
+
successful.push(filePaths[index]);
|
|
139
|
+
} else {
|
|
140
|
+
failed.push({
|
|
141
|
+
filePath: filePaths[index],
|
|
142
|
+
error: result.reason?.message || "Unknown error"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return { successful, failed };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if file exists on local disk.
|
|
150
|
+
*/
|
|
151
|
+
async exists(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
const fullPath = this.resolveAndValidate(filePath);
|
|
154
|
+
await fs.access(fullPath);
|
|
155
|
+
return true;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get public URL for a file.
|
|
162
|
+
* Returns baseUrl + relative path for Next.js static file serving.
|
|
163
|
+
*/
|
|
164
|
+
getPublicUrl(filePath) {
|
|
165
|
+
const cleanPath = filePath.replace(/^\/+/, "");
|
|
166
|
+
return `${this.baseUrl}/${cleanPath}`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get storage type identifier.
|
|
170
|
+
*/
|
|
171
|
+
getType() {
|
|
172
|
+
return "local";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read file contents from local disk.
|
|
176
|
+
* Returns the file buffer, or null if file not found.
|
|
177
|
+
*/
|
|
178
|
+
async read(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
const fullPath = this.resolveAndValidate(filePath);
|
|
181
|
+
return await fs.readFile(fullPath);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ============================================================
|
|
187
|
+
// Private Helpers
|
|
188
|
+
// ============================================================
|
|
189
|
+
/**
|
|
190
|
+
* Resolve a relative file path to an absolute path within basePath.
|
|
191
|
+
* Throws if the resolved path would escape basePath (path traversal attack).
|
|
192
|
+
*/
|
|
193
|
+
resolveAndValidate(filePath) {
|
|
194
|
+
const sanitized = filePath.replace(/^[/\\]+/, "").replace(/\.\.[/\\]/g, "");
|
|
195
|
+
const fullPath = path.resolve(this.basePath, sanitized);
|
|
196
|
+
if (!fullPath.startsWith(this.basePath)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Path traversal detected: ${filePath} resolves outside of storage directory`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return fullPath;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Auto-add the uploads directory to .gitignore on first upload.
|
|
205
|
+
* Prevents accidentally committing uploaded files to git.
|
|
206
|
+
*/
|
|
207
|
+
async ensureGitignore() {
|
|
208
|
+
if (gitignoreUpdated) return;
|
|
209
|
+
gitignoreUpdated = true;
|
|
210
|
+
try {
|
|
211
|
+
const projectRoot = path.resolve(this.basePath, "..", "..");
|
|
212
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
213
|
+
let content = "";
|
|
214
|
+
try {
|
|
215
|
+
content = await fs.readFile(gitignorePath, "utf-8");
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
const uploadsDirRelative = path.relative(projectRoot, this.basePath);
|
|
219
|
+
const ignorePattern = uploadsDirRelative + "/";
|
|
220
|
+
if (!content.includes(ignorePattern)) {
|
|
221
|
+
const newEntry = `
|
|
222
|
+
# Nextly local uploads (auto-added)
|
|
223
|
+
${ignorePattern}
|
|
224
|
+
`;
|
|
225
|
+
await fs.writeFile(gitignorePath, content + newEntry, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
function localStorage(config) {
|
|
232
|
+
if (config.enabled === false) {
|
|
233
|
+
return {
|
|
234
|
+
name: "local-storage",
|
|
235
|
+
type: "local",
|
|
236
|
+
collections: {},
|
|
237
|
+
adapter: null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const adapter = new LocalStorageAdapter({
|
|
241
|
+
basePath: config.basePath ?? "./public/uploads",
|
|
242
|
+
baseUrl: config.baseUrl ?? "/uploads"
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
name: "local-storage",
|
|
246
|
+
type: "local",
|
|
247
|
+
collections: config.collections,
|
|
248
|
+
adapter
|
|
249
|
+
// Local storage doesn't support presigned URLs or signed downloads
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { BaseStorageAdapter, localStorage };
|
|
254
|
+
//# sourceMappingURL=chunk-CV4XIHXE.mjs.map
|
|
255
|
+
//# sourceMappingURL=chunk-CV4XIHXE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../nextly/dist/chunk-G2AA4QLC.mjs"],"names":[],"mappings":"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","file":"chunk-CV4XIHXE.mjs","sourcesContent":["// src/storage/adapters/local-adapter.ts\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\n\n// src/storage/adapters/base-adapter.ts\nvar BaseStorageAdapter = class {\n /**\n * Get adapter info including capabilities.\n *\n * Default implementation that auto-detects capabilities by checking\n * if getSignedUrl and getPresignedUploadUrl methods are implemented.\n * Override in subclasses for more accurate capability reporting.\n *\n * @returns Adapter info with type, name, and capability flags\n */\n getInfo() {\n const hasSignedUrls = \"getSignedUrl\" in this && typeof this.getSignedUrl === \"function\";\n const hasClientUploads = \"getPresignedUploadUrl\" in this && typeof this.getPresignedUploadUrl === \"function\";\n return {\n type: this.getType(),\n name: this.constructor.name,\n supportsSignedUrls: hasSignedUrls,\n supportsClientUploads: hasClientUploads\n };\n }\n /**\n * Sanitize filename to prevent directory traversal and storage issues.\n *\n * Security measures:\n * - Remove path separators (/, \\)\n * - Keep only basename (no directories)\n * - Replace problematic characters with hyphens\n * - Preserve alphanumeric, dots, underscores, hyphens\n *\n * @param filename - Original filename to sanitize\n * @returns Sanitized filename safe for storage\n *\n * @example\n * ```typescript\n * this.sanitizeFilename('../../../etc/passwd') // 'passwd'\n * this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'\n * this.sanitizeFilename('photo.jpg') // 'photo.jpg'\n * ```\n */\n sanitizeFilename(filename) {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n /**\n * Generate a unique storage key with date-based prefix.\n *\n * Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}\n * This provides:\n * - Unique keys via UUID to prevent collisions\n * - Date-based organization for easier management\n * - Readable filenames for debugging\n *\n * @param filename - Original filename (will be sanitized)\n * @param folder - Optional folder/prefix for organizing uploads\n * @returns Generated storage key\n *\n * @example\n * ```typescript\n * this.generateKey('photo.jpg')\n * // 'uploads/2026/01/abc-123-...-photo.jpg'\n *\n * this.generateKey('doc.pdf', 'documents')\n * // 'documents/2026/01/abc-123-...-doc.pdf'\n * ```\n */\n generateKey(filename, folder) {\n const sanitized = this.sanitizeFilename(filename);\n const uuid = crypto.randomUUID();\n const date = /* @__PURE__ */ new Date();\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const prefix = folder ? `${folder}/${year}/${month}` : `uploads/${year}/${month}`;\n return `${prefix}/${uuid}-${sanitized}`;\n }\n};\n\n// src/storage/adapters/local-adapter.ts\nvar gitignoreUpdated = false;\nvar LocalStorageAdapter = class extends BaseStorageAdapter {\n basePath;\n baseUrl;\n constructor(config) {\n super();\n this.basePath = path.resolve(config.basePath);\n this.baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n }\n /**\n * Upload file to local disk.\n * Creates directories as needed and writes the file buffer.\n */\n async upload(buffer, options) {\n const key = this.generateKey(options.filename, options.folder);\n const fullPath = this.resolveAndValidate(key);\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\n await fs.writeFile(fullPath, buffer);\n await this.ensureGitignore();\n return {\n url: this.getPublicUrl(key),\n path: key\n };\n }\n /**\n * Delete file from local disk.\n * Silently succeeds if the file doesn't exist.\n */\n async delete(filePath) {\n let fullPath;\n try {\n fullPath = this.resolveAndValidate(filePath);\n } catch {\n return;\n }\n try {\n await fs.unlink(fullPath);\n } catch (err) {\n if (err.code !== \"ENOENT\") {\n throw err;\n }\n }\n }\n /**\n * Bulk delete files from local disk.\n * Uses parallel unlinks with Promise.allSettled for best performance.\n */\n async bulkDelete(filePaths) {\n const results = await Promise.allSettled(\n filePaths.map(async (filePath) => {\n await this.delete(filePath);\n return filePath;\n })\n );\n const successful = [];\n const failed = [];\n results.forEach((result, index) => {\n if (result.status === \"fulfilled\") {\n successful.push(filePaths[index]);\n } else {\n failed.push({\n filePath: filePaths[index],\n error: result.reason?.message || \"Unknown error\"\n });\n }\n });\n return { successful, failed };\n }\n /**\n * Check if file exists on local disk.\n */\n async exists(filePath) {\n try {\n const fullPath = this.resolveAndValidate(filePath);\n await fs.access(fullPath);\n return true;\n } catch {\n return false;\n }\n }\n /**\n * Get public URL for a file.\n * Returns baseUrl + relative path for Next.js static file serving.\n */\n getPublicUrl(filePath) {\n const cleanPath = filePath.replace(/^\\/+/, \"\");\n return `${this.baseUrl}/${cleanPath}`;\n }\n /**\n * Get storage type identifier.\n */\n getType() {\n return \"local\";\n }\n /**\n * Read file contents from local disk.\n * Returns the file buffer, or null if file not found.\n */\n async read(filePath) {\n try {\n const fullPath = this.resolveAndValidate(filePath);\n return await fs.readFile(fullPath);\n } catch {\n return null;\n }\n }\n // ============================================================\n // Private Helpers\n // ============================================================\n /**\n * Resolve a relative file path to an absolute path within basePath.\n * Throws if the resolved path would escape basePath (path traversal attack).\n */\n resolveAndValidate(filePath) {\n const sanitized = filePath.replace(/^[/\\\\]+/, \"\").replace(/\\.\\.[/\\\\]/g, \"\");\n const fullPath = path.resolve(this.basePath, sanitized);\n if (!fullPath.startsWith(this.basePath)) {\n throw new Error(\n `Path traversal detected: ${filePath} resolves outside of storage directory`\n );\n }\n return fullPath;\n }\n /**\n * Auto-add the uploads directory to .gitignore on first upload.\n * Prevents accidentally committing uploaded files to git.\n */\n async ensureGitignore() {\n if (gitignoreUpdated) return;\n gitignoreUpdated = true;\n try {\n const projectRoot = path.resolve(this.basePath, \"..\", \"..\");\n const gitignorePath = path.join(projectRoot, \".gitignore\");\n let content = \"\";\n try {\n content = await fs.readFile(gitignorePath, \"utf-8\");\n } catch {\n }\n const uploadsDirRelative = path.relative(projectRoot, this.basePath);\n const ignorePattern = uploadsDirRelative + \"/\";\n if (!content.includes(ignorePattern)) {\n const newEntry = `\n# Nextly local uploads (auto-added)\n${ignorePattern}\n`;\n await fs.writeFile(gitignorePath, content + newEntry, \"utf-8\");\n }\n } catch {\n }\n }\n};\n\n// src/storage/adapters/local-plugin.ts\nfunction localStorage(config) {\n if (config.enabled === false) {\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: {},\n adapter: null\n };\n }\n const adapter = new LocalStorageAdapter({\n basePath: config.basePath ?? \"./public/uploads\",\n baseUrl: config.baseUrl ?? \"/uploads\"\n });\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: config.collections,\n adapter\n // Local storage doesn't support presigned URLs or signed downloads\n };\n}\n\nexport {\n BaseStorageAdapter,\n LocalStorageAdapter,\n localStorage\n};\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var server = require('uploadthing/server');
|
|
4
|
+
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// ../nextly/dist/chunk-G2AA4QLC.mjs
|
|
11
|
+
var BaseStorageAdapter;
|
|
12
|
+
var init_chunk_G2AA4QLC = __esm({
|
|
13
|
+
"../nextly/dist/chunk-G2AA4QLC.mjs"() {
|
|
14
|
+
BaseStorageAdapter = class {
|
|
15
|
+
/**
|
|
16
|
+
* Get adapter info including capabilities.
|
|
17
|
+
*
|
|
18
|
+
* Default implementation that auto-detects capabilities by checking
|
|
19
|
+
* if getSignedUrl and getPresignedUploadUrl methods are implemented.
|
|
20
|
+
* Override in subclasses for more accurate capability reporting.
|
|
21
|
+
*
|
|
22
|
+
* @returns Adapter info with type, name, and capability flags
|
|
23
|
+
*/
|
|
24
|
+
getInfo() {
|
|
25
|
+
const hasSignedUrls = "getSignedUrl" in this && typeof this.getSignedUrl === "function";
|
|
26
|
+
const hasClientUploads = "getPresignedUploadUrl" in this && typeof this.getPresignedUploadUrl === "function";
|
|
27
|
+
return {
|
|
28
|
+
type: this.getType(),
|
|
29
|
+
name: this.constructor.name,
|
|
30
|
+
supportsSignedUrls: hasSignedUrls,
|
|
31
|
+
supportsClientUploads: hasClientUploads
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize filename to prevent directory traversal and storage issues.
|
|
36
|
+
*
|
|
37
|
+
* Security measures:
|
|
38
|
+
* - Remove path separators (/, \)
|
|
39
|
+
* - Keep only basename (no directories)
|
|
40
|
+
* - Replace problematic characters with hyphens
|
|
41
|
+
* - Preserve alphanumeric, dots, underscores, hyphens
|
|
42
|
+
*
|
|
43
|
+
* @param filename - Original filename to sanitize
|
|
44
|
+
* @returns Sanitized filename safe for storage
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* this.sanitizeFilename('../../../etc/passwd') // 'passwd'
|
|
49
|
+
* this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'
|
|
50
|
+
* this.sanitizeFilename('photo.jpg') // 'photo.jpg'
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
sanitizeFilename(filename) {
|
|
54
|
+
const basename = filename.split(/[/\\]/).pop() || filename;
|
|
55
|
+
return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate a unique storage key with date-based prefix.
|
|
59
|
+
*
|
|
60
|
+
* Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}
|
|
61
|
+
* This provides:
|
|
62
|
+
* - Unique keys via UUID to prevent collisions
|
|
63
|
+
* - Date-based organization for easier management
|
|
64
|
+
* - Readable filenames for debugging
|
|
65
|
+
*
|
|
66
|
+
* @param filename - Original filename (will be sanitized)
|
|
67
|
+
* @param folder - Optional folder/prefix for organizing uploads
|
|
68
|
+
* @returns Generated storage key
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* this.generateKey('photo.jpg')
|
|
73
|
+
* // 'uploads/2026/01/abc-123-...-photo.jpg'
|
|
74
|
+
*
|
|
75
|
+
* this.generateKey('doc.pdf', 'documents')
|
|
76
|
+
* // 'documents/2026/01/abc-123-...-doc.pdf'
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
generateKey(filename, folder) {
|
|
80
|
+
const sanitized = this.sanitizeFilename(filename);
|
|
81
|
+
const uuid = crypto.randomUUID();
|
|
82
|
+
const date = /* @__PURE__ */ new Date();
|
|
83
|
+
const year = date.getFullYear();
|
|
84
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
85
|
+
const prefix = folder ? `${folder}/${year}/${month}` : `uploads/${year}/${month}`;
|
|
86
|
+
return `${prefix}/${uuid}-${sanitized}`;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ../nextly/dist/chunk-7P6ASYW6.mjs
|
|
93
|
+
var init_chunk_7P6ASYW6 = __esm({
|
|
94
|
+
"../nextly/dist/chunk-7P6ASYW6.mjs"() {
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ../nextly/dist/chunk-EGXBZCGC.mjs
|
|
99
|
+
init_chunk_G2AA4QLC();
|
|
100
|
+
|
|
101
|
+
// ../nextly/dist/storage/index.mjs
|
|
102
|
+
init_chunk_G2AA4QLC();
|
|
103
|
+
init_chunk_7P6ASYW6();
|
|
104
|
+
var UploadthingStorageAdapter = class extends BaseStorageAdapter {
|
|
105
|
+
utapi;
|
|
106
|
+
constructor(config) {
|
|
107
|
+
super();
|
|
108
|
+
this.utapi = new server.UTApi({
|
|
109
|
+
...config.token ? { token: config.token } : {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Upload file to Uploadthing.
|
|
114
|
+
* Creates a File object from the buffer and uploads via UTApi.
|
|
115
|
+
*/
|
|
116
|
+
async upload(buffer, options) {
|
|
117
|
+
const sanitized = this.sanitizeFilename(options.filename);
|
|
118
|
+
const file = new File([buffer], sanitized, {
|
|
119
|
+
type: options.mimeType
|
|
120
|
+
});
|
|
121
|
+
const results = await this.utapi.uploadFiles([file], {
|
|
122
|
+
contentDisposition: options.contentDisposition ?? "attachment"
|
|
123
|
+
});
|
|
124
|
+
const result = results[0];
|
|
125
|
+
if (!result?.data) {
|
|
126
|
+
const errorMsg = result?.error?.message ?? "Unknown error";
|
|
127
|
+
throw new Error(`Uploadthing upload failed: ${errorMsg}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
url: result.data.url,
|
|
131
|
+
// Use the file key as the storage path (needed for deletion)
|
|
132
|
+
path: result.data.key
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Delete file from Uploadthing by its file key.
|
|
137
|
+
*/
|
|
138
|
+
async delete(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
await this.utapi.deleteFiles([filePath], { keyType: "fileKey" });
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Bulk delete files from Uploadthing.
|
|
146
|
+
* UTApi natively supports batch deletion.
|
|
147
|
+
*/
|
|
148
|
+
async bulkDelete(filePaths) {
|
|
149
|
+
try {
|
|
150
|
+
await this.utapi.deleteFiles(filePaths, { keyType: "fileKey" });
|
|
151
|
+
return {
|
|
152
|
+
successful: filePaths,
|
|
153
|
+
failed: []
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : "Bulk delete failed";
|
|
157
|
+
return {
|
|
158
|
+
successful: [],
|
|
159
|
+
failed: filePaths.map((fp) => ({
|
|
160
|
+
filePath: fp,
|
|
161
|
+
error: message
|
|
162
|
+
}))
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if file exists on Uploadthing.
|
|
168
|
+
* Uses getFileUrls - if it returns data with URLs, the file exists.
|
|
169
|
+
*/
|
|
170
|
+
async exists(filePath) {
|
|
171
|
+
try {
|
|
172
|
+
const result = await this.utapi.getFileUrls([filePath], {
|
|
173
|
+
keyType: "fileKey"
|
|
174
|
+
});
|
|
175
|
+
const items = Array.from(result.data);
|
|
176
|
+
return items.length > 0 && !!items[0]?.url;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get public URL for a file.
|
|
183
|
+
* Uploadthing files are served from utfs.io CDN.
|
|
184
|
+
* The URL is stored at upload time, so this reconstructs it from the key.
|
|
185
|
+
*/
|
|
186
|
+
getPublicUrl(filePath) {
|
|
187
|
+
return `https://utfs.io/f/${filePath}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get storage type identifier.
|
|
191
|
+
*/
|
|
192
|
+
getType() {
|
|
193
|
+
return "uploadthing";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Keep filename sanitization local so this adapter remains stable
|
|
197
|
+
* even if upstream base adapter type declarations drift.
|
|
198
|
+
*/
|
|
199
|
+
sanitizeFilename(filename) {
|
|
200
|
+
const basename = filename.split(/[/\\]/).pop() || filename;
|
|
201
|
+
return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/plugin.ts
|
|
206
|
+
function uploadthingStorage(config) {
|
|
207
|
+
if (config.enabled === false) {
|
|
208
|
+
return {
|
|
209
|
+
name: "uploadthing-storage",
|
|
210
|
+
type: "uploadthing",
|
|
211
|
+
collections: {},
|
|
212
|
+
adapter: null
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const token = config.token ?? process.env.UPLOADTHING_TOKEN;
|
|
216
|
+
if (!token) {
|
|
217
|
+
console.warn(
|
|
218
|
+
"[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config."
|
|
219
|
+
);
|
|
220
|
+
return {
|
|
221
|
+
name: "uploadthing-storage",
|
|
222
|
+
type: "uploadthing",
|
|
223
|
+
collections: {},
|
|
224
|
+
adapter: null
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const adapter = new UploadthingStorageAdapter({ token });
|
|
228
|
+
return {
|
|
229
|
+
name: "uploadthing-storage",
|
|
230
|
+
type: "uploadthing",
|
|
231
|
+
collections: config.collections,
|
|
232
|
+
adapter
|
|
233
|
+
// Uploadthing supports client-side uploads via its own pattern
|
|
234
|
+
// but we don't implement getClientUploadUrl here as it requires
|
|
235
|
+
// Uploadthing's specific route handler setup
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
exports.UploadthingStorageAdapter = UploadthingStorageAdapter;
|
|
240
|
+
exports.uploadthingStorage = uploadthingStorage;
|
|
241
|
+
//# sourceMappingURL=index.cjs.map
|
|
242
|
+
//# sourceMappingURL=index.cjs.map
|