data-ark 0.1.0 → 0.1.1

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 CHANGED
@@ -1,50 +1,50 @@
1
1
  # data-ark
2
2
 
3
- Cắt file lớn thành từng chunk 1.8GB lưu trữ trên Telegram, khôi phục lại được nguyên vẹn.
3
+ Split large files into 1.8GB chunks, store them on Telegram, and restore them intact.
4
4
 
5
- ## Dùng ngay
5
+ ## Quick start
6
6
 
7
7
  ```bash
8
- npx data-ark login # chỉ cần một lần
9
- npx data-ark data.tar --to @kho_backup # đích được ghi nhớ cho lần sau
10
- npx data-ark data.tar # từ lần thứ hai trở đi
8
+ npx data-ark login # once only
9
+ npx data-ark data.tar --to @my_backups # the destination is remembered
10
+ npx data-ark data.tar # from the second run on
11
11
  npx data-ark restore ark-20260905-7f3a91
12
12
  ```
13
13
 
14
- ## Cần chuẩn bị
14
+ ## What you need
15
15
 
16
- - Node.js 18 trở lên.
17
- - `api_id` `api_hash` lấy tại <https://my.telegram.org> → API development tools. Lệnh `login` sẽ hỏi hai giá trị này, kèm số điện thoại xác nhận.
16
+ - Node.js 18 or newer.
17
+ - An `api_id` and `api_hash` from <https://my.telegram.org> → API development tools. The `login` command asks for both, plus your phone number and the verification code.
18
18
 
19
- data-ark đăng nhập bằng chính tài khoản Telegram của bạn (MTProto), không phải bot. Đây điều kiện bắt buộc: Bot API chỉ cho upload 50MB mỗi file, còn tài khoản người dùng được 2GB.
19
+ data-ark signs in with your own Telegram account (MTProto), not a bot. That is a hard requirement: the Bot API caps uploads at 50MB per file, while a user account gets 2GB.
20
20
 
21
- ## Tuỳ chọn
21
+ ## Options
22
22
 
23
- | Cờ | Mặc định | Ý nghĩa |
23
+ | Flag | Default | Meaning |
24
24
  |---|---|---|
25
- | `--to <chat>` | đích đã ghi nhớ | `@username`, `-100123…`, hoặc `me` |
26
- | `--chunk-size <n>` | `1800MB` | dụ `1.8GB`, `500MB`. Trần cứng 1950MB. |
27
- | `--concurrency <n>` | `8` | Số phần 512KB gửi song song. Phải số nguyên từ 1 đến 64. |
28
- | `--out <đường-dẫn>` | basename của tên trong manifest | Nơi ghi file khi restore, tính từ thư mục hiện tại nếu đường dẫn tương đối |
25
+ | `--to <chat>` | the remembered destination | `@username`, `-100123…`, or `me` |
26
+ | `--chunk-size <n>` | `1800MB` | e.g. `1.8GB`, `500MB`. Hard ceiling 1950MB. |
27
+ | `--concurrency <n>` | `8` | 512KB parts sent in parallel. An integer from 1 to 64. |
28
+ | `--out <path>` | the basename from the manifest | Where to write the restored file; relative paths resolve against the current directory |
29
29
 
30
- ## Hoạt động thế nào
30
+ ## How it works
31
31
 
32
- Mỗi lần chạy sinh ra một `backupId`. File được đọc thẳng theo offset, không tạo file tạm, đẩy lên thành các document tên `<backupId>.partNNNN`. Xong hết, data-ark gửi thêm một manifest JSON liệt message id sha256 của từng chunk. Restore chỉ cần `backupId`: tìm manifest trong chat, tải từng chunk về đúng vị trí trong một file `.partial`, đối chiếu sha256 kích thước từng chunk, chỉ đổi tên thành file thật sau khi *toàn bộ* chunk đã khớp.
32
+ Every run mints a `backupId`. The file is read directly by offset no temporary copies and uploaded as documents named `<backupId>.partNNNN`. Once every chunk is up, data-ark sends a JSON manifest listing the message id and sha256 of each one. Restore needs only the `backupId`: it finds the manifest in the chat, downloads each chunk to its exact position in a `.partial` file, checks every chunk's sha256 and size, and renames it to the real file only after *all* chunks match.
33
33
 
34
- Đứt mạng giữa chừng lúc **upload** thì cứ chạy lại đúng lệnh tiến độ nằm `~/.data-ark/state/`, những chunk đã xong sẽ được bỏ qua (`backupId` giữ nguyên). Vài lưu ý về việc chạy lại:
34
+ If the connection drops during an **upload**, just run the same command againprogress lives in `~/.data-ark/state/` and finished chunks are skipped, keeping the same `backupId`. Two things to know about rerunning:
35
35
 
36
- - Nếu chạy lại với `--to` khác với đích đã lưu trong tiến độ dở dang, data-ark **từ chối chạy** thay tự chuyển hướng một backup không thể tách làm hai đích. Thông báo lỗi chỉ đường: bỏ `--to` để tiếp tục gửi vào đích cũ, hoặc xoá file trạng thái để bắt đầu backup mới.
37
- - Nếu chạy lại với `--chunk-size` khác, data-ark coi đó một backup mới hoàn toàn (backup id mới), không resume.
36
+ - Running again with a `--to` that differs from the destination stored in the unfinished progress makes data-ark **refuse to run** rather than silently redirectone backup cannot be split across two destinations. The error points the way out: drop `--to` to keep sending to the original destination, or delete the state file to start a new backup.
37
+ - Running again with a different `--chunk-size` is treated as an entirely new backup (new backup id), not a resume.
38
38
 
39
- **Restore không trạng thái để resume.** Nhấn `Ctrl-C` giữa lúc restore thì không có gì được lưu chạy lại sẽ tải lại từ đầu.
39
+ **Restore keeps no state to resume from.** Pressing `Ctrl-C` mid-restore saves nothingrunning again starts over.
40
40
 
41
- ## Giới hạn cần biết
41
+ ## Limits worth knowing
42
42
 
43
- - Chunk không thể vượt 1950MB: Telegram nhận tối đa 4000 phần 512KB mỗi file, tức trần số học khoảng 1953MB, data-ark chốt 1950MB để chừa biên an toàn.
44
- - Dữ liệu **không** được hoá. Đừng đẩy thứ bạn không muốn nằm trên hạ tầng của người khác.
45
- - Xoá message chunk trên Telegram mất backup, không cách cứu.
46
- - Giữ lấy `backupId`. Không thì phải tự tìm manifest trong chat bằng tay.
43
+ - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file, an arithmetic ceiling of about 1953MB, and data-ark stops at 1950MB to leave a safety margin.
44
+ - The data is **not** encrypted. Don't upload anything you would mind sitting on someone else's infrastructure.
45
+ - Deleting a chunk message on Telegram destroys the backup, with no way to recover it.
46
+ - Keep the `backupId`. Without it you have to hunt for the manifest in the chat by hand.
47
47
 
