FileShot Uploader (fileshot.io)

JavaScriptPublic
by zx-apiAug 16, 2026, 04:28 AMExpires: Never55 views
FileShot Uploader (fileshot.io)
Raw
1/**
2 * FileShot.io ZKE Uploader
3 * Node.js (native fetch, Blob, FormData)
4 * Credit by zx
5 * Usage: node fileshot-upload.js <file>
6 * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o/299
7 */
8
9const fs     = require('fs');
10const path   = require('path');
11const crypto = require('crypto');
12
13const API       = 'https://api.fileshot.io/api';
14const SITE      = 'https://fileshot.io';
15const ZKE_CHUNK = 524288;            // 512 KB — ZKE encryption chunk
16const UP_CHUNK  = 2 * 1024 * 1024;   // 2 MB  — upload transport chunk
17
18const b64url = b =>
19  Buffer.from(b).toString('base64')
20    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
21
22function chunkIv(base, idx) {
23  const iv = Buffer.from(base);
24  iv.writeUInt32BE((iv.readUInt32BE(8) + idx) >>> 0, 8);
25  return iv;
26}
27
28function zkeEncrypt(buf, name, mime) {
29  const key = crypto.randomBytes(32);
30  const iv  = crypto.randomBytes(12);
31
32  const hdrObj = {
33    v: 1, magic: 'FSZK', chunkSize: ZKE_CHUNK,
34    fileSize: buf.length, name,
35    mime: mime || 'application/octet-stream',
36    iv: b64url(iv), keyMode: 'raw', kdf: null,
37    createdAt: Date.now()
38  };
39  const hj = Buffer.from(JSON.stringify(hdrObj), 'utf-8');
40  const lb = Buffer.alloc(4);
41  lb.writeUInt32BE(hj.length, 0);
42
43  const parts = [
44    Buffer.concat([Buffer.from('FSZK', 'ascii'), Buffer.from([1]), lb, hj])
45  ];
46
47  const n = Math.ceil(buf.length / ZKE_CHUNK);
48  for (let i = 0; i < n; i++) {
49    const s = i * ZKE_CHUNK;
50    const e = Math.min(s + ZKE_CHUNK, buf.length);
51    const c = crypto.createCipheriv(
52      'aes-256-gcm', key, chunkIv(iv, i), { authTagLength: 16 }
53    );
54    parts.push(Buffer.concat([
55      c.update(buf.subarray(s, e)),
56      c.final(),
57      c.getAuthTag()          // 16-byte GCM tag (sama dgn WebCrypto)
58    ]));
59  }
60
61  return { blob: Buffer.concat(parts), key: b64url(key) };
62}
63
64class FileShotUploader {
65  constructor(o = {}) {
66    this.api   = o.api       || API;
67    this.site  = o.site      || SITE;
68    this.token = o.authToken || null;
69  }
70
71  _h(extra = {}) {
72    const h = { ...extra };
73    if (this.token) h.Authorization = `Bearer ${this.token}`;
74    return h;
75  }
76
77  async preUpload(meta) {
78    const r = await fetch(`${this.api}/files/pre-upload`, {
79      method: 'POST',
80      headers: this._h({ 'Content-Type': 'application/json' }),
81      body: JSON.stringify(meta)
82    });
83    if (!r.ok) throw new Error(`Pre-upload ${r.status}: ${await r.text()}`);
84    return r.json();
85  }
86
87  async uploadChunk(fid, data, idx, total) {
88    const fd = new FormData();
89    fd.append('chunk',
90      new Blob([data], { type: 'application/octet-stream' }), 'chunk.bin');
91    fd.append('totalChunks',  String(total));
92    fd.append('isLastChunk',  String(idx === total - 1));
93
94    const r = await fetch(
95      `${this.api}/files/upload-chunk/${fid}/${idx}`,
96      { method: 'POST', headers: this._h(), body: fd }
97    );
98    if (!r.ok) throw new Error(`Chunk ${idx}${r.status}: ${await r.text()}`);
99    return r.json();
100  }
101
102  async finalize(fid) {
103    const r = await fetch(`${this.api}/files/finalize-upload/${fid}`, {
104      method: 'POST',
105      headers: this._h({ 'Content-Type': 'application/json' })
106    });
107    if (!r.ok) throw new Error(`Finalize ${r.status}: ${await r.text()}`);
108    return r.json();
109  }
110
111  async status(fid) {
112    const r = await fetch(`${this.api}/files/upload-status/${fid}`, {
113      headers: this._h()
114    });
115    if (!r.ok) throw new Error(`Status ${r.status}: ${await r.text()}`);
116    return r.json();
117  }
118
119  async hint(fid, sent, total) {
120    await fetch(`${this.api}/files/upload-hint/${fid}`, {
121      method: 'POST',
122      headers: this._h({ 'Content-Type': 'application/json' }),
123      body: JSON.stringify({ bytesSent: sent, totalBytes: total })
124    }).catch(() => {});
125  }
126
127  async upload(fp, opts = {}) {
128    const name = path.basename(fp);
129    const buf  = fs.readFileSync(fp);
130    const mime = opts.mime || 'application/octet-stream';
131
132    console.log(`[*] Encrypting : ${name} (${buf.length} bytes)`);
133    const { blob, key } = zkeEncrypt(buf, name, mime);
134
135    const total = Math.ceil(blob.length / UP_CHUNK);
136    console.log(`[*] Encrypted  : ${blob.length} bytes → ${total} upload chunk(s)`);
137
138    const pre = await this.preUpload({
139      fileName:         name + '.encrypted',
140      fileSize:         blob.length,
141      mimeType:         'application/octet-stream',
142      isZeroKnowledge:  'true',
143      originalFileName: name,
144      originalFileSize: buf.length,
145      originalMimeType: mime,
146      chunkSize:        Math.min(blob.length, UP_CHUNK),
147      totalChunks:      total,
148      expirationHours:  opts.expirationHours ?? 2160
149    });
150    const fid = pre.fileId;
151    console.log(`[+] Pre-upload OK  fileId=${fid}`);
152
153    let sent = 0;
154    for (let i = 0; i < total; i++) {
155      const s = i * UP_CHUNK, e = Math.min(s + UP_CHUNK, blob.length);
156      await this.uploadChunk(fid, blob.subarray(s, e), i, total);
157      sent += e - s;
158      await this.hint(fid, sent, blob.length);
159      console.log(`[>] Chunk ${i + 1}/${total} uploaded (${sent} B)`);
160    }
161
162    await this.finalize(fid);
163
164    let st;
165    do {
166      await new Promise(r => setTimeout(r, 600));
167      st = await this.status(fid);
168    } while (st.uploadStatus === 'pending' || st.uploadStatus === 'uploading');
169
170    const url = `${this.site}/f/${fid}#k=${key}`;
171    console.log(`[+] Done → ${url}`);
172    return { fileId: fid, url, key };
173  }
174}
175
176/**
177const f = process.argv[2];
178if (!f) { console.error('Usage: node fileshot-upload.js <file>'); process.exit(1); }
179
180new FileShotUploader()
181  .upload(f)
182  .then(r => console.log('\nDownload:', r.url))
183  .catch(e => { console.error(`[ERROR] ${e.message}`); process.exit(1); });
184  
185**/
185 lines·5,894 chars·5.8 KB