@nightowne/tas-cli 2.4.0 → 2.4.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,232 +1,327 @@
1
1
  <p align="center">
2
- <img src="assets/demo.gif" alt="TAS — Telegram as Storage" width="640">
2
+ <img src="assets/demo.gif" alt="TAS — Telegram as Storage CLI demo" width="680">
3
3
  </p>
4
4
 
5
- <h1 align="center">
6
- 📦 TAS — Telegram as Storage
7
- </h1>
5
+ <h1 align="center">📦 TAS — Telegram as Storage</h1>
8
6
 
9
7
  <h3 align="center">
10
- Turn Telegram into your personal encrypted cloud drive.<br>
11
- Free forever. Zero-knowledge. No credit card. No limits.
8
+ Turn your Telegram bot into unlimited, encrypted cloud storage.<br>
9
+ <strong>Free forever. Zero-knowledge. No sign-up. No credit card. No limits.</strong>
12
10
  </h3>
13
11
 
14
12
  <p align="center">
15
- <a href="https://github.com/ixchio/tas/actions/workflows/ci.yml"><img src="https://github.com/ixchio/tas/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
- <a href="https://www.npmjs.com/package/@nightowne/tas-cli"><img src="https://img.shields.io/npm/v/@nightowne/tas-cli?color=cb3837&label=npm" alt="npm version"></a>
17
- <a href="https://www.npmjs.com/package/@nightowne/tas-cli"><img src="https://img.shields.io/npm/dm/@nightowne/tas-cli?color=blue" alt="Downloads"></a>
18
- <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
13
+ <a href="https://github.com/ixchio/tas/actions/workflows/ci.yml"><img src="https://github.com/ixchio/tas/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
14
+ <a href="https://www.npmjs.com/package/@nightowne/tas-cli"><img src="https://img.shields.io/npm/v/@nightowne/tas-cli?color=cb3837&label=npm&logo=npm" alt="npm version"></a>
15
+ <a href="https://www.npmjs.com/package/@nightowne/tas-cli"><img src="https://img.shields.io/npm/dm/@nightowne/tas-cli?color=blue&label=downloads&logo=npm" alt="Monthly Downloads"></a>
16
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg" alt="License: MIT"></a>
19
17
  <a href="https://github.com/ixchio/tas/stargazers"><img src="https://img.shields.io/github/stars/ixchio/tas?style=social" alt="GitHub Stars"></a>
18
+ <a href="https://github.com/ixchio/tas/network/members"><img src="https://img.shields.io/github/forks/ixchio/tas?style=social" alt="GitHub Forks"></a>
19
+ <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen?logo=node.js" alt="Node.js >= 18">
20
+ <img src="https://img.shields.io/badge/encryption-AES--256--GCM-blueviolet?logo=shield" alt="AES-256-GCM">
21
+ <img src="https://img.shields.io/badge/tests-71%20passing-success" alt="71 Tests Passing">
20
22
  </p>
21
23
 
22
24
  <p align="center">
23
- <a href="#-quick-start">Quick Start</a> •
24
- <a href="#-why-tas">Why TAS</a> •
25
- <a href="#-features">Features</a> •
26
- <a href="#%EF%B8%8F-security">Security</a> •
27
- <a href="#-cli-reference">Docs</a> •
28
- <a href="#-contributing">Contributing</a>
25
+ <a href="QUICKSTART.md"><strong>📚 Quick Start</strong></a> &nbsp;•&nbsp;
26
+ <a href="FAQ.md">FAQ</a> &nbsp;•&nbsp;
27
+ <a href="#-why-tas">Why TAS?</a> &nbsp;•&nbsp;
28
+ <a href="#-features">Features</a> &nbsp;•&nbsp;
29
+ <a href="#-security-model">Security</a> &nbsp;•&nbsp;
30
+ <a href="#-cli-reference">CLI Docs</a> &nbsp;•&nbsp;
31
+ <a href="#-docker--cicd">Docker / CI</a> &nbsp;•&nbsp;
32
+ <a href="CHANGELOG.md">Changelog</a>
29
33
  </p>
30
34
 
31
35
  ---
32
36
 
33
- <br>
37
+ > **TL;DR** — One `npm install`, one `tas init`, then `tas push yourfile.pdf`. Your file is now AES-256 encrypted and stored for free on Telegram's infrastructure. No accounts, no fees, no vendor lock-in. Seriously.
34
38
 
35
- ## The Problem
39
+ ---
40
+
41
+ ## The Problem With "Free" Cloud Storage
42
+
43
+ Every major cloud provider has one of three business models: **scanning your data**, **charging you money**, or **capping your storage**. There is no free lunch.
36
44
 
37
- Google Drive scans your files. Dropbox costs $12/mo. iCloud locks you into Apple. Every "free" cloud storage either **reads your data**, **charges you money**, or **caps your storage**.
45
+ | Provider | Free Tier | Reads Your Data? | CLI-First? | Encryption At Rest (by you)? |
46
+ |---|---|---|---|---|
47
+ | Google Drive | 15 GB | ✅ Yes (indexes for ads) | ❌ | ❌ |
48
+ | Dropbox | 2 GB | ✅ Can access | ❌ | ❌ |
49
+ | iCloud | 5 GB | ✅ Apple ToS | ❌ | ❌ |
50
+ | Mega | 20 GB | ❓ Closed-source E2EE | ❌ | ❓ |
51
+ | Backblaze B2 | 10 GB | ❌ | ✅ | ❌ (you add it) |
52
+ | **TAS + Telegram** | **♾️ Unlimited** | **❌ Impossible (AES-256)** | **✅ First-class** | **✅ Always** |
38
53
 
39
- Meanwhile, Telegram gives every user **unlimited storage** with a bot API — and nobody's using it.
54
+ Meanwhile, Telegram gives every bot **unlimited file storage** via its public Bot API — and almost nobody is using it.
55
+
56
+ ---
40
57
 
41
58
  ## The Solution
42
59
 
43
- **TAS** compresses, encrypts, and uploads your files to your own private Telegram bot chat. Your password never leaves your machine. Telegram only sees encrypted noise. You get a real CLI-powered cloud drive — with mount, sync, share, and search — for **$0/month, forever.**
60
+ **TAS** compresses, encrypts (AES-256-GCM), chunks, and uploads your files to your own private Telegram bot chat. Your password never leaves your machine. Telegram only ever sees **encrypted noise**. You get a fully-featured, CLI-native cloud drive with FUSE mount, Dropbox-style sync, expiring share links, and tagging — at **$0/month, forever.**
44
61
 
