@leogps/file-uploader 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.
Files changed (46) hide show
  1. package/.eslintrc.js +178 -0
  2. package/LICENSE +21 -0
  3. package/README.md +115 -0
  4. package/dist/client/1551f4f60c37af51121f.woff2 +0 -0
  5. package/dist/client/2285773e6b4b172f07d9.woff +0 -0
  6. package/dist/client/23f19bb08961f37aaf69.eot +0 -0
  7. package/dist/client/2f517e09eb2ca6650ff5.svg +3717 -0
  8. package/dist/client/4689f52cc96215721344.svg +801 -0
  9. package/dist/client/491974d108fe4002b2aa.ttf +0 -0
  10. package/dist/client/527940b104eb2ea366c8.ttf +0 -0
  11. package/dist/client/77206a6bb316fa0aded5.eot +0 -0
  12. package/dist/client/7a3337626410ca2f4071.woff2 +0 -0
  13. package/dist/client/7a8b4f130182d19a2d7c.svg +5034 -0
  14. package/dist/client/9bbb245e67a133f6e486.eot +0 -0
  15. package/dist/client/bb58e57c48a3e911f15f.woff +0 -0
  16. package/dist/client/be9ee23c0c6390141475.ttf +0 -0
  17. package/dist/client/d878b0a6a1144760244f.woff2 +0 -0
  18. package/dist/client/eeccf4f66002c6f2ba24.woff +0 -0
  19. package/dist/client/favicon.ico +0 -0
  20. package/dist/client/index.html +1 -0
  21. package/dist/client/main.66a16cbe5e2ce036e9a7.bundle.js +39507 -0
  22. package/dist/client/main.6db272040eaab1c51019.css +14 -0
  23. package/dist/index.js +3 -0
  24. package/dist/index.js.LICENSE.txt +273 -0
  25. package/package-gzip.js +30 -0
  26. package/package.json +107 -0
  27. package/src/globals.ts +23 -0
  28. package/src/index.ts +87 -0
  29. package/src/model/progress.ts +175 -0
  30. package/src/model/progress_utils.ts +17 -0
  31. package/src/routes/uploadChunk.ts +125 -0
  32. package/src/routes/uploadComplete.ts +53 -0
  33. package/src/routes/uploadInit.ts +83 -0
  34. package/src/routes/uploadStatus.ts +137 -0
  35. package/src/service/progress_writer.ts +52 -0
  36. package/src-client/entrypoint.ts +273 -0
  37. package/src-client/progress-handler.ts +233 -0
  38. package/src-client/public/favicon.ico +0 -0
  39. package/src-client/public/index.html +67 -0
  40. package/src-client/sha1.ts +19 -0
  41. package/src-client/style.scss +87 -0
  42. package/tsconfig.json +107 -0
  43. package/webpack-client.common.js +29 -0
  44. package/webpack-client.dev.js +51 -0
  45. package/webpack-client.prod.js +65 -0
  46. package/webpack.config.js +41 -0