48
- ## Cấu hình lưu ở đâu
48
+ ## Where the config lives
49
49
 
50
- `~/.data-ark/config.json` (quyền 600) chứa `apiId`, `apiHash`, session đích lưu mặc định. `npx data-ark logout` **chỉ xoá bản session lưu dưới máy** giữ lại phần còn lại phiên đăng nhập vẫn còn sống phía Telegram. Muốn cắt hẳn quyền truy cập thì vào Telegram → Settings → Devices (Active sessions) thu hồi phiên đó.
50
+ `~/.data-ark/config.json` (mode 600) holds `apiId`, `apiHash`, the session and the default destination. `npx data-ark logout` **only deletes the locally stored session** and keeps the restthe session is still alive on Telegram's side. To revoke access for good, open Telegram → Settings → Devices (Active sessions) and terminate that session.
package/bin/data-ark.js CHANGED
@@ -7,20 +7,20 @@ import { runUpload } from '../src/commands/upload.js'
7
7
 
8
8
  const SIGINT_EXIT_CODE = 130
9
9
 
10
- // Lệnh đang chạy khi Ctrl-C xảy ra mỗi lệnh một sự thật khác nhau về việc
11
- // tiến độ được lưu hay không, nên phải biết đang lệnh nào mới chọn đúng câu.
10
+ // Which command is running when Ctrl-C arriveseach one tells a different truth
11
+ // about whether progress was saved, so we need to know which to pick the right line.
12
12
  let currentCommand = null
13
13
 
14
14
  function sigintMessage(command) {
15
15
  if (command === 'upload') {
16
- return '\nĐã dừng. Tiến độ đã lưu, chạy lại lệnh để đi tiếp.\n'
16
+ return '\nStopped. Progress is saved, run the same command again to continue.\n'
17
17
  }
18
18
 
19
19
  if (command === 'restore') {
20
- return '\nĐã dừng. Chưa lưu tiến độ tải, chạy lại sẽ tải lại từ đầu.\n'
20
+ return '\nStopped. Download progress is not saved, running again starts over.\n'
21
21
  }
22
22
 
23
- return '\nĐã dừng.\n'
23
+ return '\nStopped.\n'
24
24
  }
25
25
 
