@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.
@@ -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
- # File-Uploader Server
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
- ## Table of Contents
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
- * [Run](#run-in-production-mode)
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
- ## Run in Production Mode
16
+ #### Running on-demand:
18
17
 
19
- ### Clean
18
+ Using `npx` you can run the script without installing it first:
20
19
 
21
- npm run clean # or bun run clean (optional)
20
+ npx @leogps/file-uploader [path] [options
22
21
 
23
- ### Build (Prod)
22
+ #### Globally via `npm`
24
23
 
25
- npm run build-prod # or bun run build-prod
24
+ npm install --global @leogps/file-uploader
26
25
 
27
- ### Serve (Prod)
26
+ #### As a dependency in your `npm` package:
28
27
 
29
- node dist/ # or bun run ./dist/
28
+ npm install @leogps/file-uploader
30
29
 
31
- ### Configuration Options
30
+ #### Using Docker
32
31
 
33
- --version Show version number [boolean]
34
- -l, --upload_location upload location
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
- ## Run in Development Mode
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
- npm run start # or bun start
46
+ ### Usage
42
47
 
43
- ## Complete list of commands (bun|npm)
48
+ file-uploader [path] [options]
44
49
 
45
- * `bun|npm run clean`
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
- ```bash
48
- rimraf dist
49
- ```
60
+ # Development
50
61
 
51
- * `bun|npm run precompile`
62
+ Checkout this repository locally, then:
52
63
 
53
- ```bash
54
- eslint -c .eslintrc.js --fix --ext .ts src src-client
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
+ ```
@@ -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.66a16cbe5e2ce036e9a7.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="multipleFiles" 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="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>
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>
@@ -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 transferSamples = progress.transferSamples;
21018
- if (!transferSamples || transferSamples.length < 2) {
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
- const first = transferSamples[0];
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='multipleFiles']");
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="multipleFiles"]')[0];
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
- await this.uploadFile(file);
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='multipleFiles']");
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