45
62
  ```
46
- Your Machine Telegram Cloud
47
- ┌──────────────────┐ ┌──────────────────────┐
48
- │ │ Compress │ │
49
- │ tas push │──→ Encrypt ──→│ 🔒 Encrypted Blobs │
50
- │ tas mount │──→ Chunk ──→│ 🔒 Private Bot Chat │
51
- │ tas sync │ │ 🔒 Your Data, Safe │
52
- │ │ Decrypt │ │
53
- │ tas pull │←── Decomp ←──│ ← Download on demand│
54
- │ │ │ │
55
- └──────────────────┘ └──────────────────────┘
56
- SQLite Index Unlimited & Free
63
+ Your Machine Telegram Cloud
64
+ ┌─────────────────────────────┐ ┌──────────────────────────┐
65
+ │ │ │ │
66
+ │ tas push secret.tar.gz │──→ gzip ──→ │ 🔒 Encrypted Blob #1 │
67
+ │ tas mount ~/cloud │──→ AES-256 ──→│ 🔒 Encrypted Blob #2 │
68
+ │ tas sync start │──→ chunk ──→ │ 🔒 Encrypted Blob #3 │
69
+ │ │ │ (Private Bot Chat) │
70
+ │ tas pull secret.tar.gz │←── decrypt ←──│ │
71
+ │ (SHA-256 verified) │←── decomp ←──│ ← Stream on demand │
72
+ │ │ │ │
73
+ └─────────────────────────────┘ └──────────────────────────┘
74
+ SQLite Index Unlimited & Free
57
75
  ```
58
76
 
59
- <br>
77
+ ---
60
78
 
61
79
  ## ⚡ Quick Start
62
80
 
63
- Three commands. Two minutes. Zero cost.
81
+ **Three commands. Under two minutes. Zero cost.**
64
82
 
65
83
  ```bash
84
+ # 1. Install globally
66
85
  npm install -g @nightowne/tas-cli
67
86
 
68
- tas init # Connect your Telegram bot (guided wizard)
69
- tas push secret.pdf # Upload — encrypted, compressed, done
70
- tas pull secret.pdf # Download — decrypted, verified, instant
87
+ # 2. Connect your Telegram bot (guided wizard — takes ~60 seconds)
88
+ tas init
89
+
90
+ # 3. Start using it
91
+ tas push secret.pdf # Encrypt + compress + upload
92
+ tas pull secret.pdf # Download + decrypt + verify
93
+ tas list # See everything you've stored
71
94
  ```
72
95
 
73
- That's it. You now have encrypted cloud storage.
96
+ > **Need a Telegram bot?** Open Telegram → search `@BotFather` → `/newbot` → copy the token. That's it.
74
97
 
75
- <br>
98
+ ---
76
99
 
77
- ## 💡 Why TAS
100
+ ## 💡 Why TAS?
78
101
 
79
102
  <table>
80
103
  <tr>
81
- <td width="50%">
82
-
83
- ### vs. Google Drive
84
- - ❌ Google scans & indexes your files
85
- - ❌ 15 GB free tier
86
- - ❌ No encryption at rest (by you)
87
-
88
- ### vs. Dropbox
89
- - ❌ $12/mo for 2 TB
90
- - ❌ Can access your data
91
- - ❌ No CLI-first experience
104
+ <td width="50%" valign="top">
92
105
 
93
- ### vs. Mega / pCloud
94
- - ❌ Freemium with tight caps
95
- - ❌ Closed source encryption
96
- - ❌ Can't self-host or script
106
+ ### ❌ The Alternative
107
+ - Google Drive scans & indexes your files for ads
108
+ - Dropbox costs $12/mo — and can read your data
109
+ - iCloud locks you into the Apple ecosystem
110
+ - Self-hosting (Nextcloud, MinIO) costs VPS money + maintenance time
111
+ - S3 / B2 needs encryption wiring and costs per GB transferred
112
+ - rclone + any backend still needs a paid backend
97
113
 
98
114
  </td>
99
- <td width="50%">
100
-
101
- ### TAS gives you
102
- - ✅ **$0/month** — forever, no caps
103
- - ✅ **Zero-knowledge** — only you can decrypt
104
- - ✅ **AES-256-GCM** — military-grade encryption
105
- - ✅ **Mount as folder** — FUSE filesystem
106
- - ✅ **Auto-sync** — Dropbox-style folder sync
107
- - ✅ **Share links** — expiring, download-limited
108
- - ✅ **CLI-first** — cron, Docker, CI/CD ready
109
- - ✅ **Open source** — audit every line
115
+ <td width="50%" valign="top">
116
+
117
+ ### ✅ TAS gives you
118
+ - **$0/month** — forever, no storage caps, no bandwidth fees
119
+ - **Zero-knowledge** — only you hold the decryption key
120
+ - **AES-256-GCM** — same cipher used by banks and governments
121
+ - **FUSE mount** — Telegram storage appears as a real folder
122
+ - **Auto-sync** — Dropbox-style folder watcher built-in
123
+ - **Expiring share links** — send files without sharing your password
124
+ - **CLI-first** — pipe to `jq`, run in cron, automate everything
125
+ - **Open source** — audit every single line of crypto code
110
126
 
111
127
  </td>
112
128
  </tr>
113
129
  </table>
114
130
 
115
- <br>
131
+ ---
116
132
 
117
133
  ## 🔥 Features
118
134
 
119
- ### 🗂️ Mount as a Local Folder
120
- Drag and drop files into Telegram storage like it's a regular drive.
135
+ ### 🗂️ Mount as a Local Folder (FUSE)
136
+
137
+ Use Telegram storage exactly like a USB drive — drag and drop, open in any app.
121
138
 
122
139
  ```bash
123
- tas mount ~/cloud # Mount your Telegram storage
124
- # Now use Finder, Explorer, or any app — files sync to Telegram
125
- tas unmount ~/cloud
140
+ tas mount ~/cloud # Mount your Telegram storage as ~/cloud
141
+ ls ~/cloud # Browse your encrypted files normally
142
+ cp report.pdf ~/cloud/ # Drop files in — auto-encrypted and uploaded
143
+ tas unmount ~/cloud # Clean unmount when done
126
144
  ```
127
145
 
128
- > Requires `libfuse` — `apt install fuse libfuse-dev` on Linux, `brew install macfuse` on macOS.
146
+ > **Requirements:** `apt install fuse libfuse-dev` (Linux) · `brew install macfuse` (macOS)
147
+
148
+ ---
149
+
150
+ ### 🔄 Auto-Sync Folders (Dropbox-style)
129
151
 
130
- ### 🔄 Auto-Sync Folders
131
- Dropbox-style: register a folder, and TAS watches for changes and uploads automatically.
152
+ Register a local folder and TAS watches it. Any new or changed file is automatically encrypted and uploaded.
132
153
 
133
154
  ```bash
134
- tas sync add ~/Documents # Register a folder
135
- tas sync start # Watch & auto-upload changes
136
- tas sync pull # Download everything back
137
- tas sync status # See what's synced
155
+ tas sync add ~/Documents # Register ~/Documents for auto-sync
156
+ tas sync start # Start the watcher (runs in background)
157
+ tas sync pull # Pull all synced files back down
158
+ tas sync status # See what's queued / synced / pending
138
159
  ```
139
160
 
