Migrate the file upload preflight to the new API
[GitHub/WoltLab/WCF.git] / ts / WoltLabSuite / Core / Component / File / Upload.ts
1 import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
2 import { StatusNotOk } from "WoltLabSuite/Core/Ajax/Error";
3 import { isPlainObject } from "WoltLabSuite/Core/Core";
4 import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
5 import { upload as filesUpload } from "WoltLabSuite/Core/Api/Files/Upload";
6 import WoltlabCoreFileElement from "./woltlab-core-file";
7
8 type UploadResponse =
9 | { completed: false }
10 | ({
11 completed: true;
12 } & UploadCompleted);
13
14 export type UploadCompleted = {
15 endpointThumbnails: string;
16 fileID: number;
17 typeName: string;
18 mimeType: string;
19 link: string;
20 data: Record<string, unknown>;
21 };
22
23 export type ThumbnailsGenerated = {
24 data: GenerateThumbnailsResponse;
25 fileID: number;
26 };
27
28 type ThumbnailData = {
29 identifier: string;
30 link: string;
31 };
32
33 type GenerateThumbnailsResponse = ThumbnailData[];
34
35 async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
36 const typeName = element.dataset.typeName!;
37
38 const fileHash = await getSha256Hash(await file.arrayBuffer());
39
40 const fileElement = document.createElement("woltlab-core-file");
41 fileElement.dataset.filename = file.name;
42
43 const event = new CustomEvent<WoltlabCoreFileElement>("uploadStart", { detail: fileElement });
44 element.dispatchEvent(event);
45
46 const response = await filesUpload(file.name, file.size, fileHash, typeName, element.dataset.context || "");
47 if (!response.ok) {
48 const validationError = response.error.getValidationError();
49 if (validationError === undefined) {
50 fileElement.uploadFailed();
51
52 throw response.error;
53 }
54
55 console.log(validationError);
56 return;
57 }
58
59 const { identifier, numberOfChunks } = response.value;
60
61 const chunkSize = Math.ceil(file.size / numberOfChunks);
62
63 // TODO: Can we somehow report any meaningful upload progress?
64
65 for (let i = 0; i < numberOfChunks; i++) {
66 const start = i * chunkSize;
67 const end = start + chunkSize;
68 const chunk = file.slice(start, end);
69
70 // TODO fix the URL
71 throw new Error("TODO: fix the url");
72 const endpoint = new URL(String(i));
73
74 const checksum = await getSha256Hash(await chunk.arrayBuffer());
75 endpoint.searchParams.append("checksum", checksum);
76
77 let response: UploadResponse;
78 try {
79 response = (await prepareRequest(endpoint.toString()).post(chunk).fetchAsJson()) as UploadResponse;
80 } catch (e) {
81 // TODO: Handle errors
82 console.error(e);
83
84 fileElement.uploadFailed();
85 throw e;
86 }
87
88 await chunkUploadCompleted(fileElement, response);
89 }
90 }
91
92 async function chunkUploadCompleted(fileElement: WoltlabCoreFileElement, response: UploadResponse): Promise<void> {
93 if (!response.completed) {
94 return;
95 }
96
97 const hasThumbnails = response.endpointThumbnails !== "";
98 fileElement.uploadCompleted(response.fileID, response.mimeType, response.link, response.data, hasThumbnails);
99
100 if (hasThumbnails) {
101 await generateThumbnails(fileElement, response.endpointThumbnails);
102 }
103 }
104
105 async function generateThumbnails(fileElement: WoltlabCoreFileElement, endpoint: string): Promise<void> {
106 let response: GenerateThumbnailsResponse;
107
108 try {
109 response = (await prepareRequest(endpoint).get().fetchAsJson()) as GenerateThumbnailsResponse;
110 } catch (e) {
111 // TODO: Handle errors
112 console.error(e);
113 throw e;
114 }
115
116 fileElement.setThumbnails(response);
117 }
118
119 async function getSha256Hash(data: BufferSource): Promise<string> {
120 const buffer = await window.crypto.subtle.digest("SHA-256", data);
121
122 return Array.from(new Uint8Array(buffer))
123 .map((b) => b.toString(16).padStart(2, "0"))
124 .join("");
125 }
126
127 export function setup(): void {
128 wheneverFirstSeen("woltlab-core-file-upload", (element) => {
129 element.addEventListener("upload", (event: CustomEvent<File>) => {
130 void upload(element, event.detail);
131 });
132 });
133 }