[login]


Includes an admin panel where you can delete images after logging in, as well as API functionality. You can simply have an AI integrate it into your own site.
It was entirely made with AI, so please don’t be too harsh. It has worked well after several days of testing.
I originally planned to deploy it on CF, but thought images couldn’t be stored permanently, so I changed it. After finishing, I discovered that images apparently can be stored permanently on CF after all, haha.
ImgTG — Telegram Image Hosting System
An image hosting service based on private Telegram channels, supporting API Key authentication and image uploads from any website/app.
Architecture
User/APP ──[API+Key]──→ Backend ──[Upload]──→ Telegram private channel (permanent storage)
│
Browser ──[Access image]──→ Backend ──[Proxy]──→ Telegram CDN
- Storage: Telegram private channel; Telegram stores files permanently for free
- Access: The backend reverse-proxies Telegram files and returns publicly accessible URLs
- Authentication: API Key (
X-API-Keyheader or?key=parameter); third parties can upload using the Key
I. Deployment Instructions
1. Preparation
| Item | Action |
|---|---|
| Telegram Bot | Find @BotFather, create a bot, and obtain its token |
| Private channel | Create a private channel and add the bot as a channel administrator |
| Channel ID | Use @userinfobot to look up the channel ID (format: -100xxxxxxxxxx) |
2. Installation
# Clone/upload the code to the server
cd /www/Node/tgimg
npm install
3. Configure .env
cp .env.example .env
TG_BOT_TOKEN=123456:ABC-DEF... # Bot Token
TG_CHAT_ID=-1001234567890 # Private channel ID
BASIC_USER=admin # Admin panel username
BASIC_PASS=你的管理密码 # Admin panel password
PORT=25741 # Backend listening port (use a high port to avoid requiring root privileges)
BASE_URL=https://img.05.si # Public domain (required; same-domain Key exemption depends on it)
SSL_KEY_PATH= # Leave blank = HTTP mode (use CF for HTTPS)
SSL_CERT_PATH=
BASE_URLmust be filled in correctly: After reverse proxying, thehostheader received by the backend is an internal address. The domain inBASE_URLis used for same-domain Key exemption checks.
4. Start
node server.js
# Or use PM2 to run it as a daemon
pm2 start server.js --name imgtg
pm2 save
5. Reverse Proxy Configuration (Baota Panel + Cloudflare)
Three-layer architecture (important: each layer has its own role; no loop):
Browser (HTTPS) → CF edge (Always Use HTTPS) → Server:80 (HTTP) → Baota reverse proxy → 127.0.0.1:25741 (HTTP) → Backend
Cloudflare Dashboard
| Setting | Value |
|---|---|
| DNS | A record img → server IP, orange cloud (proxy enabled) |
| SSL/TLS | Set encryption mode to Flexible |
| Always Use HTTPS | On (HTTP→HTTPS redirection is handled at the CF edge) |
Baota Panel
| Action | Description |
|---|---|
Add website img.05.si |
Bind the domain |
| Reverse proxy | Target: http://127.0.0.1:25741 |
| Force HTTPS | Must be turned off ← the most important step |
Why Baota’s “Force HTTPS” must be turned off: Baota performs the HTTP→HTTPS redirect at the reverse-proxy layer, which conflicts with the proxy itself and causes an infinite
Too many redirectsloop. Move the HTTPS redirect to the CF edge (Always Use HTTPS); Baota should only perform pure reverse proxying.
6. Verification
# Being able to open the upload page means deployment succeeded
curl http://img.05.si/
Visit https://img.05.si/admin and enter BASIC_USER / BASIC_PASS to access the admin panel, where you can manage API Keys and images.
II. API Usage Instructions
Basic Information
| Item | Value |
|---|---|
| Upload URL | https://img.05.si/upload or https://img.05.si/api/upload |
| Image access | https://img.05.si/image/{id} (public, no Key required) |
| Authentication | X-API-Key request header or ?key= URL parameter |
| Same-domain Key exemption | Uploading from the img.05.si site itself requires no Key (based on the BASE_URL domain) |
1. Upload an Image
Request:
POST /upload
Content-Type: multipart/form-data
X-API-Key: sk-xxxxxxxxxxxx
| Parameter | Type | Description |
|---|---|---|
file |
File | Image/video file (JPG/PNG/GIF/WebP/SVG/BMP/MP4/WebM, maximum 50 MB) |
Response:
{
"success": true,
"image_id": "a1b2c3d4e5f6",
"url": "https://img.05.si/image/a1b2c3d4e5f6",
"is_video": false,
"markdown": "",
"html": "<img src=\"https://img.05.si/image/a1b2c3d4e5f6\" alt=\"image\">",
"bbcode": "[img]https://img.05.si/image/a1b2c3d4e5f6[/img]"
}
Errors:
| Status code | Description |
|---|---|
| 403 | Invalid or missing API Key |
| 400 | No file uploaded |
| 413 | File exceeds 50 MB |
| 500 | Upload failed (Telegram API error) |
2. Access an Image (Public)
GET /image/{id}
Returns the image binary directly and can be used in <img src>. No API Key is required.
3. Examples in Various Languages
cURL
curl -X POST https://img.05.si/upload \
-H "X-API-Key: sk-xxxxxxxxxxxx" \
-F "file=@/path/to/image.jpg"
JavaScript (Browser / fetch)
async function uploadImage(file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('https://img.05.si/upload', {
method: 'POST',
headers: { 'X-API-Key': 'sk-xxxxxxxxxxxx' },
body: formData,
});
const data = await res.json();
if (data.success) {
return data.url; // https://img.05.si/image/xxxx
}
throw new Error(data.error);
}
JavaScript (XHR with Progress)
function uploadImage(file, onProgress) {
return new Promise((resolve, reject) => {
const fd = new FormData();
fd.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://img.05.si/upload');
xhr.setRequestHeader('X-API-Key', 'sk-xxxxxxxxxxxx');
xhr.upload.onprogress = e => {
if (e.lengthComputable && onProgress) {
onProgress(Math.round(e.loaded / e.total * 100));
}
};
xhr.onload = () => {
const d = JSON.parse(xhr.responseText);
d.success ? resolve(d.url) : reject(new Error(d.error));
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(fd);
});
}
Python
import requests
def upload_image(file_path, api_key='sk-xxxxxxxxxxxx'):
with open(file_path, 'rb') as f:
res = requests.post(
'https://img.05.si/upload',
headers={'X-API-Key': api_key},
files={'file': f}
)
data = res.json()
if data['success']:
return data['url']
raise Exception(data['error'])
PHP
function uploadImage($filePath, $apiKey = 'sk-xxxxxxxxxxxx') {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://img.05.si/upload',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["X-API-Key: $apiKey"],
CURLOPT_POSTFIELDS => ['file' => new CURLFile($filePath)],
CURLOPT_RETURNTRANSFER => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$data = json_decode($res, true);
return $data['success'] ? $data['url'] : throw new Exception($data['error']);
}
Java
import java.io.*;
import java.net.*;
public class ImgTG {
public static String upload(File file, String apiKey) throws Exception {
String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
HttpURLConnection conn = (HttpURLConnection)
new URL("https://img.05.si/upload").openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("X-API-Key", apiKey);
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (OutputStream os = conn.getOutputStream()) {
os.write(("--" + boundary + "\r\n").getBytes());
os.write("Content-Disposition: form-data; name=\"file\"; filename=\""
+ file.getName() + "\"\r\n\r\n".getBytes());
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buf = new byte[4096]; int n;
while ((n = fis.read(buf)) > 0) os.write(buf, 0, n);
}
os.write(("\r\n--" + boundary + "--\r\n").getBytes());
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
String line = br.readLine(); // Returns JSON; parse it to extract the URL
return line; // In practice, parse it with Gson/Jackson
}
}
}
HTML Form (Simplest)
<form action="https://img.05.si/upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="key" value="sk-xxxxxxxxxxxx">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
4. API Key Management
After logging in to the admin panel at https://img.05.si/admin with Basic Auth (BASIC_USER/BASIC_PASS):
| Action | Endpoint |
|---|---|
| Create Key | POST /api/keys body {"name":"备注"} → returns sk-xxx |
| List Keys | GET /api/keys |
| Delete Key | DELETE /api/keys/{key} |
| List images | GET /api/images |
| Delete image | DELETE /api/delete/{id} |
III. Frequently Asked Questions
Q1: Upload returns Too many redirects
Cause: “Force HTTPS” is enabled in the Baota panel, causing a loop with the reverse proxy.
Solution: Baota panel → SSL → turn off Force HTTPS, and use Cloudflare’s Always Use HTTPS instead.
Q2: Uploading from my own website returns Invalid API key
Cause: BASE_URL is missing or incorrect in .env, disabling the same-domain Key exemption.
Solution: Make sure BASE_URL=https://your-domain is set.
Q3: Uploading from another website returns Invalid API key
This is normal. Other websites must include an API Key:
headers: { 'X-API-Key': 'sk-xxxxxxxxxxxx' }
// or
fetch('https://img.05.si/upload?key=sk-xxxxxxxxxxxx', ...)
Q4: Port 80 returns EACCES: permission denied
Cause: On Linux, non-root users cannot bind to ports below 1024.
Solution: Use a high port such as PORT=25741 in .env, then reverse-proxy 80→25741 through Baota.
Q5: Image access returns 404
Image URL format: https://img.05.si/image/{id}, where {id} is the image_id returned after upload. Confirm that the ID is correct and that data/db.json has not been accidentally deleted.
tgimg.zip (1.1 MB)
[/login]