140
- ### 🔗 Share with Expiring Links
141
- Generate one-time download links with a sleek dark-themed download page. Files are decrypted on-the-fly — the link holder never sees your password.
161
+ ---
162
+
163
+ ### 🔗 Expiring Share Links
164
+
165
+ Generate time-limited, download-limited share links. Recipients get a clean dark-themed download page. **Your password is never shared — files are decrypted on-the-fly by the local server.**
142
166
 
143
167
  ```bash
144
- tas share create report.pdf --expire 1h --max-downloads 3
145
- # → http://localhost:3000/d/a1b2c3d4...
168
+ tas share create report.pdf --expire 24h --max-downloads 5
169
+ # → http://localhost:3000/d/a1b2c3d4e5f6...
146
170
 
147
- tas share list # See active shares
148
- tas share revoke a1b2c3d4 # Revoke anytime
171
+ tas share create backup.tar.gz --expire 1h --max-downloads 1 # Burn-after-read
172
+ tas share list # See active links with expiry info
173
+ tas share revoke a1b2c3d4 # Revoke instantly, anytime
149
174
  ```
150
175
 
151
- ### 🏷️ Tags & Search
152
- Organize and find files instantly.
176
+ ---
177
+
178
+ ### 🏷️ Tags & Full-Text Search
153
179
 
154
180
  ```bash
155
- tas tag add report.pdf work Q4
156
- tas search "report" # Search by filename
157
- tas search -t work # Search by tag
181
+ tas tag add report.pdf work Q4 finance
182
+ tas tag add keys.env secrets production
183
+ tas search "report" # Search by filename pattern
184
+ tas search -t work # All files tagged "work"
185
+ tas search -t secrets # Quickly find your credentials
158
186
  ```
159
187
 
188
+ ---
189
+
160
190
  ### 🩺 Self-Diagnostics
161
- One command to check if everything is healthy.
162
191
 
163
192
  ```bash
164
193
  tas doctor
165
194
  # ✓ Node.js 20.11.0
166
- # ✓ Config v2 (encrypted token)
167
- # ✓ Database: 42 files, 1.3 GB total
195
+ # ✓ Config v2 (encrypted bot token — AES-256-GCM at rest)
196
+ # ✓ Database: 42 files, 1.3 GB total across 28 chunks
168
197
  # ✓ Disk space: 50 GB free (32% used)
169
- # ✓ Encryption: AES-256-GCM, PBKDF2-SHA512 600,000 iterations
198
+ # ✓ Encryption: AES-256-GCM · PBKDF2-SHA512 · 600,000 iterations (OWASP 2025)
199
+ # ✓ Telegram connectivity: OK
170
200
  # ✨ All systems go!
171
201
  ```
172
202
 
173
- ### 🤖 Built for Automation
174
- First-class JSON output, environment variable support, and zero interactivity mode.
203
+ ---
204
+
205
+ ### 🤖 Built for Automation — CI/CD, Docker, Cron
206
+
207
+ TAS is fully scriptable. No interactive prompts needed when `TAS_PASSWORD` is set.
175
208
 
176
209
  ```bash
177
- export TAS_PASSWORD="your-password" # Skip prompts
178
- export TAS_DATA_DIR="/custom/path" # Custom data location
210
+ # Environment-based automation
211
+ export TAS_PASSWORD="your-password"
212
+ export TAS_DATA_DIR="/custom/path"
213
+
214
+ # Pipe to jq
215
+ tas list --json | jq '.[].filename'
216
+ tas list --json | jq '.[] | select(.size > 1000000)' # Files > 1MB
217
+
218
+ # GitHub Actions backup step
219
+ tas push db-backup-$(date +%Y%m%d).sql.gz
179
220
 
180
- tas push backup.tar.gz # Non-interactive upload
181
- tas list --json | jq '.[].filename' # Pipe to jq
182
- tas status --json # Machine-readable status
221
+ # cron: nightly backup at 2am
222
+ 0 2 * * * TAS_PASSWORD=$SECRET tas push /var/backups/db.tar.gz
223
+
224
+ # JSON machine output everywhere
225
+ tas status --json
226
+ tas list --json
227
+ ```
228
+
229
+ ---
230
+
231
+ ## 🐳 Docker & CI/CD
232
+
233
+ ```dockerfile
234
+ FROM node:20-alpine
235
+
236
+ RUN npm install -g @nightowne/tas-cli
237
+
238
+ ENV TAS_PASSWORD=""
239
+ ENV TAS_DATA_DIR="/data"
240
+
241
+ VOLUME ["/data"]
242
+
243
+ CMD ["tas", "status"]
244
+ ```
183
245
 
184
- # Works with: cron • GitHub Actions • Docker • systemd • any CI/CD
246
+ ```yaml
247
+ # .github/workflows/backup.yml
248
+ name: Nightly Backup
249
+
250
+ on:
251
+ schedule:
252
+ - cron: '0 2 * * *'
253
+
254
+ jobs:
255
+ backup:
256
+ runs-on: ubuntu-latest
257
+ steps:
258
+ - name: Install TAS
259
+ run: npm install -g @nightowne/tas-cli
260
+
261
+ - name: Push backup
262
+ env:
263
+ TAS_PASSWORD: ${{ secrets.TAS_PASSWORD }}
264
+ TAS_DATA_DIR: ${{ runner.temp }}/tas-data
265
+ run: |
266
+ tas init --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat ${{ secrets.TELEGRAM_CHAT_ID }}
267
+ tar czf backup-$(date +%Y%m%d).tar.gz ./important-data/
268
+ tas push backup-$(date +%Y%m%d).tar.gz
185
269
  ```
186
270
 
187
- <br>
271
+ ---
188
272
 
189
- ## 🛡️ Security
273
+ ## 🛡️ Security Model
190
274
 
191
- TAS implements **zero-knowledge encryption** — we can't read your data, Telegram can't read your data, nobody can read your data without your password.
275
+ TAS implements **zero-knowledge encryption** — we can't read your data, Telegram can't read your data, and nobody without your password ever can.
192
276
 
