@leogps/file-uploader 2.0.0 → 2.0.2
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/.github/workflows/publish.yml +31 -0
- package/Dockerfile +18 -0
- package/README.md +45 -93
- package/dist/client/index.html +1 -1
- package/dist/client/{main.66a16cbe5e2ce036e9a7.bundle.js → main.cb411c3ff6063891b944.bundle.js} +67 -12
- package/dist/index.js +1 -1
- package/dist/index.js.LICENSE.txt +23 -0
- package/package.json +13 -3
- package/src/globals.ts +22 -3
- package/src/index.ts +60 -14
- package/src/model/progress.ts +1 -0
- package/src/model/progress_utils.ts +32 -11
- package/src/routes/upload.ts +116 -0
- package/src/routes/uploadComplete.ts +3 -3
- package/src/routes/uploadInit.ts +9 -4
- package/src/service/progress_writer.ts +39 -12
- package/src-client/entrypoint.ts +40 -4
- package/src-client/progress-handler.ts +7 -1
- package/src-client/public/index.html +10 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Use Node.js
|
|
19
|
+
uses: actions/setup-node@v6
|
|
20
|
+
with:
|
|
21
|
+
node-version: '24'
|
|
22
|
+
registry-url: 'https://registry.npmjs.org'
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci --include=dev
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: npm run clean && npm run package
|
|
29
|
+
|
|
30
|
+
- name: Publish
|
|
31
|
+
run: npm publish
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM node:24-alpine as build
|
|
2
|
+
|
|
3
|
+
WORKDIR /srv/file-uploader
|
|
4
|
+
|
|
5
|
+
COPY package.json package-lock.json ./
|
|
6
|
+
RUN npm ci --production
|
|
7
|
+
|
|
8
|
+
COPY . .
|
|
9
|
+
RUN npm run clean && npm run package
|
|
10
|
+
|
|
11
|
+
FROM node:24-alpine
|
|
12
|
+
WORKDIR /srv/file-uploader
|
|
13
|
+
VOLUME /public
|
|
14
|
+
|
|
15
|
+
COPY --from=build /srv/file-uploader/dist ./dist
|
|
16
|
+
|
|
17
|
+
EXPOSE 8080
|
|
18
|
+
ENTRYPOINT ["./dist/index.js", "-p", "8080", "-l", "/public"]
|
package/README.md
CHANGED
|
@@ -1,115 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @leogps/file-uploader
|
|
2
2
|
|
|
3
3
|
Zero-config command-line tool to run a file-uploader server. Files can be uploaded typically from a browser.
|
|
4
4
|
|
|
5
5
|
Both Server and Client are written in JS.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
|
+
- Chunked/Resumable uploads
|
|
9
|
+
- Chunk size is configurable `-s | --chunk-size`
|
|
10
|
+
- Number of Parallel uploads are configurable `-n | --parallel-uploads`
|
|
11
|
+
- Uses SHA1 verification to ensure chunks are valid
|
|
12
|
+
- Optionally disable resumable uploads
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
* [Clean](#clean)
|
|
11
|
-
* [Build](#build-prod)
|
|
12
|
-
* [Serve](#serve-prod)
|
|
13
|
-
* [Configuration Options](#configuration-options)
|
|
14
|
-
* [Run in Development Mode](#run-in-development-mode)
|
|
15
|
-
* [Complete list of commands](#complete-list-of-commands-bunnpm)
|
|
14
|
+
## Installation
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
#### Running on-demand:
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Using `npx` you can run the script without installing it first:
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
npx @leogps/file-uploader [path] [options
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
#### Globally via `npm`
|
|
24
23
|
|
|
25
|
-
npm
|
|
24
|
+
npm install --global @leogps/file-uploader
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
#### As a dependency in your `npm` package:
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
npm install @leogps/file-uploader
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
#### Using Docker
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[string] [default: "/Users/username/Downloads/uploads/"]
|
|
36
|
-
-p, --port server port [number]
|
|
37
|
-
--help Show help [boolean]
|
|
32
|
+
Note: a public image is not provided currently, but you can build one yourself
|
|
33
|
+
with the provided Dockerfile.
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
1. Create an image
|
|
36
|
+
```
|
|
37
|
+
docker build -t my-image .
|
|
38
|
+
```
|
|
39
|
+
2. Run a container
|
|
40
|
+
```
|
|
41
|
+
docker run -p 8080:8080 -v "${pwd}:/public" my-image
|
|
42
|
+
```
|
|
43
|
+
In the example above we're serving the directory `./` (working directory).
|
|
44
|
+
If you wanted to serve `./test` you'd replace `${pwd}` with `${pwd}/test`.
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
### Usage
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
file-uploader [path] [options]
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
Options:
|
|
51
|
+
-l, --upload-location upload location [string] [default: "/Users/<username>/uploads/"]
|
|
52
|
+
-p, --port server port [number] [default: 8082]
|
|
53
|
+
-s, --chunk-size chunk size in bytes [number] [default: 524288]
|
|
54
|
+
-n, --parallel-uploads number of simultaneous parallel chunk uploads (per file) [number] [default: 10]
|
|
55
|
+
-c, --enable-compression enable gzip compression (server to client responses) [boolean] [default: true]
|
|
56
|
+
-m, --max-file-size maximum file size in bytes [number] [default: 107374182400]
|
|
57
|
+
--version Show version number [boolean]
|
|
58
|
+
--help Show help [boolean]
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
rimraf dist
|
|
49
|
-
```
|
|
60
|
+
# Development
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
Checkout this repository locally, then:
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* `bun|npm run compile-server`
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
./node_modules/webpack-cli/bin/cli.js --config webpack.config.js
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
* `bun|npm run compile-client-dev`
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
./node_modules/webpack-cli/bin/cli.js --config webpack-client.dev.js
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
* `bun|npm run compile-client-prod`
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
./node_modules/webpack-cli/bin/cli.js --config webpack-client.prod.js
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
* `bun|npm run compile-dev`
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npm run precompile && npm run compile-server && npm run compile-client-dev
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
* `bun|npm run build-dev`
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
npm run compile-dev
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
* `bun|npm run compile-prod`
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npm run precompile && npm run compile-server && npm run compile-client-prod
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
* `bun|npm run build-prod`
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
npm run compile-prod
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
* `bun|npm run dev:server`
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npm run build:server:once && npm-run-all --parallel nodemon:prod watch:client
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
* `bun|npm run start`
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
npm run build-dev && node dist/index.js
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
* `bun|npm run package`
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
cross-env NODE_ENV=production npm run build-prod && node ./package-gzip.js
|
|
115
|
-
```
|
|
64
|
+
```sh
|
|
65
|
+
$ npm i
|
|
66
|
+
$ npm start
|
|
67
|
+
```
|
package/dist/client/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"><title>File Uploader!</title><link rel="icon" href="favicon.ico"><script defer="defer" src="main.
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"><title>File Uploader!</title><link rel="icon" href="favicon.ico"><script defer="defer" src="main.cb411c3ff6063891b944.bundle.js"></script><link href="main.6db272040eaab1c51019.css" rel="stylesheet"></head><body><div class="content m-2"><div class="fixed-grid container has-4-columns mt-4"><div class="grid"><div class="cell is-col-span-2"><section><div>Hello there from file-uploader server.</div><div><code class="">File Uploader,</code> you know for file uploads.</div></section></div><div class="cell is-col-from-end-1"><section class="mt-4 m-2 is-pulled-right mr-6"><div class="is-position-absolute bulma-is-fixed-top is-clickable" id="themeToggle"><i class="fas fa-moon" id="themeIcon"></i></div></section></div></div></div><div class="container is-one-third"><form id="uploadForm" action="/upload" enctype="multipart/form-data" method="post"><div id="file-div" class="field file has-name is-boxed column is-flex-grow-1"><label class="file-label"><input class="file-input" type="file" name="file" multiple="multiple"> <span class="file-cta"><span class="file-icon"><i class="fas fa-upload"></i> </span><span class="file-label">Choose file(s)…</span></span></label><div id="file-name" class="mt-1 wrap-text is-multiline"></div></div><div class="field"><label class="checkbox"><input id="disableChunkedUpload" type="checkbox"> <span>Disable chunked/resumable upload</span></label></div><div class="control field is-flex-grow-1"><button type="submit" class="button is-link">Submit</button></div></form></div><hr class="is-one-third"/><div class="container"><section class="m-2 is-one-third"><h4>Progress:</h4><div class="all-progress-detail-control control field is-flex-grow-1 is-active p-1 is-hidden"><button type="button" class="button is-link">Collapse All</button></div><div class="container" id="progress-container"></div></section></div></div></body></html>
|
package/dist/client/{main.66a16cbe5e2ce036e9a7.bundle.js → main.cb411c3ff6063891b944.bundle.js}
RENAMED
|
@@ -6250,12 +6250,13 @@ class ProgressHandler {
|
|
|
6250
6250
|
<i class="fas fa-minus-circle m-0 p-0" aria-hidden="true" title="collapse"></i>
|
|
6251
6251
|
</span>
|
|
6252
6252
|
</a>
|
|
6253
|
-
<span class="ml-0 pl-0">
|
|
6253
|
+
<span class="upload-file-name ml-0 pl-0">
|
|
6254
6254
|
${progress.fileName}
|
|
6255
6255
|
</span>
|
|
6256
6256
|
</div>`);
|
|
6257
6257
|
$panel.append($panelHeading);
|
|
6258
6258
|
}
|
|
6259
|
+
$panelHeading.find(".upload-file-name").text(progress.fileName || "");
|
|
6259
6260
|
// Main progress bar (bytes)
|
|
6260
6261
|
let $progressElem = $panel.find(`progress#${progressId}`);
|
|
6261
6262
|
if (!$progressElem.length) {
|
|
@@ -6280,6 +6281,10 @@ class ProgressHandler {
|
|
|
6280
6281
|
}
|
|
6281
6282
|
// Clear previous rows
|
|
6282
6283
|
$table.empty();
|
|
6284
|
+
let progressPercent = 0;
|
|
6285
|
+
if (progress.bytesReceived !== undefined && progress.bytesExpected !== undefined) {
|
|
6286
|
+
progressPercent = (progress.bytesReceived / progress.bytesExpected) * 100;
|
|
6287
|
+
}
|
|
6283
6288
|
// Define table rows
|
|
6284
6289
|
const rows = [
|
|
6285
6290
|
// ["File Name", progress.fileName || "-"],
|
|
@@ -6291,6 +6296,7 @@ class ProgressHandler {
|
|
|
6291
6296
|
<b>|</b> Uploaded: ${uploaded}/${totalChunks}`],
|
|
6292
6297
|
["Speed", `${(0, pretty_bytes_1.default)(progress_utils_1.ProgressUtils.calculateTransferRate(progress))}/s`],
|
|
6293
6298
|
["Status", `${progress.lastState || "-"}`],
|
|
6299
|
+
["Progress", `${progressPercent}%`],
|
|
6294
6300
|
];
|
|
6295
6301
|
if (progress.completed) {
|
|
6296
6302
|
const timeTaken = ((progress.completed - (progress.timestamp || 0)) / 1000).toFixed(2);
|
|
@@ -21014,15 +21020,32 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
21014
21020
|
exports.ProgressUtils = void 0;
|
|
21015
21021
|
class ProgressUtils {
|
|
21016
21022
|
static calculateTransferRate(progress) {
|
|
21017
|
-
const
|
|
21018
|
-
if (!
|
|
21023
|
+
const samples = progress.transferSamples;
|
|
21024
|
+
if (!samples || samples.length < 2) {
|
|
21025
|
+
return 0;
|
|
21026
|
+
}
|
|
21027
|
+
let totalBytes = 0;
|
|
21028
|
+
let totalTimeMs = 0;
|
|
21029
|
+
// Case vs Reason for skipping
|
|
21030
|
+
// bytes === 0 idle / waiting / verification
|
|
21031
|
+
// bytes < 0 possibly corrupted or reset counter
|
|
21032
|
+
// timeMs === 0 divide-by-zero risk
|
|
21033
|
+
// timeMs < 0 invalid timestamp
|
|
21034
|
+
// To prevent idle gaps from dragging the rate down artificially.
|
|
21035
|
+
for (let i = 1; i < samples.length; i++) {
|
|
21036
|
+
const prev = samples[i - 1];
|
|
21037
|
+
const curr = samples[i];
|
|
21038
|
+
const bytes = curr.bytesReceived - prev.bytesReceived;
|
|
21039
|
+
const timeMs = curr.timestamp - prev.timestamp;
|
|
21040
|
+
if (bytes > 0 && timeMs > 0) {
|
|
21041
|
+
totalBytes += bytes;
|
|
21042
|
+
totalTimeMs += timeMs;
|
|
21043
|
+
}
|
|
21044
|
+
}
|
|
21045
|
+
if (totalTimeMs === 0) {
|
|
21019
21046
|
return 0;
|
|
21020
21047
|
}
|
|
21021
|
-
|
|
21022
|
-
const last = transferSamples[transferSamples.length - 1];
|
|
21023
|
-
const dataSize = last.bytesReceived - first.bytesReceived;
|
|
21024
|
-
const timeIntervalSeconds = (last.timestamp - first.timestamp) / 1000;
|
|
21025
|
-
return dataSize / timeIntervalSeconds;
|
|
21048
|
+
return totalBytes / (totalTimeMs / 1000);
|
|
21026
21049
|
}
|
|
21027
21050
|
}
|
|
21028
21051
|
exports.ProgressUtils = ProgressUtils;
|
|
@@ -28002,7 +28025,7 @@ class PageEventRegistrar {
|
|
|
28002
28025
|
registerFileInputEventHandler() {
|
|
28003
28026
|
const $fileDiv = jQuery("#file-div");
|
|
28004
28027
|
const $fileNameDiv = $fileDiv.find("#file-name");
|
|
28005
|
-
const $fileInput = jQuery("form#uploadForm input[name='
|
|
28028
|
+
const $fileInput = jQuery("form#uploadForm input[name='file']");
|
|
28006
28029
|
$fileInput.on("change", () => {
|
|
28007
28030
|
this.onFilesChange($fileNameDiv, $fileInput);
|
|
28008
28031
|
});
|
|
@@ -28029,7 +28052,7 @@ class PageEventRegistrar {
|
|
|
28029
28052
|
event.preventDefault();
|
|
28030
28053
|
// wrap async logic in an IIFE
|
|
28031
28054
|
(async () => {
|
|
28032
|
-
const formElement = $('input[name="
|
|
28055
|
+
const formElement = $('input[name="file"]')[0];
|
|
28033
28056
|
const files = formElement.files;
|
|
28034
28057
|
if (!files || files.length === 0) {
|
|
28035
28058
|
(0, toastify_js_1.default)({
|
|
@@ -28039,6 +28062,7 @@ class PageEventRegistrar {
|
|
|
28039
28062
|
}).showToast();
|
|
28040
28063
|
return;
|
|
28041
28064
|
}
|
|
28065
|
+
const disableChunked = (jQuery("#disableChunkedUpload").prop("checked") === true);
|
|
28042
28066
|
// Block form before uploading
|
|
28043
28067
|
$uploadForm.block({
|
|
28044
28068
|
message: '<h1 class="upload-block-modal p-2 m-0">Uploading...</h1>'
|
|
@@ -28046,7 +28070,12 @@ class PageEventRegistrar {
|
|
|
28046
28070
|
try {
|
|
28047
28071
|
// Upload all files sequentially
|
|
28048
28072
|
for (const file of Array.from(files)) {
|
|
28049
|
-
|
|
28073
|
+
if (disableChunked) {
|
|
28074
|
+
await this.uploadFileNonChunked(file);
|
|
28075
|
+
}
|
|
28076
|
+
else {
|
|
28077
|
+
await this.uploadFile(file);
|
|
28078
|
+
}
|
|
28050
28079
|
}
|
|
28051
28080
|
}
|
|
28052
28081
|
finally {
|
|
@@ -28054,7 +28083,7 @@ class PageEventRegistrar {
|
|
|
28054
28083
|
$uploadForm.trigger("reset");
|
|
28055
28084
|
const $fileDiv = jQuery("#file-div");
|
|
28056
28085
|
const $fileNameDiv = $fileDiv.find("#file-name");
|
|
28057
|
-
const $fileInput = jQuery("form#uploadForm input[name='
|
|
28086
|
+
const $fileInput = jQuery("form#uploadForm input[name='file']");
|
|
28058
28087
|
this.onFilesChange($fileNameDiv, $fileInput);
|
|
28059
28088
|
$uploadForm.unblock();
|
|
28060
28089
|
}
|
|
@@ -28069,6 +28098,32 @@ class PageEventRegistrar {
|
|
|
28069
28098
|
});
|
|
28070
28099
|
});
|
|
28071
28100
|
}
|
|
28101
|
+
async uploadFileNonChunked(file) {
|
|
28102
|
+
const formData = new FormData();
|
|
28103
|
+
// Server-side uses formidable({ multiples: true }) so using the same field name is fine
|
|
28104
|
+
formData.append("file", file, file.name);
|
|
28105
|
+
const resp = await fetch("/upload", {
|
|
28106
|
+
method: "POST",
|
|
28107
|
+
body: formData
|
|
28108
|
+
});
|
|
28109
|
+
let data;
|
|
28110
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
28111
|
+
if (contentType.includes("application/json")) {
|
|
28112
|
+
data = await resp.json();
|
|
28113
|
+
}
|
|
28114
|
+
else {
|
|
28115
|
+
data = await resp.text();
|
|
28116
|
+
}
|
|
28117
|
+
if (!resp.ok) {
|
|
28118
|
+
throw new Error(typeof data === "string" ? data : (data?.msg || "Upload failed"));
|
|
28119
|
+
}
|
|
28120
|
+
(0, toastify_js_1.default)({
|
|
28121
|
+
text: `Upload complete: ${file.name}`,
|
|
28122
|
+
duration: -1,
|
|
28123
|
+
close: true,
|
|
28124
|
+
style: { background: "linear-gradient(to right, #00b09b, #96c93d)" }
|
|
28125
|
+
}).showToast();
|
|
28126
|
+
}
|
|
28072
28127
|
async uploadFile(file) {
|
|
28073
28128
|
try {
|
|
28074
28129
|
// Initialize upload
|