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 CHANGED
@@ -1,78 +1,71 @@
1
1
  # telstore
2
2
 
3
- Split large files into 1.8GB chunks, store them on Telegram, and restore them intact.
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 # once only
9
- npx telstore config chat @my_backups # where backups go, from now on
10
- npx telstore data.tar # split it and send it there
11
- npx telstore data.tar --to @somewhere # somewhere else, this run only
12
- npx telstore config # every setting and where its value comes from
13
- npx telstore status # account, destination, unfinished backups
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
- ## What you need
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
- telstore 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.
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 and flags
37
+ ## Settings
28
38
 
29
- There are two ways to say what telstore should do, and they never overlap. **`config` writes;
30
- flags do not.** A flag applies to the run you typed it on and changes nothing on disk, so
31
- `--to @elsewhere` sends one backup elsewhere without moving the destination for the next one.
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 # everything, and whether it is yours or a default
35
- npx telstore config chat # one value, bare, ready to pipe
36
- npx telstore config chunkSize 500MB # change it for good
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` | `--to` | none | `@username`, `-100123…`, or `me`. A negative channel id works with a space or an `=`, as a flag or as a config value. |
43
- | `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Hard ceiling 1950MB. An unfinished backup keeps the size it started with. |
44
- | `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts sent in parallel while uploading. An integer from 1 to 64. |
45
- | `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices fetched in parallel while restoring. An integer from 1 to 64. |
46
- | `limit` | `--limit` | `20` | How many backups `list` shows, newest first |
47
- | `verbose` | `--verbose` | off | Show the Telegram client's own connection logs, hidden by default so they do not break up the progress bar |
48
-
49
- `--yes` has no setting either — it answers the confirmation `delete` asks before destroying
50
- a backup, and an answer stored in a file would be an answer to a question nobody heard.
51
-
52
- `--out <path>` has no setting: it names where one particular restore should write, and
53
- defaults to the basename in the manifest. Relative paths resolve against the current directory.
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`, and the manifest that
62
- follows carries a summary card:
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
- A backup uploaded before the card existed still gets a row, with dashes where the caption
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
- - Running again against a destination that differs from the one in the unfinished progress makes telstore **refuse to run** rather than silently redirect — one backup cannot be split across two destinations. The error names the chat to pass as `--to` to carry on, or the state file to delete to start a new backup. It reads the same whether the mismatch came from a flag or from your configured `chat`.
97
- - Running again **without** `--chunk-size` resumes at the size the backup started with, whatever your configured `chunkSize` says today. A setting is what to use when nobody asks for anything; it is not somebody asking.
98
- - Running again **with** a `--chunk-size` that differs from that size makes telstore **refuse to run**: the chunks already in the chat were cut that way and cannot be re-cut. Drop the flag to carry on, or delete the state file to start a new backup — which leaves the chunks already sent in the chat with nothing pointing at them.
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
- `Ctrl-C` during an upload names the backup it was working on, so `telstore status` and a later `restore` have something to go on. telstore keeps the **20 most recent** unfinished backups in `~/.telstore/state/`; starting a new one past that drops the oldest record and says which id it dropped. Only the local record goes — the chunks that backup sent stay in the chat, searchable by that id, but it can no longer be resumed.
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 delete telstore-20260905-7f3a91
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
- It prints what it is about to destroy, asks once, and then removes every chunk message and
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
- It also works on a backup that never finished: those have chunks in the chat but no manifest,
115
- so `list` cannot see them and only `status` knows they exist. `delete` reads the local record
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
- Two things it refuses rather than guesses at: a manifest whose body names a *different*
126
- backup (a file renamed in the chat — its message ids point at somebody else's chunks), and a
127
- manifest or local record giving a message id that is not a whole positive number. Neither
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
- ## Running on a machine you do not trust
104
+ Upload these 3 files? [y/N]
105
+ ```
132
106
 
133
- `login` leaves your Telegram session on the machine you run it on, in plain text. When that
134
- machine is not yours, print a **session token** on one that is and log in with that instead:
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
- ```bash
137
- # on the machine you are already logged in on
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
- # on the other machine
142
- npx telstore login --token # paste the token, then type the passphrase
143
- npx telstore restore telstore-20260905-7f3a91
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
- The token holds your `api_id`, `api_hash`, session and settings, encrypted with the
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
- `login --token` stores the token exactly as it arrived, so **the session never exists on that
153
- machine in a form anyone can read**. Every command that talks to Telegram asks for the
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
- ```json
157
- { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
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
- `npx telstore status` shows which of the two you are running:
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
- ### What is left behind
170
-
171
- - **No readable session.** `logout` removes the sealed blob, and with it the `api_hash`,
172
- which lives inside it.
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
- ## Limits worth knowing
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
- - A chunk cannot exceed 1950MB: Telegram accepts at most 4000 parts of 512KB per file, an arithmetic ceiling of about 1953MB, and telstore stops at 1950MB to leave a safety margin.
195
- - The data is **not** encrypted. Don't upload anything you would mind sitting on someone else's infrastructure. The one thing telstore does encrypt is a session token, and that protects your login rather than your files.
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
- ## Where the config lives
149
+ ## Running on a machine you do not trust
200
150
 
201
- `~/.telstore/config.json` (mode 600) holds `apiId`, `apiHash` and the session at the top level, with everything `config` manages under `settings`:
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
- ```json
204
- {
205
- "apiId": 123456,
206
- "apiHash": "…",
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
- A machine logged in with `npx telstore login --token` holds `sealed` instead of those three
213
- fields, and nothing else about the account:
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
- ```json
216
- { "sealed": "tls1.…", "settings": { "chat": "@my_backups" } }
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
- A file holding both shapes is refused rather than guessed at — there would be no way to know
220
- which account was meant.
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
- Editing it by hand is fine, and a value that cannot be used is named on the next run — with the file and the key, never with a flag you did not type.
183
+ ## License
223
184
 
224
- `npx telstore logout` **only deletes the locally stored session** and keeps the rest — the session is still alive on Telegram's side. To revoke access for good, open Telegram → Settings → Devices (Active sessions) and terminate that session.
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(interruptMessage(currentCommand, { backupId: currentBackupId }))
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 { runUpload } = await import('../src/commands/upload.js')
97
+ const { runUploads } = await import('../src/commands/upload.js')
92
98
 
93
- await runUpload(parsed.args[0], parsed.options, {
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 { runRestore } = await import('../src/commands/restore.js')
122
+ const { runRestores } = await import('../src/commands/restore.js')
107
123
 
108
- await runRestore(parsed.args[0], parsed.options)
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 { runDelete } = await import('../src/commands/delete.js')
135
+ const { runDeletes } = await import('../src/commands/delete.js')
136
+
137
+ const { failed } = await runDeletes(parsed.args, parsed.options)
118
138
 
119
- await runDelete(parsed.args[0], parsed.options)
139
+ if (failed > 0) process.exitCode = 1
120
140
  return
121
141
  }
122
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telstore",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Split large files into chunks and store them on Telegram",
5
5
  "keywords": [
6
6
  "telegram",
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
- `🗄 ${oneLine(name)}`,
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
  }