@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 +350 -191
- package/package.json +3 -3
- package/src/cli.js +1 -1
- package/src/index.js +142 -136
- package/src/telegram/client.js +9 -3
- package/src/utils/branding.js +1 -1
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="
|
|
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
|
|
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-
|
|
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="
|
|
24
|
-
<a href="
|
|
25
|
-
<a href="#-
|
|
26
|
-
<a href="
|
|
27
|
-
<a href="#-
|
|
28
|
-
<a href="#-
|
|
25
|
+
<a href="QUICKSTART.md"><strong>📚 Quick Start</strong></a> •
|
|
26
|
+
<a href="FAQ.md">FAQ</a> •
|
|
27
|
+
<a href="#-why-tas">Why TAS?</a> •
|
|
28
|
+
<a href="#-features">Features</a> •
|
|
29
|
+
<a href="#-security-model">Security</a> •
|
|
30
|
+
<a href="#-cli-reference">CLI Docs</a> •
|
|
31
|
+
<a href="#-docker--cicd">Docker / CI</a> •
|
|
32
|
+
<a href="CHANGELOG.md">Changelog</a>
|
|
29
33
|
</p>
|
|
30
34
|
|
|
31
35
|
---
|
|
32
36
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
47
|
-
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
│
|
|
51
|
-
│
|
|
52
|
-
│
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
77
|
+
---
|
|
60
78
|
|
|
61
79
|
## ⚡ Quick Start
|
|
62
80
|
|
|
63
|
-
Three commands.
|
|
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
|
-
|
|
69
|
-
tas
|
|
70
|
-
|
|
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
|
-
|
|
96
|
+
> **Need a Telegram bot?** Open Telegram → search `@BotFather` → `/newbot` → copy the token. That's it.
|
|
74
97
|
|
|
75
|
-
|
|
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
|
-
###
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
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
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
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
|
-
|
|
131
|
+
---
|
|
116
132
|
|
|
117
133
|
## 🔥 Features
|
|
118
134
|
|
|
119
|
-
### 🗂️ Mount as a Local Folder
|
|
120
|
-
|
|
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
|
|
124
|
-
#
|
|
125
|
-
|
|
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
|
-
>
|
|
146
|
+
> **Requirements:** `apt install fuse libfuse-dev` (Linux) · `brew install macfuse` (macOS)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### 🔄 Auto-Sync Folders (Dropbox-style)
|
|
129
151
|
|
|
130
|
-
|
|
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
|
|
135
|
-
tas sync start
|
|
136
|
-
tas sync pull
|
|
137
|
-
tas sync status
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
145
|
-
# → http://localhost:3000/d/
|
|
168
|
+
tas share create report.pdf --expire 24h --max-downloads 5
|
|
169
|
+
# → http://localhost:3000/d/a1b2c3d4e5f6...
|
|
146
170
|
|
|
147
|
-
tas share
|
|
148
|
-
tas share
|
|
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
|
-
|
|
152
|
-
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### 🏷️ Tags & Full-Text Search
|
|
153
179
|
|
|
154
180
|
```bash
|
|
155
|
-
tas tag add report.pdf work Q4
|
|
156
|
-
tas
|
|
157
|
-
tas search
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
export
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 |
|
|
194
|
-
|
|
195
|
-
| **Cipher** | AES-256-GCM
|
|
196
|
-
| **Key Derivation** | PBKDF2-SHA512, **
|
|
197
|
-
| **Salt** | 32 bytes,
|
|
198
|
-
| **IV** | 12 bytes,
|
|
199
|
-
| **Auth Tag** | 16 bytes GCM
|
|
200
|
-
| **Bot Token** | Encrypted at rest (AES-256-GCM) |
|
|
201
|
-
| **Password
|
|
202
|
-
| **Config Permissions** | `chmod 600
|
|
203
|
-
| **Integrity** | SHA-256 verified on every download | Bit-perfect
|
|
204
|
-
| **Share Server** |
|
|
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
|
-
📦
|
|
293
|
+
📦 a7f3b2c1e9d4f820.tas — 12.4 MB — application/octet-stream
|
|
210
294
|
```
|
|
211
295
|
|
|
212
|
-
|
|
296
|
+
An opaque, encrypted blob. No filename. No content type. No metadata. Just noise.
|
|
297
|
+
|
|
298
|
+
### Threat Model
|
|
213
299
|
|
|
214
|
-
|
|
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
|
|
313
|
+
Built with the same philosophy as professional backup tools (restic, borg, rclone):
|
|
219
314
|
|
|
220
|
-
| Feature |
|
|
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
|
|
224
|
-
| **Integrity Verification** | SHA-256 hash
|
|
225
|
-
| **Resume Uploads** |
|
|
226
|
-
| **Graceful Shutdown** | SIGINT/SIGTERM handled
|
|
227
|
-
| **Self-Diagnostics** | `tas doctor` validates your entire setup
|
|
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
|
-
|
|
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
|
|
238
|
-
tas push <file>
|
|
239
|
-
tas pull <file|hash>
|
|
240
|
-
tas list [-l] [--json]
|
|
241
|
-
tas delete <file|hash>
|
|
242
|
-
tas status [--json]
|
|
243
|
-
tas search <query>
|
|
244
|
-
tas resume
|
|
245
|
-
tas verify
|
|
246
|
-
tas doctor
|
|
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
|
|
256
|
-
tas mount <path>
|
|
257
|
-
tas unmount <path>
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
tas sync add <folder>
|
|
261
|
-
tas sync start
|
|
262
|
-
tas sync pull
|
|
263
|
-
tas 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
|
-
#
|
|
273
|
-
tas share create <file> [--expire 24h] [--max-downloads
|
|
274
|
-
tas share list
|
|
275
|
-
tas share revoke <token>
|
|
276
|
-
|
|
277
|
-
# File
|
|
278
|
-
tas tag add <file> <
|
|
279
|
-
tas tag remove <file> <
|
|
280
|
-
tas tag list [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
|
-
<
|
|
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
|
|
292
|
-
├── index.js
|
|
396
|
+
├── cli.js # Commander-based CLI — all commands defined here
|
|
397
|
+
├── index.js # Core streaming upload/download pipeline
|
|
293
398
|
├── crypto/
|
|
294
|
-
│ └── encryption.js
|
|
399
|
+
│ └── encryption.js # AES-256-GCM + PBKDF2-SHA512 (600k iterations)
|
|
295
400
|
├── db/
|
|
296
|
-
│ └── index.js
|
|
401
|
+
│ └── index.js # SQLite index: files, chunks, tags, shares, sync
|
|
297
402
|
├── telegram/
|
|
298
|
-
│ └── client.js
|
|
403
|
+
│ └── client.js # Bot API wrapper — retry, rate-limit, streaming
|
|
299
404
|
├── fuse/
|
|
300
|
-
│ └── mount.js
|
|
405
|
+
│ └── mount.js # FUSE filesystem — mount Telegram as a local folder
|
|
301
406
|
├── share/
|
|
302
|
-
│ └── server.js
|
|
407
|
+
│ └── server.js # HTTP server — expiring encrypted share links
|
|
303
408
|
├── sync/
|
|
304
|
-
│ └── sync.js
|
|
409
|
+
│ └── sync.js # fs.watch folder watcher — Dropbox-style auto-sync
|
|
305
410
|
└── utils/
|
|
306
|
-
├── download-stream.js
|
|
307
|
-
├── compression.js
|
|
308
|
-
├── chunker.js
|
|
309
|
-
├── progress.js
|
|
310
|
-
├── throttle.js
|
|
311
|
-
├── branding.js
|
|
312
|
-
└── cli-helpers.js
|
|
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
|
-
|
|
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
|
|
324
|
-
| 📌 **49 MB
|
|
325
|
-
| 📌 **Single-user** | Designed for personal use.
|
|
326
|
-
| 📌 **FUSE = Linux/macOS** | Mount
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
+
---
|
|
342
486
|
|
|
343
487
|
## 🌟 Contributing
|
|
344
488
|
|
|
345
|
-
TAS is open source and
|
|
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
|
|
349
|
-
- 🔧 **Want to contribute?** Fork →
|
|
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
|
-
|
|
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,
|
|
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.
|
|
4
|
-
"description": "Telegram
|
|
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": "^
|
|
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.
|
|
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
|
-
|
|
41
|
-
db.
|
|
42
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
currentChunkBuffer = Buffer.concat([currentChunkBuffer, chunk]);
|
|
50
|
+
const encryptor = new Encryptor(password);
|
|
51
|
+
const encryptStream = encryptor.getEncryptStream();
|
|
138
52
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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(
|
|
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
|
-
|
|
169
|
+
const readStream = fs.createReadStream(filePath);
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
// Run the pipeline: Read -> Compress -> Encrypt -> Chunk & Upload
|
|
172
|
+
await pipeline(readStream, compressStream, encryptStream, chunkingStream);
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
/**
|
package/src/telegram/client.js
CHANGED
|
@@ -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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
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
|
}
|