@@ -0,0 +1,273 @@
1
+ import { ProgressHandler } from "./progress-handler";
2
+ import "jquery-blockui/jquery.blockUI.js";
3
+ import "./style.scss";
4
+ import Toastify from "toastify-js";
5
+ import {computeSHA1} from "./sha1";
6
+
7
+ const MAX_COMPLETE_CHECK_RETRIES = 20;
8
+ const COMPLETE_CHECK_RETRY_DELAY_MS = 1000;
9
+
10
+ jQuery(() => {
11
+ const progressHandler = new ProgressHandler();
12
+ progressHandler.registerHandler();
13
+
14
+ const pageEventRegistrar = new PageEventRegistrar();
15
+ pageEventRegistrar.registerEvents();
16
+ });
17
+
18
+ class PageEventRegistrar {
19
+ public registerEvents(): void {
20
+ this.registerThemeSelectionEventHandler();
21
+ this.registerFileInputEventHandler();
22
+ this.registerFormSubmissionEventHandler();
23
+ }
24
+
25
+ private registerThemeSelectionEventHandler() {
26
+ const $themeToggle = $('#themeToggle');
27
+ const $themeIcon = $('#themeIcon');
28
+ const $htmlEl = $('html');
29
+
30
+ const applyTheme = (theme: string) => {
31
+ $htmlEl.attr('data-theme', theme);
32
+ localStorage.setItem('theme', theme);
33
+ $themeIcon.removeClass('fa-sun fa-moon');
34
+ $themeIcon.addClass(theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon');
35
+ };
36
+
37
+ // Detect saved theme or system preference
38
+ const savedTheme = localStorage.getItem('theme');
39
+ if (savedTheme) {
40
+ applyTheme(savedTheme);
41
+ } else {
42
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
43
+ applyTheme(prefersDark ? 'dark' : 'light');
44
+ }
45
+
46
+ // Toggle on click
47
+ $themeToggle.on('click', () => {
48
+ const currentTheme = $htmlEl.attr('data-theme');
49
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
50
+ applyTheme(newTheme);
51
+ });
52
+ }
53
+
54
+ private registerFileInputEventHandler() {
55
+ const $fileDiv = jQuery("#file-div");
56
+ const $fileNameDiv = $fileDiv.find("#file-name");
57
+ const $fileInput = jQuery("form#uploadForm input[name='multipleFiles']");
58
+ $fileInput.on("change", () => {
59
+ this.onFilesChange($fileNameDiv, $fileInput);
60
+ });
61
+ }
62
+
63
+ private onFilesChange($fileNameDiv: JQuery<HTMLElement>, $fileInput: JQuery<HTMLElement>) {
64
+ $fileNameDiv.html("");
65
+ const files: FileList = $fileInput.prop("files");
66
+ if (files && files.length > 0) {
67
+ const ul = document.createElement("ul");
68
+ for (const file of Array.from(files)) {
69
+ const li = document.createElement("li");
70
+ li.textContent = file.name;
71
+ ul.appendChild(li);
72
+ }
73
+ $fileNameDiv.append(ul);
74
+ } else {
75
+ $fileNameDiv.html("No files selected.");
76
+ }
77
+ }
78
+
79
+ private registerFormSubmissionEventHandler() {
80
+ const $uploadForm = jQuery("form#uploadForm");
81
+ $uploadForm.on("submit", (event) => {
82
+ event.preventDefault();
83
+
84
+ // wrap async logic in an IIFE
85
+ (async () => {
86
+ const formElement: any = $('input[name="multipleFiles"]')[0];
87
+ const files: FileList = formElement.files;
88
+
89
+ if (!files || files.length === 0) {
90
+ Toastify({
91
+ text: "Please select files",
92
+ duration: 3000,
93
+ close: true
94
+ }).showToast();
95
+ return;
96
+ }
97
+
98
+ // Block form before uploading
99
+ $uploadForm.block({
100
+ message: '<h1 class="upload-block-modal p-2 m-0">Uploading...</h1>'
101
+ });
102
+
103
+ try {
104
+ // Upload all files sequentially
105
+ for (const file of Array.from(files)) {
106
+ await this.uploadFile(file);
107
+ }
108
+ } finally {
109
+ // Unblock and reset form after all files finish
110
+ $uploadForm.trigger("reset");
111
+
112
+ const $fileDiv = jQuery("#file-div");
113
+ const $fileNameDiv = $fileDiv.find("#file-name");
114
+ const $fileInput = jQuery("form#uploadForm input[name='multipleFiles']");
115
+ this.onFilesChange($fileNameDiv, $fileInput);
116
+
117
+ $uploadForm.unblock();
118
+ }
119
+ })().catch(err => {
120
+ console.error("Error during upload:", err);
121
+ Toastify({
122
+ text: `Upload error: ${err}`,
123
+ duration: -1,
124
+ close: true,
125
+ style: { background: "linear-gradient(to right, #F39454, #FF6600)" }
126
+ }).showToast();
127
+ });
128
+ });
129
+ }
130
+
131
+ private async uploadFile(file: File): Promise<void> {
132
+ try {
133
+ // Initialize upload
134
+ const initResp = await fetch("/upload/init", {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json" },
137
+ body: JSON.stringify({ fileName: file.name, fileSize: file.size })
138
+ });
139
+ const initData = await initResp.json();
140
+ const fileId: string = initData.fileId;
141
+ const chunkSize: number = initData.chunkSize;
142
+ const maxParallel: number = initData.maxParallel || 3;
143
+ const totalChunks: number = Math.ceil(file.size / chunkSize);
144
+
145
+ // Active upload pool
146
+ const pool: Promise<void>[] = [];
147
+
148
+ // Function to handle one chunk
149
+ const uploadChunkTask = async (chunkIndex: number) => {
150
+ const start = chunkIndex * chunkSize;
151
+ const end = Math.min(start + chunkSize, file.size);
152
+ const chunk = file.slice(start, end);
153
+
154
+ // Compute SHA-1 for just this chunk
155
+ const chunkHash = await computeSHA1(chunk);
156
+
157
+ // Check if chunk already exists on server
158
+ const statusResp = await fetch(
159
+ `/upload/status?fileId=${fileId}&chunkIndex=${chunkIndex}&chunkSize=${chunkSize}&hash=${chunkHash}`
160
+ );
161
+ const statusData = await statusResp.json();
162
+ console.log(`chunk-status for index ${chunkIndex}: hash-matches? ${statusData.hashMatches}`);
163
+
164
+ if (statusData.hashMatches) {
165
+ // already uploaded
166
+ return;
167
+ }
168
+
169
+ // Upload chunk
170
+ const formData = new FormData();
171
+ formData.append("chunk", chunk, file.name);
172
+ await this.uploadChunk(fileId, chunkIndex, chunkHash, formData);
173
+ };
174
+
175
+ // Loop through chunks and dynamically manage pool
176
+ for (let i = 0; i < totalChunks; i++) {
177
+ const taskPromise: Promise<void> = uploadChunkTask(i)
178
+ .finally(() => {
179
+ const index = pool.indexOf(taskPromise);
180
+ if (index > -1) {
181
+ // remove finished task
182
+ pool.splice(index, 1);
183
+ }
184
+ })
185
+ .catch(err => {
186
+ console.error(`Chunk ${i} failed:`, err);
187
+ throw err;
188
+ });
189
+
190
+ pool.push(taskPromise);
191
+
192
+ // If pool is full, wait for at least one to finish
193
+ if (pool.length >= maxParallel) {
194
+ await Promise.race(pool).catch((err) => {
195
+ console.warn(`Pool full, but one task failed, err: ${err}`);
196
+ }); // don't block other tasks
197
+ }
198
+ }
199
+
200
+ // Wait for remaining chunks
201
+ await Promise.all(pool);
202
+
203
+ // Complete upload
204
+ let completeData: any = null;
205
+ let markUploadFailed = false;
206
+ for (let attempt = 1; attempt <= MAX_COMPLETE_CHECK_RETRIES; attempt++) {
207
+ markUploadFailed = attempt === MAX_COMPLETE_CHECK_RETRIES;
208
+ if (markUploadFailed) {
209
+ console.warn(`Marking upload as failed after ${attempt} retries...`);
210
+ }
211
+ const completeResp = await fetch(`/upload/complete?fileId=${fileId}&markUploadFailed=${markUploadFailed}`,
212
+ {
213
+ method: "POST"
214
+ });
215
+ completeData = await completeResp.json();
216
+
217
+ if (completeResp.ok && completeData.msg !== 'File incomplete') {
218
+ // Upload is confirmed complete
219
+ break;
220
+ }
221
+
222
+ console.log(`Attempt ${attempt}: file still incomplete, retrying in ${COMPLETE_CHECK_RETRY_DELAY_MS}ms...`);
223
+ await new Promise(res => setTimeout(res, COMPLETE_CHECK_RETRY_DELAY_MS));
224
+ }
225
+
226
+ if (!completeData || completeData.msg === 'File incomplete') {
227
+ Toastify({
228
+ text: `Upload failed: file incomplete after ${MAX_COMPLETE_CHECK_RETRIES} retries`,
229
+ duration: -1,
230
+ close: true,
231
+ style: { background: "linear-gradient(to right, #F39454, #FF6600)" }
232
+ }).showToast();
233
+ } else {
234
+ Toastify({
235
+ text: `Upload complete: ${file.name}, saved to: ${completeData.savedLocation}`,
236
+ duration: -1,
237
+ close: true,
238
+ style: { background: "linear-gradient(to right, #00b09b, #96c93d)" }
239
+ }).showToast();
240
+ }
241
+
242
+ } catch (err: any) {
243
+ console.error(`Error uploading file ${file.name}:`, err);
244
+ Toastify({
245
+ text: `Error uploading file ${file.name}, ${err}`,
246
+ duration: -1,
247
+ close: true,
248
+ style: { background: "linear-gradient(to right, #F39454, #FF6600)" }
249
+ }).showToast();
250
+ }
251
+ }
252
+
253
+ private async uploadChunk(fileId: string, chunkIndex: number, hash: string, formData: FormData) {
254
+ let uploaded = false;
255
+ while (!uploaded) {
256
+ try {
257
+ const chunkResp = await fetch(`/upload/chunk?fileId=${fileId}&chunkIndex=${chunkIndex}&hash=${hash}`, {
258
+ method: "POST",
259
+ body: formData
260
+ });
261
+ if (chunkResp.ok) {
262
+ uploaded = true;
263
+ } else {
264
+ console.warn(`Retrying chunk ${chunkIndex}...`);
265
+ await new Promise(res => setTimeout(res, 500));
266
+ }
267
+ } catch (err) {
268
+ console.error(`Chunk ${chunkIndex} upload error:`, err);
269
+ await new Promise(res => setTimeout(res, 500));
270
+ }
271
+ }
272
+ }
273
+ }
@@ -0,0 +1,233 @@
1
+ import { io } from "socket.io-client";
2
+ import {FileTransferProgress} from "../src/model/progress";
3
+ import { ProgressUtils } from "../src/model/progress_utils";
4
+ import moment from "moment";
5
+ import prettyBytes from "pretty-bytes";
6
+
7
+ const _progressDivCache: Map<string, JQuery<HTMLElement>> = new Map();
8
+ const stateColorMap: Record<"COMPLETE" | "FAILED", string> = {
9
+ COMPLETE: "is-success",
10
+ FAILED: "is-danger",
11
+ };
12
+
13
+ export class ProgressHandler {
14
+
15
+ private addToDomCache(progressId: string, $panel: JQuery):void {
16
+ _progressDivCache.set(progressId, $panel);
17
+ this.updateCollapseAllVisibility();
18
+ }
19
+
20
+ private getFromDomCache(progressId: string): JQuery | undefined {
21
+ return _progressDivCache.get(progressId);
22
+ }
23
+
24
+ private domCacheSize(): number {
25
+ return _progressDivCache.size;
26
+ }
27
+
28
+ public registerHandler(): void {
29
+ console.log("Registering Progress Handler...");
30
+ const socket = io();
31
+ socket.emit("message", "Connected.");
32
+
33
+ socket.on("progresses", (progresses: FileTransferProgress[]) => {
34
+ this.handleProgresses(progresses);
35
+ });
36
+
37
+ this.registerAllDetailToggleEventHandler();
38
+ console.log("Progress handler registration complete.");
39
+ }
40
+
41
+ private updateCollapseAllVisibility(): void {
42
+ const $control = $(".all-progress-detail-control");
43
+ if (this.domCacheSize() > 0) {
44
+ $control.removeClass("is-hidden");
45
+ } else {
46
+ $control.addClass("is-hidden");
47
+ }
48
+ const $parentElem = jQuery(".single-progress-container");
49
+ const $btn = jQuery(".all-progress-detail-control button");
50
+ const $progressDetailElems = $parentElem
51
+ .find("[id^='progressTableContainer-']");
52
+ const $hiddenProgressDetailElems = $parentElem
53
+ .find("[id^='progressTableContainer-'].is-hidden");
54
+ const allHidden = $hiddenProgressDetailElems.length === $progressDetailElems.length;
55
+ const buttonLabel = allHidden ? "Expand All" : "Collapse All";
56
+ $btn.text(buttonLabel);
57
+ }
58
+
59
+ private registerAllDetailToggleEventHandler(): void {
60
+ const $btn = jQuery(".all-progress-detail-control button");
61
+
62
+ $btn.on("click", () => {
63
+ const collapseAll = $btn.text().trim() === "Collapse All";
64
+ const newLabel = collapseAll ? "Expand All" : "Collapse All";
65
+ $btn.text(newLabel);
66
+
67
+ // Loop through every progress panel
68
+ jQuery(".single-progress-container").each((_, panel) => {
69
+ const $panel = jQuery(panel);
70
+ const $toggle = $panel.find(".progress-detail-control");
71
+ const $icon = $toggle.find("i");
72
+
73
+ // table container selector
74
+ const panelId = $panel.attr("id")!;
75
+ const tableContainerId = `progressTableContainer-${panelId}`;
76
+ const $tableContainer = jQuery(`#${tableContainerId}`);
77
+
78
+ if (collapseAll) {
79
+ // collapse everything
80
+ $toggle.removeClass("is-active");
81
+ $tableContainer.addClass("is-hidden");
82
+ $icon.removeClass("fa-minus-circle").addClass("fa-plus-circle")
83
+ .attr("title", "expand");
84
+ } else {
85
+ // expand everything
86
+ $toggle.addClass("is-active");
87
+ $tableContainer.removeClass("is-hidden");
88
+ $icon.removeClass("fa-plus-circle").addClass("fa-minus-circle")
89
+ .attr("title", "collapse");
90
+ }
91
+ });
92
+ });
93
+ }
94
+
95
+ private registerDetailToggleEventHandler($panel: JQuery, tableContainerId: string) {
96
+ $panel.on("click", ".progress-detail-control", (event) => {
97
+ const $btn = jQuery(event.currentTarget);
98
+
99
+ const $tableContainer = jQuery(`#${tableContainerId}`);
100
+
101
+ // toggle active class
102
+ const isActive: boolean = $btn.toggleClass("is-active").hasClass("is-active");
103
+
104
+ // toggle content visibility
105
+ if (isActive) {
106
+ $tableContainer.removeClass("is-hidden");
107
+ } else {
108
+ $tableContainer.addClass("is-hidden");
109
+ }
110
+
111
+ // toggle icon
112
+ const $icon = $btn.find("i");
113
+
114
+ if (isActive) {
115
+ // expanded → show minus
116
+ $icon.removeClass("fa-plus-circle").addClass("fa-minus-circle");
117
+ $icon.attr("title", "collapse");
118
+ } else {
119
+ // collapsed → show plus
120
+ $icon.removeClass("fa-minus-circle").addClass("fa-plus-circle");
121
+ $icon.attr("title", "expand");
122
+ }
123
+
124
+ // no need for _this alias
125
+ this.updateCollapseAllVisibility();
126
+ });
127
+ }
128
+
129
+ private handleProgresses(progresses: FileTransferProgress[]): void {
130
+ const $progressContainer = jQuery("div#progress-container");
131
+
132
+ progresses.forEach(progress => {
133
+ const progressId = `progress-${progress.uuid}`;
134
+ const uploaded = (progress.uploadedChunks as any)?.length ?? 0;
135
+ const uploading = (progress.uploadingChunks as any)?.length ?? 0;
136
+ const totalChunks = progress.totalChunks ?? 1;
137
+
138
+ const colorKey = progress.lastState as keyof typeof stateColorMap;
139
+ const stateColor = stateColorMap[colorKey] || "";
140
+
141
+ // Container box
142
+ let $panel = this.getFromDomCache(progressId);
143
+ const tableContainerId = `progressTableContainer-${progressId}`;
144
+ if (!$panel) {
145
+ $panel = jQuery(`<article id="${progressId}" class="progress-panel panel is-bordered single-progress-container mb-4"></article>`);
146
+ $progressContainer.prepend($panel);
147
+ this.addToDomCache(progressId, $panel);
148
+ this.registerDetailToggleEventHandler($panel, tableContainerId);
149
+ }
150
+ // Remove existing progress colors
151
+ $panel.removeClass("is-success is-danger is-info is-warning");
152
+ $panel.addClass(stateColor || "is-info");
153
+
154
+ // Add file name as a heading above the table
155
+ const panelHeadingId = `panelHeading-${progressId}`;
156
+ let $panelHeading = $panel.find(`#${panelHeadingId}`);
157
+ if (!$panelHeading.length) {
158
+ $panelHeading = jQuery(`<div id="${panelHeadingId}" class="panel-heading wrap-text is-flex-wrap-wrap p-2 has-text-weight-normal is-size-6 mb-1">
159
+ <a class="progress-detail-control is-active ml-1 mr-1">
160
+ <span class="panel-icon m-0 p-0">
161
+ <i class="fas fa-minus-circle m-0 p-0" aria-hidden="true" title="collapse"></i>
162
+ </span>
163
+ </a>
164
+ <span class="ml-0 pl-0">
165
+ ${progress.fileName}
166
+ </span>
167
+ </div>`);
168
+ $panel.append($panelHeading);
169
+ }
170
+
171
+ // Main progress bar (bytes)
172
+ let $progressElem = $panel.find(`progress#${progressId}`);
173
+ if (!$progressElem.length) {
174
+ $progressElem = jQuery(`<progress id="${progressId}" class="progress is-info is-small">`);
175
+ $panel.append($progressElem);
176
+ }
177
+ $progressElem.attr("max", (progress.bytesExpected || 100));
178
+ $progressElem.attr("value", (progress.bytesReceived || 0));
179
+ // Remove existing progress colors
180
+ $progressElem.removeClass("is-success is-danger is-info is-warning");
181
+ $progressElem.addClass(stateColor || "is-info");
182
+
183
+
184
+ let $tableContainer = $panel.find(`#${tableContainerId}`);
185
+ if (!$tableContainer.length) {
186
+ $tableContainer = jQuery(`<div id="${tableContainerId}" class="panel-block table-container m-0 p-0"></div>`);
187
+ $panel.append($tableContainer);
188
+ }
189
+
190
+ const tableId = `progressTable-${progressId}`;
191
+ let $table = $tableContainer.find(`#${tableId}`);
192
+ if (!$table.length) {
193
+ $table = jQuery(`<table id="${tableId}" class="table is-fullwidth is-bordered is-striped"></table>`);
194
+ $tableContainer.append($table);
195
+ }
196
+
197
+ // Clear previous rows
198
+ $table.empty();
199
+
200
+ // Define table rows
201
+ const rows: [string, string][] = [
202
+ // ["File Name", progress.fileName || "-"],
203
+ // ["Location", progress.savedLocation || "-"],
204
+ ["Started", moment(progress.timestamp).format("MMMM Do YYYY, h:mm:ss a")],
205
+ ["Transferred", `${prettyBytes(progress.bytesReceived || 0)} / ${prettyBytes(progress.bytesExpected || 0)}`],
206
+ ["Chunks", `Verified ${progress.chunkVerificationCount || 0}
207
+ <b>|</b> Uploading: ${uploading}
208
+ <b>|</b> Uploaded: ${uploaded}/${totalChunks}`],
209
+ ["Speed", `${prettyBytes(ProgressUtils.calculateTransferRate(progress))}/s`],
210
+ ["Status", `${progress.lastState || "-"}`],
211
+ ];
212
+
213
+ if (progress.completed) {
214
+ const timeTaken = ((progress.completed - (progress.timestamp || 0)) / 1000).toFixed(2);
215
+ rows.push(["Completed in", `${timeTaken} sec`]);
216
+ }
217
+
218
+ // Append rows
219
+ for (const [key, value] of rows) {
220
+ $table.append(`<tr><th class="is-narrow">${key}</th><td>${value}</td></tr>`);
221
+ }
222
+
223
+ const horizontalRulerId = `horizontalRuler-${progressId}`;
224
+ let $horizontalRuler = $panel.find(`#${horizontalRulerId}`);
225
+ if (!$horizontalRuler.length) {
226
+ $horizontalRuler = jQuery(`<hr id="${horizontalRulerId}" class="is-one-third mt-3 mb-1">`);
227
+ $panel.append($horizontalRuler);
228
+ }
229
+
230
+ this.updateCollapseAllVisibility();
231
+ });
232
+ }
233
+ }
Binary file
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>File Uploader!</title>
7
+
8
+ </head>
9
+ <body>
10
+ <div class="content m-2">
11
+ <div class="fixed-grid container has-4-columns mt-4">
12
+ <div class="grid">
13
+ <div class="cell is-col-span-2">
14
+ <section>
15
+ <div>
16
+ Hello there from file-uploader server.
17
+ </div>
18
+ <div>
19
+ <code class="">File Uploader,</code> you know for file uploads.
20
+ </div>
21
+ </section>
22
+ </div>
23
+ <div class="cell is-col-from-end-1">
24
+ <section class="mt-4 m-2 is-pulled-right mr-6">
25
+ <div class="is-position-absolute bulma-is-fixed-top is-clickable" id="themeToggle">
26
+ <i class="fas fa-moon" id="themeIcon"></i>
27
+ </div>
28
+ </section>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="container is-one-third">
33
+ <form id="uploadForm" action="/upload" enctype="multipart/form-data" method="post">
34
+ <div id="file-div" class="field file has-name is-boxed column is-flex-grow-1">
35
+ <label class="file-label">
36
+ <input class="file-input" type="file" name="multipleFiles" multiple="multiple">
37
+ <span class="file-cta">
38
+ <span class="file-icon">
39
+ <i class="fas fa-upload"></i>
40
+ </span>
41
+ <span class="file-label">
42
+ Choose file(s)…
43
+ </span>
44
+ </span>
45
+ </label>
46
+ <div id="file-name" class="mt-1 wrap-text is-multiline"></div>
47
+ </div>
48
+
49
+ <div class="control field is-flex-grow-1">
50
+ <button type="submit" class="button is-link">Submit</button>
51
+ </div>
52
+ </form>
53
+ </div>
54
+
55
+ <hr class="is-one-third"/>
56
+ <div class="container">
57
+ <section class="m-2 is-one-third">
58
+ <h4>Progress:</h4>
59
+ <div class="all-progress-detail-control control field is-flex-grow-1 is-active p-1 is-hidden">
60
+ <button type="button" class="button is-link">Collapse All</button>
61
+ </div>
62
+ <div class="container" id="progress-container"></div>
63
+ </section>
64
+ </div>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,19 @@
1
+ import { sha1 } from "js-sha1";
2
+
3
+ export const computeSHA1 = async (blob: Blob): Promise<string> => {
4
+ if (window.crypto && crypto.subtle && crypto.subtle.digest) {
5
+ try {
6
+ const buffer = await blob.arrayBuffer();
7
+ const hashBuffer = await crypto.subtle.digest("SHA-1", buffer);
8
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
9
+ return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
10
+ } catch (err) {
11
+ console.warn("WebCrypto SHA-1 failed, falling back to js-sha1:", err);
12
+ }
13
+ }
14
+
15
+ // Fallback to js-sha1
16
+ const arrayBuffer = await blob.arrayBuffer();
17
+ const bytes = new Uint8Array(arrayBuffer);
18
+ return sha1(bytes);
19
+ };
@@ -0,0 +1,87 @@
1
+ @import "bulma/css/bulma.min.css";
2
+ @import "bulma/sass/themes/light";
3
+ @import "bulma/sass/themes/dark";
4
+ @import "toastify-js/src/toastify.css";
5
+ @import "@fortawesome/fontawesome-free/css/all.min.css";
6
+
7
+ :root {
8
+ --file-item-hover-bg: rgba(0, 0, 0, 0.05); // light theme default
9
+ --file-item-border-color: #9e9e9e;
10
+ --file-item-icon-color: #9e9e9e;
11
+
12
+ --upload-block-modal-bg: #ffffff;
13
+ }
14
+
15
+ [data-theme="dark"] {
16
+ --file-item-hover-bg: rgba(255, 255, 255, 0.1); // dark theme
17
+ --file-item-border-color: #6e6e6e;
18
+ --file-item-icon-color: #b0b0b0;
19
+
20
+ --upload-block-modal-bg: #1e1e1e;
21
+ }
22
+
23
+ h1.upload-block-modal {
24
+ background-color: var(--upload-block-modal-bg);
25
+ }
26
+
27
+ #file-name.wrap-text {
28
+ white-space: normal;
29
+ word-break: break-word;
30
+ overflow-wrap: anywhere;
31
+ }
32
+
33
+ #file-name ul {
34
+ list-style-type: none;
35
+ padding: 0;
36
+ margin: 0 1rem;
37
+ }
38
+
39
+ #file-name li {
40
+ position: relative;
41
+ padding-left: 1.5em;
42
+ margin-bottom: 0.25em;
43
+ border-bottom: 1px solid var(--file-item-border-color);
44
+ transition: background-color 0.2s ease;
45
+ }
46
+
47
+ #file-name li:first-child {
48
+ border-top: 1px solid var(--file-item-border-color);
49
+ }
50
+
51
+ #file-name li:hover {
52
+ background-color: var(--file-item-hover-bg);
53
+ cursor: pointer;
54
+ }
55
+
56
+ #file-name li::before {
57
+ content: "\f15b";
58
+ font-family: "Font Awesome 5 Free";
59
+ font-weight: 900;
60
+ position: absolute;
61
+ left: 0;
62
+ color: var(--file-item-icon-color);
63
+ }
64
+
65
+ .progress-panel {
66
+ box-shadow: none;
67
+ }
68
+
69
+ .panel-heading.wrap-text {
70
+ border-radius: 5px 5px 0 0;
71
+ white-space: normal;
72
+ word-break: break-word;
73
+ overflow-wrap: anywhere;
74
+ }
75
+
76
+ .progress.is-small {
77
+ height: 0.25rem;
78
+ margin-bottom: 0;
79
+ }
80
+
81
+ .panel-heading a.progress-detail-control .panel-icon {
82
+ vertical-align: middle;
83
+ }
84
+
85
+ .panel-heading a.progress-detail-control .fas {
86
+ color: blue;
87
+ }