A secure, efficient command-line tool for managing file uploads to Telegram channels with multi-account support.
- Bulk file uploads - Upload single files or entire directories to channels
- Telegram storage - Store, download, and list files using a Telegram channel as cloud storage with automatic file splitting for large files
- Multi-account support - Manage multiple Telegram accounts
- Automatic retry - Handles rate limits and flood waits with exponential backoff
- Progress tracking - Real-time upload/download progress bars
- Smart image processing - Automatic resizing for Telegram dimension limits
- Video optimization - MP4 files with proper metadata extraction
- Resume support - Interrupted storage uploads can be resumed
- Secure configuration - Environment-based credential management with session file protection
- Comprehensive logging - Detailed logs with rotation
- Input validation - Secure path handling, sanitization, and validation
- Process locking - Prevents multiple instances from running with the same account
- Bun 1.2 or higher — the runtime the CLI, tests, Docker image, and binaries all use
- FFmpeg (for video metadata extraction via
ffprobe)
- Clone the repository:
git clone https://github.com/yourusername/tgmanager.git
cd tgmanager- Install dependencies:
bun install- Create your environment file:
cp .env.example .env- Edit
.envwith your Telegram credentials:
# Account: your_account_name
YOUR_ACCOUNT_API_ID=123456
YOUR_ACCOUNT_API_HASH=your_api_hash_here
YOUR_ACCOUNT_PHONE=+1234567890
YOUR_ACCOUNT_PASSWORD=your_password_hereThat is the whole setup — Bun runs the TypeScript sources directly, so no build step is required before using the CLI.
Examples below are written as tgmanager. Substitute whichever form you use:
| Form | Command | When |
|---|---|---|
| From source | bun run src/index.ts |
Development, and the default way to use the tool |
| Standalone binary | ./dist/uploader-linux |
Machines without Bun installed (bun run build-native) |
A shell alias keeps the examples copy-pasteable:
alias tgmanager='bun run /path/to/tgmanager/src/index.ts'- Visit https://my.telegram.org
- Log in with your phone number
- Go to "API Development Tools"
- Create a new application
- Copy your
api_idandapi_hash
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL |
Logging level (error, warn, info, debug) | info |
SESSION_DIR |
Directory for session storage | sessions (in project) |
UPLOAD_DIR |
Default upload directory | uploads (in project) |
TGMANAGER_CONFIG |
Path to a custom .env file |
Auto-detected |
TGMANAGER_HOME |
Base directory for sessions and config | Project directory / ~/.tgmanager |
TGMANAGER_DEFAULT_ACCOUNT |
Default account name (skip -a flag) |
None |
Set a default account to skip the -a flag:
export TGMANAGER_DEFAULT_ACCOUNT=myaccountUse short command aliases with positional arguments:
# Upload to storage (store = upload-storage)
./uploader-linux store /path/to/file.zip /backups/file.zip
# Download from storage (get = download-storage)
./uploader-linux get /backups/file.zip --output-path ./restored.zip
# List storage files (ls = list-storage)
./uploader-linux ls /backups/
# Aliases work with flags too
./uploader-linux store /path/to/file.zip /backups/file.zip --delete-source --waitAll original flag-based syntax remains fully supported.
Directly from source:
bun run src/index.ts -a your_account -c upload -i @channel_username -f /path/to/file.mp4With auto-restart while editing:
bun run dev -- -a your_account -c upload -i @channel_username -f /path/to/file.mp4Or using standalone binaries (see Building Binaries):
./uploader-linux -a your_account -c upload -i @channel_username -f /path/to/file.mp4Upload a single file:
tgmanager -a myaccount -c upload -i @mychannel -f /path/to/file.mp4Upload a directory (each file becomes a queue job, processed in order):
tgmanager -a myaccount -c upload -i @mychannel -f /path/to/directory/Upload with source deletion after success:
tgmanager -a myaccount -c upload -i @mychannel -f video.mp4 --delete-sourceUpload to a specific chat ID:
tgmanager -a myaccount -c upload -i -1001234567890 -f document.pdftgmanager -a myaccount -c create -n "My New Channel"Uploads a file to a dedicated storage channel. Large files are automatically split into chunks. Files are identified by a virtual path (like a filesystem).
tgmanager -a myaccount -c upload-storage -f /path/to/largefile.zip --virtual-path /backups/largefile.zipWith a specific storage channel:
tgmanager -a myaccount -c upload-storage -f data.db --virtual-path /databases/data.db --storage-channel -1001234567890Downloads a previously stored file by its virtual path. Chunks are downloaded with retry logic and merged automatically.
tgmanager -a myaccount -c download-storage --virtual-path /backups/largefile.zipDownload to a specific location:
tgmanager -a myaccount -c download-storage --virtual-path /backups/largefile.zip --output-path /tmp/restored.zipForce overwrite existing file:
tgmanager -a myaccount -c download-storage --virtual-path /backups/largefile.zip --forceList all stored files:
tgmanager -a myaccount -c list-storageFilter by virtual path prefix:
tgmanager -a myaccount -c list-storage --virtual-path /backups/Both upload and upload-storage enqueue their work before doing anything
else, so every file is a durable job. See Upload queue.
tgmanager -a myaccount -c queue-status # outstanding work, in the order it will run
tgmanager -a myaccount -c queue-status --status failed # just failures, with the reason each one failed
tgmanager -a myaccount -c queue-status --limit 20 # show more rows
tgmanager -a myaccount -c queue-cancel -n a3f9 # partial job id is enough
tgmanager -a myaccount -c queue-retry # requeue every failed job
tgmanager -a myaccount -c queue-retry -n a3f9 # requeue one job by idWith no --status, queue-status leads with whatever is uploading now and the
few jobs behind it, and says up front whether any work is left at all. The job
currently uploading shows its percentage in place of its status:
Upload Queue (account: myaccount)
36 jobs queued (1 processing, 35 pending)
# ID File Status Created
1 ea3e0bd9 ...holiday-footage-004.mp4 41% 2d ago
2 4e383c9d ...holiday-footage-005.mp4 pending 2d ago
None of these three need the account lock or a Telegram connection, so they all work while an upload is running.
queue-retry skips any failed job whose source file no longer exists, since it
could only fail again — usually that means a later attempt already uploaded it
successfully. It also refuses to touch a job a live worker still owns, rather
than racing it and uploading the file twice.
Processes whatever is already queued. Unlike upload, it enqueues nothing, so
it is the way to resume after a run was interrupted without re-pointing at the
original source directory:
tgmanager -a myaccount -c queue-runIt also recovers jobs abandoned by a worker that died — see
Upload queue below. This is the command to reach for whenever
queue-status reports stalled jobs.
| Option | Description | Required |
|---|---|---|
-a, --account <name> |
Account name from config | Yes |
-c, --command <cmd> |
Command to execute | Yes |
-i, --chat-id <id> |
Chat ID or @username | For upload |
-f, --file-path <path> |
File or directory path | For upload, upload-storage |
-n, --name <name> |
Channel name | For create |
--delete-source |
Delete files after successful upload | No |
--virtual-path <path> |
Virtual path for storage operations | For upload-storage, download-storage |
--output-path <path> |
Output path for downloads | For download-storage |
--storage-channel <id> |
Storage channel ID (auto-creates if omitted) | No |
--force |
Force overwrite existing files | No |
--wait |
Block until the queued upload finishes | No |
--priority <n> |
Queue priority; higher runs first (default 0) | No |
--at <when> |
Do not start before this time (ISO 8601) | No |
--status <status> |
Filter queue-status by job status |
No |
--limit <n> |
Cap how many jobs queue-status prints |
No |
| Command | Description |
|---|---|
upload |
Upload files or directories to a Telegram channel |
create |
Create a new Telegram broadcast channel |
upload-storage / store |
Upload a file to Telegram storage with automatic splitting |
download-storage / get |
Download a file from Telegram storage by virtual path |
list-storage / ls |
List all files stored in Telegram storage |
queue-run |
Drain queued jobs without enqueueing anything new |
queue-status |
Show what is uploading now and what is waiting |
queue-retry |
Requeue failed jobs, all of them or one by id |
queue-cancel |
Cancel a queued job by id |
- Videos: MP4 files with automatic metadata extraction (width, height, duration)
- Images: JPG, JPEG, PNG, GIF with automatic resizing when exceeding Telegram limits
- Documents: All other file types uploaded as-is
| Account Type | Direct Upload Limit | Storage Upload |
|---|---|---|
| Regular | 2 GB per file | Unlimited (auto-split) |
| Premium | 3.91 GB per file | Unlimited (auto-split) |
A premium account is entitled to 4 GB, but the upload protocol is the tighter
constraint: large files go up through upload.SaveBigFilePart, which accepts at
most 8000 parts of 512 KB — 4,194,304,000 bytes, or 3.91 GB. A file between
3.91 GB and 4 GB cannot complete however long it runs, so upload rejects it
immediately rather than transferring for hours and failing at the end with
FILE_PARTS_INVALID.
Use upload-storage for anything larger: it splits files into chunks and has no
practical size ceiling.
- Image dimensions: Max 5000x5000 pixels (auto-resized if exceeded)
- Storage uploads: Files exceeding the account limit are automatically split into chunks and reassembled on download
- Never commit credentials - Use environment variables
- Secure your .env file - Set proper file permissions
- Use strong passwords - Enable 2FA on Telegram
- Rotate API keys - Regenerate periodically
- Monitor logs - Check for unauthorized access
Logs are stored in the logs/ directory:
app.log- All application logserror.log- Error logs only
Log rotation is automatic after 10MB.
Both upload and upload-storage enqueue their work before taking the
account lock, then whichever process holds the lock drains the queue. There is
no daemon: jobs move only while an upload command is running. But the queue is
durable, so anything left behind is picked up by the next run.
- No other instance running — your process takes the lock, becomes the worker, and drains the queue until it is empty.
- Another instance already running — your jobs join the queue and that
worker picks them up. You get
✓ Worker is activeand exit immediately. Pass--waitto block until your own files are done instead.
Because each file is its own job, a run killed part-way resumes rather than rescanning, and a failure is recorded rather than only logged:
tgmanager -a myaccount -c queue-status --status failedJobs are kept after they finish, so that stays useful long afterwards. Each
failure records why it failed — the size limit, an unreadable file, or
Telegram's own error text — so --status failed explains itself without
digging through the log.
A worker killed mid-upload — a reboot, an OOM kill, a closed terminal — leaves
its job marked processing with the progress it had reached. queue-status
checks whether that worker is still alive and says so plainly, instead of
showing a frozen percentage that reads like an upload in flight:
36 jobs queued (1 processing, 35 pending)
Warning: 1 job is stalled — claimed by a worker that is no longer running.
No upload is in progress. Run queue-run to requeue and resume.
# ID File Status Created
1 ea3e0bd9 ...holiday-footage-004.mp4 stalled 52% 2d ago
Running queue-run (or any upload command) returns such jobs to pending
automatically and carries on. The interrupted file restarts from the beginning;
progress is reported, not resumed.
Ordering is by priority (descending), then oldest-first. A job given --at is
not offered to a worker until that time has passed.
The queue lives in ~/.tgmanager/queue.db, one SQLite database shared by all
accounts.
# Run from source with auto-restart on change
bun run dev
# Type checking (the correctness gate; no output emitted)
bun run type-check
# Run tests (vitest — note `bun run test`, not `bun test`)
bun run test
# Run tests in watch mode
bun run test:watch
# Run tests with coverage
bun run test:coverage
# Run ESLint
bun run lint
# Full build (dist/ + native binaries for all four targets)
bun run buildCI runs these gates in order of signal strength — type-check, test,
compile, then lint — so a correctness failure is never masked by a style
failure. tsc is only ever a type checker here; the binaries come from
bun build --compile.
Binaries are compiled with Bun. Build all four targets:
bun run build-nativeBuild a single platform:
# linux-x64 (swap the target for bun-linux-arm64, bun-windows-x64, bun-darwin-x64)
bun build src/index.ts --compile --minify --target=bun-linux-x64 --outfile dist/uploader-linuxImage resizing is unavailable in the standalone binaries.
bun build --compilecannot embed native addons, sosharpdoes not load there and images are uploaded without resizing. Telegram rejects images beyond its dimension limits, so use the Docker image (bun run build-docker) or run from source if you upload oversized images.
The compiled executables support multiple configuration methods:
- Environment file (
.env) in multiple locations - System environment variables
- Custom config path via
TGMANAGER_CONFIG
See docs/standalone.md for detailed instructions on using standalone executables with credentials.
Build the Docker image:
docker build -t tgmanager .Run with Docker:
docker run -v ~/.tgmanager:/root/.tgmanager tgmanager -a account -c upload -i @channel -f /path/to/fileThe storage feature uses a dedicated Telegram channel as a file storage backend. Files are addressed by virtual paths (e.g., /backups/db.sql) and tracked via JSON manifests.
- Upload: Files exceeding the Telegram size limit are split into chunks automatically. Each chunk is uploaded as a separate message. A manifest message tracks all chunks, checksums, and metadata.
- Download: The manifest is fetched by virtual path, chunks are downloaded with retry logic and integrity verification (SHA-256), then merged back into the original file.
- List: Searches the storage channel for manifest messages and displays stored files grouped by directory.
- If
--storage-channelis not specified, TGManager auto-creates a channel named "TGManager Storage" - The channel is reused across sessions (found by title)
- Do not delete the storage channel or its messages manually
If a storage upload is interrupted, re-running the same command will skip already-uploaded chunks and resume from where it left off.
Below are practical workflows for managing files with Telegram storage. They use
the tgmanager shorthand described in How to run it.
# Upload today's database dump
tgmanager -a myaccount -c upload-storage \
-f /var/backups/postgres-2026-02-05.sql.gz \
--virtual-path /backups/db/postgres-2026-02-05.sql.gz
# List all database backups
tgmanager -a myaccount -c list-storage --virtual-path /backups/db/
# Restore a specific backup
tgmanager -a myaccount -c download-storage \
--virtual-path /backups/db/postgres-2026-02-05.sql.gz \
--output-path /tmp/restore.sql.gz# Upload a large video (auto-splits if it exceeds Telegram's limit)
tgmanager -a myaccount -c upload-storage \
-f ~/Videos/recording-4k.mkv \
--virtual-path /videos/recording-4k.mkv \
--delete-source
# Verify it's stored
tgmanager -a myaccount -c list-storage --virtual-path /videos/
# Download it later on another machine
tgmanager -a myaccount -c download-storage \
--virtual-path /videos/recording-4k.mkv \
--output-path ~/Downloads/recording-4k.mkv# Upload multiple project archives with a folder structure
tgmanager -a myaccount -c upload-storage \
-f ./project-v1.tar.gz --virtual-path /archives/myapp/v1.0.0.tar.gz
tgmanager -a myaccount -c upload-storage \
-f ./project-v2.tar.gz --virtual-path /archives/myapp/v2.0.0.tar.gz
# List only files under /archives/myapp/
tgmanager -a myaccount -c list-storage --virtual-path /archives/myapp/By default, TGManager auto-creates a channel. If you want to use a specific channel (e.g., to separate personal and work storage):
# Upload to a specific channel
tgmanager -a myaccount -c upload-storage \
-f ./report.pdf \
--virtual-path /work/reports/q1-2026.pdf \
--storage-channel -1001234567890
# List and download from the same channel
tgmanager -a myaccount -c list-storage \
--storage-channel -1001234567890
tgmanager -a myaccount -c download-storage \
--virtual-path /work/reports/q1-2026.pdf \
--storage-channel -1001234567890# Start uploading a 10 GB file — gets interrupted at 60%
tgmanager -a myaccount -c upload-storage \
-f ~/iso/ubuntu-server.iso \
--virtual-path /iso/ubuntu-server.iso
# ^C (interrupted)
# Re-run the exact same command — skips already uploaded chunks
tgmanager -a myaccount -c upload-storage \
-f ~/iso/ubuntu-server.iso \
--virtual-path /iso/ubuntu-server.iso
# Resuming upload: 6/10 chunks already uploaded, uploading remaining 4...# First download
tgmanager -a myaccount -c download-storage \
--virtual-path /backups/db/latest.sql.gz \
--output-path ./latest.sql.gz
# Download again — fails because file already exists
tgmanager -a myaccount -c download-storage \
--virtual-path /backups/db/latest.sql.gz \
--output-path ./latest.sql.gz
# Error: Output file already exists. Use --force to overwrite.
# Force overwrite
tgmanager -a myaccount -c download-storage \
--virtual-path /backups/db/latest.sql.gz \
--output-path ./latest.sql.gz \
--force-
Authentication failed
- Check your API credentials
- Ensure phone number format includes country code
- Verify 2FA password if enabled
-
File upload fails
- Run
-c queue-status --status failed— each failure records its own reason - Check file size limits (see Size Limits)
- Verify file permissions
- Ensure sufficient disk space
- Run
-
The queue looks stuck / progress has not moved
- Run
-c queue-status. A job whose worker died shows asstalled, with a warning saying no upload is in progress — nothing is running, so nothing will move until you start a worker - Run
-c queue-runto requeue the stalled job and drain the rest - A percentage with no
stalledmarker means a live worker really is uploading; large files can sit at one percentage for a while
- Run
-
"File is 3.93 GB, over the 3.91 GB limit"
- Telegram's upload protocol caps a single transfer at 3.91 GB, below the 4 GB a premium account is otherwise allowed (see Size Limits)
- Use
-c upload-storageinstead, which splits the file into chunks
-
Rate limiting
- Tool handles this automatically
- Increase flood wait multiplier in config if needed
-
Session errors
- Delete session folder and re-authenticate
- Check session directory permissions
-
Storage download fails
- Verify the virtual path is correct with
list-storage - Ensure the storage channel and its messages haven't been deleted
- Use
--forceif the output file already exists
- Verify the virtual path is correct with
-
Another instance already running
- A process lock prevents concurrent use of the same account
- Wait for the other instance to finish, or check for stale lock files in the
locks/directory
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Language: TypeScript 5.x with ES modules
- Runtime: Bun 1.2+
- Type Safety: Full TypeScript support with strict mode
- Telegram API: teleproto (maintained fork of GramJS)
- Image Processing: Sharp
- Video Processing: FFmpeg
- Logging: Winston with rotation
- CLI: Commander.js
- Testing: Vitest
- Build: Bun (
bun build --compilefor native binaries)
- Built with teleproto, the maintained fork of GramJS
- Image processing by Sharp
- Video processing with FFmpeg
By default, all data is stored within the project directory:
- Sessions:
./sessions/- Telegram session files - Uploads:
./uploads/- Default directory for files to upload - Logs:
./logs/- Application and error logs
You can customize these locations using environment variables to use absolute paths or different relative paths.
For issues and feature requests, please use the GitHub issue tracker.