193
- | Layer | Implementation | Why It Matters |
194
- |-------|----------------|----------------|
195
- | **Cipher** | AES-256-GCM | Same cipher used by governments & banks |
196
- | **Key Derivation** | PBKDF2-SHA512, **600k iterations** | OWASP 2025 compliant — brute-force resistant |
197
- | **Salt** | 32 bytes, cryptographically random | Unique per file — no rainbow tables |
198
- | **IV** | 12 bytes, cryptographically random | Unique per file — no pattern analysis |
199
- | **Auth Tag** | 16 bytes GCM authentication | Tamper detection — any bit flip = rejected |
200
- | **Bot Token** | Encrypted at rest (AES-256-GCM) | Even your config file is protected |
201
- | **Password Hash** | Timing-safe PBKDF2 verification | Resistant to timing side-channel attacks |
202
- | **Config Permissions** | `chmod 600` on config.json | Other users on your system can't read your credentials |
203
- | **Integrity** | SHA-256 verified on every download | Bit-perfect downloads, guaranteed |
204
- | **Share Server** | Localhost-only, XSS-safe, RFC 6266 | Binds to 127.0.0.1 by default — your LAN doesn't see it |
277
+ | Layer | Implementation | Standard |
278
+ |---|---|---|
279
+ | **Cipher** | AES-256-GCM (authenticated encryption) | NIST FIPS 197 |
280
+ | **Key Derivation** | PBKDF2-SHA512, **600,000 iterations** | OWASP 2025 |
281
+ | **Salt** | 32 bytes, `crypto.randomBytes()` — unique per file | No rainbow tables |
282
+ | **IV/Nonce** | 12 bytes, `crypto.randomBytes()` — unique per file | No nonce reuse |
283
+ | **Auth Tag** | 16 bytes GCM tag — any tampered bit = instant rejection | Tamper detection |
284
+ | **Bot Token** | Encrypted at rest in `config.json` (AES-256-GCM) | Config v2 |
285
+ | **Password Verification** | `crypto.timingSafeEqual()` on both PBKDF2 and legacy paths | Timing-safe |
286
+ | **Config Permissions** | `chmod 600 config.json` on creation | No world-readable secrets |
287
+ | **Integrity** | SHA-256 hash verified on every single download | Bit-perfect guarantee |
288
+ | **Share Server** | Binds `127.0.0.1` by default, XSS-escaped, RFC 6266 filenames | LAN-safe |
205
289
 
206
- ### What Telegram Sees
290
+ ### What Telegram Actually Sees
207
291
 
208
292
  ```
209
- 📦 a7f3b2c1e9d4.tas — 12.4 MB — application/octet-stream
293
+ 📦 a7f3b2c1e9d4f820.tas — 12.4 MB — application/octet-stream
210
294
  ```
211
295
 
212
- That's it. An opaque encrypted blob. No filename, no content, no metadata. Just noise.
296
+ An opaque, encrypted blob. No filename. No content type. No metadata. Just noise.
297
+
298
+ ### Threat Model
213
299
 
214
- <br>
300
+ | Threat | Mitigated? | How |
301
+ |---|---|---|
302
+ | Telegram reads your files | ✅ Yes | AES-256-GCM — mathematically impossible without key |
303
+ | Someone steals your config.json | ✅ Yes | Bot token encrypted at rest; password hash is PBKDF2 |
304
+ | Brute-force your password | ✅ Yes | 600k PBKDF2 iterations ≈ 100ms/attempt minimum |
305
+ | Tampered download | ✅ Yes | SHA-256 check + GCM auth tag on every download |
306
+ | Timing attack on password | ✅ Yes | `crypto.timingSafeEqual()` on all comparisons |
307
+ | Share link exposure | ✅ Yes | Localhost-only by default; expiry + download limits |
308
+
309
+ ---
215
310
 
216
311
  ## 🔄 Reliability
217
312
 
218
- Built like professional backup tools (inspired by restic, rclone, borg):
313
+ Built with the same philosophy as professional backup tools (restic, borg, rclone):
219
314
 
220
- | Feature | Details |
221
- |---------|---------|
315
+ | Feature | Implementation |
316
+ |---|---|
222
317
  | **Exponential Backoff** | Auto-retry with jitter on Telegram 429 errors and network timeouts |
223
- | **Rate Limiting** | Built-in 1 msg/sec limiter — never hits Telegram's rate limits |
224
- | **Integrity Verification** | SHA-256 hash check after every single download |
225
- | **Resume Uploads** | Interrupted? Run `tas resume` to pick up where you left off |
226
- | **Graceful Shutdown** | SIGINT/SIGTERM handled cleanly — zero data corruption risk |
227
- | **Self-Diagnostics** | `tas doctor` validates your entire setup in seconds |
318
+ | **Rate Limiting** | Built-in 1 msg/sec — never trips Telegram's rate limits |
319
+ | **Integrity Verification** | SHA-256 hash verified after every single download |
320
+ | **Resume Uploads** | `tas resume` picks up interrupted multi-chunk uploads |
321
+ | **Graceful Shutdown** | SIGINT/SIGTERM handled — zero corruption risk on Ctrl-C |
322
+ | **Self-Diagnostics** | `tas doctor` validates your entire setup end-to-end |
228
323
 
229
- <br>
324
+ ---
230
325
 
231
326
  ## 📋 CLI Reference
232
327
 
@@ -234,16 +329,16 @@ Built like professional backup tools (inspired by restic, rclone, borg):
234
329
  <summary><strong>Core Commands</strong></summary>
235
330
 
236
331
  ```bash
237
- tas init # 🚀 Interactive setup wizard
238
- tas push <file> # ⬆️ Upload (encrypt + compress + upload)
239
- tas pull <file|hash> # ⬇️ Download (download + decrypt + verify)
240
- tas list [-l] [--json] # 📋 List all files
241
- tas delete <file|hash> # 🗑️ Remove from index (--hard to delete from Telegram)
242
- tas status [--json] # 📊 Storage stats
243
- tas search <query> # 🔍 Find files by name or tag
244
- tas resume # 🔄 Resume interrupted uploads
245
- tas verify # ✅ Verify all files exist & are intact
246
- tas doctor # 🩺 System health check
332
+ tas init # 🚀 Interactive setup wizard (create bot in ~60s)
333
+ tas push <file> [file2...] # ⬆️ Encrypt + compress + upload
334
+ tas pull <file|hash> # ⬇️ Download + decrypt + verify
335
+ tas list [-l] [--json] # 📋 List all stored files
336
+ tas delete <file|hash> # 🗑️ Remove from index (--hard removes from Telegram)
337
+ tas status [--json] # 📊 Storage stats & database health
338
+ tas search <query> [-t tag] # 🔍 Find by filename or tag
339
+ tas resume # 🔄 Resume interrupted uploads
340
+ tas verify # ✅ Verify every file still exists and is intact
341
+ tas doctor # 🩺 Full system health check
247
342
  ```
248
343
 
249
344
  </details>
@@ -252,15 +347,15 @@ tas doctor # 🩺 System health check
252
347
  <summary><strong>Mount & Sync</strong></summary>
253
348
 
254
349
  ```bash
255
- # FUSE Mount (use Telegram like a local folder)
256
- tas mount <path> # Mount
257
- tas unmount <path> # Unmount
258
-
259
- # Folder Sync (Dropbox-style auto-upload)
260
- tas sync add <folder> # Register a folder to sync
261
- tas sync start # Start watching for changes
262
- tas sync pull # Download all synced files
263
- tas sync status # Show sync status
350
+ # FUSE Mount
351
+ tas mount <path> # Mount Telegram storage as a local folder
352
+ tas unmount <path> # Clean unmount
353
+
354
+ # Dropbox-style Folder Sync
355
+ tas sync add <folder> # Register folder for auto-sync
356
+ tas sync start # Start watching for changes
357
+ tas sync pull # Download all synced files locally
358
+ tas sync status # Show sync queue and status
264
359
  ```
