telstore 0.1.5 → 0.1.7
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 +122 -161
- package/bin/telstore.js +27 -7
- package/package.json +1 -1
- package/src/caption.js +42 -4
- package/src/cli.js +73 -20
- package/src/commands/delete.js +196 -1
- package/src/commands/list.js +29 -5
- package/src/commands/restore.js +118 -1
- package/src/commands/status.js +4 -4
- package/src/commands/upload.js +255 -13
- package/src/manifest.js +21 -1
- package/src/settings.js +6 -6
- package/src/sources.js +163 -0
package/README.md
CHANGED
|
@@ -1,78 +1,71 @@
|
|
|
1
1
|
# telstore
|
|
2
2
|
|
|
3
|
-
Split large files into
|
|
3
|
+
Split large files into chunks, store them on Telegram, and restore them byte-for-byte.
|
|
4
|
+
|
|
5
|
+
telstore signs in with your own Telegram account over MTProto, not a bot: the Bot API caps
|
|
6
|
+
uploads at 50MB per file, a user account gets 2GB.
|
|
7
|
+
|
|
8
|
+
Needs Node.js 18+ and an `api_id`/`api_hash` from <https://my.telegram.org> → API
|
|
9
|
+
development tools; `login` asks for those, your phone number and the verification code.
|
|
4
10
|
|
|
5
11
|
## Quick start
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
|
-
npx telstore login
|
|
9
|
-
npx telstore config chat @my_backups
|
|
10
|
-
npx telstore data.tar
|
|
11
|
-
npx telstore
|
|
12
|
-
npx telstore
|
|
13
|
-
npx telstore
|
|
14
|
-
npx telstore list # what is already stored in the destination
|
|
14
|
+
npx telstore login # once only
|
|
15
|
+
npx telstore config chat @my_backups # where backups go, from now on
|
|
16
|
+
npx telstore data.tar # split it and send it there
|
|
17
|
+
npx telstore a.tar b.tar c.tar # or several: one backup each, one after another
|
|
18
|
+
npx telstore ./backups # or a folder: every file one level inside it
|
|
19
|
+
npx telstore list # what is already in the destination
|
|
15
20
|
npx telstore restore telstore-20260905-7f3a91
|
|
16
|
-
npx telstore delete telstore-20260905-7f3a91 # take it back out of the chat, for good
|
|
17
|
-
npx telstore token # a session token for a machine you do not trust
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
- Node.js 18 or newer.
|
|
23
|
-
- 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.
|
|
23
|
+
## Commands
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
| Command | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `telstore login` | Log in to Telegram. Add `--token` to log in with a session token instead. |
|
|
28
|
+
| `telstore <file\|folder\|pattern>...` | Split each file and upload it. Prints the `backupId` you restore with. |
|
|
29
|
+
| `telstore list` | The backups stored in the destination, newest first. |
|
|
30
|
+
| `telstore restore <backup-id>...` | Download every chunk and reassemble the file. Several ids run one after another. |
|
|
31
|
+
| `telstore delete <backup-id>...` | Remove a backup's chunks and manifest from the chat, for good. Several ids are listed and confirmed once. |
|
|
32
|
+
| `telstore status` | Account, destination, and unfinished backups. |
|
|
33
|
+
| `telstore config` | Show or change settings. |
|
|
34
|
+
| `telstore token` | Print a session token for a machine you do not trust. |
|
|
35
|
+
| `telstore logout` | Remove the locally stored session. |
|
|
26
36
|
|
|
27
|
-
## Settings
|
|
37
|
+
## Settings
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
`config` writes; flags do not. A flag applies to the run you typed it on and changes nothing
|
|
40
|
+
on disk, so `--chat @elsewhere` sends one backup elsewhere without moving the destination for
|
|
41
|
+
the next one.
|
|
32
42
|
|
|
33
43
|
```bash
|
|
34
|
-
npx telstore config
|
|
35
|
-
npx telstore config
|
|
36
|
-
npx telstore config chunkSize
|
|
37
|
-
npx telstore config chunkSize --unset # back to the default
|
|
44
|
+
npx telstore config # every setting, and whether it is yours or a default
|
|
45
|
+
npx telstore config chunkSize 500MB # change it for good
|
|
46
|
+
npx telstore config chunkSize --unset # back to the default
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
| Setting | Flag | Default | Meaning |
|
|
41
50
|
|---|---|---|---|
|
|
42
|
-
| `chat` | `--
|
|
43
|
-
| `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`.
|
|
44
|
-
| `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts
|
|
45
|
-
| `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices
|
|
46
|
-
| `limit` | `--limit` | `20` | How many backups `list` shows
|
|
47
|
-
| `verbose` | `--verbose` | off | Show the Telegram client's own connection logs
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`config` reads and writes only settings — the `api_id`, `api_hash` and session that `login`
|
|
56
|
-
stores in the same file are not reachable from it. A value is checked before it is written,
|
|
57
|
-
so `config chunkSize 9GB` is refused there and then rather than at the start of a long upload.
|
|
51
|
+
| `chat` | `--chat` | none | `@username`, `-100123…`, or `me` |
|
|
52
|
+
| `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Ceiling 1950MB. |
|
|
53
|
+
| `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts in parallel while uploading, 1–64 |
|
|
54
|
+
| `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices in parallel while restoring, 1–64 |
|
|
55
|
+
| `limit` | `--limit` | `20` | How many backups `list` shows |
|
|
56
|
+
| `verbose` | `--verbose` | off | Show the Telegram client's own connection logs |
|
|
57
|
+
|
|
58
|
+
Three flags have no setting behind them: `--out <path>` names where one restore writes,
|
|
59
|
+
`--yes` skips the confirmation an upload batch and `delete` ask for, and `--token` takes no
|
|
60
|
+
value — a token written
|
|
61
|
+
on the command line would sit in `ps` and in that machine's shell history, so it is pasted at
|
|
62
|
+
a prompt that does not echo.
|
|
58
63
|
|
|
59
64
|
## What the chat looks like
|
|
60
65
|
|
|
61
|
-
Every chunk goes up as a document captioned `📦 <backupId> · 3/12`,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
🗄 data.tar
|
|
66
|
-
💾 21.4 GB · 12 chunks
|
|
67
|
-
🆔 telstore-20260905-7f3a91
|
|
68
|
-
📅 2026-09-05 16:40 UTC
|
|
69
|
-
|
|
70
|
-
↩ npx telstore restore telstore-20260905-7f3a91
|
|
71
|
-
#telstore
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
`npx telstore list` reads those cards straight out of the chat — one search, no downloads —
|
|
75
|
-
and lays them out as a table:
|
|
66
|
+
Every chunk goes up as a document captioned `📦 <backupId> · 3/12`, followed by a manifest
|
|
67
|
+
carrying a summary card — file name, size, id, date and the restore command. `list` reads
|
|
68
|
+
those cards straight out of the chat, one search and no downloads:
|
|
76
69
|
|
|
77
70
|
```
|
|
78
71
|
Destination https://web.telegram.org/k/#@my_backups
|
|
@@ -84,141 +77,109 @@ telstore-20260901-9de447 photos.zip 940.3 MB 1 2026-09-01
|
|
|
84
77
|
2 backups. Restore with: npx telstore restore <backup-id>
|
|
85
78
|
```
|
|
86
79
|
|
|
87
|
-
|
|
88
|
-
says nothing — `list` reports what the chat holds and never fills gaps with guesses.
|
|
89
|
-
|
|
90
|
-
## How it works
|
|
91
|
-
|
|
92
|
-
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, telstore 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.
|
|
93
|
-
|
|
94
|
-
If the connection drops during an **upload**, just run the same command again — progress lives in `~/.telstore/state/` and finished chunks are skipped, keeping the same `backupId`. Two things to know about rerunning:
|
|
80
|
+
## Several files at once
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
`telstore a.tar b.tar c.tar` uploads them one after another over a single connection. Each
|
|
83
|
+
file becomes its own backup with its own `backupId`, exactly as three separate runs would
|
|
84
|
+
have produced.
|
|
99
85
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
**Restore keeps no state to resume from.** Pressing `Ctrl-C` mid-restore saves nothing — running again starts over.
|
|
103
|
-
|
|
104
|
-
## Deleting a backup
|
|
86
|
+
A **folder** stands for the files one level inside it — subfolders are named on screen and
|
|
87
|
+
left alone, hidden files are skipped. A **pattern** stands for the names it matches:
|
|
105
88
|
|
|
106
89
|
```bash
|
|
107
|
-
npx telstore
|
|
90
|
+
npx telstore ./backups # every file directly inside ./backups
|
|
91
|
+
npx telstore 'logs/*.tar' # quoted, so telstore matches it rather than the shell
|
|
92
|
+
npx telstore logs/abc* # unquoted: your shell expands it first, same result
|
|
108
93
|
```
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
the manifest from the chat and drops the local record if there is one. `--yes` skips the
|
|
112
|
-
question. **There is no undo** — Telegram is the only copy.
|
|
95
|
+
More than one file is listed, added up and confirmed before the first byte goes out:
|
|
113
96
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
instead and clears both.
|
|
117
|
-
|
|
118
|
-
The chunks go first and the manifest goes last, deliberately. The manifest is the only list
|
|
119
|
-
of the message ids, so if a delete is interrupted — Ctrl-C, a dropped connection — running
|
|
120
|
-
the same command again finds that list still there and finishes the job. Telegram says
|
|
121
|
-
nothing about an id that is already gone, so a second run costs nothing. In between the two
|
|
122
|
-
runs the backup still shows up in `list`, and a `restore` of it fails loudly rather than
|
|
123
|
-
handing over a partial file.
|
|
97
|
+
```
|
|
98
|
+
3 files, 4.20 GB, to https://web.telegram.org/k/#@my_backups
|
|
124
99
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
deletes anything at all. A manifest too damaged for `restore` to use *can* still be deleted —
|
|
129
|
-
that is usually the one you want gone.
|
|
100
|
+
a.tar 1.20 GB
|
|
101
|
+
b.tar 2.00 GB
|
|
102
|
+
c.tar 1.00 GB
|
|
130
103
|
|
|
131
|
-
|
|
104
|
+
Upload these 3 files? [y/N]
|
|
105
|
+
```
|
|
132
106
|
|
|
133
|
-
`
|
|
134
|
-
|
|
107
|
+
`--yes` skips the question; without a terminal to ask in, the run stops and says so rather
|
|
108
|
+
than reading an empty line as "no". Names that do not exist, and a file named twice, are
|
|
109
|
+
refused before anything is sent; a file that fails mid-transfer does not stop the ones after
|
|
110
|
+
it, and the run ends with a line per file and a non-zero exit code:
|
|
135
111
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
npx telstore token # asks for a passphrase, twice
|
|
139
|
-
# prints one line: tls1.…
|
|
112
|
+
```
|
|
113
|
+
3 files: 2 uploaded, 1 failed.
|
|
140
114
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
npx telstore logout # when you are done
|
|
115
|
+
a.tar telstore-20260905-7f3a91 (12 chunks)
|
|
116
|
+
b.tar failed: connection dropped mid-transfer
|
|
117
|
+
c.tar telstore-20260905-9de447 (1 chunk)
|
|
145
118
|
```
|
|
146
119
|
|
|
147
|
-
|
|
148
|
-
passphrase (AES-256-GCM, key derived with scrypt). It is one line of base64url, so it
|
|
149
|
-
survives a chat message, an email or a QR code — and a copy that leaks without the
|
|
150
|
-
passphrase is not enough to use your account.
|
|
120
|
+
## Several backups at once
|
|
151
121
|
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
passphrase and opens it in memory:
|
|
122
|
+
`restore` and `delete` take a list of ids the same way, over one connection, with a summary
|
|
123
|
+
and a non-zero exit code if any of them failed:
|
|
155
124
|
|
|
156
|
-
```
|
|
157
|
-
|
|
125
|
+
```bash
|
|
126
|
+
npx telstore restore telstore-20260905-7f3a91 telstore-20260901-9de447
|
|
127
|
+
npx telstore delete telstore-20260905-7f3a91 telstore-20260901-9de447
|
|
158
128
|
```
|
|
159
129
|
|
|
160
|
-
|
|
130
|
+
With several ids, `restore` writes each file under the name in its own manifest, so `--out`
|
|
131
|
+
— which names exactly one file — is refused rather than quietly used three times. `delete`
|
|
132
|
+
looks every id up first and shows the whole list before asking once; an id that is nowhere to
|
|
133
|
+
be found stops the run before anything is destroyed.
|
|
161
134
|
|
|
162
|
-
|
|
163
|
-
Session /home/you/.telstore/config.json (sealed — opened with a passphrase)
|
|
164
|
-
Account Sho (@shovity)
|
|
165
|
-
Destination https://web.telegram.org/k/#@my_backups
|
|
166
|
-
Unfinished none
|
|
167
|
-
```
|
|
135
|
+
## Resuming an upload
|
|
168
136
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- **State files, yes.** An upload writes `~/.telstore/state/<key>.json` so it can be resumed
|
|
174
|
-
after an interruption. It holds the file path, the chunk sizes and the message ids — no
|
|
175
|
-
secret — and losing it would strand chunks in the chat with nothing able to name them.
|
|
176
|
-
|
|
177
|
-
### Things worth knowing before you use one
|
|
178
|
-
|
|
179
|
-
- **`--token` takes no value, on purpose.** A token written on the command line would sit in
|
|
180
|
-
`ps` for the whole life of the command and stay in that machine's shell history afterwards.
|
|
181
|
-
It is pasted at a prompt that does not echo it.
|
|
182
|
-
- **The passphrase may be left empty**, and then the token says so on its face: it starts
|
|
183
|
-
`tls0.` instead of `tls1.`, `token` warns, and `login` stores the session in plain text
|
|
184
|
-
exactly as an ordinary login would. telstore will not write a file that looks encrypted and
|
|
185
|
-
is not.
|
|
186
|
-
- **telstore cannot take a token back.** There is no expiry and no revocation. Like `logout`,
|
|
187
|
-
it says nothing to Telegram — to end the session for good, open Telegram → Settings →
|
|
188
|
-
Devices (Active sessions) and terminate it. Every copy of the token dies with it.
|
|
189
|
-
- **A wrong passphrase and a damaged token are the same event** to AES-GCM: one failed
|
|
190
|
-
authentication check. telstore names both possibilities rather than guessing which it was.
|
|
137
|
+
Progress lives in `~/.telstore/state/` (the 20 most recent), so running the same command
|
|
138
|
+
again skips the finished chunks and keeps the same `backupId`. A resumed backup keeps the
|
|
139
|
+
chunk size it started with; passing a `--chunk-size` or a destination that differs from its
|
|
140
|
+
own makes telstore refuse to run rather than re-cut or redirect it silently.
|
|
191
141
|
|
|
192
|
-
|
|
142
|
+
After a batch, run telstore again with **only the files that are left**: the finished ones
|
|
143
|
+
have had their records cleared, so repeating the whole command would upload them a second
|
|
144
|
+
time as new backups. `npx telstore status` lists what is unfinished.
|
|
193
145
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- Deleting a chunk message on Telegram destroys the backup, with no way to recover it. Use `npx telstore delete <backup-id>` when that is what you actually want.
|
|
197
|
-
- Keep the `backupId`. Without it you have to hunt for the manifest in the chat by hand.
|
|
146
|
+
**Restore keeps no state** — `Ctrl-C` mid-restore saves nothing, running again starts over.
|
|
147
|
+
And `delete` has **no undo**: Telegram is the only copy.
|
|
198
148
|
|
|
199
|
-
##
|
|
149
|
+
## Running on a machine you do not trust
|
|
200
150
|
|
|
201
|
-
|
|
151
|
+
`login` leaves your session on disk in plain text. When the machine is not yours, print a
|
|
152
|
+
**session token** on one that is and log in with that instead:
|
|
202
153
|
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"session": "…",
|
|
208
|
-
"settings": { "chat": "@my_backups", "chunkSize": 524288000 }
|
|
209
|
-
}
|
|
154
|
+
```bash
|
|
155
|
+
npx telstore token # on your own machine: asks for a passphrase, prints one line
|
|
156
|
+
npx telstore login --token # on the other one: paste the token, then the passphrase
|
|
157
|
+
npx telstore logout # when you are done
|
|
210
158
|
```
|
|
211
159
|
|
|
212
|
-
|
|
213
|
-
|
|
160
|
+
The token holds your `api_id`, `api_hash`, session and settings, encrypted with the
|
|
161
|
+
passphrase (AES-256-GCM, scrypt), as one line of base64url that survives a chat message or a
|
|
162
|
+
QR code. `login --token` stores it exactly as it arrived and every command asks for the
|
|
163
|
+
passphrase, so the session never exists on that machine in readable form.
|
|
214
164
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
165
|
+
There is no expiry and no revocation: to end a session for good, terminate it under Telegram
|
|
166
|
+
→ Settings → Devices. An empty passphrase is allowed, and then the token says so on its face
|
|
167
|
+
— it starts `tls0.` and `login` stores the session in plain text.
|
|
168
|
+
|
|
169
|
+
## Limits worth knowing
|
|
170
|
+
|
|
171
|
+
- **Your data is not encrypted.** Don't upload anything you would mind sitting on someone
|
|
172
|
+
else's infrastructure — the one thing telstore encrypts is a session token, and that
|
|
173
|
+
protects your login rather than your files.
|
|
174
|
+
- A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file.
|
|
175
|
+
- Deleting a chunk message in the Telegram app destroys the backup, and keeping the
|
|
176
|
+
`backupId` is what saves you hunting for its manifest in the chat by hand.
|
|
218
177
|
|
|
219
|
-
|
|
220
|
-
|
|
178
|
+
Settings and credentials live in `~/.telstore/config.json`, mode 600 — `apiId`, `apiHash` and
|
|
179
|
+
the session at the top level (or a single `sealed` blob after `login --token`), everything
|
|
180
|
+
`config` manages under `settings`. Editing it by hand is fine: a value that cannot be used is
|
|
181
|
+
named on the next run, with the file and the key.
|
|
221
182
|
|
|
222
|
-
|
|
183
|
+
## License
|
|
223
184
|
|
|
224
|
-
|
|
185
|
+
MIT
|
package/bin/telstore.js
CHANGED
|
@@ -14,12 +14,18 @@ const SIGINT_EXIT_CODE = 130
|
|
|
14
14
|
let currentCommand = null
|
|
15
15
|
let currentBackupId = null
|
|
16
16
|
|
|
17
|
+
// A batch clears each finished file's record as it goes, so by the time Ctrl-C lands these
|
|
18
|
+
// are backups no second run should touch. Ctrl-C needs their names to say so.
|
|
19
|
+
const finishedUploads = []
|
|
20
|
+
|
|
17
21
|
process.on('SIGINT', () => {
|
|
18
22
|
// A passphrase prompt has stdin in raw mode, and process.exit skips readline's own cleanup.
|
|
19
23
|
// Without this, Ctrl-C hands back a shell that no longer echoes what is typed into it.
|
|
20
24
|
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
21
25
|
|
|
22
|
-
process.stderr.write(
|
|
26
|
+
process.stderr.write(
|
|
27
|
+
interruptMessage(currentCommand, { backupId: currentBackupId, done: finishedUploads }),
|
|
28
|
+
)
|
|
23
29
|
process.exit(SIGINT_EXIT_CODE)
|
|
24
30
|
})
|
|
25
31
|
|
|
@@ -88,13 +94,23 @@ async function main() {
|
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
case 'upload': {
|
|
91
|
-
const {
|
|
97
|
+
const { runUploads } = await import('../src/commands/upload.js')
|
|
92
98
|
|
|
93
|
-
await
|
|
99
|
+
const { failed } = await runUploads(parsed.args, parsed.options, {
|
|
100
|
+
// Only route saw the command line, and an unquoted note is told apart from a plain
|
|
101
|
+
// missing file by where the words sat on it.
|
|
102
|
+
filesAfterNote: parsed.filesAfterNote,
|
|
94
103
|
onBackupId: (id) => {
|
|
95
104
|
currentBackupId = id
|
|
96
105
|
},
|
|
106
|
+
onFileDone: (file) => {
|
|
107
|
+
if (file.id) finishedUploads.push(file)
|
|
108
|
+
},
|
|
97
109
|
})
|
|
110
|
+
|
|
111
|
+
// A batch reports its own failures by name and has already said so on stdout; the exit
|
|
112
|
+
// code is what carries that out to whatever ran telstore.
|
|
113
|
+
if (failed > 0) process.exitCode = 1
|
|
98
114
|
return
|
|
99
115
|
}
|
|
100
116
|
|
|
@@ -103,9 +119,11 @@ async function main() {
|
|
|
103
119
|
throw new Error('Missing backup id. Example: npx telstore restore telstore-20260905-7f3a91')
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
const {
|
|
122
|
+
const { runRestores } = await import('../src/commands/restore.js')
|
|
107
123
|
|
|
108
|
-
await
|
|
124
|
+
const { failed } = await runRestores(parsed.args, parsed.options)
|
|
125
|
+
|
|
126
|
+
if (failed > 0) process.exitCode = 1
|
|
109
127
|
return
|
|
110
128
|
}
|
|
111
129
|
|
|
@@ -114,9 +132,11 @@ async function main() {
|
|
|
114
132
|
throw new Error('Missing backup id. Example: npx telstore delete telstore-20260905-7f3a91')
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
const {
|
|
135
|
+
const { runDeletes } = await import('../src/commands/delete.js')
|
|
136
|
+
|
|
137
|
+
const { failed } = await runDeletes(parsed.args, parsed.options)
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
if (failed > 0) process.exitCode = 1
|
|
120
140
|
return
|
|
121
141
|
}
|
|
122
142
|
|
package/package.json
CHANGED
package/src/caption.js
CHANGED
|
@@ -14,6 +14,37 @@ function oneLine(name) {
|
|
|
14
14
|
return String(name).replace(/\s+/g, ' ').trim()
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Telegram takes 1024 characters in a caption, and the card around the note already spends
|
|
18
|
+
// some of them — a file name alone may be 255. 500 leaves both room to spare.
|
|
19
|
+
export const MAX_NOTE_LENGTH = 500
|
|
20
|
+
|
|
21
|
+
// A note is written at a shell prompt and read in two places: the manifest body and the card
|
|
22
|
+
// in the chat. Folding it here, once, is what keeps those two from holding slightly different
|
|
23
|
+
// notes and leaving nobody able to say which one was typed.
|
|
24
|
+
export function parseNote(raw) {
|
|
25
|
+
if (raw === undefined || raw === null) return null
|
|
26
|
+
|
|
27
|
+
const note = oneLine(raw)
|
|
28
|
+
|
|
29
|
+
if (note === '') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'--note is empty. Write the note itself, or leave the flag off — a backup with a blank ' +
|
|
32
|
+
'note is one telstore had something to say about and did not.',
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (note.length > MAX_NOTE_LENGTH) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`--note is ${note.length} characters, and a caption has only room for ${MAX_NOTE_LENGTH} ` +
|
|
39
|
+
'once the rest of the card has had its share. Shorten it: telstore will not cut it ' +
|
|
40
|
+
'short by itself, because half a note read as a whole one is exactly the kind of ' +
|
|
41
|
+
'plausible wrong answer this tool exists to refuse.',
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return note
|
|
46
|
+
}
|
|
47
|
+
|
|
17
48
|
function utcMinutes(createdAt) {
|
|
18
49
|
return `${new Date(createdAt).toISOString().slice(0, 16).replace('T', ' ')} UTC`
|
|
19
50
|
}
|
|
@@ -22,12 +53,15 @@ export function chunkCaption({ id, number, total }) {
|
|
|
22
53
|
return `📦 ${id} · ${number}/${total}`
|
|
23
54
|
}
|
|
24
55
|
|
|
25
|
-
export function manifestCaption({ id, name, size, chunks, createdAt }) {
|
|
56
|
+
export function manifestCaption({ id, name, size, chunks, createdAt, note = null }) {
|
|
26
57
|
return [
|
|
27
|
-
|
|
58
|
+
`📄 ${oneLine(name)}`,
|
|
28
59
|
`💾 ${formatBytes(size)} · ${chunks} chunk${chunks === 1 ? '' : 's'}`,
|
|
29
60
|
`🆔 ${id}`,
|
|
30
61
|
`📅 ${utcMinutes(createdAt)}`,
|
|
62
|
+
// Below the facts telstore knows, above the line that says how to get the file back:
|
|
63
|
+
// the note is the one part of the card a person wrote, so it reads last of the four.
|
|
64
|
+
...(note ? [`📝 ${oneLine(note)}`] : []),
|
|
31
65
|
'',
|
|
32
66
|
`↩ npx telstore restore ${id}`,
|
|
33
67
|
MANIFEST_TAG,
|
|
@@ -45,16 +79,20 @@ function marker(lines, emoji) {
|
|
|
45
79
|
export function parseManifestCaption(text) {
|
|
46
80
|
const lines = String(text ?? '').split('\n')
|
|
47
81
|
|
|
48
|
-
const name = marker(lines, '
|
|
82
|
+
const name = marker(lines, '📄')
|
|
49
83
|
const totals = marker(lines, '💾')
|
|
50
84
|
const id = marker(lines, '🆔')
|
|
51
85
|
const createdAt = marker(lines, '📅')
|
|
52
86
|
|
|
87
|
+
// Every card telstore wrote before --note existed is a complete card, so the note is the
|
|
88
|
+
// one marker whose absence means "there is no note" rather than "this is not a card".
|
|
89
|
+
const note = marker(lines, '📝')
|
|
90
|
+
|
|
53
91
|
if (!name || !totals || !id || !createdAt) return null
|
|
54
92
|
|
|
55
93
|
const match = /^(.+) · (\d+) chunks?$/.exec(totals)
|
|
56
94
|
|
|
57
95
|
if (!match) return null
|
|
58
96
|
|
|
59
|
-
return { id, name, size: match[1], chunks: Number(match[2]), createdAt }
|
|
97
|
+
return { id, name, size: match[1], chunks: Number(match[2]), createdAt, note }
|
|
60
98
|
}
|