vpssever

vpssever

Blogerwiki

import 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)

Report Page