265
360
 
266
361
  </details>
@@ -269,94 +364,158 @@ tas sync status # Show sync status
269
364
  <summary><strong>Share & Tags</strong></summary>
270
365
 
271
366
  ```bash
272
- # Temporary Share Links
273
- tas share create <file> [--expire 24h] [--max-downloads 3]
274
- tas share list # Active shares
275
- tas share revoke <token> # Revoke a share link
276
-
277
- # File Tags
278
- tas tag add <file> <tags...>
279
- tas tag remove <file> <tags...>
280
- tas tag list [tag] # List tags or files with a specific tag
367
+ # Expiring Share Links
368
+ tas share create <file> [--expire 1h|24h|7d] [--max-downloads N]
369
+ tas share list # Active links with expiry countdown
370
+ tas share revoke <token> # Instantly revoke a share
371
+
372
+ # File Tagging
373
+ tas tag add <file> <tag> [tag2...]
374
+ tas tag remove <file> <tag>
375
+ tas tag list [tag] # List all tags, or files with a specific tag
281
376
  ```
282
377
 
283
378
  </details>
284
379
 
285
- <br>
380
+ <details>
381
+ <summary><strong>Environment Variables</strong></summary>
382
+
383
+ ```bash
384
+ TAS_PASSWORD="..." # Skip password prompts (CI/CD, cron, Docker)
385
+ TAS_DATA_DIR="/custom/path" # Override default ~/.tas data directory
386
+ ```
387
+
388
+ </details>
389
+
390
+ ---
286
391
 
287
392
  ## 🏗️ Architecture
288
393
 
289
394
  ```
290
395
  src/
291
- ├── cli.js # Commander-based CLI — all commands
292
- ├── index.js # Streaming upload/download pipeline
396
+ ├── cli.js # Commander-based CLI — all commands defined here
397
+ ├── index.js # Core streaming upload/download pipeline
293
398
  ├── crypto/
294
- │ └── encryption.js # AES-256-GCM + PBKDF2-SHA512 key derivation
399
+ │ └── encryption.js # AES-256-GCM + PBKDF2-SHA512 (600k iterations)
295
400
  ├── db/
296
- │ └── index.js # SQLite index (files, chunks, tags, shares, sync)
401
+ │ └── index.js # SQLite index: files, chunks, tags, shares, sync
297
402
  ├── telegram/
298
- │ └── client.js # Bot API wrapper — retry, rate-limit, streaming
403
+ │ └── client.js # Bot API wrapper — retry, rate-limit, streaming
299
404
  ├── fuse/
300
- │ └── mount.js # FUSE filesystem — mount Telegram as a folder
405
+ │ └── mount.js # FUSE filesystem — mount Telegram as a local folder
301
406
  ├── share/
302
- │ └── server.js # HTTP server — expiring download links
407
+ │ └── server.js # HTTP server — expiring encrypted share links
303
408
  ├── sync/
304
- │ └── sync.js # Folder watcher — Dropbox-style auto-sync
409
+ │ └── sync.js # fs.watch folder watcher — Dropbox-style auto-sync
305
410
  └── utils/
306
- ├── download-stream.js # Shared Telegram→Decrypt→Decompress pipeline
307
- ├── compression.js # Smart gzip (skips already-compressed formats)
308
- ├── chunker.js # 49MB chunking with custom WAS1 file headers
309
- ├── progress.js # Terminal progress bar with speed + ETA
310
- ├── throttle.js # Bandwidth limiter (stream transform)
311
- ├── branding.js # ASCII art + formatting
312
- └── cli-helpers.js # Password management + config resolution
411
+ ├── download-stream.js # Shared Telegram→Decrypt→Decompress pipeline
412
+ ├── compression.js # Smart gzip (skips already-compressed formats)
413
+ ├── chunker.js # 49 MB chunks + WAS1 binary file headers
414
+ ├── progress.js # Terminal progress bars with MB/s + ETA
415
+ ├── throttle.js # Bandwidth limiter (stream transform)
416
+ ├── branding.js # ASCII art + version display
417
+ └── cli-helpers.js # Password management + config resolution
313
418
  ```
314
419
 
315
- **Tech stack:** Node.js · better-sqlite3 · node-telegram-bot-api · fuse-native · Commander · Chalk · Ora · Inquirer
420
+ **Tech stack:** Node.js 18+ · better-sqlite3 · node-telegram-bot-api · fuse-native · Commander · Chalk · Ora · Inquirer
421
+
422
+ ---
423
+
424
+ ## 💡 Perfect For
425
+
426
+ | Use Case | Example |
427
+ |---|---|
428
+ | 📄 **Personal document vault** | Taxes, contracts, scans, receipts — encrypted |
429
+ | 🔑 **Secrets & credentials** | `.env` files, SSH private keys, API tokens |
430
+ | 🗝️ **Password manager sync** | KeePass `.kdbx`, 1Password vaults, Bitwarden exports |
431
+ | 📦 **Code project backups** | Git bundles, build artifacts, config files |
432
+ | 🎬 **Private media archive** | Photos, videos, music — encrypted & searchable |
433
+ | 🔗 **Ephemeral file sharing** | Burn-after-read links with download limits |
434
+ | 💾 **Offsite backup** | Nightly database dumps, system configs via cron |
435
+ | 🤖 **CI/CD artifacts** | Store build outputs, test reports, deployment keys |
436
+
437
+ **Not ideal for:** Mission-critical business data (use professional backup tools alongside this), team collaboration (no multi-user support yet), or replacing full backup systems — **Telegram can theoretically delete old messages.**
438
+
439
+ ---
440
+
441
+ ## ❓ Is This Allowed? (The Legal Question)
442
+
443
+ ### Will Telegram ban me?
316
444
 
317
- <br>
445
+ **No.** Here's the complete picture:
446
+
447
+ - ✅ **Bot API is a public, documented feature** — Telegram designed file uploads into the Bot API intentionally
448
+ - ✅ **You're sending to your own private bot chat** — not a public channel, not spamming
449
+ - ✅ **Content is encrypted** — Telegram cannot detect what you're storing
450
+ - ✅ **No published storage limits** — individual files cap at 2 GB (TAS chunks automatically)
451
+ - ✅ **Strong precedent** — thousands of file-sharing bots, backup tools, and media archives use this API
452
+ - ⚠️ **Worst case** — Telegram might prune old messages to free infrastructure space. They won't ban you for using a documented API
453
+
454
+ **Your responsibility:** Don't store illegal content. Telegram's ToS prohibits copyright infringement, malware, CSAM, etc. Use responsibly. See [FAQ.md](FAQ.md) for the full legal breakdown.
455
+
456
+ ---
318
457
 
319
458
  ## ⚠️ Good to Know
320
459
 
321
460
  | | |
