@uploadbox/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generate-components.d.ts +5 -0
- package/dist/generate-components.d.ts.map +1 -0
- package/dist/generate-components.js +9 -0
- package/dist/generate-components.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/multipart.d.ts +25 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +130 -0
- package/dist/multipart.js.map +1 -0
- package/dist/progress-tracker.d.ts +13 -0
- package/dist/progress-tracker.d.ts.map +1 -0
- package/dist/progress-tracker.js +93 -0
- package/dist/progress-tracker.js.map +1 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +12 -0
- package/dist/provider.js.map +1 -0
- package/dist/retry.d.ts +5 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +47 -0
- package/dist/retry.js.map +1 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-button.d.ts +4 -0
- package/dist/upload-button.d.ts.map +1 -0
- package/dist/upload-button.js +25 -0
- package/dist/upload-button.js.map +1 -0
- package/dist/upload-dropzone.d.ts +4 -0
- package/dist/upload-dropzone.d.ts.map +1 -0
- package/dist/upload-dropzone.js +47 -0
- package/dist/upload-dropzone.js.map +1 -0
- package/dist/use-uploadbox.d.ts +4 -0
- package/dist/use-uploadbox.d.ts.map +1 -0
- package/dist/use-uploadbox.js +246 -0
- package/dist/use-uploadbox.js.map +1 -0
- package/package.json +56 -0
- package/src/generate-components.ts +20 -0
- package/src/index.ts +22 -0
- package/src/multipart.ts +189 -0
- package/src/progress-tracker.ts +107 -0
- package/src/provider.tsx +34 -0
- package/src/retry.ts +62 -0
- package/src/styles.css +126 -0
- package/src/types.ts +96 -0
- package/src/upload-button.tsx +76 -0
- package/src/upload-dropzone.tsx +138 -0
- package/src/use-uploadbox.ts +333 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { useDropzone } from "react-dropzone";
|
|
5
|
+
import { useUploadbox } from "./use-uploadbox.js";
|
|
6
|
+
export function UploadDropzone(props) {
|
|
7
|
+
const { endpoint, onClientUploadComplete, onUploadError, onUploadProgress, onBeforeUploadBegin, className, disabled, content, } = props;
|
|
8
|
+
const [state, setState] = useState("idle");
|
|
9
|
+
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
10
|
+
const { startUpload, isUploading, progress } = useUploadbox(endpoint, {
|
|
11
|
+
onClientUploadComplete: (files) => {
|
|
12
|
+
setState("complete");
|
|
13
|
+
setSelectedFiles([]);
|
|
14
|
+
onClientUploadComplete?.(files);
|
|
15
|
+
setTimeout(() => setState("idle"), 2000);
|
|
16
|
+
},
|
|
17
|
+
onUploadError: (error) => {
|
|
18
|
+
setState("error");
|
|
19
|
+
onUploadError?.(error);
|
|
20
|
+
setTimeout(() => setState("idle"), 3000);
|
|
21
|
+
},
|
|
22
|
+
onUploadProgress,
|
|
23
|
+
onBeforeUploadBegin,
|
|
24
|
+
});
|
|
25
|
+
const onDrop = useCallback((acceptedFiles) => {
|
|
26
|
+
setSelectedFiles(acceptedFiles);
|
|
27
|
+
setState("idle");
|
|
28
|
+
}, []);
|
|
29
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
30
|
+
onDrop,
|
|
31
|
+
disabled: disabled || isUploading,
|
|
32
|
+
});
|
|
33
|
+
const handleUpload = useCallback(async () => {
|
|
34
|
+
if (selectedFiles.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
setState("uploading");
|
|
37
|
+
await startUpload(selectedFiles);
|
|
38
|
+
}, [selectedFiles, startUpload]);
|
|
39
|
+
const stateClass = isDragActive ? "dragover" : state;
|
|
40
|
+
return (_jsxs("div", { ...getRootProps(), className: `uploadbox-dropzone uploadbox-dropzone--${stateClass} ${className ?? ""}`, children: [_jsx("input", { ...getInputProps() }), state === "complete" ? (_jsx("div", { className: "uploadbox-dropzone-content", children: _jsx("p", { className: "uploadbox-dropzone-label", children: "Upload complete!" }) })) : state === "error" ? (_jsx("div", { className: "uploadbox-dropzone-content", children: _jsx("p", { className: "uploadbox-dropzone-label", children: "Upload failed. Try again." }) })) : (_jsxs("div", { className: "uploadbox-dropzone-content", children: [_jsxs("svg", { className: "uploadbox-dropzone-icon", xmlns: "http://www.w3.org/2000/svg", width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), _jsx("polyline", { points: "17 8 12 3 7 8" }), _jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })] }), _jsx("p", { className: "uploadbox-dropzone-label", children: isDragActive
|
|
41
|
+
? "Drop files here"
|
|
42
|
+
: content?.label ?? "Drag & drop files here, or click to browse" }), content?.allowedContent && (_jsx("p", { className: "uploadbox-dropzone-allowed", children: content.allowedContent })), selectedFiles.length > 0 && !isUploading && (_jsxs("div", { className: "uploadbox-dropzone-selected", children: [_jsxs("p", { children: [selectedFiles.length, " file(s) selected"] }), _jsx("button", { type: "button", className: "uploadbox-button", onClick: (e) => {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
handleUpload();
|
|
45
|
+
}, children: content?.button ?? "Upload" })] })), isUploading && (_jsx("div", { className: "uploadbox-progress-bar", children: _jsx("div", { className: "uploadbox-progress-bar-fill", style: { width: `${progress}%` } }) }))] }))] }));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=upload-dropzone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-dropzone.js","sourceRoot":"","sources":["../src/upload-dropzone.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileRouter } from "@uploadbox/core";
|
|
2
|
+
import type { UseUploadboxOpts, UseUploadboxReturn } from "./types.js";
|
|
3
|
+
export declare function useUploadbox<TRouter extends FileRouter, TEndpoint extends keyof TRouter & string>(endpoint: TEndpoint, opts?: Omit<UseUploadboxOpts<TRouter, TEndpoint>, "endpoint">): UseUploadboxReturn;
|
|
4
|
+
//# sourceMappingURL=use-uploadbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-uploadbox.d.ts","sourceRoot":"","sources":["../src/use-uploadbox.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAmGpB,wBAAgB,YAAY,CAC1B,OAAO,SAAS,UAAU,EAC1B,SAAS,SAAS,MAAM,OAAO,GAAG,MAAM,EAExC,QAAQ,EAAE,SAAS,EACnB,IAAI,CAAC,EAAE,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,UAAU,CAAC,GAC5D,kBAAkB,CAyNpB"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
3
|
+
import { ProgressTracker } from "./progress-tracker.js";
|
|
4
|
+
import { withRetry, DEFAULT_RETRY_CONFIG } from "./retry.js";
|
|
5
|
+
import { shouldUseMultipart, uploadFileMultipart } from "./multipart.js";
|
|
6
|
+
import { useUploadboxConfig } from "./provider.js";
|
|
7
|
+
const DEFAULT_API_URL = "/api/uploadbox";
|
|
8
|
+
function buildHeaders(providerHeaders, providerApiKey, optHeaders) {
|
|
9
|
+
const merged = {};
|
|
10
|
+
if (providerHeaders)
|
|
11
|
+
Object.assign(merged, providerHeaders);
|
|
12
|
+
if (providerApiKey)
|
|
13
|
+
merged["Authorization"] = `Bearer ${providerApiKey}`;
|
|
14
|
+
if (optHeaders)
|
|
15
|
+
Object.assign(merged, optHeaders);
|
|
16
|
+
return merged;
|
|
17
|
+
}
|
|
18
|
+
async function fetchRouterConfig(apiUrl, headers) {
|
|
19
|
+
const res = await fetch(apiUrl, { headers });
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
throw new Error("Failed to fetch router config");
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
|
24
|
+
async function requestPresignedUrls(apiUrl, routeKey, files, headers) {
|
|
25
|
+
const res = await fetch(apiUrl, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
28
|
+
body: JSON.stringify({ action: "upload", routeKey, files }),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.json();
|
|
32
|
+
throw new Error(err.message || "Failed to get upload URLs");
|
|
33
|
+
}
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
async function confirmUploads(apiUrl, routeKey, keys, headers) {
|
|
37
|
+
const res = await fetch(apiUrl, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
40
|
+
body: JSON.stringify({ action: "complete", routeKey, keys }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const err = await res.json();
|
|
44
|
+
throw new Error(err.message || "Failed to confirm uploads");
|
|
45
|
+
}
|
|
46
|
+
return res.json();
|
|
47
|
+
}
|
|
48
|
+
function uploadFileWithProgress(url, file, contentType, onProgress, signal) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const xhr = new XMLHttpRequest();
|
|
51
|
+
xhr.open("PUT", url);
|
|
52
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
53
|
+
if (signal) {
|
|
54
|
+
signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
55
|
+
}
|
|
56
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
57
|
+
if (event.lengthComputable) {
|
|
58
|
+
onProgress?.(event.loaded);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
xhr.addEventListener("load", () => {
|
|
62
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
|
|
70
|
+
xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
|
|
71
|
+
xhr.send(file);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function useUploadbox(endpoint, opts) {
|
|
75
|
+
const providerConfig = useUploadboxConfig();
|
|
76
|
+
const apiUrl = providerConfig.apiUrl ?? DEFAULT_API_URL;
|
|
77
|
+
const mergedHeaders = useMemo(() => buildHeaders(providerConfig.headers, providerConfig.apiKey, opts?.headers), [providerConfig.headers, providerConfig.apiKey, opts?.headers]);
|
|
78
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
79
|
+
const [progress, setProgress] = useState(0);
|
|
80
|
+
const [fileProgress, setFileProgress] = useState([]);
|
|
81
|
+
const [routeConfig, setRouteConfig] = useState();
|
|
82
|
+
const trackerRef = useRef(new ProgressTracker());
|
|
83
|
+
const abortControllerRef = useRef(null);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
fetchRouterConfig(apiUrl, mergedHeaders).then((config) => {
|
|
86
|
+
setRouteConfig(config[endpoint]);
|
|
87
|
+
}).catch(console.error);
|
|
88
|
+
}, [endpoint, apiUrl, mergedHeaders]);
|
|
89
|
+
const abort = useCallback(() => {
|
|
90
|
+
abortControllerRef.current?.abort();
|
|
91
|
+
}, []);
|
|
92
|
+
const startUpload = useCallback(async (inputFiles) => {
|
|
93
|
+
const abortController = new AbortController();
|
|
94
|
+
abortControllerRef.current = abortController;
|
|
95
|
+
const tracker = trackerRef.current;
|
|
96
|
+
tracker.reset();
|
|
97
|
+
try {
|
|
98
|
+
setIsUploading(true);
|
|
99
|
+
setProgress(0);
|
|
100
|
+
let filesToUpload = inputFiles;
|
|
101
|
+
if (opts?.onBeforeUploadBegin) {
|
|
102
|
+
filesToUpload = opts.onBeforeUploadBegin(filesToUpload);
|
|
103
|
+
}
|
|
104
|
+
// Initialize progress tracker
|
|
105
|
+
filesToUpload.forEach((f, i) => {
|
|
106
|
+
tracker.init(String(i), f.name, f.size, f.type || "application/octet-stream");
|
|
107
|
+
});
|
|
108
|
+
// 1. Build file infos with metadata
|
|
109
|
+
const fileInfos = filesToUpload.map((f) => ({
|
|
110
|
+
name: f.name,
|
|
111
|
+
size: f.size,
|
|
112
|
+
type: f.type || "application/octet-stream",
|
|
113
|
+
...(opts?.getFileMetadata ? { customMetadata: opts.getFileMetadata(f) } : {}),
|
|
114
|
+
...(opts?.ttlSeconds != null ? { ttlSeconds: opts.ttlSeconds } : {}),
|
|
115
|
+
}));
|
|
116
|
+
// Split into small files (single-part) and large files (multipart)
|
|
117
|
+
const smallFileIndices = [];
|
|
118
|
+
const largeFileIndices = [];
|
|
119
|
+
filesToUpload.forEach((f, i) => {
|
|
120
|
+
if (shouldUseMultipart(f.size)) {
|
|
121
|
+
largeFileIndices.push(i);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
smallFileIndices.push(i);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// 2. Request presigned URLs for small files
|
|
128
|
+
const smallFileInfos = smallFileIndices.map((i) => fileInfos[i]);
|
|
129
|
+
let presignedResults = [];
|
|
130
|
+
if (smallFileInfos.length > 0) {
|
|
131
|
+
presignedResults = await requestPresignedUrls(apiUrl, endpoint, smallFileInfos, mergedHeaders);
|
|
132
|
+
}
|
|
133
|
+
const retryConfig = opts?.retry === false ? undefined : (opts?.retry ?? DEFAULT_RETRY_CONFIG);
|
|
134
|
+
// Helper to emit progress
|
|
135
|
+
const emitProgress = () => {
|
|
136
|
+
const snapshot = tracker.getSnapshot();
|
|
137
|
+
setProgress(snapshot.percent);
|
|
138
|
+
setFileProgress(snapshot.fileProgress);
|
|
139
|
+
opts?.onUploadProgress?.(snapshot);
|
|
140
|
+
};
|
|
141
|
+
// 3. Upload small files with retry
|
|
142
|
+
const smallUploadPromises = presignedResults.map((result, idx) => {
|
|
143
|
+
const fileIndex = smallFileIndices[idx];
|
|
144
|
+
const file = filesToUpload[fileIndex];
|
|
145
|
+
const fileId = String(fileIndex);
|
|
146
|
+
tracker.setKey(fileId, result.key);
|
|
147
|
+
tracker.setStatus(fileId, "uploading");
|
|
148
|
+
const doUpload = async () => {
|
|
149
|
+
await uploadFileWithProgress(result.url, file, file.type || "application/octet-stream", (loaded) => {
|
|
150
|
+
tracker.updateProgress(fileId, loaded);
|
|
151
|
+
emitProgress();
|
|
152
|
+
}, abortController.signal);
|
|
153
|
+
};
|
|
154
|
+
if (retryConfig) {
|
|
155
|
+
return withRetry(() => doUpload(), retryConfig, (attempt) => {
|
|
156
|
+
tracker.incrementRetry(fileId);
|
|
157
|
+
emitProgress();
|
|
158
|
+
}, abortController.signal).then(() => {
|
|
159
|
+
tracker.setStatus(fileId, "complete");
|
|
160
|
+
emitProgress();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return doUpload().then(() => {
|
|
164
|
+
tracker.setStatus(fileId, "complete");
|
|
165
|
+
emitProgress();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
// 4. Upload large files via multipart
|
|
169
|
+
const largeUploadPromises = largeFileIndices.map(async (fileIndex) => {
|
|
170
|
+
const file = filesToUpload[fileIndex];
|
|
171
|
+
const fileId = String(fileIndex);
|
|
172
|
+
const info = fileInfos[fileIndex];
|
|
173
|
+
tracker.setStatus(fileId, "uploading");
|
|
174
|
+
const result = await uploadFileMultipart({
|
|
175
|
+
file,
|
|
176
|
+
routeKey: endpoint,
|
|
177
|
+
fileInfo: info,
|
|
178
|
+
apiUrl,
|
|
179
|
+
headers: mergedHeaders,
|
|
180
|
+
retryConfig: retryConfig ?? undefined,
|
|
181
|
+
signal: abortController.signal,
|
|
182
|
+
onProgress: (loaded) => {
|
|
183
|
+
tracker.updateProgress(fileId, loaded);
|
|
184
|
+
emitProgress();
|
|
185
|
+
},
|
|
186
|
+
onRetry: () => {
|
|
187
|
+
tracker.incrementRetry(fileId);
|
|
188
|
+
emitProgress();
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
tracker.setKey(fileId, result.key);
|
|
192
|
+
tracker.setStatus(fileId, "complete");
|
|
193
|
+
emitProgress();
|
|
194
|
+
return result;
|
|
195
|
+
});
|
|
196
|
+
// Wait for all uploads
|
|
197
|
+
await Promise.all([...smallUploadPromises, ...largeUploadPromises]);
|
|
198
|
+
// 5. Confirm small file uploads
|
|
199
|
+
const smallKeys = presignedResults.map((r) => r.key);
|
|
200
|
+
let allResults = [];
|
|
201
|
+
if (smallKeys.length > 0) {
|
|
202
|
+
const smallConfirm = await confirmUploads(apiUrl, endpoint, smallKeys, mergedHeaders);
|
|
203
|
+
allResults.push(...smallConfirm);
|
|
204
|
+
}
|
|
205
|
+
// Large file results are already confirmed server-side during multipart complete
|
|
206
|
+
// Add their results too
|
|
207
|
+
for (const idx of largeFileIndices) {
|
|
208
|
+
const fileId = String(idx);
|
|
209
|
+
const fp = tracker.getSnapshot().fileProgress.find((f) => f.fileId === fileId);
|
|
210
|
+
if (fp?.key) {
|
|
211
|
+
allResults.push({
|
|
212
|
+
file: {
|
|
213
|
+
key: fp.key,
|
|
214
|
+
name: fp.name,
|
|
215
|
+
size: fp.size,
|
|
216
|
+
type: fp.type,
|
|
217
|
+
url: "", // URL will come from confirm
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const uploadedFiles = allResults.map((r) => ({
|
|
223
|
+
key: r.file.key,
|
|
224
|
+
name: r.file.name,
|
|
225
|
+
size: r.file.size,
|
|
226
|
+
type: r.file.type,
|
|
227
|
+
url: r.file.url,
|
|
228
|
+
customMetadata: r.file.customMetadata,
|
|
229
|
+
}));
|
|
230
|
+
setProgress(100);
|
|
231
|
+
opts?.onClientUploadComplete?.(uploadedFiles);
|
|
232
|
+
return uploadedFiles;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const error = err instanceof Error ? err : new Error("Upload failed");
|
|
236
|
+
opts?.onUploadError?.(error);
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
setIsUploading(false);
|
|
241
|
+
abortControllerRef.current = null;
|
|
242
|
+
}
|
|
243
|
+
}, [apiUrl, endpoint, mergedHeaders, opts]);
|
|
244
|
+
return { startUpload, isUploading, progress, routeConfig, fileProgress, abort };
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=use-uploadbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-uploadbox.js","sourceRoot":"","sources":["../src/use-uploadbox.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadbox/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles.css": "./src/styles.css"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react-dropzone": "^14.3.0",
|
|
17
|
+
"@uploadbox/core": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
21
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.7.0",
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"@types/react-dom": "^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"description": "React components and hooks for Uploadbox — UploadButton, UploadDropzone, useUploadbox",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"uploadbox",
|
|
31
|
+
"react",
|
|
32
|
+
"upload",
|
|
33
|
+
"file-upload",
|
|
34
|
+
"upload-button",
|
|
35
|
+
"dropzone"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/federicostarace/uploadbox",
|
|
41
|
+
"directory": "packages/react"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"src"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc",
|
|
52
|
+
"dev": "tsc --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"clean": "rm -rf dist"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FileRouter } from "@uploadbox/core";
|
|
2
|
+
import type { UploadButtonProps, UploadDropzoneProps } from "./types.js";
|
|
3
|
+
import { UploadButton } from "./upload-button.js";
|
|
4
|
+
import { UploadDropzone } from "./upload-dropzone.js";
|
|
5
|
+
|
|
6
|
+
export function generateUploadButton<
|
|
7
|
+
TRouter extends FileRouter
|
|
8
|
+
>() {
|
|
9
|
+
return UploadButton as <TEndpoint extends keyof TRouter & string>(
|
|
10
|
+
props: UploadButtonProps<TRouter, TEndpoint>
|
|
11
|
+
) => React.JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function generateUploadDropzone<
|
|
15
|
+
TRouter extends FileRouter
|
|
16
|
+
>() {
|
|
17
|
+
return UploadDropzone as <TEndpoint extends keyof TRouter & string>(
|
|
18
|
+
props: UploadDropzoneProps<TRouter, TEndpoint>
|
|
19
|
+
) => React.JSX.Element;
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { UploadButton } from "./upload-button.js";
|
|
2
|
+
export { UploadDropzone } from "./upload-dropzone.js";
|
|
3
|
+
export { useUploadbox } from "./use-uploadbox.js";
|
|
4
|
+
export { generateUploadButton, generateUploadDropzone } from "./generate-components.js";
|
|
5
|
+
export { UploadboxProvider, useUploadboxConfig } from "./provider.js";
|
|
6
|
+
export type { UploadboxContextValue } from "./provider.js";
|
|
7
|
+
export { withRetry, DEFAULT_RETRY_CONFIG, isRetryableError } from "./retry.js";
|
|
8
|
+
export { ProgressTracker } from "./progress-tracker.js";
|
|
9
|
+
export { shouldUseMultipart, uploadFileMultipart } from "./multipart.js";
|
|
10
|
+
export type {
|
|
11
|
+
UploadButtonProps,
|
|
12
|
+
UploadDropzoneProps,
|
|
13
|
+
UseUploadboxOpts,
|
|
14
|
+
UseUploadboxReturn,
|
|
15
|
+
UploadedFile,
|
|
16
|
+
UploadProgressEvent,
|
|
17
|
+
EndpointHelper,
|
|
18
|
+
FileUploadStatus,
|
|
19
|
+
FileProgress,
|
|
20
|
+
EnhancedUploadProgressEvent,
|
|
21
|
+
RetryConfig,
|
|
22
|
+
} from "./types.js";
|
package/src/multipart.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { RetryConfig } from "./types.js";
|
|
2
|
+
import { withRetry, DEFAULT_RETRY_CONFIG } from "./retry.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_API_URL = "/api/uploadbox";
|
|
5
|
+
const MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10MB
|
|
6
|
+
const DEFAULT_PART_SIZE = 10 * 1024 * 1024; // 10MB
|
|
7
|
+
const MAX_CONCURRENT_PARTS = 4;
|
|
8
|
+
|
|
9
|
+
export function shouldUseMultipart(fileSize: number): boolean {
|
|
10
|
+
return fileSize >= MULTIPART_THRESHOLD;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface MultipartUploadOptions {
|
|
14
|
+
file: File;
|
|
15
|
+
routeKey: string;
|
|
16
|
+
fileInfo: { name: string; size: number; type: string; customMetadata?: Record<string, string>; ttlSeconds?: number };
|
|
17
|
+
apiUrl?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
retryConfig?: RetryConfig;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
onProgress?: (loaded: number) => void;
|
|
22
|
+
onRetry?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MultipartInitResponse {
|
|
26
|
+
fileKey: string;
|
|
27
|
+
uploadId: string;
|
|
28
|
+
parts: { partNumber: number; url: string }[];
|
|
29
|
+
partSize: number;
|
|
30
|
+
totalParts: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function uploadPartWithXhr(
|
|
34
|
+
url: string,
|
|
35
|
+
blob: Blob,
|
|
36
|
+
onProgress?: (loaded: number) => void,
|
|
37
|
+
signal?: AbortSignal
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const xhr = new XMLHttpRequest();
|
|
41
|
+
xhr.open("PUT", url);
|
|
42
|
+
|
|
43
|
+
if (signal) {
|
|
44
|
+
signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
48
|
+
if (event.lengthComputable) {
|
|
49
|
+
onProgress?.(event.loaded);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
xhr.addEventListener("load", () => {
|
|
54
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
55
|
+
const etag = xhr.getResponseHeader("ETag");
|
|
56
|
+
if (!etag) {
|
|
57
|
+
reject(new Error("Missing ETag in upload response — check S3 CORS exposeHeaders"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
resolve(etag);
|
|
61
|
+
} else {
|
|
62
|
+
reject(new Error(`Part upload failed with status ${xhr.status}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
xhr.addEventListener("error", () => reject(new Error("Part upload failed")));
|
|
67
|
+
xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
|
|
68
|
+
|
|
69
|
+
xhr.send(blob);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function uploadFileMultipart(
|
|
74
|
+
options: MultipartUploadOptions
|
|
75
|
+
): Promise<{ key: string; uploadId: string }> {
|
|
76
|
+
const { file, routeKey, fileInfo, apiUrl = DEFAULT_API_URL, headers, retryConfig, signal, onProgress, onRetry } = options;
|
|
77
|
+
|
|
78
|
+
// 1. Create multipart upload on server
|
|
79
|
+
const initRes = await fetch(apiUrl, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
action: "create-multipart",
|
|
84
|
+
routeKey,
|
|
85
|
+
file: fileInfo,
|
|
86
|
+
}),
|
|
87
|
+
signal,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!initRes.ok) {
|
|
91
|
+
const err = await initRes.json();
|
|
92
|
+
throw new Error(err.message || "Failed to create multipart upload");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const initData: MultipartInitResponse = await initRes.json();
|
|
96
|
+
const { fileKey, uploadId, parts, partSize } = initData;
|
|
97
|
+
|
|
98
|
+
// 2. Upload parts with concurrency
|
|
99
|
+
const completedParts: { partNumber: number; etag: string }[] = [];
|
|
100
|
+
const partLoaded = new Map<number, number>();
|
|
101
|
+
let aborted = false;
|
|
102
|
+
|
|
103
|
+
const uploadPart = async (part: { partNumber: number; url: string }) => {
|
|
104
|
+
if (signal?.aborted) throw new Error("Upload aborted");
|
|
105
|
+
|
|
106
|
+
const start = (part.partNumber - 1) * partSize;
|
|
107
|
+
const end = Math.min(start + partSize, file.size);
|
|
108
|
+
const blob = file.slice(start, end);
|
|
109
|
+
|
|
110
|
+
const retry = retryConfig ?? DEFAULT_RETRY_CONFIG;
|
|
111
|
+
|
|
112
|
+
const etag = await withRetry(
|
|
113
|
+
async () => {
|
|
114
|
+
return uploadPartWithXhr(
|
|
115
|
+
part.url,
|
|
116
|
+
blob,
|
|
117
|
+
(loaded) => {
|
|
118
|
+
partLoaded.set(part.partNumber, loaded);
|
|
119
|
+
const totalLoaded = Array.from(partLoaded.values()).reduce((a, b) => a + b, 0);
|
|
120
|
+
onProgress?.(totalLoaded);
|
|
121
|
+
},
|
|
122
|
+
signal
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
retry,
|
|
126
|
+
() => {
|
|
127
|
+
partLoaded.set(part.partNumber, 0);
|
|
128
|
+
onRetry?.();
|
|
129
|
+
},
|
|
130
|
+
signal
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
completedParts.push({ partNumber: part.partNumber, etag });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Process parts with concurrency limit
|
|
137
|
+
const queue = [...parts];
|
|
138
|
+
const workers: Promise<void>[] = [];
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < Math.min(MAX_CONCURRENT_PARTS, queue.length); i++) {
|
|
141
|
+
workers.push(
|
|
142
|
+
(async () => {
|
|
143
|
+
while (queue.length > 0 && !aborted) {
|
|
144
|
+
const part = queue.shift();
|
|
145
|
+
if (!part) break;
|
|
146
|
+
await uploadPart(part);
|
|
147
|
+
}
|
|
148
|
+
})()
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await Promise.all(workers);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
aborted = true;
|
|
156
|
+
// Abort the multipart upload on failure
|
|
157
|
+
await fetch(apiUrl, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
action: "abort-multipart",
|
|
162
|
+
fileKey,
|
|
163
|
+
uploadId,
|
|
164
|
+
}),
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 3. Complete multipart upload on server
|
|
170
|
+
const completeRes = await fetch(apiUrl, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
action: "complete-multipart",
|
|
175
|
+
routeKey,
|
|
176
|
+
fileKey,
|
|
177
|
+
uploadId,
|
|
178
|
+
parts: completedParts,
|
|
179
|
+
}),
|
|
180
|
+
signal,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!completeRes.ok) {
|
|
184
|
+
const err = await completeRes.json();
|
|
185
|
+
throw new Error(err.message || "Failed to complete multipart upload");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { key: fileKey, uploadId };
|
|
189
|
+
}
|