telstore 0.1.3 โ 0.1.5
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 +0 -1
- package/bin/telstore.js +50 -23
- package/package.json +2 -2
- package/src/caption.js +0 -3
- package/src/client.js +8 -20
- package/src/commands/delete.js +1 -1
- package/src/commands/list.js +1 -1
- package/src/commands/login.js +8 -7
- package/src/commands/status.js +2 -1
- package/src/commands/token.js +1 -2
- package/src/commands/upload.js +2 -2
- package/src/downloader.js +8 -4
- package/src/retry.js +7 -1
- package/src/session.js +14 -1
- package/src/stall.js +11 -9
- package/src/token.js +1 -1
- package/src/uploader.js +7 -7
package/README.md
CHANGED
package/bin/telstore.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { route, HELP, interruptMessage } from '../src/cli.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { runRestore } from '../src/commands/restore.js'
|
|
9
|
-
import { runStatus } from '../src/commands/status.js'
|
|
10
|
-
import { runToken } from '../src/commands/token.js'
|
|
11
|
-
import { runUpload } from '../src/commands/upload.js'
|
|
3
|
+
|
|
4
|
+
// Each command is imported where it runs, not here. Importing all nine up front pulled
|
|
5
|
+
// teleproto into every invocation โ about 0.3s and 45MB โ including `--help`, `config` and
|
|
6
|
+
// `logout`, which never open a socket. `src/cli.js` stays a static import because parsing the
|
|
7
|
+
// arguments is the one thing every run does, and nothing it touches reaches the network.
|
|
12
8
|
|
|
13
9
|
const SIGINT_EXIT_CODE = 130
|
|
14
10
|
|
|
@@ -45,66 +41,97 @@ async function main() {
|
|
|
45
41
|
process.stdout.write(HELP)
|
|
46
42
|
return
|
|
47
43
|
|
|
48
|
-
case 'login':
|
|
44
|
+
case 'login': {
|
|
45
|
+
const { runLogin } = await import('../src/commands/login.js')
|
|
46
|
+
|
|
49
47
|
await runLogin({
|
|
50
48
|
args: parsed.args,
|
|
51
49
|
token: Boolean(parsed.options.token),
|
|
52
50
|
verbose: Boolean(parsed.options.verbose),
|
|
53
51
|
})
|
|
54
52
|
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'logout': {
|
|
56
|
+
const { runLogout } = await import('../src/commands/logout.js')
|
|
55
57
|
|
|
56
|
-
case 'logout':
|
|
57
58
|
await runLogout()
|
|
58
59
|
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'list': {
|
|
63
|
+
const { runList } = await import('../src/commands/list.js')
|
|
59
64
|
|
|
60
|
-
case 'list':
|
|
61
65
|
await runList(parsed.options)
|
|
62
66
|
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'status': {
|
|
70
|
+
const { runStatus } = await import('../src/commands/status.js')
|
|
63
71
|
|
|
64
|
-
case 'status':
|
|
65
72
|
await runStatus(parsed.options)
|
|
66
73
|
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'config': {
|
|
77
|
+
const { runConfig } = await import('../src/commands/config.js')
|
|
67
78
|
|
|
68
|
-
case 'config':
|
|
69
79
|
await runConfig(parsed.args, parsed.options)
|
|
70
80
|
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'token': {
|
|
84
|
+
const { runToken } = await import('../src/commands/token.js')
|
|
71
85
|
|
|
72
|
-
case 'token':
|
|
73
86
|
await runToken(parsed.args, parsed.options)
|
|
74
87
|
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'upload': {
|
|
91
|
+
const { runUpload } = await import('../src/commands/upload.js')
|
|
75
92
|
|
|
76
|
-
case 'upload':
|
|
77
93
|
await runUpload(parsed.args[0], parsed.options, {
|
|
78
94
|
onBackupId: (id) => {
|
|
79
95
|
currentBackupId = id
|
|
80
96
|
},
|
|
81
97
|
})
|
|
82
98
|
return
|
|
99
|
+
}
|
|
83
100
|
|
|
84
|
-
case 'restore':
|
|
101
|
+
case 'restore': {
|
|
85
102
|
if (!parsed.args[0]) {
|
|
86
103
|
throw new Error('Missing backup id. Example: npx telstore restore telstore-20260905-7f3a91')
|
|
87
104
|
}
|
|
105
|
+
|
|
106
|
+
const { runRestore } = await import('../src/commands/restore.js')
|
|
107
|
+
|
|
88
108
|
await runRestore(parsed.args[0], parsed.options)
|
|
89
109
|
return
|
|
110
|
+
}
|
|
90
111
|
|
|
91
|
-
case 'delete':
|
|
112
|
+
case 'delete': {
|
|
92
113
|
if (!parsed.args[0]) {
|
|
93
114
|
throw new Error('Missing backup id. Example: npx telstore delete telstore-20260905-7f3a91')
|
|
94
115
|
}
|
|
116
|
+
|
|
117
|
+
const { runDelete } = await import('../src/commands/delete.js')
|
|
118
|
+
|
|
95
119
|
await runDelete(parsed.args[0], parsed.options)
|
|
96
120
|
return
|
|
121
|
+
}
|
|
97
122
|
|
|
98
123
|
default:
|
|
99
124
|
throw new Error(`Unknown command: ${parsed.command}`)
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
// GramJS
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
128
|
+
// Written for GramJS, which kept "exported senders" alive behind a 30-second release timer
|
|
129
|
+
// that neither disconnect() nor destroy() could clear โ both walked a Map with Object.values
|
|
130
|
+
// and missed every entry, so a command printed "Done" and then hung for half a minute, during
|
|
131
|
+
// which Ctrl-C falsely reported that nothing had been saved. teleproto replaced that pool
|
|
132
|
+
// wholesale and closes it on destroy(). This stays anyway: a CLI that has written its last
|
|
133
|
+
// line has nothing left to wait for, and one stray timer in any dependency is all it takes
|
|
134
|
+
// for the wait to come back.
|
|
108
135
|
function exitWhenFlushed(code) {
|
|
109
136
|
// The empty writes exist only to borrow their callbacks: they fire after everything
|
|
110
137
|
// queued earlier has flushed, so nothing is lost when stdout/stderr is not a TTY.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telstore",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Split large files into chunks and store them on Telegram",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telegram",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test": "node --test 'test/**/*.test.js'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"
|
|
40
|
+
"teleproto": "^1.229.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/caption.js
CHANGED
|
@@ -4,8 +4,6 @@ import { formatBytes } from './progress.js'
|
|
|
4
4
|
// but that turns every file name into something that has to be escaped correctly, and
|
|
5
5
|
// the fake client the tests talk to would never notice a mistake there.
|
|
6
6
|
|
|
7
|
-
const DIVIDER = 'โ'.repeat(15)
|
|
8
|
-
|
|
9
7
|
// The hashtag is what `list` searches for, and it lives on the manifest alone: chunk
|
|
10
8
|
// captions stay out of that search so a twelve-chunk backup is one hit, not thirteen.
|
|
11
9
|
export const MANIFEST_TAG = '#telstore'
|
|
@@ -27,7 +25,6 @@ export function chunkCaption({ id, number, total }) {
|
|
|
27
25
|
export function manifestCaption({ id, name, size, chunks, createdAt }) {
|
|
28
26
|
return [
|
|
29
27
|
`๐ ${oneLine(name)}`,
|
|
30
|
-
DIVIDER,
|
|
31
28
|
`๐พ ${formatBytes(size)} ยท ${chunks} chunk${chunks === 1 ? '' : 's'}`,
|
|
32
29
|
`๐ ${id}`,
|
|
33
30
|
`๐
${utcMinutes(createdAt)}`,
|
package/src/client.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { Api, TelegramClient } from '
|
|
2
|
-
import { Logger } from '
|
|
3
|
-
import { LogLevel } from '
|
|
4
|
-
import { StringSession } from '
|
|
1
|
+
import { Api, TelegramClient } from 'teleproto'
|
|
2
|
+
import { Logger } from 'teleproto/extensions/index.js'
|
|
3
|
+
import { LogLevel } from 'teleproto/extensions/Logger.js'
|
|
4
|
+
import { StringSession } from 'teleproto/sessions/index.js'
|
|
5
5
|
|
|
6
6
|
import { manifestFileName } from './manifest.js'
|
|
7
7
|
import { withRetry } from './retry.js'
|
|
8
|
-
import { unlockConfig } from './session.js'
|
|
8
|
+
import { assertLoggedIn, unlockConfig } from './session.js'
|
|
9
9
|
import { DEFAULT_STALL_MS, withStallTimeout } from './stall.js'
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// teleproto narrates its version, every connection and every disconnect at info level, and
|
|
12
12
|
// those timestamped lines land in the middle of the progress bar. The client reads this
|
|
13
13
|
// logger before it prints anything, so LogLevel.NONE silences all of it; --verbose asks
|
|
14
14
|
// for the running commentary back when a connection needs diagnosing.
|
|
@@ -58,7 +58,7 @@ export async function readMessageBytes(client, message) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// The one place telstore removes messages from a chat, and the mirror of searchDocuments
|
|
61
|
-
// above.
|
|
61
|
+
// above. teleproto has its own deleteMessages, and it is the right thing to call โ it resolves
|
|
62
62
|
// the peer and picks between channels.DeleteMessages and messages.DeleteMessages, which is
|
|
63
63
|
// exactly the choice a fake client would never catch us getting wrong.
|
|
64
64
|
//
|
|
@@ -87,7 +87,7 @@ export async function deleteMessages(client, peer, ids, options = {}) {
|
|
|
87
87
|
|
|
88
88
|
await withRetry(
|
|
89
89
|
() =>
|
|
90
|
-
// The options object is not optional:
|
|
90
|
+
// The options object is not optional: teleproto destructures `{ revoke }` with no
|
|
91
91
|
// default of its own, so a two-argument call throws a TypeError before it ever
|
|
92
92
|
// reaches the network. revoke is passed explicitly anyway โ a backup has to go for
|
|
93
93
|
// everyone who can see the chat, and that intent belongs in our code rather than in
|
|
@@ -122,18 +122,6 @@ export async function closeQuietly(client, disconnect, onWarn) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Two shapes count as logged in: the ordinary one login writes, and the sealed blob that
|
|
126
|
-
// "login --token" leaves, which holds the same three fields behind a passphrase. Checked
|
|
127
|
-
// before anything asks for that passphrase, so somebody who never logged in is told so rather
|
|
128
|
-
// than asked to type a secret for an account that is not there.
|
|
129
|
-
export function assertLoggedIn(config) {
|
|
130
|
-
if (config.sealed) return
|
|
131
|
-
|
|
132
|
-
if (!config.session || !config.apiId || !config.apiHash) {
|
|
133
|
-
throw new Error('Not logged in โ run "npx telstore login" first.')
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
125
|
// Every command that needs Telegram comes through here, which makes this the one place a
|
|
138
126
|
// sealed session has to be opened. Doing it anywhere else would mean eight places to keep in
|
|
139
127
|
// step, and a ninth command would simply forget.
|
package/src/commands/delete.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { chatName, describeChat } from '../chat.js'
|
|
2
2
|
import {
|
|
3
3
|
DELETE_BATCH_SIZE,
|
|
4
|
-
assertLoggedIn,
|
|
5
4
|
closeQuietly,
|
|
6
5
|
connect as realConnect,
|
|
7
6
|
deleteMessages as realDeleteMessages,
|
|
@@ -12,6 +11,7 @@ import { askConfirm } from '../confirm.js'
|
|
|
12
11
|
import { configFile, defaultConfigDir, loadConfig } from '../config.js'
|
|
13
12
|
import { manifestFileName, manifestMessageIds, parseManifestJson } from '../manifest.js'
|
|
14
13
|
import { formatBytes, formatDuration } from '../progress.js'
|
|
14
|
+
import { assertLoggedIn } from '../session.js'
|
|
15
15
|
import { requireChat, resolveSettings } from '../settings.js'
|
|
16
16
|
import { clearState, findStates } from '../state.js'
|
|
17
17
|
|
package/src/commands/list.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { MANIFEST_TAG, parseManifestCaption } from '../caption.js'
|
|
2
2
|
import { chatName, describeChat } from '../chat.js'
|
|
3
3
|
import {
|
|
4
|
-
assertLoggedIn,
|
|
5
4
|
closeQuietly,
|
|
6
5
|
connect as realConnect,
|
|
7
6
|
searchDocuments,
|
|
8
7
|
} from '../client.js'
|
|
9
8
|
import { configFile, defaultConfigDir, loadConfig } from '../config.js'
|
|
9
|
+
import { assertLoggedIn } from '../session.js'
|
|
10
10
|
import { requireChat, resolveSettings } from '../settings.js'
|
|
11
11
|
|
|
12
12
|
const UNKNOWN = 'โ'
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TelegramClient } from '
|
|
2
|
-
import { StringSession } from '
|
|
1
|
+
import { TelegramClient } from 'teleproto'
|
|
2
|
+
import { StringSession } from 'teleproto/sessions/index.js'
|
|
3
3
|
|
|
4
4
|
import { loadConfig, saveConfig, defaultConfigDir } from '../config.js'
|
|
5
5
|
import { normalizeChatTarget } from '../chat.js'
|
|
@@ -26,11 +26,12 @@ export function describeLoginError(err) {
|
|
|
26
26
|
return `${description} (${message})`
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// printed straight over the destination question login asks after signing in.
|
|
33
|
-
// command shuts down
|
|
29
|
+
// An update loop starts the moment a client connects, and destroy() is what puts the whole
|
|
30
|
+
// client down โ the sender pools included. Under GramJS, disconnect() alone left that loop
|
|
31
|
+
// pinging a closed socket, every ping failing with "Error: TIMEOUT" and asking for a
|
|
32
|
+
// reconnect, printed straight over the destination question login asks after signing in.
|
|
33
|
+
// Every command shuts down through destroy() for that reason, and login is the seam tests
|
|
34
|
+
// need to reach it.
|
|
34
35
|
const createTelegramClient = (apiId, apiHash, options) =>
|
|
35
36
|
new TelegramClient(new StringSession(''), apiId, apiHash, options)
|
|
36
37
|
|
package/src/commands/status.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { countChunks } from '../chunking.js'
|
|
2
2
|
import { describeChat } from '../chat.js'
|
|
3
|
-
import {
|
|
3
|
+
import { closeQuietly, connect as realConnect } from '../client.js'
|
|
4
4
|
import { configFile, defaultConfigDir, loadConfig } from '../config.js'
|
|
5
5
|
import { formatBytes } from '../progress.js'
|
|
6
|
+
import { assertLoggedIn } from '../session.js'
|
|
6
7
|
import { resolveSettings } from '../settings.js'
|
|
7
8
|
import { canResume, listStates } from '../state.js'
|
|
8
9
|
|
package/src/commands/token.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { assertLoggedIn } from '../client.js'
|
|
2
1
|
import { defaultConfigDir, loadConfig } from '../config.js'
|
|
3
2
|
import { createPrompts } from '../prompt.js'
|
|
4
|
-
import { unlockConfig } from '../session.js'
|
|
3
|
+
import { assertLoggedIn, unlockConfig } from '../session.js'
|
|
5
4
|
import { knownSettings } from '../settings.js'
|
|
6
5
|
import { encodeToken } from '../token.js'
|
|
7
6
|
|
package/src/commands/upload.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { Api } from '
|
|
5
|
-
import { CustomFile } from '
|
|
4
|
+
import { Api } from 'teleproto'
|
|
5
|
+
import { CustomFile } from 'teleproto/client/uploads.js'
|
|
6
6
|
|
|
7
7
|
import { PART_SIZE, planChunks } from '../chunking.js'
|
|
8
8
|
import { chunkCaption, manifestCaption } from '../caption.js'
|
package/src/downloader.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { read as readCallback, write as writeCallback } from 'node:fs'
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
|
|
5
|
-
import { returnBigInt } from '
|
|
5
|
+
import { returnBigInt } from 'teleproto/Helpers.js'
|
|
6
6
|
|
|
7
7
|
import { DEFAULT_DOWNLOAD_CONCURRENCY, PART_SIZE, SLICE_SIZE } from './chunking.js'
|
|
8
8
|
import { withRetry } from './retry.js'
|
|
@@ -105,10 +105,14 @@ export async function downloadToFile(
|
|
|
105
105
|
// Iterated by hand rather than with `for await` so each part can be given a deadline:
|
|
106
106
|
// a stalled stream yields nothing and raises nothing, and only a race against a timer
|
|
107
107
|
// turns that silence into an error withRetry can act on. Nothing is lost by stepping
|
|
108
|
-
// outside `for await` โ
|
|
108
|
+
// outside `for await` โ teleproto's download iterator exposes `next` alone, so breaking
|
|
109
109
|
// out of the loop never closed anything either.
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
//
|
|
111
|
+
// The media goes in its own argument, not inside the options: teleproto takes
|
|
112
|
+
// `iterDownload(file, params)` where GramJS took a single `{ file, ... }` object, and
|
|
113
|
+
// the options object arriving where the file belongs is not something a fake client
|
|
114
|
+
// would ever refuse โ it fails only against the real one, at getFileInfo.
|
|
115
|
+
const stream = client.iterDownload(message.media, {
|
|
112
116
|
offset: returnBigInt(start + done),
|
|
113
117
|
requestSize: PART_SIZE,
|
|
114
118
|
})
|
package/src/retry.js
CHANGED
|
@@ -10,8 +10,14 @@ function defaultSleep(ms) {
|
|
|
10
10
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Every "wait this long" answer Telegram gives arrives as code 420 with the seconds on the
|
|
14
|
+
// error itself โ FLOOD_WAIT_n, SLOW_MODE_WAIT_n and the rest. Those two fields are what this
|
|
15
|
+
// reads, not how the message is spelled: under GramJS every flood error carried the literal
|
|
16
|
+
// errorMessage "FLOOD", so a check for "FLOOD_WAIT" in it matched nothing and each one was
|
|
17
|
+
// retried on the ordinary backoff โ asking again inside a ban that was still running.
|
|
18
|
+
// A 420 with no seconds (a frozen account) has nothing to wait for and takes the backoff.
|
|
13
19
|
function floodWaitSeconds(err) {
|
|
14
|
-
if (
|
|
20
|
+
if (err?.code === 420 && typeof err.seconds === 'number') {
|
|
15
21
|
return err.seconds
|
|
16
22
|
}
|
|
17
23
|
return null
|
package/src/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readSecret as realReadSecret } from './prompt.js'
|
|
2
2
|
import { decodeToken } from './token.js'
|
|
3
3
|
|
|
4
|
-
// How a stored config becomes the three things
|
|
4
|
+
// How a stored config becomes the three things teleproto needs. There are two shapes on disk โ
|
|
5
5
|
// the ordinary login, and the sealed blob `login --token` writes โ and this is the one place
|
|
6
6
|
// that knows the difference, so `connect` stays about Telegram and the commands stay about
|
|
7
7
|
// their own narrative.
|
|
@@ -22,3 +22,16 @@ export async function unlockConfig(config, { readSecret = realReadSecret } = {})
|
|
|
22
22
|
|
|
23
23
|
return { apiId, apiHash, session }
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
// Two shapes count as logged in: the ordinary one login writes, and the sealed blob that
|
|
27
|
+
// "login --token" leaves, which holds the same three fields behind a passphrase. It lives
|
|
28
|
+
// here rather than beside connect because knowing both shapes is this file's whole job, and
|
|
29
|
+
// because `token` is an offline command: reaching for it through src/client.js pulled the
|
|
30
|
+
// whole of teleproto into a run that never opens a socket.
|
|
31
|
+
export function assertLoggedIn(config) {
|
|
32
|
+
if (config.sealed) return
|
|
33
|
+
|
|
34
|
+
if (!config.session || !config.apiId || !config.apiHash) {
|
|
35
|
+
throw new Error('Not logged in โ run "npx telstore login" first.')
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/stall.js
CHANGED
|
@@ -10,16 +10,18 @@ const MAX_TIMER_MS = 2 ** 31 - 1
|
|
|
10
10
|
// A request that never comes back is not the same as one that fails, and only one of the
|
|
11
11
|
// two is something withRetry can do anything about.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// queue with no send loop left to drain it (network/MTProtoSender.js:148,795).
|
|
13
|
+
// This was written for a GramJS bug that abandoned requests outright: its abort path tested
|
|
14
|
+
// `_currentRetries > _reconnectRetries` against a `reconnectRetries` of Infinity and so never
|
|
15
|
+
// fired, and `_reconnect()` read a `connect()` that merely returned false as success. A real
|
|
16
|
+
// network cut mid-restore froze a transfer for eleven minutes on Linux and ended it without a
|
|
17
|
+
// printed line on Windows. teleproto closes both halves โ `connect()` throws once its attempts
|
|
18
|
+
// are spent, and `_reconnect()` catches that and rejects every pending request.
|
|
20
19
|
//
|
|
21
|
-
// The
|
|
22
|
-
//
|
|
20
|
+
// The deadline stays because the library was never the only way to arrive here. A server that
|
|
21
|
+
// accepts a request and answers nothing, a socket that stays open with nothing coming down it:
|
|
22
|
+
// there is no failure for withRetry to see, only silence. The timer is what turns that silence
|
|
23
|
+
// into an error โ and, deliberately not unref'd, it is also the handle that stops the event
|
|
24
|
+
// loop running dry and ending a transfer without a word.
|
|
23
25
|
export async function withStallTimeout(promise, ms, describe) {
|
|
24
26
|
if (!(ms > 0)) return await promise
|
|
25
27
|
|
package/src/token.js
CHANGED
|
@@ -46,7 +46,7 @@ function isPlainObject(value) {
|
|
|
46
46
|
// A token is untrusted input in the same category as a manifest or a state file. Opening one
|
|
47
47
|
// proves whoever made it knew the passphrase, not that they made it correctly โ and the
|
|
48
48
|
// unprotected format proves nothing at all. An apiId of undefined does not throw here; it
|
|
49
|
-
// reaches
|
|
49
|
+
// reaches teleproto and fails much later as something that reads like a network problem.
|
|
50
50
|
export function checkTokenBundle(bundle) {
|
|
51
51
|
if (!isPlainObject(bundle)) {
|
|
52
52
|
throw new Error(
|
package/src/uploader.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createHash, randomBytes } from 'node:crypto'
|
|
|
2
2
|
import { read as readCallback } from 'node:fs'
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
|
|
5
|
-
import { Api } from '
|
|
6
|
-
import { readBigIntFromBuffer } from '
|
|
5
|
+
import { Api } from 'teleproto'
|
|
6
|
+
import { readBigIntFromBuffer } from 'teleproto/Helpers.js'
|
|
7
7
|
|
|
8
8
|
import { DEFAULT_UPLOAD_CONCURRENCY, PART_SIZE, MAX_PARTS } from './chunking.js'
|
|
9
9
|
import { withRetry } from './retry.js'
|
|
@@ -12,8 +12,8 @@ import { DEFAULT_STALL_MS, withStallTimeout } from './stall.js'
|
|
|
12
12
|
const read = promisify(readCallback)
|
|
13
13
|
|
|
14
14
|
// Telegram splits its upload API by file size: only files above 10MB may use the
|
|
15
|
-
// "big" family.
|
|
16
|
-
// node_modules/
|
|
15
|
+
// "big" family. teleproto picks the same threshold (LARGE_FILE_THRESHOLD in
|
|
16
|
+
// node_modules/teleproto/client/uploads.js), and telstore follows it.
|
|
17
17
|
export const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024
|
|
18
18
|
|
|
19
19
|
async function readExactly(fd, length, position) {
|
|
@@ -105,9 +105,9 @@ export async function uploadRange(client, fd, options) {
|
|
|
105
105
|
sending.push(
|
|
106
106
|
withRetry(
|
|
107
107
|
() =>
|
|
108
|
-
// Same exposure as the download path: a request
|
|
109
|
-
//
|
|
110
|
-
//
|
|
108
|
+
// Same exposure as the download path: a request the server accepts and never
|
|
109
|
+
// answers settles neither way, so without a deadline this await would hold the
|
|
110
|
+
// batch open forever and the upload would end without a word.
|
|
111
111
|
withStallTimeout(
|
|
112
112
|
client.invoke(partRequest(part, bytes)),
|
|
113
113
|
stallMs,
|