322
461
  |---|---|
323
- | 📌 **Not a backup solution** | Telegram can delete content. Use TAS alongside proper backups, not instead of them. |
324
- | 📌 **49 MB chunks** | Files are automatically split due to Telegram Bot API limits. Fully transparent. |
325
- | 📌 **Single-user** | Designed for personal use. Not multi-tenant. |
326
- | 📌 **FUSE = Linux/macOS** | Mount feature requires `libfuse`. CLI works everywhere Node.js runs. |
462
+ | 📌 **Not a replacement for backups** | Telegram can purge old messages. Use TAS alongside, not instead of, real backup solutions. |
463
+ | 📌 **49 MB chunk size** | Files are split automatically — fully transparent to you. Telegram's Bot API limit is 50 MB. |
464
+ | 📌 **Single-user** | Designed for personal use. No multi-tenant or shared-account support. |
465
+ | 📌 **FUSE = Linux/macOS only** | Mount requires `libfuse`. The CLI itself works anywhere Node.js 18+ runs. |
327
466
  | 📌 **No versioning (yet)** | Overwriting a file replaces the previous version. |
467
+ | 📌 **Internet required** | Telegram-backed — offline access requires files pulled locally first. |
328
468
 
329
- <br>
469
+ ---
330
470
 
331
471
  ## 🛠️ Development
332
472
 
333
473
  ```bash
334
474
  git clone https://github.com/ixchio/tas
335
475
  cd tas && npm install
336
- npm test # 71 tests, all passing
476
+
477
+ npm test # Run all 71 tests (encryption, WAS1 headers, tags, sync, shares)
478
+ npm test -- --watch # Watch mode for active development
337
479
  ```
338
480
 
339
- PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
481
+ **Test coverage:** streaming encrypt/decrypt roundtrips · cross-API compat (buffer↔stream) · small-chunk stress testing · truncation/corruption error paths · Unicode filename handling · WAS1 binary header parsing · timing-safe comparison paths
482
+
483
+ PRs welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
340
484
 
341
- <br>
485
+ ---
342
486
 
343
487
  ## 🌟 Contributing
344
488
 
345
- TAS is open source and we love contributions:
489
+ TAS is open source and contributions are genuinely appreciated:
346
490
 
