mrpoddaa commited on
Commit
bd3de29
Β·
verified Β·
1 Parent(s): a3dc52e

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +128 -81
index.js CHANGED
@@ -248,6 +248,23 @@ app.post('/upload', checkRAM, (req, res) => {
248
  let fileName = 'unknown';
249
  let mimeType = 'application/octet-stream';
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  // ── Per-TG-document state ─────────────────────────────────────────────
252
  const finalDocs = [];
253
  let docIdx = 0;
@@ -255,12 +272,14 @@ app.post('/upload', checkRAM, (req, res) => {
255
  let tgPartIdx = 0; // part index within current TG doc
256
  let tgFileId = makeTgFileId(0);
257
  let totalBytes = 0;
 
258
 
259
- // Accumulation ring β€” drained only in exact PART_SIZE slices
260
- const partChunks = []; // Buffer[] pending
261
- let partLen = 0; // total bytes in partChunks
 
262
 
263
- // In-flight SaveBigFilePart promises
264
  const inFlight = [];
265
 
266
  async function flushInFlight() {
@@ -268,26 +287,69 @@ app.post('/upload', checkRAM, (req, res) => {
268
  await Promise.all(inFlight.splice(0));
269
  }
270
 
271
- // Send one part (exactly PART_SIZE, or final smaller part).
272
- // fileTotalParts is a running UPPER BOUND β€” Telegram only requires that
273
- // (a) filePart < fileTotalParts on every call, and
274
- // (b) InputFileBig.parts == the ACTUAL count at SendMedia time.
275
- // We use declaredSize when available for a tight estimate; otherwise we
276
- // fall back to PIECE_BYTES (the max a single TG doc can hold).
277
- // The value is recalculated each call so it stays valid as tgPartIdx grows.
278
- async function sendPart(buf, fileStream) {
279
- const partNo = tgPartIdx++;
280
-
281
- // Bytes remaining in this TG doc (conservative: use declared size if
282
- // it fits in one doc, else use the full doc ceiling).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  const docCeiling = (declaredSize > 0 && declaredSize <= PIECE_BYTES)
284
- ? declaredSize
285
- : PIECE_BYTES;
286
- // +1 because partNo is 0-indexed and we just incremented tgPartIdx.
287
- // Add a small headroom (+4) for any rounding edge cases.
288
- const estTotal = Math.max(
289
- tgPartIdx + 4, // always > current partNo
290
- Math.ceil(docCeiling / PART_SIZE) + 4 // ceiling for remaining parts
291
  );
292
 
293
  const p = client.invoke(new Api.upload.SaveBigFilePart({
@@ -305,16 +367,15 @@ app.post('/upload', checkRAM, (req, res) => {
305
  }
306
  }
307
 
308
- // Drain partChunks into exactly PART_SIZE sends.
309
- // If `final` is true, also flush whatever remains (< PART_SIZE).
310
  async function drainPartBuf(fileStream, final) {
311
  while (partLen >= PART_SIZE) {
312
- // Collect exactly PART_SIZE bytes
313
  const slices = [];
314
  let need = PART_SIZE;
315
  while (need > 0) {
316
  const top = partChunks[0];
317
- if (!top) break; // safety guard
318
  if (top.length <= need) {
319
  slices.push(top);
320
  need -= top.length;
@@ -327,22 +388,17 @@ app.post('/upload', checkRAM, (req, res) => {
327
  }
328
  partLen -= PART_SIZE;
329
  if (partLen < 0) { console.warn('[BUG] partLen underflow, clamping'); partLen = 0; }
330
- await sendPart(Buffer.concat(slices), fileStream);
331
  }
332
 
333
- // Final call: flush remaining bytes (last part of the ENTIRE file β€” the
334
- // only place a sub-512KB part is legal).
335
  if (final && partLen > 0) {
336
  const buf = Buffer.concat(partChunks.splice(0));
337
  partLen = 0;
338
- await sendPart(buf, null);
339
  }
340
  }
341
 
342
- // Finalize current TG document -> SendMedia.
343
- // `committedBytes` = exact bytes uploaded into THIS TG doc (supplied by
344
- // caller so we never depend on the mutable docBytes variable during
345
- // boundary splits where docBytes has already been updated).
346
  async function finalizeDoc(committedBytes) {
347
  await flushInFlight();
348
  if (tgPartIdx === 0) return;
@@ -351,12 +407,12 @@ app.post('/upload', checkRAM, (req, res) => {
351
  const sent = await client.invoke(new Api.messages.SendMedia({
352
  peer,
353
  media: new Api.InputMediaUploadedDocument({
354
- file: new Api.InputFileBig({
355
  id: tgFileId,
356
- parts: tgPartIdx, // ACTUAL parts sent β€” critical
357
  name: tgName,
358
  }),
359
- mimeType: 'application/octet-stream',
360
  forceFile: true,
361
  attributes: [new Api.DocumentAttributeFilename({ fileName: tgName })],
362
  }),
@@ -375,18 +431,13 @@ app.post('/upload', checkRAM, (req, res) => {
375
  partLabel: `.part${docIdx + 1}`,
376
  });
377
 
378
- console.log(`[Upload] part ${docIdx + 1} done -> msgId=${msgId} ${formatBytes(realSize || committedBytes)}`);
379
-
380
- // Reset only TG-doc level counters.
381
- // docBytes is intentionally NOT reset here; the data-handler loop
382
- // manages it correctly (sets docBytes = partLen after a split, so the
383
- // next document's byte accounting starts from the carry-over size).
384
  docIdx++;
385
  tgPartIdx = 0;
386
  tgFileId = makeTgFileId(docIdx);
387
  }
388
 
389
- // ── Stream: busboy -> exact 512KB parts -> Telegram ───────────────────
390
  await new Promise((resolve, reject) => {
391
  const bb = busboy({ headers: req.headers });
392
 
@@ -398,47 +449,30 @@ app.post('/upload', checkRAM, (req, res) => {
398
  fileStream.on('data', async (chunk) => {
399
  fileStream.pause();
400
  try {
401
- let offset = 0;
402
-
403
- while (offset < chunk.length) {
404
- // How many bytes can this doc still accept before hitting PIECE_BYTES?
405
- const docRemaining = PIECE_BYTES - docBytes;
406
- const take = Math.min(chunk.length - offset, docRemaining);
407
-
408
- // Accumulate exactly `take` bytes into the part buffer
409
- const slice = (offset === 0 && take === chunk.length)
410
- ? chunk // no-copy fast path
411
- : chunk.slice(offset, offset + take);
412
-
413
- partChunks.push(slice);
414
- partLen += take;
415
- totalBytes += take;
416
- docBytes += take;
417
- offset += take;
418
-
419
- // Drain complete 512 KB parts (never flushes sub-512KB here)
420
  await drainPartBuf(fileStream, false);
421
 
422
- // If we have hit the 1.9 GB boundary, finalize this TG document.
423
- // CRITICAL: do NOT call drainPartBuf(final=true) here.
424
- // Any bytes still in partChunks (< 512 KB leftover) stay in the
425
- // buffer and will be prepended to the NEXT document's stream,
426
- // satisfying Telegram's rule that only the very last part of
427
- // the ENTIRE file may be < 512 KB.
428
  if (docBytes >= PIECE_BYTES) {
429
- // Drain any whole 512KB parts that exactly fill up right here
430
- // (handles the edge case where docBytes lands on a part boundary)
431
- await drainPartBuf(fileStream, false);
432
- // committedBytes = bytes that actually went into this TG doc.
433
- // partLen is the carry-over that will belong to the NEXT doc.
434
  const committedToThisDoc = PIECE_BYTES - partLen;
435
  await finalizeDoc(committedToThisDoc);
436
- // partChunks / partLen carry over β€” intentional
437
- // Reset docBytes to the carry-over size so the next doc's
438
- // byte accounting is correct from the start.
439
- docBytes = partLen;
440
  }
 
 
 
 
 
 
441
  }
 
442
 
443
  fileStream.resume();
444
  } catch (e) { reject(e); }
@@ -446,8 +480,21 @@ app.post('/upload', checkRAM, (req, res) => {
446
 
447
  fileStream.on('end', async () => {
448
  try {
449
- await drainPartBuf(null, true); // flush last partial part (true final)
450
- await finalizeDoc(docBytes); // finalize last TG doc
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  resolve();
452
  } catch (e) { reject(e); }
453
  });
 
248
  let fileName = 'unknown';
249
  let mimeType = 'application/octet-stream';
250
 
251
+ // ── Telegram upload API thresholds ────────────────────────────────────
252
+ // Files <= SMALL_FILE_THRESHOLD β†’ SaveFilePart + InputFile (small-file API)
253
+ // Files > SMALL_FILE_THRESHOLD β†’ SaveBigFilePart + InputFileBig (big-file API)
254
+ //
255
+ // Telegram enforces this split strictly:
256
+ // β€’ SaveBigFilePart rejects parts < 512 KB unless it's the very last part
257
+ // of a >10 MB file.
258
+ // β€’ SaveFilePart has no minimum part size but is limited to files ≀ 10 MB.
259
+ //
260
+ // Since we don't always know the size upfront (streaming), we use
261
+ // declaredSize (x-file-size header) when available. If unknown we default
262
+ // to the big-file path (safe for all sizes) but only once we know the
263
+ // file is actually large. For truly unknown sizes we buffer up to
264
+ // SMALL_FILE_THRESHOLD + 1 byte: if the file fits, use small API; if not,
265
+ // switch to big-file streaming with the buffered bytes as the first parts.
266
+ const SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
267
+
268
  // ── Per-TG-document state ─────────────────────────────────────────────
269
  const finalDocs = [];
270
  let docIdx = 0;
 
272
  let tgPartIdx = 0; // part index within current TG doc
273
  let tgFileId = makeTgFileId(0);
274
  let totalBytes = 0;
275
+ let isBigFile = (declaredSize > SMALL_FILE_THRESHOLD); // resolved at stream-end if unknown
276
 
277
+ // Accumulation ring β€” drained only in exact PART_SIZE slices (big-file path)
278
+ // Also used as a full-file buffer on the small-file path.
279
+ const partChunks = [];
280
+ let partLen = 0;
281
 
282
+ // In-flight SaveBigFilePart promises (big-file path only)
283
  const inFlight = [];
284
 
285
  async function flushInFlight() {
 
287
  await Promise.all(inFlight.splice(0));
288
  }
289
 
290
+ // ── Small-file path ───────────────────────────────────────────────────
291
+ // Called once, after the entire file has been buffered (≀ 10 MB).
292
+ // Uses SaveFilePart (no minimum part size) + InputFile.
293
+ async function uploadSmallFile(fullBuf, committedBytes) {
294
+ const SMALL_PART = 512 * 1024; // can use any size ≀ 512 KB; 512 KB is fine
295
+ const numParts = Math.ceil(fullBuf.length / SMALL_PART);
296
+ const smallId = tgFileId; // already a BigInt
297
+
298
+ for (let i = 0; i < numParts; i++) {
299
+ const slice = fullBuf.slice(i * SMALL_PART, (i + 1) * SMALL_PART);
300
+ await client.invoke(new Api.upload.SaveFilePart({
301
+ fileId: smallId,
302
+ filePart: i,
303
+ bytes: Buffer.isBuffer(slice) ? slice : Buffer.from(slice),
304
+ }));
305
+ }
306
+
307
+ const tgName = `${fileName}.part${docIdx + 1}`;
308
+ const sent = await client.invoke(new Api.messages.SendMedia({
309
+ peer,
310
+ media: new Api.InputMediaUploadedDocument({
311
+ file: new Api.InputFile({ // ← InputFile, NOT InputFileBig
312
+ id: smallId,
313
+ parts: numParts,
314
+ name: tgName,
315
+ md5Checksum: '',
316
+ }),
317
+ mimeType: mimeType || 'application/octet-stream',
318
+ forceFile: true,
319
+ attributes: [new Api.DocumentAttributeFilename({ fileName: tgName })],
320
+ }),
321
+ message: `\ud83d\udcc2 ${fileName} | Part ${docIdx + 1}`,
322
+ randomId: BigInt(Date.now()) + BigInt(Math.floor(Math.random() * 1e9)),
323
+ }));
324
+
325
+ const { msgId, realSize, dcId } = extractMsgInfo(sent, committedBytes);
326
+ if (!msgId) throw new Error(`SendMedia returned no messageId for part ${docIdx + 1}`);
327
+
328
+ finalDocs.push({
329
+ index: docIdx,
330
+ messageId: msgId,
331
+ size: realSize || committedBytes,
332
+ dcId,
333
+ partLabel: `.part${docIdx + 1}`,
334
+ });
335
+
336
+ console.log(`[Upload] small-file part ${docIdx + 1} done -> msgId=${msgId} ${formatBytes(realSize || committedBytes)}`);
337
+ docIdx++;
338
+ tgPartIdx = 0;
339
+ tgFileId = makeTgFileId(docIdx);
340
+ }
341
+
342
+ // ── Big-file path: send one part (exactly PART_SIZE, or true final part) ─
343
+ // fileTotalParts is a running upper bound. Telegram requires:
344
+ // (a) filePart < fileTotalParts on every SaveBigFilePart call
345
+ // (b) InputFileBig.parts == exact count at SendMedia
346
+ async function sendBigPart(buf, fileStream) {
347
+ const partNo = tgPartIdx++;
348
  const docCeiling = (declaredSize > 0 && declaredSize <= PIECE_BYTES)
349
+ ? declaredSize : PIECE_BYTES;
350
+ const estTotal = Math.max(
351
+ tgPartIdx + 4,
352
+ Math.ceil(docCeiling / PART_SIZE) + 4
 
 
 
353
  );
354
 
355
  const p = client.invoke(new Api.upload.SaveBigFilePart({
 
367
  }
368
  }
369
 
370
+ // Drain accumulation ring into exactly PART_SIZE big-file sends.
371
+ // If `final` is true, also flush the sub-512KB remainder (true last part only).
372
  async function drainPartBuf(fileStream, final) {
373
  while (partLen >= PART_SIZE) {
 
374
  const slices = [];
375
  let need = PART_SIZE;
376
  while (need > 0) {
377
  const top = partChunks[0];
378
+ if (!top) break;
379
  if (top.length <= need) {
380
  slices.push(top);
381
  need -= top.length;
 
388
  }
389
  partLen -= PART_SIZE;
390
  if (partLen < 0) { console.warn('[BUG] partLen underflow, clamping'); partLen = 0; }
391
+ await sendBigPart(Buffer.concat(slices), fileStream);
392
  }
393
 
 
 
394
  if (final && partLen > 0) {
395
  const buf = Buffer.concat(partChunks.splice(0));
396
  partLen = 0;
397
+ await sendBigPart(buf, null);
398
  }
399
  }
400
 
401
+ // Finalize a big-file TG document -> SendMedia with InputFileBig
 
 
 
402
  async function finalizeDoc(committedBytes) {
403
  await flushInFlight();
404
  if (tgPartIdx === 0) return;
 
407
  const sent = await client.invoke(new Api.messages.SendMedia({
408
  peer,
409
  media: new Api.InputMediaUploadedDocument({
410
+ file: new Api.InputFileBig({ // ← InputFileBig for >10 MB files
411
  id: tgFileId,
412
+ parts: tgPartIdx, // ACTUAL parts sent β€” critical
413
  name: tgName,
414
  }),
415
+ mimeType: mimeType || 'application/octet-stream',
416
  forceFile: true,
417
  attributes: [new Api.DocumentAttributeFilename({ fileName: tgName })],
418
  }),
 
431
  partLabel: `.part${docIdx + 1}`,
432
  });
433
 
434
+ console.log(`[Upload] big-file part ${docIdx + 1} done -> msgId=${msgId} ${formatBytes(realSize || committedBytes)}`);
 
 
 
 
 
435
  docIdx++;
436
  tgPartIdx = 0;
437
  tgFileId = makeTgFileId(docIdx);
438
  }
439
 
440
+ // ── Stream: busboy -> dual-path Telegram upload ───────────────────────
441
  await new Promise((resolve, reject) => {
442
  const bb = busboy({ headers: req.headers });
443
 
 
449
  fileStream.on('data', async (chunk) => {
450
  fileStream.pause();
451
  try {
452
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
453
+ partChunks.push(buf);
454
+ partLen += buf.length;
455
+ totalBytes += buf.length;
456
+ docBytes += buf.length;
457
+
458
+ if (isBigFile) {
459
+ // ── Big-file path: drain complete 512 KB parts, enforce doc boundary ─
 
 
 
 
 
 
 
 
 
 
 
460
  await drainPartBuf(fileStream, false);
461
 
 
 
 
 
 
 
462
  if (docBytes >= PIECE_BYTES) {
463
+ await drainPartBuf(fileStream, false); // drain any newly-aligned parts
 
 
 
 
464
  const committedToThisDoc = PIECE_BYTES - partLen;
465
  await finalizeDoc(committedToThisDoc);
466
+ docBytes = partLen; // carry-over belongs to next doc
 
 
 
467
  }
468
+
469
+ } else if (declaredSize <= 0 && partLen > SMALL_FILE_THRESHOLD) {
470
+ // ── Unknown size crossed threshold mid-stream: switch to big-file ──
471
+ isBigFile = true;
472
+ console.log(`[Upload] "${fileName}" exceeds ${formatBytes(SMALL_FILE_THRESHOLD)}, switching to big-file path`);
473
+ await drainPartBuf(fileStream, false);
474
  }
475
+ // else: small-file path β€” just keep buffering until stream end
476
 
477
  fileStream.resume();
478
  } catch (e) { reject(e); }
 
480
 
481
  fileStream.on('end', async () => {
482
  try {
483
+ // Resolve path for files with unknown declared size
484
+ if (!isBigFile && declaredSize <= 0) {
485
+ isBigFile = (totalBytes > SMALL_FILE_THRESHOLD);
486
+ }
487
+
488
+ if (isBigFile) {
489
+ // ── Big-file path: flush remainder and finalize ──────────────
490
+ await drainPartBuf(null, true);
491
+ await finalizeDoc(docBytes);
492
+ } else {
493
+ // ── Small-file path: entire file is in partChunks (≀ 10 MB) ─
494
+ const fullBuf = Buffer.concat(partChunks.splice(0));
495
+ partLen = 0;
496
+ await uploadSmallFile(fullBuf, totalBytes);
497
+ }
498
  resolve();
499
  } catch (e) { reject(e); }
500
  });