Skip to content

Commit 25f28fc

Browse files
committed
fix(uploadBigFile): workaround timeouts when closing big file
1 parent 84e5fad commit 25f28fc

File tree

4 files changed

+87
-19
lines changed

4 files changed

+87
-19
lines changed

examples/simple-form/src/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const onRequest = sequence(astroForms({
55
forms: {
66
bigFilesUpload: {
77
bigFileServerOptions: {
8-
maxUploadSize: 1024 * 1024 * 1024, // 1GB
8+
maxUploadSize: 1024 * 1024 * 1024 * 2.5, // 2.5GB
99
maxDirectorySize: 1024 * 1024 * 1024 * 10, // 10GB
1010
}
1111
}

packages/forms/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"watch": "onchange 'src/**/*' -- npm run build",
8-
"build": "rm -r dist/*; tsc; mkdir dist/components; cp -r src/components/* dist/components/",
8+
"build": "rm -r dist/*; tsc; mkdir dist/components; cp -r src/components/* dist/components/; find dist/components/ -name '*.ts' -delete ",
99
"prepack": "npm run build"
1010
},
1111
"keywords": [

packages/forms/src/components/form/UploadBigFile/uploadBigFileClient.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { v4 as uuid } from 'uuid';
22

3+
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
4+
35
type ProgressCallback = (progress: number, total: number) => void;
46

57
export type BigFileUploadOptions = {
@@ -8,6 +10,7 @@ export type BigFileUploadOptions = {
810
chunkSize: number;
911
parallelChunks: number;
1012
parallelUploads: number;
13+
waitFinishDelay?: number;
1114
};
1215

1316
const UPLOAD_BIG_FILE_OPTIONS: BigFileUploadOptions = {
@@ -16,6 +19,7 @@ const UPLOAD_BIG_FILE_OPTIONS: BigFileUploadOptions = {
1619
chunkSize: 1024 * 1024 * 5,
1720
parallelChunks: 3,
1821
parallelUploads: 3,
22+
waitFinishDelay: 1000,
1923
};
2024

2125
const clientWFS = (window as any).clientWFS;
@@ -54,6 +58,52 @@ async function uploadChunkWithXHR(file: Blob, info: Record<string, any>, progres
5458
});
5559
}
5660

61+
async function finishUpload(uploadId: string, options: BigFileUploadOptions) {
62+
let maxError = options.retryChunks;
63+
while (true) {
64+
try {
65+
const response = await new Promise<any>((resolve, reject) => {
66+
const xhr = new XMLHttpRequest();
67+
const formData = new FormData();
68+
xhr.responseType = "text";
69+
70+
formData.append('wait', uploadId);
71+
formData.append("astroBigFileUpload", "true");
72+
73+
if (clientWFS.csrf) {
74+
formData.append(clientWFS.csrf.filed, clientWFS.csrf.token);
75+
}
76+
77+
xhr.onload = () => {
78+
if (xhr.status >= 200 && xhr.status < 300) {
79+
resolve(JSON.parse(xhr.responseText));
80+
} else {
81+
reject({ ok: false, error: xhr.responseText });
82+
}
83+
};
84+
xhr.onerror = () => {
85+
reject({ ok: false, error: xhr.responseText });
86+
};
87+
88+
xhr.open('POST', location.href, true);
89+
xhr.send(formData);
90+
});
91+
92+
if (!response.wait) {
93+
break;
94+
}
95+
96+
await sleep(options.waitFinishDelay);
97+
} catch (error) {
98+
if(maxError === 0){
99+
throw error;
100+
}
101+
maxError--;
102+
await sleep(options.retryChunks);
103+
}
104+
}
105+
}
106+
57107
async function uploadBigFile(fileId: string, file: File, progressCallback: ProgressCallback, options: BigFileUploadOptions) {
58108
const totalSize = file.size;
59109
const totalChunks = Math.ceil(totalSize / options.chunkSize);
@@ -119,6 +169,7 @@ async function uploadBigFile(fileId: string, file: File, progressCallback: Progr
119169
}
120170

121171
await Promise.all(activeChunks);
172+
await finishUpload(fileId, options);
122173
}
123174

124175
export async function uploadAllFiles(els: NodeListOf<HTMLInputElement>, options: BigFileUploadOptions = { ...UPLOAD_BIG_FILE_OPTIONS, ...clientWFS.bigFileUploadOptions }) {
@@ -231,7 +282,7 @@ async function retry(fn: () => Promise<void>, options: { retries: number, delay:
231282
if (attempts >= options.retries) {
232283
throw error;
233284
}
234-
await new Promise(res => setTimeout(res, options.delay));
285+
await sleep(options.delay);
235286
}
236287
}
237288
}

packages/forms/src/components/form/UploadBigFile/uploadBigFileServer.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const DEFAULT_BIG_FILE_UPLOAD_OPTIONS_SERVER: LoadUploadFilesOptions = {
4343
tempDirectory: path.join(os.tmpdir(), "astro_forms_big_files_uploads"),
4444
};
4545

46+
const ACTIVE_FINISHED_UPLOADS = new Set<string>();
47+
4648
async function loadUploadFiles(astro: AstroGlobal, options: Partial<LoadUploadFilesOptions> = {}) {
4749
const { allowUpload, onFinished, maxUploadTime, maxUploadSize, maxDirectorySize, tempDirectory } = { ...DEFAULT_BIG_FILE_UPLOAD_OPTIONS_SERVER, ...options };
4850
if (astro.request.method !== "POST" || !await validateFrom(astro)) {
@@ -53,6 +55,13 @@ async function loadUploadFiles(astro: AstroGlobal, options: Partial<LoadUploadFi
5355
return false;
5456
}
5557

58+
const hasWait = await getFormValue(astro.request, "wait");
59+
if (hasWait) {
60+
const thisWait = String(hasWait);
61+
return Response.json({ ok: true, wait: ACTIVE_FINISHED_UPLOADS.has(thisWait) });
62+
}
63+
64+
5665
await fsExtra.ensureDir(tempDirectory);
5766
await deleteOldUploads(tempDirectory, maxUploadTime);
5867
const uploadInfo = await getFormValue(astro.request, "info");
@@ -127,28 +136,36 @@ async function loadUploadFiles(astro: AstroGlobal, options: Partial<LoadUploadFi
127136
return await sendError(`Missing chunks ${missingChunks}, upload failed`, false, { missingChunks });
128137
}
129138

130-
const outputStream = oldFs.createWriteStream(uploadFilePath, { flags: 'a' });
131-
for (let i = 1; i <= total; i++) {
132-
const fileFullPath = path.join(uploadDir, `${i}-${total}`);
133-
const inputStream = oldFs.createReadStream(fileFullPath);
134-
await new Promise((resolve, reject) => {
135-
inputStream.on("data", (chunk) => {
136-
outputStream.write(chunk);
137-
});
138-
inputStream.on("end", resolve);
139-
inputStream.on("error", reject);
140-
});
141-
await fsExtra.remove(fileFullPath);
142-
}
143-
await fsExtra.remove(uploadDir);
139+
(async () => {
140+
try {
141+
ACTIVE_FINISHED_UPLOADS.add(uploadId);
142+
const outputStream = oldFs.createWriteStream(uploadFilePath, { flags: 'a' });
143+
for (let i = 1; i <= total; i++) {
144+
const fileFullPath = path.join(uploadDir, `${i}-${total}`);
145+
const inputStream = oldFs.createReadStream(fileFullPath);
146+
await new Promise((resolve, reject) => {
147+
inputStream.on("data", (chunk) => {
148+
outputStream.write(chunk);
149+
});
150+
inputStream.on("end", resolve);
151+
inputStream.on("error", reject);
152+
});
153+
await fsExtra.remove(fileFullPath);
154+
}
155+
await fsExtra.remove(uploadDir);
156+
157+
await onFinished?.(uploadId, files.length);
158+
} finally {
159+
ACTIVE_FINISHED_UPLOADS.delete(uploadId);
160+
}
161+
})();
144162

145-
await onFinished?.(uploadId, files.length);
146163
return Response.json({ ok: true, finished: true });
147164
}
148165

149166
export async function processBigFileUpload(astro: AstroGlobal, options: Partial<LoadUploadFilesOptions> = astro.locals.__formsInternalUtils.FORM_OPTIONS.forms?.bigFilesUpload?.bigFileServerOptions) {
150167
const haveFileUpload = await loadUploadFiles(astro, options);
151-
if(haveFileUpload) {
168+
if (haveFileUpload) {
152169
throw new ThrowOverrideResponse(haveFileUpload);
153170
}
154171
}

0 commit comments

Comments
 (0)