filesharing-onl 0.0.0-stage → 1.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.
- package/README.md +11 -2
- package/bin/filesharing.js +111 -0
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Filesharing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Upload files to ShareIt.onl and receive a shareable link.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install --global filesharing-onl
|
|
7
|
+
filesharing report.pdf
|
|
8
|
+
filesharing photo.jpg notes.txt --expiry 7d
|
|
9
|
+
filesharing archive.zip --password
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The hidden password prompt keeps share passwords out of shell history. Anonymous limits and retention rules are the same as on <https://shareit.onl>.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const base = (process.env.FILESHARING_URL || 'https://shareit.onl').replace(/\/$/, '');
|
|
9
|
+
const client = 'shareit-browser-extension-v1';
|
|
10
|
+
|
|
11
|
+
function help() {
|
|
12
|
+
return `Filesharing ${pkg.version}
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
filesharing <file...> [--expiry 1h|1d|7d|30d] [--password] [--one-time]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--password Prompt for a share password without echo
|
|
19
|
+
--one-time Expire after the first successful download
|
|
20
|
+
--expiry Retention selection
|
|
21
|
+
--json Print JSON`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parse(argv) {
|
|
25
|
+
const args = [...argv];
|
|
26
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) return { help: true };
|
|
27
|
+
if (args.includes('--version') || args.includes('-v')) return { version: true };
|
|
28
|
+
const takeBoolean = (name) => { const found = args.includes(name); if (found) args.splice(args.indexOf(name), 1); return found; };
|
|
29
|
+
const take = (name, fallback) => { const index = args.indexOf(name); if (index < 0) return fallback; const value = args[index + 1]; if (!value) throw new Error(`${name} requires a value.`); args.splice(index, 2); return value; };
|
|
30
|
+
return { files: args, password: takeBoolean('--password'), oneTime: takeBoolean('--one-time'), json: takeBoolean('--json'), expiry: take('--expiry', '7d') };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function secret(label) {
|
|
34
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) throw new Error('Password prompting requires a terminal.');
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let value = '';
|
|
37
|
+
process.stdout.write(label); process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8');
|
|
38
|
+
const done = (error) => { process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.off('data', input); process.stdout.write('\n'); error ? reject(error) : resolve(value); };
|
|
39
|
+
const input = (key) => { if (key === '\u0003') return done(new Error('Cancelled.')); if (key === '\r' || key === '\n') return done(); if (key === '\u007f' || key === '\b') value = value.slice(0, -1); else if (key >= ' ') value += key; };
|
|
40
|
+
process.stdin.on('data', input);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class Session {
|
|
45
|
+
constructor() { this.cookie = ''; this.csrf = ''; }
|
|
46
|
+
capture(response) {
|
|
47
|
+
const values = response.headers.getSetCookie?.() || [response.headers.get('set-cookie')].filter(Boolean);
|
|
48
|
+
if (values.length) this.cookie = values.map((value) => value.split(';', 1)[0]).join('; ');
|
|
49
|
+
}
|
|
50
|
+
headers(extra = {}) { return { Cookie: this.cookie, 'X-CSRFToken': this.csrf, 'X-ShareIt-Client': client, 'User-Agent': `filesharing/${pkg.version}`, ...extra }; }
|
|
51
|
+
async start() {
|
|
52
|
+
const response = await fetch(`${base}/api/extension/session`, { headers: { 'User-Agent': `filesharing/${pkg.version}` } });
|
|
53
|
+
this.capture(response); const data = await response.json().catch(() => ({}));
|
|
54
|
+
if (!response.ok || !data.csrf) throw new Error(data.error || `ShareIt.onl session failed with HTTP ${response.status}.`);
|
|
55
|
+
this.csrf = data.csrf; this.limits = data;
|
|
56
|
+
}
|
|
57
|
+
async json(endpoint, method, body) {
|
|
58
|
+
const response = await fetch(`${base}${endpoint}`, { method, headers: this.headers({ 'Content-Type': 'application/json' }), body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(120_000) });
|
|
59
|
+
const data = await response.json().catch(() => ({}));
|
|
60
|
+
if (!response.ok) throw new Error(data.error || `ShareIt.onl returned HTTP ${response.status}.`);
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
const options = parse(process.argv.slice(2));
|
|
67
|
+
if (options.help) return process.stdout.write(`${help()}\n`);
|
|
68
|
+
if (options.version) return process.stdout.write(`${pkg.version}\n`);
|
|
69
|
+
const files = options.files.map((item) => {
|
|
70
|
+
const filename = path.resolve(item);
|
|
71
|
+
if (!fs.existsSync(filename) || !fs.statSync(filename).isFile()) throw new Error(`File not found: ${item}`);
|
|
72
|
+
return { path: filename, name: path.basename(filename), size: fs.statSync(filename).size };
|
|
73
|
+
});
|
|
74
|
+
if (!files.length) throw new Error('Add at least one file.');
|
|
75
|
+
const password = options.password ? await secret('Share password: ') : '';
|
|
76
|
+
if (password && password.length < 8) throw new Error('Share passwords must contain at least 8 characters.');
|
|
77
|
+
const session = new Session(); await session.start();
|
|
78
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
79
|
+
if (files.length > session.limits.max_files) throw new Error(`This session accepts at most ${session.limits.max_files} files.`);
|
|
80
|
+
if (total > session.limits.max_bytes) throw new Error(`The selected files exceed the ${session.limits.max_label} limit.`);
|
|
81
|
+
const upload = await session.json('/api/upload/init', 'POST', { files: files.map(({ name, size }) => ({ name, size })), expiry: options.expiry, password, one_time: options.oneTime });
|
|
82
|
+
const completion = [];
|
|
83
|
+
for (const remote of upload.files) {
|
|
84
|
+
const local = files[remote.idx];
|
|
85
|
+
const handle = await fs.promises.open(local.path, 'r');
|
|
86
|
+
const parts = [];
|
|
87
|
+
try {
|
|
88
|
+
for (let number = 1; number <= remote.parts; number += 1) {
|
|
89
|
+
const start = (number - 1) * upload.part_size;
|
|
90
|
+
const length = Math.min(upload.part_size, local.size - start);
|
|
91
|
+
const buffer = Buffer.alloc(length);
|
|
92
|
+
await handle.read(buffer, 0, length, start);
|
|
93
|
+
process.stderr.write(`Uploading ${local.name}: part ${number}/${remote.parts}\r`);
|
|
94
|
+
const response = await fetch(`${base}/api/upload/part/${upload.token}/${remote.idx}/${number}`, {
|
|
95
|
+
method: 'POST', headers: session.headers({ 'Content-Type': 'application/octet-stream', 'Content-Length': String(length) }), body: buffer, signal: AbortSignal.timeout(180_000),
|
|
96
|
+
});
|
|
97
|
+
const data = await response.json().catch(() => ({}));
|
|
98
|
+
if (!response.ok || !data.etag) throw new Error(data.error || `Part ${number} failed with HTTP ${response.status}.`);
|
|
99
|
+
parts.push({ n: number, etag: data.etag });
|
|
100
|
+
}
|
|
101
|
+
} finally { await handle.close(); }
|
|
102
|
+
completion.push({ idx: remote.idx, parts });
|
|
103
|
+
}
|
|
104
|
+
if (process.stderr.isTTY) process.stderr.write('\n');
|
|
105
|
+
const result = await session.json(`/api/upload/complete/${upload.token}`, 'POST', { files: completion, expiry: options.expiry, password, one_time: options.oneTime });
|
|
106
|
+
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `${result.url}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (require.main === module) main().catch((error) => { process.stderr.write(`filesharing: ${error.message}\n`); process.exitCode = 1; });
|
|
110
|
+
|
|
111
|
+
module.exports = { parse };
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filesharing-onl",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Upload files and create shareable Filesharing links from your terminal.",
|
|
5
|
+
"bin": { "filesharing": "bin/filesharing.js" },
|
|
6
|
+
"files": ["bin", "README.md"],
|
|
7
|
+
"scripts": { "test": "node --check bin/filesharing.js && node bin/filesharing.js --version", "prepublishOnly": "npm test" },
|
|
8
|
+
"engines": { "node": ">=20" },
|
|
9
|
+
"keywords": ["file-sharing", "upload", "share-files", "transfer", "cli", "shareit"],
|
|
10
|
+
"homepage": "https://shareit.onl",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"author": "ShareIt.onl",
|
|
13
|
+
"publishConfig": { "access": "public" }
|
|
14
|
+
}
|