vpssever
Blogerwikiimport os
import re
import time
import uuid
import threading
import subprocess
from urllib.parse import urlparse
import requests
from flask import Flask, request, redirect, url_for, jsonify, send_from_directory, abort
# ----------------------------
# Config
# ----------------------------
DOWNLOAD_DIR = os.environ.get("DOWNLOAD_DIR", os.path.abspath("./downloads"))
MAX_CONCURRENT = int(os.environ.get("MAX_CONCURRENT", "2"))
REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "20")) # seconds
CHUNK_SIZE = 1024 * 256 # 256KB
# MEGA
MEGA_PUT_CMD = os.environ.get("MEGA_PUT_CMD", "mega-put") # command name
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
app = Flask(__name__)
JOBS = {}
JOBS_LOCK = threading.Lock()
ACTIVE_SEMAPHORE = threading.Semaphore(MAX_CONCURRENT)
# ----------------------------
# Helpers
# ----------------------------
def safe_filename(name: str) -> str:
name = name.strip().replace("\\", "_").replace("/", "_")
name = re.sub(r"[^A-Za-z0-9.\-_\(\) ]+", "_", name)
name = re.sub(r"\s+", " ", name).strip()
return name[:180] if name else "download.bin"
def guess_filename_from_url(url: str) -> str:
try:
path = urlparse(url).path
base = os.path.basename(path) or "download.bin"
return safe_filename(base)
except Exception:
return "download.bin"
def set_job(job_id, **updates):
with JOBS_LOCK:
job = JOBS.get(job_id)
if not job:
return
job.update(updates)
def get_job(job_id):
with JOBS_LOCK:
return JOBS.get(job_id)
def list_files():
items = []
for fn in os.listdir(DOWNLOAD_DIR):
fp = os.path.join(DOWNLOAD_DIR, fn)
if os.path.isfile(fp):
items.append(
{"name": fn, "bytes": os.path.getsize(fp), "mtime": int(os.path.getmtime(fp))}
)
items.sort(key=lambda x: x["mtime"], reverse=True)
return items
def is_safe_local_file(filename: str) -> bool:
# prevent path traversal
if not filename or filename.startswith(("/", "\\")) or ".." in filename:
return False
full = os.path.abspath(os.path.join(DOWNLOAD_DIR, filename))
return full.startswith(os.path.abspath(DOWNLOAD_DIR) + os.sep) and os.path.isfile(full)
def parse_percent(line: str) -> float | None:
try:
m = re.search(r"(\d+(?:\.\d+)?)\s*%", line)
if not m:
return None
return float(m.group(1))
except Exception:
return None
# ----------------------------
# Download logic
# ----------------------------
def download_direct(job_id: str, url: str, filename: str, headers: dict | None = None):
ACTIVE_SEMAPHORE.acquire()
tmp_path = os.path.join(DOWNLOAD_DIR, f".{filename}.part-{job_id}")
try:
final_path = os.path.join(DOWNLOAD_DIR, filename)
set_job(job_id, status="downloading", method="direct", started_at=int(time.time()))
with requests.get(
url,
stream=True,
timeout=REQUEST_TIMEOUT,
headers=headers or {},
allow_redirects=True,
) as r:
r.raise_for_status()
total = r.headers.get("Content-Length")
total = int(total) if total and total.isdigit() else None
set_job(job_id, total_bytes=total)
downloaded = 0
last_update = 0
with open(tmp_path, "wb") as f:
for chunk in r.iter_content(chunk_size=CHUNK_SIZE):
job = get_job(job_id)
if not job or job.get("cancel"):
set_job(job_id, status="cancelled")
try:
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception:
pass
return
if not chunk:
continue
f.write(chunk)
downloaded += len(chunk)
now = time.time()
if now - last_update > 0.4:
set_job(job_id, downloaded_bytes=downloaded, updated_at=int(now))
last_update = now
out_path = final_path
if os.path.exists(out_path):
root, ext = os.path.splitext(filename)
out_path = os.path.join(DOWNLOAD_DIR, f"{root}-{job_id[:8]}{ext}")
os.replace(tmp_path, out_path)
set_job(
job_id,
status="done",
downloaded_bytes=downloaded,
saved_as=os.path.basename(out_path),
finished_at=int(time.time()),
)
except Exception as e:
set_job(job_id, status="error", error=str(e), finished_at=int(time.time()))
try:
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception:
pass
finally:
ACTIVE_SEMAPHORE.release()
def download_with_ytdlp(job_id: str, url: str):
ACTIVE_SEMAPHORE.acquire()
try:
set_job(job_id, status="downloading", method="yt-dlp", started_at=int(time.time()))
outtmpl = os.path.join(DOWNLOAD_DIR, "%(title).150s-%(id)s.%(ext)s")
cmd = [
"yt-dlp",
"--newline",
"-f",
"bv*+ba/best",
"--merge-output-format",
"mp4",
"-o",
outtmpl,
url,
]
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
set_job(job_id, pid=proc.pid)
last_line = ""
while True:
job = get_job(job_id)
if job and job.get("cancel"):
try:
proc.terminate()
except Exception:
pass
set_job(job_id, status="cancelled", finished_at=int(time.time()))
return
line = proc.stdout.readline() if proc.stdout else ""
if not line:
if proc.poll() is not None:
break
time.sleep(0.05)
continue
last_line = line.strip()
set_job(job_id, log_tail=last_line, updated_at=int(time.time()))
rc = proc.wait()
if rc != 0:
set_job(
job_id,
status="error",
error=f"yt-dlp failed (code {rc}). Last: {last_line}",
finished_at=int(time.time()),
)
return
files = list_files()
set_job(
job_id,
status="done",
saved_as=files[0]["name"] if files else None,
finished_at=int(time.time()),
)
except FileNotFoundError:
set_job(job_id, status="error", error="yt-dlp not installed. Run: pip3 install yt-dlp", finished_at=int(time.time()))
except Exception as e:
set_job(job_id, status="error", error=str(e), finished_at=int(time.time()))
finally:
ACTIVE_SEMAPHORE.release()
# ----------------------------
# MEGA upload logic
# ----------------------------
def mega_upload(job_id: str, filename: str):
"""
Upload a local file from DOWNLOAD_DIR to MEGA "main root".
We call: mega-put <localfile>
(No remote folder argument)
"""
ACTIVE_SEMAPHORE.acquire()
proc = None
try:
if not is_safe_local_file(filename):
set_job(job_id, status="error", error="Invalid file selection.", finished_at=int(time.time()))
return
local_path = os.path.abspath(os.path.join(DOWNLOAD_DIR, filename))
set_job(
job_id,
status="uploading",
method="mega",
started_at=int(time.time()),
saved_as=filename,
progress_percent=None,
)
cmd = [MEGA_PUT_CMD, local_path] # upload into main MEGA root
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
set_job(job_id, pid=proc.pid)
last_line = ""
while True:
job = get_job(job_id)
if job and job.get("cancel") and proc and proc.poll() is None:
try:
proc.terminate()
except Exception:
pass
set_job(job_id, status="cancelled", finished_at=int(time.time()))
return
line = proc.stdout.readline() if proc and proc.stdout else ""
if not line:
if proc and proc.poll() is not None:
break
time.sleep(0.05)
continue
last_line = line.strip()
pct = parse_percent(last_line)
updates = {"log_tail": last_line, "updated_at": int(time.time())}
if pct is not None:
updates["progress_percent"] = pct
set_job(job_id, **updates)
rc = proc.wait() if proc else 1
if rc != 0:
set_job(job_id, status="error", error=f"mega-put failed (code {rc}). Last: {last_line}", finished_at=int(time.time()))
return
set_job(job_id, status="done", finished_at=int(time.time()))
except FileNotFoundError:
set_job(job_id, status="error", error="mega-put not found. Install megacmd and ensure mega-put exists.", finished_at=int(time.time()))
except Exception as e:
set_job(job_id, status="error", error=str(e), finished_at=int(time.time()))
finally:
ACTIVE_SEMAPHORE.release()
# ----------------------------
# Routes
# ----------------------------
@app.route("/", methods=["GET"])
def index():
return f"""
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VPS Downloader</title>
<style>
body {{ font-family: system-ui, Arial; max-width: 1000px; margin: 24px auto; padding: 0 14px; }}
input, button, select {{ padding: 10px; font-size: 14px; }}
input[type=text] {{ width: 100%; }}
.row {{ display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }}
.card {{ border: 1px solid #ddd; border-radius: 12px; padding: 14px; margin: 14px 0; }}
.muted {{ color: #555; font-size: 13px; }}
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 6px; }}
.small {{ font-size: 12px; }}
.btn {{ cursor: pointer; }}
</style>
</head>
<body>
<h2>VPS Downloader + MEGA Upload (TEST MODE)</h2>
<p class="muted">Downloads saved to <code>{DOWNLOAD_DIR}</code>. MEGA upload goes to your main root (no folder args).</p>
<div class="card">
<h3>Download into VPS</h3>
<form method="POST" action="/start">
<label>Download URL</label><br/>
<input name="url" type="text" placeholder="https://example.com/file.zip" required /><br/><br/>
<div class="row">
<div style="flex: 1; min-width: 220px;">
<label>Mode</label><br/>
<select name="mode">
<option value="auto">Auto (direct, fallback yt-dlp)</option>
<option value="direct">Direct file URL</option>
<option value="ytdlp">yt-dlp (video pages)</option>
</select>
</div>
<div style="flex: 2; min-width: 320px;">
<label>Optional filename (direct mode)</label><br/>
<input name="filename" type="text" placeholder="leave empty to auto-detect" />
</div>
<div>
<button class="btn" type="submit">Download</button>
</div>
</div>
</form>
</div>
<div class="card">
<h3>Upload selected files to MEGA</h3>
<div class="small muted">Select files below, then click upload. Selection will stay even while the page auto-refreshes.</div>
<div style="margin-top:10px;">
<button class="btn" onclick="startMegaUpload()">Upload selected</button>
</div>
<div id="file_select_area" class="muted" style="margin-top:10px;">Loading files...</div>
</div>
<div class="card">
<h3>Jobs</h3>
<div id="jobs" class="muted">Loading...</div>
</div>
<div class="card">
<h3>Downloaded files</h3>
<div id="files" class="muted">Loading...</div>
</div>
<script>
const selectedFiles = new Set();
function fmtBytes(n) {{
if (n === null || n === undefined) return "";
const units = ["B","KB","MB","GB","TB"];
let i = 0;
let x = n;
while (x >= 1024 && i < units.length-1) {{ x /= 1024; i++; }}
return x.toFixed(i === 0 ? 0 : 2) + " " + units[i];
}}
function renderFileSelector(files) {{
const sel = document.getElementById("file_select_area");
if (!files.length) {{
sel.innerHTML = "No files to upload yet.";
return;
}}
sel.innerHTML = files.map(f => {{
const checked = selectedFiles.has(f.name) ? "checked" : "";
return `<label style="display:block; padding:6px 0; border-top:1px solid #eee;">
<input type="checkbox" data-fn="${{encodeURIComponent(f.name)}}" ${{checked}} onchange="toggleFile(this)">
${{f.name}} <span class="small muted">(${{fmtBytes(f.bytes)}})</span>
</label>`;
}}).join("");
}}
function toggleFile(cb) {{
const fn = decodeURIComponent(cb.getAttribute("data-fn") || "");
if (!fn) return;
if (cb.checked) selectedFiles.add(fn);
else selectedFiles.delete(fn);
}}
async function startMegaUpload() {{
if (!selectedFiles.size) {{
alert("Select at least one file.");
return;
}}
const payload = new URLSearchParams();
Array.from(selectedFiles).forEach(f => payload.append("files", f));
const res = await fetch("/mega/upload", {{
method: "POST",
headers: {{"Content-Type":"application/x-www-form-urlencoded"}},
body: payload.toString()
}});
if (!res.ok) {{
const txt = await res.text();
alert("Upload start failed: " + txt);
return;
}}
// Optional: clear selection after starting upload
selectedFiles.clear();
refresh();
}}
async function refresh() {{
const res = await fetch("/api/status");
const data = await res.json();
// jobs
const jobsDiv = document.getElementById("jobs");
if (!data.jobs.length) {{
jobsDiv.innerHTML = "No jobs yet.";
}} else {{
jobsDiv.innerHTML = data.jobs.map(j => {{
const isMega = j.method === "mega";
const pct = (j.progress_percent !== null && j.progress_percent !== undefined) ? Math.floor(j.progress_percent) : null;
const total = j.total_bytes || null;
const done = j.downloaded_bytes || 0;
const pct2 = total ? Math.floor((done/total)*100) : null;
const displayPct = isMega ? pct : pct2;
const bar = (displayPct !== null)
? `<div style="height:8px;background:#eee;border-radius:999px;overflow:hidden;">
<div style="height:8px;width:${{displayPct}}%;background:#555;"></div>
</div>` : "";
const tail = j.log_tail ? `<div class="small"><code>${{j.log_tail}}</code></div>` : "";
const cancelBtn = (j.status === "downloading" || j.status === "uploading")
? `<button onclick="cancelJob('${{j.id}}')">Cancel</button>` : "";
const badge = isMega
? `<span class="small" style="padding:2px 8px;border-radius:999px;background:#f2fff2;border:1px solid #bff0bf;">MEGA</span>`
: `<span class="small" style="padding:2px 8px;border-radius:999px;background:#f4f4f4;">DL</span>`;
const line2 = isMega
? (displayPct !== null ? (displayPct + "%") : "")
: (total ? (fmtBytes(done) + " / " + fmtBytes(total) + " (" + displayPct + "%)") : fmtBytes(done));
return `<div style="border-top:1px solid #eee; padding:10px 0;">
<div>${{badge}} <b>${{j.status}}</b> — <code>${{j.id}}</code></div>
<div class="small muted">${{j.url || ""}}</div>
<div class="small">${{line2}}</div>
${{bar}}
<div class="small">File: ${{j.saved_as || "-"}}</div>
<div class="small" style="color:#a00;">${{j.error || ""}}</div>
<div style="margin-top:6px;">${{cancelBtn}}</div>
${{tail}}
</div>`;
}}).join("");
}}
// files list & download links
const filesDiv = document.getElementById("files");
if (!data.files.length) {{
filesDiv.innerHTML = "No files yet.";
}} else {{
filesDiv.innerHTML = data.files.map(f => {{
return `<div style="border-top:1px solid #eee; padding:10px 0;">
<div><a href="/files/${{encodeURIComponent(f.name)}}">${{f.name}}</a></div>
<div class="small muted">${{fmtBytes(f.bytes)}}</div>
</div>`;
}}).join("");
}}
// selector (re-applies checked state from Set)
renderFileSelector(data.files);
}}
async function cancelJob(id) {{
await fetch("/cancel/" + encodeURIComponent(id), {{method:"POST"}});
refresh();
}}
refresh();
setInterval(refresh, 1200);
</script>
</body>
</html>
"""
@app.route("/start", methods=["POST"])
def start():
url = (request.form.get("url") or "").strip()
if not url.lower().startswith(("http://", "https://")):
abort(400, "URL must start with http:// or https://")
mode = (request.form.get("mode") or "auto").strip()
filename = (request.form.get("filename") or "").strip()
job_id = uuid.uuid4().hex
if not filename:
filename = guess_filename_from_url(url)
job = {
"id": job_id,
"url": url,
"status": "queued",
"method": None,
"downloaded_bytes": 0,
"total_bytes": None,
"saved_as": None,
"error": None,
"cancel": False,
"created_at": int(time.time()),
"updated_at": int(time.time()),
"log_tail": None,
"pid": None,
}
with JOBS_LOCK:
JOBS[job_id] = job
def runner():
if mode == "direct":
download_direct(job_id, url, filename)
return
if mode == "ytdlp":
download_with_ytdlp(job_id, url)
return
path = urlparse(url).path.lower()
common_ext = (
".zip",".rar",".7z",".tar",".gz",".bz2",".xz",
".mp4",".mkv",".webm",".mov",".mp3",".wav",".flac",
".iso",".exe",".msi",".apk",".pdf",".jpg",".jpeg",".png",".gif",".webp",
".txt",".csv",".json",".doc",".docx",".ppt",".pptx",".xls",".xlsx"
)
if any(path.endswith(e) for e in common_ext):
download_direct(job_id, url, filename)
return
try:
set_job(job_id, status="probing")
r = requests.head(url, allow_redirects=True, timeout=REQUEST_TIMEOUT)
ctype = (r.headers.get("Content-Type") or "").lower()
if ctype and ("text/html" not in ctype):
download_direct(job_id, url, filename)
return
except Exception:
pass
download_with_ytdlp(job_id, url)
threading.Thread(target=runner, daemon=True).start()
return redirect(url_for("index"))
@app.route("/mega/upload", methods=["POST"])
def mega_upload_route():
files = request.form.getlist("files")
if not files:
abort(400, "No files selected.")
started = []
for fn in files:
fn = (fn or "").strip()
job_id = uuid.uuid4().hex
job = {
"id": job_id,
"url": None,
"status": "queued",
"method": "mega",
"progress_percent": None,
"saved_as": fn,
"error": None,
"cancel": False,
"created_at": int(time.time()),
"updated_at": int(time.time()),
"log_tail": None,
"pid": None,
}
with JOBS_LOCK:
JOBS[job_id] = job
threading.Thread(target=mega_upload, args=(job_id, fn), daemon=True).start()
started.append(job_id)
return jsonify({"ok": True, "started": started})
@app.route("/api/status", methods=["GET"])
def api_status():
with JOBS_LOCK:
jobs = list(JOBS.values())
jobs.sort(key=lambda x: x.get("created_at", 0), reverse=True)
return jsonify(
{"jobs": jobs[:80], "files": list_files()[:400], "download_dir": DOWNLOAD_DIR}
)
@app.route("/cancel/<job_id>", methods=["POST"])
def cancel(job_id):
if not get_job(job_id):
abort(404, "Job not found")
set_job(job_id, cancel=True, updated_at=int(time.time()))
return ("OK", 200)
@app.route("/files/<path:filename>", methods=["GET"])
def files(filename):
if not is_safe_local_file(filename):
abort(404)
return send_from_directory(DOWNLOAD_DIR, filename, as_attachment=True)
if __name__ == "__main__":
print("Download dir:", DOWNLOAD_DIR)
print("MEGA put cmd:", MEGA_PUT_CMD)
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5000")), debug=True)