26
26
  process.on('SIGINT', () => {
@@ -34,7 +34,7 @@ async function main() {
34
34
  try {
35
35
  parsed = route(process.argv.slice(2))
36
36
  } catch (err) {
37
- process.stderr.write(`Lỗi: ${err.message}\n\n${HELP}`)
37
+ process.stderr.write(`Error: ${err.message}\n\n${HELP}`)
38
38
  process.exitCode = 2
39
39
  return
40
40
  }
@@ -60,24 +60,24 @@ async function main() {
60
60
 
61
61
  case 'restore':
62
62
  if (!parsed.args[0]) {
63
- throw new Error('Thiếu backup id. dụ: npx data-ark restore ark-20260905-7f3a91')
63
+ throw new Error('Missing backup id. Example: npx data-ark restore ark-20260905-7f3a91')
64
64
  }
65
65
  await runRestore(parsed.args[0], parsed.options)
66
66
  return
67
67
 
68
68
  default:
69
- throw new Error(`Lệnh không nhận ra: ${parsed.command}`)
69
+ throw new Error(`Unknown command: ${parsed.command}`)
70
70
  }
71
71
  }
72
72
 
73
- // GramJS giữ lại các "exported sender" cùng một timer 30 giây để giải phóng
74
- // chúng, client.disconnect()/destroy() không dọn nổi: cả hai map đó là Map
75
- // nhưng code duyệt bằng Object.values nên bỏ sót sạch. Hệ quả: lệnh in "Xong"
76
- // rồi vẫn treo thêm ~30 giây, Ctrl-C trong khoảng đó lại báo sai rằng chưa
77
- // lưu được gì. Chạy xong việc thoát hẳn, không nấn ná.
73
+ // GramJS keeps "exported senders" around together with a 30-second timer to release
74
+ // them, and neither client.disconnect() nor destroy() cleans them up: both of those
75
+ // maps are Maps, but the code walks them with Object.values and so misses everything.
76
+ // The result is a command that prints "Done" and then hangs for another ~30 seconds,
77
+ // during which Ctrl-C falsely reports that nothing was saved. Finish the work, exit.
78
78
  function exitWhenFlushed(code) {
79
- // Ghi rỗng chỉ để mượn callback: chạy sau khi mọi thứ xếp hàng trước đó đã
80
- // xả xuống pipe, nên không mất chữ nào khi stdout/stderr không phải TTY.
79
+ // The empty writes exist only to borrow their callbacks: they fire after everything
80
+ // queued earlier has flushed, so nothing is lost when stdout/stderr is not a TTY.
81
81
  let pending = 2
82
82
 
83
83
  const done = () => {
@@ -85,7 +85,7 @@ function exitWhenFlushed(code) {
85
85
  if (pending === 0) process.exit(code)
86
86
  }
87
87
 
88
- // Lưới an toàn: nếu callback không bao giờ tới (pipe đã đóng) thì vẫn thoát.
88
+ // Safety net: exit anyway if a callback never arrives (the pipe is already closed).
89
89
  setTimeout(() => process.exit(code), 2000).unref()
90
90
 
91
91
  process.stdout.write('', done)
@@ -97,7 +97,7 @@ main().then(
97
97
  exitWhenFlushed(process.exitCode ?? 0)
98
98
  },
99
99
  (err) => {
100
- process.stderr.write(`Lỗi: ${err.message}\n`)
100
+ process.stderr.write(`Error: ${err.message}\n`)
101
101
  process.exitCode = 1
102
102
  exitWhenFlushed(1)
103
103
  },
package/package.json CHANGED
@@ -1,9 +1,27 @@
1
1
  {
2
2
  "name": "data-ark",
3
- "version": "0.1.0",
4
- "description": "Cắt file lớn thành chunk lưu trữ trên Telegram",
3
+ "version": "0.1.1",
4
+ "description": "Split large files into chunks and store them on Telegram",
5
+ "keywords": [
6
+ "telegram",
7
+ "backup",
8
+ "chunk",
9
+ "split",
10
+ "restore",
11
+ "cli",
12
+ "mtproto"
13
+ ],
14
+ "homepage": "https://github.com/shovity/data-ark#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/shovity/data-ark/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/shovity/data-ark.git"
21
+ },
5
22
  "type": "module",
6
23
  "license": "MIT",
24
+ "author": "shovity",
7
25
  "bin": {
8
26
  "data-ark": "bin/data-ark.js"
9
27
  },
package/src/chunking.js CHANGED
@@ -17,19 +17,19 @@ export function parseSize(input) {
17
17
  const match = text.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/)
18
18
 
19
19
  if (!match) {
20
- throw new Error(`Kích thước không hợp lệ: "${input}". dụ hợp lệ: 1800MB, 1.8GB, 524288.`)
20
+ throw new Error(`Invalid size: "${input}". Valid examples: 1800MB, 1.8GB, 524288.`)
21
21
  }
22
22
 
23
23
  const bytes = Math.floor(Number(match[1]) * UNITS[match[2] ?? 'b'])
24
24
 
25
25
  if (bytes <= 0) {
26
- throw new Error('Kích thước phải lớn hơn 0.')
26
+ throw new Error('Size must be greater than 0.')
27
27
  }
28
28
 
29
29
  if (bytes > MAX_CHUNK_SIZE) {
30
30
  throw new Error(
31
- 'Chunk tối đa 1950MB. Telegram chỉ nhận 4000 phần 512KB cho mỗi file, ' +
32
- 'tức trần số học khoảng 1953MB, nên cần chừa biên an toàn.',
31
+ 'Maximum chunk size is 1950MB. Telegram accepts only 4000 parts of 512KB per file, ' +
32
+ 'an arithmetic ceiling of about 1953MB, so a safety margin is needed.',
33
33
  )
34
34
  }
35
35
 
@@ -38,7 +38,7 @@ export function parseSize(input) {
38
38
 
39
39
  export function planChunks(fileSize, chunkSize) {
40
40
  if (fileSize <= 0) {
41
- throw new Error('File rỗng, không gì để upload.')
41
+ throw new Error('File is empty, nothing to upload.')
42
42
  }
43
43
 
44
44
  const chunks = []
package/src/cli.js CHANGED
@@ -10,20 +10,20 @@ const OPTIONS = {
10
10
  help: { type: 'boolean', short: 'h' },
11
11
  }
12
12
 
13
- export const HELP = `data-ark — cắt file lớn thành chunk lưu trữ trên Telegram
14
-
15
- Cách dùng:
16
- npx data-ark login Đăng nhập Telegram, chỉ cần chạy một lần
17
- npx data-ark <file> Cắt file đẩy lên Telegram
18
- npx data-ark restore <backup-id> Tải về ghép lại file gốc
19
- npx data-ark logout Xoá phiên đăng nhập đã lưu
20
-
21
- Tuỳ chọn:
22
- --to <chat> Đích lưu: @username, -100123..., hoặc me. Được ghi nhớ cho lần sau.
23
- --chunk-size <n> Kích thước mỗi chunk, mặc định 1800MB. dụ: 1.8GB, 500MB.
24
- --concurrency <n> Số phần 512KB gửi song song, mặc định 8, tối đa 64.
25
- --out <đường-dẫn> Nơi ghi file khi restore. Mặc định lấy basename của tên trong manifest.
26
- -h, --help Hiện trợ giúp này.
13
+ export const HELP = `data-ark — split large files into chunks and store them on Telegram
14
+
15
+ Usage:
16
+ npx data-ark login Log in to Telegram, only needed once
17
+ npx data-ark <file> Split a file and upload it to Telegram
18
+ npx data-ark restore <backup-id> Download the chunks and reassemble the file
19
+ npx data-ark logout Remove the saved session
20
+
21
+ Options:
22
+ --to <chat> Destination: @username, -100123..., or me. Remembered for next time.
23
+ --chunk-size <n> Size of each chunk, default 1800MB. Examples: 1.8GB, 500MB.
24
+ --concurrency <n> 512KB parts sent in parallel, default 8, max 64.
25
+ --out <path> Where to write the restored file. Defaults to the basename in the manifest.
26
+ -h, --help Show this help.
27
27
  `
28
28
 
29
29
  export function route(argv) {
package/src/client.js CHANGED
@@ -5,7 +5,7 @@ export function normalizeChatTarget(input) {
5
5
  const text = String(input).trim()
6
6
 
7
7
  if (text === '') {
8
- throw new Error('Đích lưu không được để rỗng.')
8
+ throw new Error('Destination must not be empty.')
9
9
  }
10
10
 
11
11
  if (/^-?\d+$/.test(text)) {
@@ -20,8 +20,8 @@ export function requireChat(options, config) {
20
20
 
21
21
  if (!raw) {
22
22
  throw new Error(
23
- 'Chưa đích lưu chạy lại với --to @kho_backup (hoặc --to me để dùng Saved Messages). ' +
24
- 'Lần sau data-ark sẽ tự nhớ.',
23
+ 'No destination setrun again with --to @my_backups (or --to me for Saved Messages). ' +
24
+ 'data-ark will remember it next time.',
25
25
  )
26
26
  }
27
27
 
@@ -30,7 +30,7 @@ export function requireChat(options, config) {
30
30
 
31
31
  export function assertLoggedIn(config) {
32
32
  if (!config.session || !config.apiId || !config.apiHash) {
33
- throw new Error('Chưa đăng nhậpchạy "npx data-ark login" trước.')
33
+ throw new Error('Not logged inrun "npx data-ark login" first.')
34
34
  }
35
35
  }
36
36
 
@@ -45,7 +45,7 @@ export async function connect(config) {
45
45
  await client.connect()
46
46
 
47
47
  if (!(await client.isUserAuthorized())) {
48
- throw new Error('Phiên đăng nhập đã hết hạn chạy "npx data-ark login".')
48
+ throw new Error('Session expiredrun "npx data-ark login".')
49
49
  }
50
50
 
51
51
  return client
@@ -16,11 +16,11 @@ function createPrompts() {
16
16
  }
17
17
 
18
18
  const LOGIN_ERROR_MESSAGES = {
19
- PHONE_NUMBER_INVALID: 'số điện thoại không hợp lệ',
20
- PHONE_CODE_INVALID: ' xác nhận sai',
21
- PHONE_CODE_EXPIRED: ' xác nhận đã hết hạn',
22
- PASSWORD_HASH_INVALID: 'mật khẩu hai lớp sai',
23
- FLOOD_WAIT: 'bị Telegram giới hạn tần suất, cần chờ',
19
+ PHONE_NUMBER_INVALID: 'invalid phone number',
20
+ PHONE_CODE_INVALID: 'wrong verification code',
21
+ PHONE_CODE_EXPIRED: 'verification code expired',
22
+ PASSWORD_HASH_INVALID: 'wrong two-step password',
23
+ FLOOD_WAIT: 'rate limited by Telegram, need to wait',
24
24
  }
25
25
 
26
26
  export function describeLoginError(err) {
@@ -36,30 +36,30 @@ export function describeLoginError(err) {
36
36
  export async function runLogin({ configDir = defaultConfigDir(), prompts = createPrompts() } = {}) {
37
37
  const config = await loadConfig(configDir)
38
38
 
39
- console.log('Cần api_id api_hash của riêng bạn. Lấy tại https://my.telegram.org → API development tools.\n')
39
+ console.log('You need your own api_id and api_hash. Get them at https://my.telegram.org → API development tools.\n')
40
40
 
41
41
  const apiIdAnswer = (await prompts.ask(`api_id${config.apiId ? ` [${config.apiId}]` : ''}: `)).trim()
42
42
 
43
43
  if (apiIdAnswer !== '' && !/^\d+$/.test(apiIdAnswer)) {
44
44
  prompts.close()
45
- throw new Error('api_id phải số nguyên.')
45
+ throw new Error('api_id must be an integer.')
46
46
  }
47
47
 
48
48
  const apiId = apiIdAnswer === '' ? config.apiId : Number(apiIdAnswer)
49
- const apiHash = (await prompts.ask(`api_hash${config.apiHash ? ' [giữ nguyên]' : ''}: `)) || config.apiHash
49
+ const apiHash = (await prompts.ask(`api_hash${config.apiHash ? ' [keep current]' : ''}: `)) || config.apiHash
50
50
 
51
51
  if (!apiId || !apiHash) {
52
52
  prompts.close()
53
- throw new Error('Thiếu api_id hoặc api_hash.')
53
+ throw new Error('Missing api_id or api_hash.')
54
54
  }
55
55
 
56
56
  const client = new TelegramClient(new StringSession(''), apiId, apiHash, { connectionRetries: 5 })
57
57
 
58
58
  await client.start({
59
- phoneNumber: () => prompts.ask('Số điện thoại (dạng +84...): '),
60
- phoneCode: () => prompts.ask(' xác nhận Telegram vừa gửi: '),
61
- password: () => prompts.ask('Mật khẩu hai lớp (bỏ trống nếu không bật): '),
62
- onError: (err) => console.error(`Lỗi đăng nhập: ${describeLoginError(err)}`),
59
+ phoneNumber: () => prompts.ask('Phone number (e.g. +1...): '),
60
+ phoneCode: () => prompts.ask('Verification code Telegram just sent: '),
61
+ password: () => prompts.ask('Two-step password (leave blank if not enabled): '),
62
+ onError: (err) => console.error(`Login failed: ${describeLoginError(err)}`),
63
63
  })
64
64
 
65
65
  const me = await client.getMe()
@@ -68,7 +68,7 @@ export async function runLogin({ configDir = defaultConfigDir(), prompts = creat
68
68
 
69
69
  const chatAnswer = (
70
70
  await prompts.ask(
71
- `Đẩy backup vào chat nào? (@username, -100..., hoặc me)${config.defaultChat ? ` [${config.defaultChat}]` : ''}, Enter để bỏ qua: `,
71
+ `Which chat should backups go to? (@username, -100..., or me)${config.defaultChat ? ` [${config.defaultChat}]` : ''}, Enter to skip: `,
72
72
  )
73
73
  ).trim()
74
74
 
@@ -82,6 +82,6 @@ export async function runLogin({ configDir = defaultConfigDir(), prompts = creat
82
82
 
83
83
  await saveConfig(next, configDir)
84
84
 
85
- console.log(`\nĐã đăng nhập với tài khoản ${me.username ? `@${me.username}` : me.firstName}.`)
86
- console.log(`Cấu hình đã lưu vào ${configDir}/config.json`)
85
+ console.log(`\nLogged in as ${me.username ? `@${me.username}` : me.firstName}.`)
86
+ console.log(`Config saved to ${configDir}/config.json`)
87
87
  }
@@ -3,11 +3,11 @@ import { clearSession, defaultConfigDir } from '../config.js'
3
3
  export async function runLogout({ configDir = defaultConfigDir() } = {}) {
4
4
  await clearSession(configDir)
5
5
  console.log(
6
- 'Đã xoá phiên đăng nhập lưu trên máy này. api_id, api_hash đích lưu vẫn được giữ lại.',
6
+ 'Removed the session stored on this machine. api_id, api_hash and the destination are kept.',
7
7
  )
8
8
  console.log(
9
- 'Lưu ý: lệnh này chỉ xoá bản lưu dưới máy, phiên vẫn còn sống phía Telegram. ' +
10
- 'Muốn cắt hẳn quyền truy cập, mở Telegram → Settings → Devices (Active sessions) ' +
11
- ' thu hồi phiên đó.',
9
+ 'Note: this only deletes the local copy the session is still alive on Telegram\'s side. ' +
10
+ 'To revoke access for good, open Telegram → Settings → Devices (Active sessions) ' +
11
+ 'and terminate that session.',
12
12
  )
13
13
  }
@@ -20,8 +20,8 @@ function documentFileName(message) {
20
20
  async function realSearchManifest(client, peer, backupId) {
21
21
  const wanted = manifestFileName(backupId)
22
22
 
23
- // Dùng client.getMessages thay raw Api.messages.Search: tự lo offset,
24
- // hash phân trang, nên không phải tự dựng các trường dễ sai kiểu.
23
+ // Use client.getMessages instead of a raw Api.messages.Search: it handles offsets,
24
+ // hashes and pagination itself, so we don't hand-build easily mistyped fields.
25
25
  const messages = await client.getMessages(peer, {
26
26
  search: backupId,
27
27
  filter: new Api.InputMessagesFilterDocument(),
@@ -44,17 +44,17 @@ export async function realDownloadChunk(client, message, handle, offset, onProgr
44
44
  return await downloadToFile(client, message, handle.fd, { offset, onProgress })
45
45
  }
46
46
 
47
- // manifest.name đến từ dữ liệu tải về Telegram — không tin khi tự chọn đường
48
- // dẫn. path.basename chặn được "../../x" nhưng vẫn trả về "..", "." hay "" cho
49
- // vài tên bệnh: path.resolve('..') thư mục cha, nên file .partial hàng GB sẽ
50
- // nằm ngoài thư mục hiện tại chỉ vỡ ra bước rename.
47
+ // manifest.name comes from data downloaded off Telegram — don't trust it when picking
48
+ // a path ourselves. path.basename stops "../../x" but still returns "..", "." or "" for
49
+ // a few pathological names: path.resolve('..') is the parent directory, so a multi-GB
50
+ // .partial file would land outside the current directory and only blow up at rename.
51
51
  function safeOutName(name) {
52
52
  const base = path.basename(String(name ?? ''))
53
53
 
54
54
  if (base === '' || base === '.' || base === '..') {
55
55
  throw new Error(
56
- `Tên file trong manifest ("${name}") không dùng được làm tên file. ` +
57
- 'Chạy lại kèm --out <đường-dẫn> để tự chỉ định nơi ghi.',
56
+ `The name in the manifest ("${name}") cannot be used as a file name. ` +
57
+ 'Run again with --out <path> to choose where to write.',
58
58
  )
59
59
  }
60
60
 
@@ -65,7 +65,7 @@ async function askConfirm(question) {
65
65
  const rl = readline.createInterface({ input: stdin, output: stdout })
66
66
  const answer = await rl.question(question)
67
67
  rl.close()
68
- return /^(c|y)/i.test(answer.trim())
68
+ return /^y/i.test(answer.trim())
69
69
  }
70
70
 
71
71
  export async function runRestore(backupId, options = {}, deps = {}) {
@@ -94,18 +94,18 @@ export async function runRestore(backupId, options = {}, deps = {}) {
94
94
 
95
95
  if (!manifestMessage) {
96
96
  throw new Error(
97
- `Không tìm thấy manifest của ${backupId} trong ${chat}. ` +
98
- 'Kiểm tra lại backup id, hoặc dùng --to để trỏ đúng chat.',
97
+ `No manifest found for ${backupId} in ${chat}. ` +
98
+ 'Check the backup id, or use --to to point at the right chat.',
99
99
  )
100
100
  }
101
101
 
102
102
  const manifest = parseManifest(await readMessageBytes(client, manifestMessage))
103
- // Khi người dùng tự chỉ định --out thì tôn trọng nguyên văn đường dẫn đó.
103
+ // When the user passes --out, respect that path verbatim.
104
104
  const target = path.resolve(options.out ?? safeOutName(manifest.name))
105
105
  const partial = `${target}.partial`
106
106
 
107
- // Chỉ ENOENT mới có nghĩa là "chưa file". Lỗi quyền hay lỗi I/O bị coi
108
- // vắng mặt thì data-ark sẽ ghi đè file của người dùng mà không hỏi.
107
+ // Only ENOENT means "no file yet". Treating a permission or I/O error as absence
108
+ // would have data-ark overwrite the user's file without asking.
109
109
  let exists = true
110
110
  try {
111
111
  await fs.stat(target)
@@ -114,12 +114,12 @@ export async function runRestore(backupId, options = {}, deps = {}) {
114
114
  exists = false
115
115
  }
116
116
 
117
- if (exists && !(await confirm(`${target} đã tồn tại. Ghi đè? [c/K] `))) {
118
- throw new Error('Đã huỷ theo yêu cầu.')
117
+ if (exists && !(await confirm(`${target} already exists. Overwrite? [y/N] `))) {
118
+ throw new Error('Cancelled on request.')
119
119
  }
120
120
 
121
121
  log(`Backup ${manifest.id}`)
122
- log(`File ${target} (${formatBytes(manifest.size)}, ${manifest.chunks.length} chunk)\n`)
122
+ log(`File ${target} (${formatBytes(manifest.size)}, ${manifest.chunks.length} chunks)\n`)
123
123
 
124
124
  const handle = await fs.open(partial, 'w+')
125
125
 
@@ -131,8 +131,8 @@ export async function runRestore(backupId, options = {}, deps = {}) {
131
131
 
132
132
  if (!message) {
133
133
  throw new Error(
134
- `Thiếu chunk ${chunk.i + 1}/${manifest.chunks.length}: message ${chunk.msgId} không còn trong ${chat}. ` +
135
- 'Backup này không khôi phục được.',
134
+ `Missing chunk ${chunk.i + 1}/${manifest.chunks.length}: message ${chunk.msgId} is no longer in ${chat}. ` +
135
+ 'This backup cannot be restored.',
136
136
  )
137
137
  }
138
138
 
@@ -156,13 +156,13 @@ export async function runRestore(backupId, options = {}, deps = {}) {
156
156
 
157
157
  if (size !== chunk.size) {
158
158
  throw new Error(
159
- `Chunk ${chunk.i + 1} ${size} byte, manifest ghi ${chunk.size} bytekhông khớp.`,
159
+ `Chunk ${chunk.i + 1} has ${size} bytes, the manifest records ${chunk.size} bytesmismatch.`,
160
160
  )
161
161
  }
162
162
 
163
163
  if (sha256 !== chunk.sha256) {
164
164
  throw new Error(
165
- `Chunk ${chunk.i + 1} sha256 không khớp manifest. File tải về giữ ${partial} để kiểm tra.`,
165
+ `Chunk ${chunk.i + 1} has a sha256 that does not match the manifest. The download is kept at ${partial} for inspection.`,
166
166
  )
167
167
  }
168
168
  }
@@ -170,28 +170,29 @@ export async function runRestore(backupId, options = {}, deps = {}) {
170
170
  await handle.close()
171
171
  }
172
172
 
173
- // Chốt chặn cuối: mọi chunk khớp sha256 file vẫn sai độ dài thì bố cục đã
174
- // lệch đâu đó. Thà báo lỗi còn hơn đổi tên một file sai thành file thật.
173
+ // Last line of defence: if every chunk matched its sha256 and the file is still the
174
+ // wrong length, the layout went wrong somewhere. Better to fail than to rename a
175
+ // wrong file into the real one.
175
176
  const written = await fs.stat(partial)
176
177
 
177
178
  if (written.size !== manifest.size) {
178
179
  throw new Error(
179
- `File ghép xong ${written.size} byte, manifest ghi ${manifest.size} bytekhông khớp. ` +
180
- `File tải về giữ ${partial} để kiểm tra.`,
180
+ `The assembled file has ${written.size} bytes, the manifest records ${manifest.size} bytesmismatch. ` +
181
+ `The download is kept at ${partial} for inspection.`,
181
182
  )
182
183
  }
183
184
 
184
185
  await fs.rename(partial, target)
185
186
 
186
- log(`\nXong. Đã ghi ${formatBytes(manifest.size)} vào ${target}`)
187
+ log(`\nDone. Wrote ${formatBytes(manifest.size)} to ${target}`)
187
188
 
188
189
  return { path: target, size: manifest.size }
189
190
  } finally {
190
- // Ngắt kết nối hỏng thì cũng không được nuốt mất lỗi thật đang bay lên.
191
+ // A failing disconnect must not swallow the real error already on its way up.
191
192
  try {
192
193
  await disconnect(client)
193
194
  } catch (err) {
194
- warn(`\nCảnh báo: không đóng được kết nối Telegram: ${err.message}\n`)
195
+ warn(`\nWarning: could not close the Telegram connection: ${err.message}\n`)
195
196
  }
196
197
  }
197
198
  }
@@ -25,7 +25,7 @@ import { createProgress, formatBytes, formatDuration } from '../progress.js'
25
25
  import { clearState, loadState, markChunkDone, saveState, stateDir, stateKey } from '../state.js'
26
26
  import { uploadRange } from '../uploader.js'
27
27
 
28
- // Trên ngưỡng này thì phải nói đang chờ bao lâu, theo spec §8.
28
+ // Above this threshold the wait must be spelled out, per spec §8.
29
29
  const LONG_WAIT_MS = 60_000
30
30
 
31
31
  async function realSendChunk(client, peer, { inputFile, fileName, caption }) {
@@ -65,12 +65,12 @@ export async function runUpload(filePath, options = {}, deps = {}) {
65
65
  try {
66
66
  stat = await fs.stat(absPath)
67
67
  } catch (err) {
68
- if (err.code === 'ENOENT') throw new Error(`File không tồn tại: ${absPath}`)
68
+ if (err.code === 'ENOENT') throw new Error(`File does not exist: ${absPath}`)
69
69
  throw err
70
70
  }
71
71
 
72
72
  if (!stat.isFile()) {
73
- throw new Error(`${absPath} không phải file.`)
73
+ throw new Error(`${absPath} is not a file.`)
74
74
  }
75
75
 
76
76
  const config = await loadConfig(configDir)
@@ -80,9 +80,9 @@ export async function runUpload(filePath, options = {}, deps = {}) {
80
80
 
81
81
  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
82
82
  throw new Error(
83
- `--concurrency không hợp lệ: "${options.concurrency}". ` +
84
- `Phải số nguyên từ 1 đến ${MAX_CONCURRENCY} — mỗi luồng giữ một phần 512KB trong RAM ` +
85
- ' Telegram sẽ bắt chờ FLOOD_WAIT nếu bắn quá nhiều request cùng lúc.',
83
+ `Invalid --concurrency: "${options.concurrency}". ` +
84
+ `Must be an integer from 1 to ${MAX_CONCURRENCY} — each slot holds a 512KB part in RAM ` +
85
+ 'and Telegram answers with FLOOD_WAIT if too many requests go out at once.',
86
86
  )
87
87
  }
88
88
 
@@ -95,9 +95,9 @@ export async function runUpload(filePath, options = {}, deps = {}) {
95
95
  if (resuming && state.chat !== String(chat)) {
96
96
  const stateFile = path.join(stateDir(configDir), `${key}.json`)
97
97
  throw new Error(
98
- `Backup dở dang này đang gửi vào ${state.chat}, nhưng lệnh hiện tại chỉ định đích ${chat} — ` +
99
- `không thể tách một backup ra hai đích khác nhau. Chạy lại không kèm --to để tiếp tục gửi vào ` +
100
- `${state.chat}, hoặc xoá ${stateFile} rồi chạy lại để bắt đầu backup mới vào ${chat}.`,
98
+ `This unfinished backup is going to ${state.chat}, but the current command targets ${chat} — ` +
99
+ `a single backup cannot be split across two destinations. Run again without --to to keep ` +
100
+ `sending to ${state.chat}, or delete ${stateFile} and run again to start a new backup in ${chat}.`,
101
101
  )
102
102
  }
103
103
 
@@ -121,33 +121,33 @@ export async function runUpload(filePath, options = {}, deps = {}) {
121
121
  const log = silent ? () => {} : (line) => console.log(line)
122
122
  const warn = silent ? () => {} : writeErr
123
123
 
124
- // Thử lại FLOOD_WAIT phải nói ra: một FLOOD_WAIT_3600 im lặng thì người
125
- // dùng chỉ thấy thanh tiến độ đứng hình cả tiếng tưởng máy treo.
124
+ // Retries and FLOOD_WAIT must be announced: a silent FLOOD_WAIT_3600 leaves the user
125
+ // staring at a frozen progress bar for an hour, assuming the process has hung.
126
126
  function onRetry(err, attempt, delayMs) {
127
127
  if (delayMs > LONG_WAIT_MS) {
128
128
  warn(
129
- `\nTelegram bắt chờ ${formatDuration(delayMs / 1000)} rồi mới cho gửi tiếp ` +
130
- `(${err.message}). data-ark đang đợi sẽ tự đi tiếp, đừng tắt.\n`,
129
+ `\nTelegram wants ${formatDuration(delayMs / 1000)} of waiting before the next send ` +
130
+ `(${err.message}). data-ark is waiting and will carry on by itself, leave it running.\n`,
131
131
  )
132
132
  return
133
133
  }
134
134
 
135
135
  warn(
136
- `\nLỗi tạm thời (${err.message}), thử lại lần ${attempt} sau ` +
136
+ `\nTemporary error (${err.message}), retry ${attempt} in ` +
137
137
  `${formatDuration(delayMs / 1000)}.\n`,
138
138
  )
139
139
  }
140
140
 
141
141
  log(`Backup ${state.id}`)
142
- log(`File ${absPath} (${formatBytes(stat.size)}, ${chunks.length} chunk)`)
143
- log(`Đích ${chat}\n`)
142
+ log(`File ${absPath} (${formatBytes(stat.size)}, ${chunks.length} chunks)`)
143
+ log(`To ${chat}\n`)
144
144
 
145
145
  const client = await connect(config)
146
146
 
147
147
  try {
148
148
  for (const chunk of chunks) {
149
149
  if (state.done[String(chunk.i)]) {
150
- log(`Chunk ${chunk.i + 1}/${chunks.length} đã có, bỏ qua.`)
150
+ log(`Chunk ${chunk.i + 1}/${chunks.length} already uploaded, skipping.`)
151
151
  continue
152
152
  }
153
153
 
@@ -193,17 +193,17 @@ export async function runUpload(filePath, options = {}, deps = {}) {
193
193
  }
194
194
  }
195
195
 
196
- // File thể bị ghi đè trong lúc upload — với file 50GB thì cả tiếng đồng
197
- // hồ trôi qua giữa chunk đầu chunk cuối. Khi đó manifest tả một file
198
- // lai chưa từng tồn tại: restore vẫn khớp sha256 nhưng dữ liệu rác.
196
+ // The file can be overwritten mid-upload — with a 50GB file an hour passes between
197
+ // the first and last chunk. The manifest would then describe a hybrid file that never
198
+ // existed: restore still matches every sha256, but the data is garbage.
199
199
  const after = await fs.stat(absPath)
200
200
 
201
201
  if (after.size !== stat.size || after.mtimeMs !== stat.mtimeMs) {
202
202
  throw new Error(
203
- `${absPath} đã thay đổi trong lúc upload ` +
204
- `(kích thước ${stat.size} → ${after.size}, mtime ${stat.mtimeMs} → ${after.mtimeMs}). ` +
205
- 'Backup này trộn dữ liệu với dữ liệu mới nên không đáng tin — data-ark không gửi manifest. ' +
206
- 'Chờ file ổn định rồi chạy lại để tạo một backup mới.',
203
+ `${absPath} changed during the upload ` +
204
+ `(size ${stat.size} → ${after.size}, mtime ${stat.mtimeMs} → ${after.mtimeMs}). ` +
205
+ 'This backup mixes old and new data and cannot be trusted — data-ark is not sending the manifest. ' +
206
+ 'Wait until the file settles, then run again to create a new backup.',
207
207
  )
208
208
  }
209
209
 
@@ -223,15 +223,15 @@ export async function runUpload(filePath, options = {}, deps = {}) {
223
223
 
224
224
  await clearState(key, configDir)
225
225
 
226
- log(`\nXong. Khôi phục bằng:\n npx data-ark restore ${state.id}`)
226
+ log(`\nDone. Restore with:\n npx data-ark restore ${state.id}`)
227
227
 
228
228
  return { id: state.id, chunks: chunks.length }
229
229
  } finally {
230
- // Ngắt kết nối hỏng thì cũng không được nuốt mất lỗi thật đang bay lên.
230
+ // A failing disconnect must not swallow the real error already on its way up.
231
231
  try {
232
232
  await disconnect(client)
233
233
  } catch (err) {
234
- warn(`\nCảnh báo: không đóng được kết nối Telegram: ${err.message}\n`)
234
+ warn(`\nWarning: could not close the Telegram connection: ${err.message}\n`)
235
235
  }
236
236
  }
237
237
  }
package/src/config.js CHANGED
@@ -22,7 +22,7 @@ export async function loadConfig(dir = defaultConfigDir()) {
22
22
  try {
23
23
  return JSON.parse(raw)
24
24
  } catch {
25
- throw new Error(`File cấu hình hỏng: ${file}. Xoá rồi chạy lại "data-ark login".`)
25
+ throw new Error(`Corrupt config file: ${file}. Delete it and run "data-ark login" again.`)
26
26
  }
27
27
  }
28
28
 
package/src/downloader.js CHANGED
@@ -19,17 +19,17 @@ export async function downloadToFile(client, message, fd, { offset, onProgress }
19
19
  const document = message?.media?.document
20
20
 
21
21
  if (!document) {
22
- throw new Error(`Message ${message?.id} không chứa file đính kèm.`)
22
+ throw new Error(`Message ${message?.id} has no file attached.`)
23
23
  }
24
24
 
25
25
  if (!Number.isFinite(offset)) {
26
- throw new Error(`offset phải một số hữu hạn, nhận được: ${offset}`)
26
+ throw new Error(`offset must be a finite number, got: ${offset}`)
27
27
  }
28
28
 
29
29
  const hash = createHash('sha256')
30
30
  let written = 0
31
31
 
32
- for await (const buffer of client.iterDownload({ file: document, requestSize: PART_SIZE })) {
32
+ for await (const buffer of client.iterDownload({ file: message.media, requestSize: PART_SIZE })) {
33
33
  await writeExactly(fd, buffer, offset + written)
34
34
  hash.update(buffer)
35
35
  written += buffer.length
package/src/manifest.js CHANGED
@@ -42,48 +42,49 @@ export function parseManifest(input) {
42
42
  try {
43
43
  manifest = JSON.parse(text)
44
44
  } catch {
45
- throw new Error('Không đọc được manifest: nội dung không phải JSON hợp lệ.')
45
+ throw new Error('Cannot read manifest: content is not valid JSON.')
46
46
  }
47
47
 
48
48
  if (manifest.v !== MANIFEST_VERSION) {
49
49
  throw new Error(
50
- `Manifest dùng phiên bản ${manifest.v}, bản data-ark này chỉ hiểu phiên bản ${MANIFEST_VERSION}.`,
50
+ `Manifest uses version ${manifest.v}, this build of data-ark only understands version ${MANIFEST_VERSION}.`,
51
51
  )
52
52
  }
53
53
 
54
54
  if (!Array.isArray(manifest.chunks) || manifest.chunks.length === 0) {
55
- throw new Error('Manifest thiếu danh sách chunk.')
55
+ throw new Error('Manifest has no chunk list.')
56
56
  }
57
57
 
58
58
  manifest.chunks.forEach((chunk, index) => {
59
59
  if (chunk.i !== index) {
60
- throw new Error(`Manifest thiếu chunk số ${index}: danh sách chunk không liên tục.`)
60
+ throw new Error(`Manifest is missing chunk ${index}: the chunk list is not contiguous.`)
61
61
  }
62
62
  })
63
63
 
64
64
  const expectedChunks = Math.ceil(manifest.size / manifest.chunkSize)
65
65
  if (manifest.chunks.length !== expectedChunks) {
66
- throw new Error(`Manifest thiếu ${expectedChunks - manifest.chunks.length} chunk(s).`)
66
+ throw new Error(`Manifest is missing ${expectedChunks - manifest.chunks.length} chunk(s).`)
67
67
  }
68
68
 
69
69
  const total = manifest.chunks.reduce((sum, chunk) => sum + chunk.size, 0)
70
70
  if (total !== manifest.size) {
71
71
  throw new Error(
72
- `Tổng kích thước các chunk (${total}) không khớp kích thước file trong manifest (${manifest.size}).`,
72
+ `Chunk sizes add up to ${total}, but the manifest records a file size of ${manifest.size}.`,
73
73
  )
74
74
  }
75
75
 
76
- // Restore ghi chunk i vào đúng offset i * chunkSize, nên bố cục phải đều:
77
- // mọi chunk đúng bằng chunkSize, riêng chunk cuối phần dư. Tổng đúng mà
78
- // từng chunk lệch thì file ghép ra sẽ thủng lỗ hoặc dài thêm mà sha256 từng
79
- // chunk vẫn khớp sai âm thầm, đúng thứ data-ark không được phép để xảy ra.
76
+ // Restore writes chunk i at exactly offset i * chunkSize, so the layout must be
77
+ // uniform: every chunk is chunkSize, except the last one which is the remainder.
78
+ // A correct total with individually wrong sizes yields a file with a hole or
79
+ // extra length while every per-chunk sha256 still matches silently wrong data,
80
+ // precisely what data-ark must never produce.
80
81
  manifest.chunks.forEach((chunk, index) => {
81
82
  const expected = Math.min(manifest.chunkSize, manifest.size - index * manifest.chunkSize)
82
83
  if (chunk.size !== expected) {
83
84
  throw new Error(
84
- `Chunk ${index + 1} trong manifest ghi ${chunk.size} byte, nhưng theo bố cục ` +
85
- `${manifest.chunkSize} byte mỗi chunk thì phải là ${expected} byte. ` +
86
- 'Manifest này tả sai vị trí các chunk, khôi phục sẽ ra file hỏng.',
85
+ `Manifest records ${chunk.size} bytes for chunk ${index + 1}, but a layout of ` +
86
+ `${manifest.chunkSize} bytes per chunk requires ${expected} bytes. ` +
87
+ 'This manifest describes the wrong chunk positions; restoring it would produce a corrupt file.',
87
88
  )
88
89
  }
89
90
  })
package/src/progress.js CHANGED
@@ -33,13 +33,13 @@ export function renderProgress({ done, total, elapsedMs, label, width = 24 }) {
33
33
  const bar = `${'█'.repeat(filled)}${'░'.repeat(width - filled)}`
34
34
 
35
35
  const bytesPerSecond = elapsedMs > 0 ? done / (elapsedMs / 1000) : 0
36
- // Kẹp về 0: done thể vượt total (đếm một nhịp) ETA âm thì nghĩa.
36
+ // Clamp to 0: done can overshoot total (one extra tick) and a negative ETA is meaningless.
37
37
  const remaining = bytesPerSecond > 0 ? Math.max(0, (total - done) / bytesPerSecond) : Infinity
38
38
 
39
39
  const percent = String(Math.floor(ratio * 100)).padStart(3)
40
40
  const speed = `${formatBytes(Math.round(bytesPerSecond))}/s`
41
41
 
42
- return `${label} ${bar} ${percent}% ${formatBytes(done)}/${formatBytes(total)} ${speed} còn ${formatDuration(remaining)}`
42
+ return `${label} ${bar} ${percent}% ${formatBytes(done)}/${formatBytes(total)} ${speed} ETA ${formatDuration(remaining)}`
43
43
  }
44
44
 
45
45
  export function createProgress({
package/src/uploader.js CHANGED
@@ -10,9 +10,9 @@ import { withRetry } from './retry.js'
10
10
 
11
11
  const read = promisify(readCallback)
12
12
 
13
- // Telegram tách hai API upload theo kích thước file: trên 10MB mới được dùng
14
- // nhóm "big". GramJS chọn đúng ngưỡng này (LARGE_FILE_THRESHOLD trong
15
- // node_modules/telegram/client/uploads.js), data-ark bám theo.
13
+ // Telegram splits its upload API by file size: only files above 10MB may use the
14
+ // "big" family. GramJS picks the same threshold (LARGE_FILE_THRESHOLD in
15
+ // node_modules/telegram/client/uploads.js), and data-ark follows it.
16
16
  export const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024
17
17
 
18
18
  async function readExactly(fd, length, position) {
@@ -22,7 +22,7 @@ async function readExactly(fd, length, position) {
22
22
  while (filled < length) {
23
23
  const { bytesRead } = await read(fd, buffer, filled, length - filled, position + filled)
24
24
  if (bytesRead === 0) {
25
- throw new Error(`Đọc hụt: cần ${length} byte từ vị trí ${position} nhưng file đã hết.`)
25
+ throw new Error(`Short read: needed ${length} bytes at offset ${position} but the file ended.`)
26
26
  }
27
27
  filled += bytesRead
28
28
  }
@@ -45,7 +45,7 @@ export async function uploadRange(client, fd, options) {
45
45
 
46
46
  if (totalParts > MAX_PARTS) {
47
47
  throw new Error(
48
- `Dải byte này cần ${totalParts} phần, trong khi Telegram chỉ nhận tối đa 4000 phần mỗi file.`,
48
+ `This byte range needs ${totalParts} parts, but Telegram accepts at most 4000 parts per file.`,
49
49
  )
50
50
  }
51
51
 
@@ -73,7 +73,7 @@ export async function uploadRange(client, fd, options) {
73
73
  let sendFailed = false
74
74
  let readError = null
75
75
 
76
- // Đọc tuần tự để hash đúng thứ tự, gửi song song.
76
+ // Read sequentially so the hash sees parts in order, but send in parallel.
77
77
  try {
78
78
  for (let part = start; part < end; part += 1) {
79
79
  const partOffset = part * partSize
@@ -82,17 +82,18 @@ export async function uploadRange(client, fd, options) {
82
82
 
83
83
  hash.update(bytes)
84
84
 
85
- // Gắn handler ngay lúc tạo promise, không đợi Promise.all cuối lô:
86
- // nếu readExactly ném part sau, một promise đã push chưa ai bắt sẽ
87
- // nổ thành unhandledRejection che mất lỗi thật.
85
+ // Attach the handler when the promise is created rather than waiting for the
86
+ // Promise.all at the end of the batch: if readExactly throws on a later part,
87
+ // an already-pushed promise with no handler becomes an unhandledRejection and
88
+ // hides the real error.
88
89
  sending.push(
89
90
  withRetry(() => client.invoke(partRequest(part, bytes)), retryOptions).then(
90
91
  () => onProgress?.(partLength),
91
92
  (err) => {
92
- // Không dùng `sendError ??= err`: nếu do reject falsy
93
- // (undefined/null), phép gán đó vẫn set sendError = err (falsy)
94
- // rồi `if (sendError)` bên dưới coi như không có lỗi nuốt mất
95
- // một lần gửi thất bại biến thành "thành công" giả.
93
+ // Not `sendError ??= err`: if the rejection reason is falsy
94
+ // (undefined/null), that assignment still sets sendError to a falsy
95
+ // value and the `if (sendError)` below reads as "no error"swallowing
96
+ // a failed send and turning it into a fake success.
96
97
  if (!sendFailed) {
97
98
  sendFailed = true
98
99
  sendError = err
@@ -105,8 +106,8 @@ export async function uploadRange(client, fd, options) {
105
106
  readError = err
106
107
  }
107
108
 
108
- // Mọi promise trong sending đều đã tự nuốt lỗi trên nên luôn resolve;
109
- // await đây chỉ để chắc chắn không còn request nào đang bay khi ném lỗi.
109
+ // Every promise in `sending` already absorbed its own error above, so they all
110
+ // resolve; this await only guarantees no request is still in flight when we throw.
110
111
  await Promise.all(sending)
111
112
 
112
113
  if (readError) throw readError