347
- - 🐛 **Found a bug?** [Open an issue](https://github.com/ixchio/tas/issues)
348
- - 💡 **Have an idea?** [Start a discussion](https://github.com/ixchio/tas/issues)
349
- - 🔧 **Want to contribute?** Fork → Branch → PR → 🎉
491
+ - 🐛 **Found a bug?** [Open an issue](https://github.com/ixchio/tas/issues) — include `tas doctor` output
492
+ - 💡 **Have a feature idea?** [Start a discussion](https://github.com/ixchio/tas/discussions)
493
+ - 🔧 **Want to contribute code?** Fork → branch → PR → 🎉
494
+ - ⭐ **Just want to help?** A GitHub star dramatically increases discoverability
350
495
 
351
- <br>
496
+ ---
352
497
 
353
498
  ## 📄 License
354
499
 
355
- MIT — use it, fork it, ship it, sell it. Do whatever you want.
500
+ MIT — use it, fork it, ship it, sell it. Do whatever you want with it.
501
+
502
+ ---
503
+
504
+ ## Related Projects
505
+
506
+ If TAS fits your workflow, you might also find these useful:
507
+
508
+ - [rclone](https://github.com/rclone/rclone) — rsync for cloud storage (dozens of backends)
509
+ - [restic](https://github.com/restic/restic) — encrypted, deduplicated backup program
510
+ - [age](https://github.com/FiloSottile/age) — simple, modern file encryption tool
511
+ - [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) — encrypted file transfer between machines
356
512
 
357
513
  ---
358
514
 
359
515
  <p align="center">
360
516
  <sub>Built with ☕ and stubbornness by <a href="https://github.com/ixchio">@ixchio</a></sub><br>
361
- <sub>If TAS saved you money, consider giving it a ⭐</sub>
517
+ <sub>If TAS saved you money, a ⭐ on GitHub is the best way to say thanks — it helps others find the project.</sub><br><br>
518
+ <a href="https://github.com/ixchio/tas/stargazers">
519
+ <img src="https://img.shields.io/github/stars/ixchio/tas?style=for-the-badge&logo=github&label=Star%20TAS&color=ffd700" alt="Star TAS on GitHub">
520
+ </a>
362
521
  </p>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nightowne/tas-cli",
3
- "version": "2.4.0",
4
- "description": "Telegram as Storage - Automated encrypted cloud backup. Free, encrypted, scriptable. Mount as folder or use with cron/Docker.",
3
+ "version": "2.4.1",
4
+ "description": "Turn Telegram into unlimited free cloud storage ($0/month forever) • AES-256-GCM encryption • FUSE mount • CLI-first • Zero-knowledge • No signup required",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "chalk": "^5.3.0",
48
48
  "commander": "^12.1.0",
49
49
  "inquirer": "^12.2.0",
50
- "node-telegram-bot-api": "^0.66.0",
50
+ "node-telegram-bot-api": "^1.1.2",
51
51
  "ora": "^8.1.1"
52
52
  },
53
53
  "optionalDependencies": {
package/src/cli.js CHANGED
@@ -744,7 +744,7 @@ syncCmd
744
744
  if (options.limit) {
745
745
  const match = options.limit.match(/^(\d+)([kmg]?)$/i);
746
746
  if (!match) {
747
- console.error(chalk.red('Invalid limit format. Use e.g. 500{}, 1m'));
747
+ console.error(chalk.red('Invalid limit format. Use e.g. 500k, 1m'));
748
748
  process.exit(1);
749
749
  }
750
750
  const val = parseInt(match[1]);
package/src/index.js CHANGED
@@ -37,158 +37,164 @@ export async function processFile(filePath, options) {
37
37
  const db = new FileIndex(path.join(dataDir, 'index.db'));
38
38
  db.init();
39
39
 
40
- if (db.exists(hash)) {
41
- db.close();
42
- throw new Error('File already uploaded (duplicate hash)');
43
- }
44
-
45
- // Prepare processing components
46
- const compressor = new Compressor();
47
- const { stream: compressStream, compressed } = compressor.getCompressStream(filename);
48
- const flags = compressed ? 1 : 0;
49
-
50
- const encryptor = new Encryptor(password);
51
- const encryptStream = encryptor.getEncryptStream();
52
-
53
- const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
54
- if (!fs.existsSync(tempDir)) {
55
- fs.mkdirSync(tempDir, { recursive: true });
56
- }
57
-
58
- // Connect to Telegram
59
- onProgress?.('Connecting to Telegram...');
60
- const client = new TelegramClient(dataDir);
61
- await client.initialize(config.botToken);
62
- client.setChatId(config.chatId);
63
-
64
- // We will stream through a custom Writable chunker
65
- const { Writable } = await import('stream');
66
-
67
- // First pass estimation (for calculating total chunks and progress)
68
- // We don't know the exact final size due to compression and encryption overhead,
69
- // so we'll estimate total chunks and update it if needed.
70
- // For small files < 49MB we assume 1 chunk.
71
- let estimatedSize = compressed ? originalSize : originalSize + 128; // Add encryption overhead
72
- if (compressed && originalSize > 1024 * 1024) estimatedSize = originalSize * 0.8; // Rough guess
73
- let estimatedChunks = Math.ceil(estimatedSize / TELEGRAM_CHUNK_SIZE) || 1;
74
-
75
- // Register file in DB
76
- const fileId = db.addFile({
77
- filename,
78
- hash,
79
- originalSize,
80
- storedSize: 0, // Will update later
81
- chunks: estimatedChunks,
82
- compressed
83
- });
84
-
85
- onProgress?.('Processing and uploading streams...');
86
- let uploadedBytes = 0;
87
- let chunkIndex = 0;
88
-
89
- let currentChunkBuffer = Buffer.alloc(0);
90
- let totalStoredSize = 0;
91
-
92
- // Helper to upload a single chunk
93
- const uploadCurrentChunk = async (isFinal = false) => {
94
- if (currentChunkBuffer.length === 0 && !isFinal) return; // Nothing to upload
95
- if (currentChunkBuffer.length === 0 && isFinal && chunkIndex > 0) return; // Empty final chunk after perfect split
96
-
97
- // At this point we know if it's the final chunk, so we know the total chunks
98
- const totalChunks = isFinal ? chunkIndex + 1 : Math.max(estimatedChunks, chunkIndex + 1);
99
-
100
- const header = createHeader(filename, originalSize, chunkIndex, totalChunks, flags);
101
- const chunkData = Buffer.concat([header, currentChunkBuffer]);
102
-
103
- const chunkFilename = totalChunks > 1
104
- ? `${hash.substring(0, 12)}.part${chunkIndex}.tas`
105
- : `${hash.substring(0, 12)}.tas`;
106
-
107
- const chunkPath = path.join(tempDir, chunkFilename);
108
- fs.writeFileSync(chunkPath, chunkData);
109
-
110
- const caption = totalChunks > 1
111
- ? `📦 ${filename} (${chunkIndex + 1}/${totalChunks})`
112
- : `📦 ${filename}`;
113
-
114
- onProgress?.(`Uploading chunk ${chunkIndex + 1}...`);
115
-
116
- const result = await client.sendFile(chunkPath, caption);
117
-
118
- uploadedBytes += chunkData.length;
119
- totalStoredSize += currentChunkBuffer.length;
120
-
121
- onByteProgress?.({ uploaded: uploadedBytes, total: estimatedSize, chunk: chunkIndex + 1, totalChunks });
122
-
123
- // Store file_id
124
- db.addChunk(fileId, chunkIndex, result.messageId.toString(), chunkData.length);
125
- db.db.prepare('UPDATE chunks SET file_telegram_id = ? WHERE file_id = ? AND chunk_index = ?')
126
- .run(result.fileId, fileId, chunkIndex);
127
-
128
- // Clean up temp file immediately to save disk space
129
- fs.unlinkSync(chunkPath);
40
+ try {
41
+ if (db.exists(hash)) {
42
+ throw new Error('File already uploaded (duplicate hash)');
43
+ }
130
44
 
131
- chunkIndex++;
132
- currentChunkBuffer = Buffer.alloc(0);
133
- };
45
+ // Prepare processing components
46
+ const compressor = new Compressor();
47
+ const { stream: compressStream, compressed } = compressor.getCompressStream(filename);
48
+ const flags = compressed ? 1 : 0;
134
49
 
135
- const chunkingStream = new Writable({
136
- async write(chunk, encoding, callback) {
137
- currentChunkBuffer = Buffer.concat([currentChunkBuffer, chunk]);
50
+ const encryptor = new Encryptor(password);
51
+ const encryptStream = encryptor.getEncryptStream();
138
52
 
139
- // If we exceeded the chunk limit, flush it
140
- if (currentChunkBuffer.length >= TELEGRAM_CHUNK_SIZE) {
141
- const overflow = currentChunkBuffer.subarray(TELEGRAM_CHUNK_SIZE);
142
- currentChunkBuffer = currentChunkBuffer.subarray(0, TELEGRAM_CHUNK_SIZE);
53
+ const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
54
+ if (!fs.existsSync(tempDir)) {
55
+ try {
56
+ fs.mkdirSync(tempDir, { recursive: true });
57
+ } catch (err) {
58
+ throw new Error(`Failed to create temporary directory at ${tempDir}: ${err.message}. Check permissions or set TAS_TMP_DIR to a writable location.`);
59
+ }
60
+ }
143
61
 
62
+ // Connect to Telegram
63
+ onProgress?.('Connecting to Telegram...');
64
+ const client = new TelegramClient(dataDir);
65
+ await client.initialize(config.botToken);
66
+ client.setChatId(config.chatId);
67
+
68
+ // We will stream through a custom Writable chunker
69
+ const { Writable } = await import('stream');
70
+
71
+ // First pass estimation (for calculating total chunks and progress)
72
+ // We don't know the exact final size due to compression and encryption overhead,
73
+ // so we'll estimate total chunks and update it if needed.
74
+ // For small files < 49MB we assume 1 chunk.
75
+ let estimatedSize = compressed ? originalSize : originalSize + 128; // Add encryption overhead
76
+ if (compressed && originalSize > 1024 * 1024) estimatedSize = originalSize * 0.8; // Rough guess
77
+ let estimatedChunks = Math.ceil(estimatedSize / TELEGRAM_CHUNK_SIZE) || 1;
78
+
79
+ // Register file in DB
80
+ const fileId = db.addFile({
81
+ filename,
82
+ hash,
83
+ originalSize,
84
+ storedSize: 0, // Will update later
85
+ chunks: estimatedChunks,
86
+ compressed
87
+ });
88
+
89
+ onProgress?.('Processing and uploading streams...');
90
+ let uploadedBytes = 0;
91
+ let chunkIndex = 0;
92
+
93
+ let currentChunkBuffer = Buffer.alloc(0);
94
+ let totalStoredSize = 0;
95
+
96
+ // Helper to upload a single chunk
97
+ const uploadCurrentChunk = async (isFinal = false) => {
98
+ if (currentChunkBuffer.length === 0 && !isFinal) return; // Nothing to upload
99
+ if (currentChunkBuffer.length === 0 && isFinal && chunkIndex > 0) return; // Empty final chunk after perfect split
100
+
101
+ // At this point we know if it's the final chunk, so we know the total chunks
102
+ const totalChunks = isFinal ? chunkIndex + 1 : Math.max(estimatedChunks, chunkIndex + 1);
103
+
104
+ const header = createHeader(filename, originalSize, chunkIndex, totalChunks, flags);
105
+ const chunkData = Buffer.concat([header, currentChunkBuffer]);
106
+
107
+ const chunkFilename = totalChunks > 1
108
+ ? `${hash.substring(0, 12)}.part${chunkIndex}.tas`
109
+ : `${hash.substring(0, 12)}.tas`;
110
+
111
+ const chunkPath = path.join(tempDir, chunkFilename);
112
+ fs.writeFileSync(chunkPath, chunkData);
113
+
114
+ const caption = totalChunks > 1
115
+ ? `📦 ${filename} (${chunkIndex + 1}/${totalChunks})`
116
+ : `📦 ${filename}`;
117
+
118
+ onProgress?.(`Uploading chunk ${chunkIndex + 1}...`);
119
+
120
+ const result = await client.sendFile(chunkPath, caption);
121
+
122
+ uploadedBytes += chunkData.length;
123
+ totalStoredSize += currentChunkBuffer.length;
124
+
125
+ onByteProgress?.({ uploaded: uploadedBytes, total: estimatedSize, chunk: chunkIndex + 1, totalChunks });
126
+
127
+ // Store file_id
128
+ db.addChunk(fileId, chunkIndex, result.messageId.toString(), chunkData.length);
129
+ db.db.prepare('UPDATE chunks SET file_telegram_id = ? WHERE file_id = ? AND chunk_index = ?')
130
+ .run(result.fileId, fileId, chunkIndex);
131
+
132
+ // Clean up temp file immediately to save disk space
133
+ fs.unlinkSync(chunkPath);
134
+
135
+ chunkIndex++;
136
+ currentChunkBuffer = Buffer.alloc(0);
137
+ };
138
+
139
+ const chunkingStream = new Writable({
140
+ async write(chunk, encoding, callback) {
141
+ currentChunkBuffer = Buffer.concat([currentChunkBuffer, chunk]);
142
+
143
+ // If we exceeded the chunk limit, flush it
144
+ if (currentChunkBuffer.length >= TELEGRAM_CHUNK_SIZE) {
145
+ const overflow = currentChunkBuffer.subarray(TELEGRAM_CHUNK_SIZE);
146
+ currentChunkBuffer = currentChunkBuffer.subarray(0, TELEGRAM_CHUNK_SIZE);
147
+
148
+ try {
149
+ await uploadCurrentChunk(false);
150
+ currentChunkBuffer = overflow; // carry over
151
+ callback();
152
+ } catch (err) {
153
+ callback(err);
154
+ }
155
+ } else {
156
+ callback();
157
+ }
158
+ },
159
+ async final(callback) {
144
160
  try {
145
- await uploadCurrentChunk(false);
146
- currentChunkBuffer = overflow; // carry over
161
+ await uploadCurrentChunk(true);
147
162
  callback();
148
163
  } catch (err) {
149
164
  callback(err);
150
165
  }
151
- } else {
152
- callback();
153
166
  }
154
- },
155
- async final(callback) {
156
- try {
157
- await uploadCurrentChunk(true);
158
- callback();
159
- } catch (err) {
160
- callback(err);
161
- }
162
- }
163
- });
167
+ });
164
168
 
165
- const readStream = fs.createReadStream(filePath);
169
+ const readStream = fs.createReadStream(filePath);
166
170
 
167
- // Run the pipeline: Read -> Compress -> Encrypt -> Chunk & Upload
168
- await pipeline(readStream, compressStream, encryptStream, chunkingStream);
171
+ // Run the pipeline: Read -> Compress -> Encrypt -> Chunk & Upload
172
+ await pipeline(readStream, compressStream, encryptStream, chunkingStream);
169
173
 
170
- // Update the DB with the final accurate values
171
- db.db.prepare('UPDATE files SET stored_size = ?, chunks = ? WHERE id = ?')
172
- .run(totalStoredSize, chunkIndex, fileId);
174
+ // Update the DB with the final accurate values
175
+ db.db.prepare('UPDATE files SET stored_size = ?, chunks = ? WHERE id = ?')
176
+ .run(totalStoredSize, chunkIndex, fileId);
173
177
 
174
- db.close();
178
+ // Clean up temp dir (only if empty)
179
+ try {
180
+ const remaining = fs.readdirSync(tempDir);
181
+ if (remaining.length === 0) fs.rmdirSync(tempDir);
182
+ } catch (e) {
183
+ // Ignore cleanup errors
184
+ }
175
185
 
176
- // Clean up temp dir (only if empty)
177
- try {
178
- const remaining = fs.readdirSync(tempDir);
179
- if (remaining.length === 0) fs.rmdirSync(tempDir);
180
- } catch (e) {
181
- // Ignore cleanup errors
186
+ return {
187
+ filename,
188
+ hash,
189
+ originalSize,
190
+ storedSize: totalStoredSize,
191
+ chunks: chunkIndex,
192
+ compressed
193
+ };
194
+ } finally {
195
+ // Always close DB connection, even on error
196
+ db.close();
182
197
  }
183
-
184
- return {
185
- filename,
186
- hash,
187
- originalSize,
188
- storedSize: totalStoredSize,
189
- chunks: chunkIndex,
190
- compressed
191
- };
192
198
  }
193
199
 
194
200
  /**
@@ -86,13 +86,16 @@ export class TelegramClient {
86
86
  async waitForChatId(timeout = 120000) {
87
87
  return new Promise((resolve, reject) => {
88
88
  const pollingBot = new TelegramBot(this.bot.token, { polling: true });
89
+ let isCompleted = false;
89
90
 
90
91
  const timer = setTimeout(() => {
92
+ isCompleted = true;
91
93
  pollingBot.stopPolling();
92
94
  reject(new Error('Timeout waiting for message. Please message your bot on Telegram.'));
93
95
  }, timeout);
94
96
 
95
97
  pollingBot.on('message', (msg) => {
98
+ isCompleted = true;
96
99
  clearTimeout(timer);
97
100
  pollingBot.stopPolling();
98
101
  this.chatId = msg.chat.id;
@@ -104,10 +107,13 @@ export class TelegramClient {
104
107
  });
105
108
 
106
109
  pollingBot.on('polling_error', (err) => {
107
- // Ignore polling errors during shutdown
108
- if (!err.message.includes('ETELEGRAM')) {
109
- console.error('Polling error:', err.message);
110
+ // Only suppress errors after successful completion or during cleanup
111
+ // During active polling, log all errors for debugging
112
+ if (isCompleted && err.message.includes('ETELEGRAM')) {
113
+ // Suppress expected cleanup errors
114
+ return;
110
115
  }
116
+ console.error('Polling error:', err.message);
111
117
  });
112
118
  });
113
119
  }
@@ -14,7 +14,7 @@ export const LOGO = `
14
14
  `;
15
15
 
16
16
  export const TAGLINE = 'Telegram as Storage';
17
- export const VERSION = '2.4.0';
17
+ export const VERSION = '2.4.1';
18
18
 
19
19
  /**
20
20
  * Print the TAS banner