📄 gost-files-server.py 📝 编辑 ⬇ 下载 ← 返回列表
# -*- coding: utf-8 -*-
# GOST 文件分发源 v4 最终版: 列表 + 查看 + 编辑(保存) + 上传 + 分页 + 文件夹 + serverip
# 单文件, 阿里云(relly同理可部署) 
 os, urllib.parse, html, json, time, re
 http.server  BaseHTTPRequestHandler, ThreadingHTTPServer

 os  _os
PUB_DIR = _os.environ.get("MIRROR_PUB_DIR", "/tmp/pub")
AUTH_USER = _os.environ.get("MIRROR_USER", "admin")
AUTH_PASS = _os.environ.get("MIRROR_PASS", "oyh159159oiw.@")
UPSTREAM = _os.environ.get("MIRROR_UPSTREAM", "")

DESCRIPTIONS = {
    "gost-deploy.sh":       ("🚀 GOST 三件套一键部署/管理", "安装 · 菜单管理 · 启停重启 · 升级 · 卸载(内嵌完整面板,自包含)"),
    "npm.sh":               ("🟦 Nginx Proxy Manager 一键部署", "在线拉取 / 离线 docker load 双模式 · 启停 · 升级 · 备份 · 卸载"),
    "npm.tar":              ("📦 NPM 离线镜像包", "docker load -i npm.tar 导入后配合 npm.sh 离线安装"),
    "panel.html":           ("🖥️ GOST 面板页面 (v2)", "多API切换 · 启停开关 · 探活 · 限速 · yaml查看 · 操作日志 · 主题切换"),
    "gost-panel-server.py": ("⚙️ GOST 面板后端服务", "同源代理 · 状态监控 · 端口冲突检测 · 探活 · 操作日志接口"),
    "gost-save-server.py":  ("💾 GOST 规则保存服务", "/save 持久化 · /yaml /rawyaml · 60秒自动同步守护"),
    "frp单版本":             ("🔀 FRP 单版本脚本集", "FRP 服务端/客户端部署脚本"),
    "nps单版本":             ("🔀 NPS 单版本脚本集", "NPS 服务端/客户端部署脚本"),
}


 auto_desc(fp, base):
    :
         open(fp, "rb")  f:
            head = f.read().decode("utf-", errors="replace")
     Exception:
        head = ""
    lines = head.split("\n")[:]
    title = ""
    subs = []
     re  _re
     ln  lines:
        s = ln.strip().lstrip("#").strip()
          s  s.startswith("!"):
            
        # 跳过纯分隔线
         _re.fullmatch(r"[=\-*_]+", s):
            
         key  ("描述:", "说明:", "简介:", "功能:"):
             s.startswith(key):
                title = s[len(key):].strip()
                
         title:
            
         s.startswith("用法")  s.startswith("Usage")  s.startswith("子命令"):
             ":"  s  len(s) > :
                subs.append(s)  # 带内容的说明行
         s.startswith("bash ")  s.startswith("./"):
            subs.append(s)  # 命令示例
         len(s) >=    title   subs  len(s) <=    _re.search(r"[=\-]{,}", s):
            title = s
    title = title.strip("=").strip("-").strip(" ").strip("*").strip()
      title:
        title = "📄 " + base
    sub_text = " · ".join(subs[:])  subs  "脚本文件"
     (title[:], sub_text[:])

 check_auth(headers):
    a = headers.get("Authorization", "")
     a.startswith("Basic "):
         base64
        :
            u, p = base64.b64decode(a[:]).decode().split(":", )
             u == AUTH_USER  p == AUTH_PASS
         Exception:
             
     


# 登录session存储(文件持久化, 重启不丢失)
 secrets  _secrets
_SESS_FILE = "/tmp/pub_real/.sessions"
:
     open(_SESS_FILE)  _f:
        SESSIONS = set(l.strip()  l  _f  l.strip())
 Exception:
    SESSIONS = set()

 _save_sessions():
    :
         open(_SESS_FILE, "w")  f:
            f.write('\n'.join(SESSIONS))
     Exception:
        

 _new_token():
     _secrets.token_hex()

 _get_cookie(self):
    """从请求头解析cookie, 返回session token或None"""
    ck = self.headers.get("Cookie", "")
     part  ck.split(";"):
        part = part.strip()
         part.startswith("gsess="):
             part[:]
     

 _is_logged_in(self):
    """检查请求是否已登录(session cookie 或 Basic Auth)"""
    tok = _get_cookie(self)
     tok  tok  SESSIONS:
         
     check_auth(self.headers)

 safe_path(rel):
    fpath = os.path.realpath(os.path.join(PUB_DIR, rel))
     root, dirs, files  os.walk(PUB_DIR):
         f  files:
            :
                rp = os.path.realpath(os.path.join(root, f))
                 fpath == rp:  fpath
             Exception: 
         d  dirs:
            :
                rd = os.path.realpath(os.path.join(root, d))
                 fpath == rd:  fpath
             Exception: 
     

 get_real_files():
    result = []
     root, dirs, files  os.walk(PUB_DIR):
        rel_root = os.path.relpath(root, PUB_DIR)
         d  dirs:
            rel = os.path.join(rel_root, d)  rel_root != "."  d
             rel   [r[]  r  result]:
                result.append(("dir", rel, ))
         f  files:
             f.startswith(".")  f.endswith(".bak")  f == "desc.json" :
                  # 隐藏文件/备份不显示
            rel = os.path.join(rel_root, f)  rel_root != "."  f
            fp = os.path.join(root, f)
             os.path.isfile(fp)  os.path.islink(fp):
                : sz = os.path.getsize(os.path.realpath(fp))
                 Exception: sz = 
                result.append(("file", rel, sz))
     sorted(result, key= x: (x[] == "file", x[]))


 fsize_h(n):
    f = float(n); i = 
     u  ["B","KB","MB","GB"]:
         f <   i == : 
        f /= ; i += 
     f"{f:.1f}{u}"  i  f"{int(n)}B"

 detect_ip():
     urllib.request  u
     src  ("http://members.org/dyndns/getip", "http://ip.net", "https://api.ipify.org"):
        :
            v = u.urlopen(src, timeout=).read().decode().strip()
             v:  v
         Exception: 
    :
         socket  s
        sk = s.socket(s.AF_INET, s.SOCK_DGRAM); sk.connect((".", ))
        ip = sk.getsockname()[]; sk.close();  ip
     Exception:
         "."

CSS = ''':root{--bg1:#0f0c29;--bg2:#302b63;--bg3:#24243e;--pink:#ff8ac9;--purple:#b39dff;--cyan:#7af0ff;--bg:rgba(,,,.);--panel:rgba(,,,.);--panel2:rgba(,,,.);--line:#3d3868;--text:#e8e4ff;--muted:#9a94c9;--accent:#7af0ff;--green:#6ee7b7;--red:#fda4af;--amber:#fbbf24}
*{box-sizing:border-box}
body{font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:linear-gradient(rgba(,,,.),rgba(,,,.)),url("https://t.alcy.cc/moez") center/cover no-repeat fixed;margin:;padding:40px 24px;color:(--text);min-height:100vh;position:relative;overflow-x:hidden}
.stars,.stars2,.stars3{position:fixed;inset:;pointer-events:none;z-index:}
.stars{background-image:radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %),radial-gradient(2px 2px at % %,#d0e9ff %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %),radial-gradient(5px 5px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#d0e9ff %,transparent %);animation:twinkle 4s ease--out infinite alternate}
.stars2{background-image:radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#e0c3ff %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(2px 2px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffe9f7 %,transparent %);animation:twinkle 6s ease--out infinite alternate-reverse;opacity:.}
.stars3{background-image:radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(5px 5px at % %,#7af0ff55 %,transparent %),radial-gradient(1px 1px at % %,#fff %,transparent %),radial-gradient(1px 1px at % %,#ffc9e055 %,transparent %);animation:twinkle 8s ease--out infinite alternate;opacity:.}
@keyframes twinkle{%{opacity:.}%{opacity:.}}
.wrap,.card,.top,.uploadform,.cmd,.vpage,.vpage *{position:relative;z-index:}
.wrap{max-width:900px;margin: auto}
header{text-align:center;margin-bottom:30px}
header h1{font-size:24px;margin:  8px}
header .sub{color:(--muted);font-size:13px}
header .sub code{background:(--panel2);border:1px solid (--line);padding:2px 10px;border-radius:99px;color:(--accent)}
.card{background:(--panel);border:1px solid (--line);border-radius:16px;overflow:hidden;box-shadow: 8px 32px rgba(,,,.);backdrop-filter:blur(20px) saturate()}
.fitem{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid (--line);transition:background .15s}
.fitem:last-child{border-bottom:none}
.fitem:hover{background:#ffffff08}
.ficon{font-size:26px;width:36px;text-align:center;flex-shrink:}
.finfo{flex:;min-width:}
.fname{font-size:15px;font-weight:}
.fname a{color:(--accent);text-decoration:none}
.fname a:hover{text-decoration:underline}
.fsize{color:(--muted);font-size:12px;margin-left:8px}
.fdesc{color:(--muted);font-size:5px;margin-top:3px;line-height:}
.fbtn{flex-shrink:;display:flex;gap:8px}
.fbtn a{color:#fff;text-decoration:none;padding:7px 14px;border-radius:9px;font-size:13px;font-weight:;display:inline-block}
.fbtn a:hover{filter:brightness();box-shadow:  16px rgba(,,,.)}
.fbtn a.edit{background:linear-gradient(135deg,#10b981,#)}
.fbtn a.view{background:linear-gradient(135deg,#38bdf8,#0284c7)}
.fbtn a.dl{background:linear-gradient(135deg,#0ea5e9,#2563eb)}
.fbtn a.copy{background:linear-gradient(135deg,#8b5cf6,#6d28d9)}
.fbtn a.bash{background:linear-gradient(135deg,#,#)}
.fbtn a.dir{background:linear-gradient(135deg,#6366f1,#4338ca)}
.cmd{background:(--panel2);border:1px solid (--line);border-radius:10px;padding:14px 16px;margin-top:26px;font-size:5px;line-height:;color:(--muted)}
.cmd code{background:(--bg);border:1px solid (--line);border-radius:6px;padding:3px 10px;color:#7dd3fc;font-size:12px;user-select:all}
footer{text-align:center;color:#64748b;font-size:12px;margin-top:26px}
.badge{display:inline-block;background:#10b98122;border:1px solid #10b98155;color:(--green);border-radius:99px;padding:2px 12px;font-size:12px;margin-left:8px}
input#search{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:8px 14px;font-size:13px;width:%;margin-bottom:16px}
.uploadform input[type=file]{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:8px;padding:8px 12px;font-size:13px}
.uploadform button{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:13px;font-weight:;cursor:pointer}
.uploadform{padding: 22px 18px}
.uploadform b{font-size:14px;color:(--text)}
#pager{display:flex;gap:8px;justify-content:center;padding:14px;align-items:center;font-size:13px;color:var(--muted)}
#pager button{background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:7px;padding:5px 12px;cursor:pointer}
.msg{display:none;padding:8px 16px;border-radius:8px;font-size:13px;position:fixed;top:16px;right:16px;z-index:}
.msg.ok{background:#10b98122;border:1px solid #10b98155;color:#34d399;display:block}
.msg.bad{background:#f8717122;border:1px solid #f8717155;color:#f87171;display:block}
.vpage textarea{width:%;box-sizing:border-box;min-height:66vh;background:rgba(,,,.);border:1px solid (--line);border-radius:14px;padding:18px;font-size:13px;line-height:;color:#d5f0ff;font-family:Consolas,"JetBrains Mono",monospace;white-space:pre;overflow:auto;tab-size:;outline:none;resize:vertical;backdrop-filter:blur(18px) saturate();box-shadow: 8px 32px rgba(,,,.)}
.vpage textarea:focus{border-color:(--purple);box-shadow:   3px rgba(,,,.)}
.vpage .top{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.vpage .top b{font-size:15px}
.vpage .top a,.vpage .top button{color:#38bdf8;text-decoration:none;cursor:pointer;background:none;border:1px solid #;font-size:13px;padding:6px 14px;border-radius:8px}
.vpage .top button.save{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;font-weight:}
.vpage .top button.save:hover{filter:brightness()}
/* 语法高亮 */
.kw{color:#7af0ff;font-weight:}
.str{color:#ff8ac9}
.cmt{color:#6b6699;font-style:italic}
.num{color:#fbbf77}
.fn{color:#ffd479}
.op{color:#b39dff}
.shellcmd{color:#6ee7b7}
'''

 icon_for(base):
     "gost-deploy"  base:  "🚀"
     base == "npm.sh":  "🟦"
     base == "panel.html":  "🖥"
     "panel-server"  base:  "⚙"
     "save-server"  base:  "💾"
     "📄"

 build_list_page():
    items = []
     typ, rel, sz  get_real_files():
        fn = html.escape(rel)
        base = os.path.basename(rel)
        ic = icon_for(base)
         typ == "dir":
            desc, sub = DESCRIPTIONS.get(rel, ("📁 文件夹", "子目录"))
            items.append('<div ="fitem">'
                '<div ="ficon">' + ic + '</div>'
                '<div ="finfo"><div ="fname"><a href="' + rel + '/">' + fn + '</a></div>'
                '<div ="fdesc"><b style="color:(--text)">' + desc + '</b><br>' + sub + '</div></div>'
                '<div ="fbtn"><a ="dir" href="' + rel + '/">打开</a></div></div>')
            
         rel  DESCRIPTIONS:
            desc, sub = DESCRIPTIONS[rel]
        :
            desc, sub = auto_desc(safe_path(rel)  (PUB_DIR + "/" + rel), base)
        :
             json  _J2
             open("/tmp/pub_real/desc.json")  _f:
                _cd = _J2.load(_f).get(rel)
             _cd  _cd[]:
                desc = _cd[]
                 _cd[]: sub = _cd[]
         Exception:
            
        qrel = urllib.parse.quote(rel)
        szs = fsize_h(sz)
        items.append('<div ="fitem">'
            '<div ="ficon">' + ic + '</div>'
            '<div ="finfo"><div ="fname"><a href="/edit/' + qrel + '">' + fn + '</a><span ="fsize">' + szs + '</span></div>'
            '<div ="fdesc" style="cursor:pointer" onclick="editDesc(\'' + rel + '\')" title="点击修改描述"><b style="color:(--text)">' + desc + '</b> <span style="font-size:11px;opacity:">✏️</span><br>' + sub + '</div></div>'
            '<div ="fbtn">'
            '<a ="edit" href="/edit/' + qrel + '">📝 编辑</a> '
            '<a ="view" href="/view/' + qrel + '">👁 查看</a> '
            '<a ="dl" href="/dl/' + qrel + '" download>⬇ 下载</a> '
            '<a ="rn" style="background:linear-gradient(135deg,#f59e0b,#d97706)" href="javascript:void()" onclick="renameItem(\'' + rel + '\')">✏️ 重命名</a> '
            '<a ="del" style="background:linear-gradient(135deg,#f87171,#dc2626)" href="javascript:void()" onclick="delItem(\'' + rel + '\')">🗑 删除</a>'
            '</div></div>')

    CSS_TOP = CSS + """
input#search{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:8px 14px;font-size:13px;width:%;margin-bottom:16px}
.uploadform input[type=file]{background:(--panel2);border:1px solid (--line);color:(--text);border-radius:8px;padding:8px 12px;font-size:13px}
.uploadform button{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:13px;font-weight:;cursor:pointer}
.uploadform{padding: 22px 18px}
.uploadform b{font-size:14px;color:(--text)}
#pager{display:flex;gap:8px;justify-content:center;padding:14px;align-items:center;font-size:13px;color:var(--muted)}
#pager button{background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:7px;padding:5px 12px;cursor:pointer}
.fbtn a.rn{background:linear-gradient(135deg,#f59e0b,#d97706)}
.fbtn a.del{background:linear-gradient(135deg,#f87171,#dc2626)}"""

    page = "<!doctype html>\n<html lang=\"zh\">\n<head>\n"
    page += "<meta charset=\"utf-\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=\">\n"
    page += "<title>GOST 分发源</title>\n<style>" + CSS_TOP + "</style>\n</head>\n<body>\n<div =\"stars\"></div><div =\"stars2\"></div><div =\"stars3\"></div>\n"
    page += "<div =\"wrap\">\n"
    page += "  <header>\n"
    page += "    <h1>\U0001F4E6 GOST / NPM 分发源 <span =\"badge\">\u25cf 在线</span></h1>\n"
    page += "    <div =\"sub\">所有文件实时同步最新版 · 使用: <code id=\"baseurl\">bash &lt;(curl -Ls http://当前地址/文件名)</code></div>\n"
    page += "  </header>\n"
    page += "  <input id=\"search\" placeholder=\"🔍 搜索…\" oninput=\"filterList()\">\n"
    page += "  <div =\"card\" id=\"list\">\n"
    page += "\n".join(items)
    page += "\n  </div>\n"
    page += """  <div ="card" style="margin-top:16px">
    <div style="padding:16px 22px "><b style="font-size:14px;color:(--text)">📤 上传文件到分发源</b></div>
    <div ="uploadform">
      <form action="/upload" method="post" enctype="multipart/form-data" style="margin-top:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
        <input type="file" name="file" required>
        <button type="submit">上传</button>
      </form>
      <div style="font-size:11px;color:(--muted);margin-top:6px">上传后自动出现在列表 · 单文件最大 2GB</div>
    </div>
  </div>
  <div ="cmd">
    <b style="color:(--text)">快速使用:</b><br>
    GOST 三件套: <code ="dynurl">bash &lt;(curl -Ls http://当前地址/gost-deploy.sh)</code><br>
    NPM 面板: &nbsp;&nbsp;&nbsp;&nbsp;<code ="dynurl">bash &lt;(curl -Ls http://当前地址/npm.sh)</code><br>
    已装机器升级: <code>bash gost-deploy.sh update</code> (源默认指向本服务)
  </div>
  <footer>GOST Files Distribution · 阿里云 · 实时同步</footer>
</div>
<script>
 PER_PAGE=;
 CUR_PAGE=;
 SERVER_IP='';
fetch('/serverip').then((r){ r.text()}).then((ip){SERVER_IP=ip.trim()}).catch((){});
 filterList(){
   q=(document.getElementById('search')?document.getElementById('search').value:'').toLowerCase();
   items=Array.(document.querySelectorAll('.fitem'));
   matched=items.filter((el){ el.textContent.toLowerCase().indexOf(q)>=});
   totalPages=Math.max(,Math.ceil(matched.length/PER_PAGE));
  (CUR_PAGE>totalPages)CUR_PAGE=totalPages;
  items.forEach((el){el.style.display='none'});
   start=(CUR_PAGE-)*PER_PAGE;
  matched.slice(start,start+PER_PAGE).forEach((el){el.style.display='flex'});
   pc=document.getElementById('pager');
  (!pc){
    pc=document.createElement('div');
    pc.id='pager';
    document.getElementById('list').after(pc);
  }
  (matched.length<=PER_PAGE){pc.innerHTML='共 '+matched.length+' 条';}
  pc.innerHTML='<button onclick="pageGo(-)">‹ 上一页</button> 第 '+CUR_PAGE+' / '+totalPages+' 页 (共'+matched.length+'条) <button onclick="pageGo()">下一页 ›</button>';
}
 pageGo(d){CUR_PAGE+=d;filterList()}
((){
   host = location.host;  // 含端口
   base = 'http://' + host;
   b = document.getElementById('baseurl');
  (b) b.textContent = 'bash <(curl -Ls ' + base + '/文件名)';
  document.querySelectorAll('.dynurl').forEach((el){
    el.textContent = el.textContent.replace('http://当前地址', base);
  });
})();
 renameItem(rel){
   m=showModal({title:'✏️ 重命名',sub:rel,fields:[
    {label:'新名称',value:rel,placeholder:'输入新名称'}
  ]});
  bindModalSave(m,{onSave:(vals,done){
     nn=(vals[]||'').trim();
    (!nn||nn===rel){ done(); ; }
     xhr= XMLHttpRequest();
    xhr.open('POST','/rename');
    xhr.setRequestHeader('Content-Type','application/json');
     a='';
    { ( AUTH!=='undefined'&&AUTH) a=AUTH; }catch(e){}
    (a) xhr.setRequestHeader('Authorization',a);
    xhr.onload=(){ (xhr.status===){ done(); location.reload(); }  { alert('失败: '+xhr.responseText.slice(,)); } };
    xhr.send(JSON.stringify({old: rel, : nn}));
  }});
}
 delItem(rel){
  (!confirm('确定删除 ' + rel + ' ?')) ;
   xhr =  XMLHttpRequest();
  xhr.open('POST', '/delete');
  xhr.onload = () { (xhr.status === ) location.reload(); };
  xhr.send(JSON.stringify({path: rel}));
}
/* 美化编辑模态 */
 showModal(cfg){
   mask=document.getElementById('emask');
  (!mask){
     css=document.createElement('style');
    css.textContent='#emask{position:fixed;inset:;background:rgba(,,,.);backdrop-filter:blur(6px);z-index:;display:none;align-items:center;justify-content:center}'+
      '#emodal{background:#1c1839;border:1px solid #4a4378;border-radius:14px;width:420px;max-width:92vw;padding:26px 28px;box-shadow: 16px 48px rgba(,,,.)}'+
      '#emodal h3{margin:  6px;font-size:16px}'+
      '#emodal .esub{color:(--muted);font-size:5px;margin-bottom:16px}'+
      '#emodal label{display:block;font-size:5px;color:(--muted);margin:12px  6px}'+
      '#emodal input{width:%;box-sizing:border-box;background:#;border:1px solid #4a4378;color:(--text);border-radius:9px;padding:10px 14px;font-size:14px;outline:none}'+
      '#emodal input:focus{border-color:(--accent)}'+
      '#emodal .row{display:flex;gap:10px;justify-content:flex-end;margin-top:20px}'+
      '#emodal .btn{padding:9px 20px;border-radius:9px;font-size:13px;font-weight:;cursor:pointer;border:none}'+
      '#emodal .bc{background:(--panel2);border:1px solid (--line);color:(--text)}'+
      '#emodal .bs{background:linear-gradient(135deg,#10b981,#);color:#fff}';
    document.head.appendChild(css);
    mask=document.createElement('div');
    mask.id='emask';
    mask.innerHTML='<div id="emodal"><h3 id="etitle"></h3><div ="esub" id="esub"></div><div id="efields"></div><div ="row"><button ="btn bc" id="ecancel">取消</button><button ="btn bs" id="esave">保存</button></div></div>';
    document.body.appendChild(mask);
    mask.addEventListener('click',(e){ (e.target===mask) mask.style.display='none'; });
  }
  document.getElementById('etitle').textContent=cfg.title;
  document.getElementById('esub').textContent=cfg.sub||'';
   f=document.getElementById('efields');
  f.innerHTML='';
   inputs=[];
  cfg.fields.forEach((fd){
     l=document.createElement('label'); l.textContent=fd.label;
     i=document.createElement('input'); i.value=fd.value||''; i.placeholder=fd.placeholder||'';
    f.appendChild(l); f.appendChild(i);
    inputs.push(i);
  });
  document.getElementById('esave').onclick=(){
     vals=inputs.map((i){ i.value});
    cfg.onSave(vals, (){ mask.style.display='none'; });
  };
  document.getElementById('ecancel').onclick=(){ mask.style.display='none'; };
  mask.style.display='flex';
  setTimeout((){ (inputs[]) inputs[].focus(); },);
   {inputs:inputs, close:(){mask.style.display='none';}};
}
 bindModalSave(m, cfg){
  document.getElementById('esave').onclick=(){ cfg.onSave(m.inputs.map((i){ i.value}), (){ m.close(); }); };
  document.getElementById('ecancel').onclick=(){ m.close(); };
}
 editDesc(rel){
   ga='';
  { ( AUTH!=='undefined'&&AUTH) ga=AUTH; }catch(e){}
   gh={};
  (ga) gh['Authorization']=ga;
  fetch('/filedesc/'+encodeURIComponent(rel),{headers:gh}).then(r=>r.json()).then(d=>{
     m=showModal({title:'✏️ 编辑描述',sub:rel,fields:[
      {label:'标题',value:d.title||'',placeholder:'显示在文件名下方的粗体行'},
      {label:'副说明',value:d.sub||'',placeholder:'灰色说明行'}
    ]});
    bindModalSave(m,{onSave:(vals,done){
       xhr= XMLHttpRequest();
      xhr.open('POST','/filedesc/'+encodeURIComponent(rel));
      xhr.setRequestHeader('Content-Type','application/json');
      (ga) xhr.setRequestHeader('Authorization',ga);
      xhr.onload=(){ (xhr.status===){ done(); location.reload(); }  { alert('保存失败(需要登录)'); } };
      xhr.send(JSON.stringify({title:vals[],sub:vals[]}));
    }});
  });
}
document.addEventListener('DOMContentLoaded',(){filterList()});
</script>
</body>
</html>"""
     page.encode("utf-")


LOGIN_PAGE = """<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-">
<meta name="viewport" content="width=device-width, initial-scale=">
<title>登录 - GOST 分发源</title>
<style>
:root{--bg:#0f172a;--panel:#1e293b;--panel2:#;--line:#;--text:#e2e8f0;--muted:#94a3b8;--accent:#38bdf8;--green:#34d399;--red:#f87171}
*{box-sizing:border-box}
body{font-family:"Segoe UI",system-ui,sans-serif;background:linear-gradient(rgba(,,,.),rgba(,,,.)),url("https://t.alcy.cc/moez") center/cover no-repeat fixed;margin:;padding:40px 24px;color:(--text);display:flex;align-items:center;justify-content:center;min-height:90vh}
.login-card{background:(--panel);border:1px solid (--line);border-radius:16px;padding:36px 32px;width:360px;max-width:92vw;box-shadow: 12px 40px rgba(,,,.)}
h1{font-size:20px;text-align:center;margin:  6px}
.sub{color:(--muted);font-size:5px;text-align:center;margin-bottom:24px}
label{display:block;font-size:5px;color:(--muted);margin:12px  6px}
input{width:%;box-sizing:border-box;background:(--panel2);border:1px solid (--line);color:(--text);border-radius:9px;padding:10px 14px;font-size:14px;outline:none}
input:focus{border-color:(--accent)}
button{width:%;margin-top:20px;background:linear-gradient(135deg,#0ea5e9,#2563eb);color:#fff;border:none;border-radius:9px;padding:11px;font-size:14px;font-weight:;cursor:pointer}
button:hover{filter:brightness()}
.err{display:none;background:#f8717122;border:1px solid #f8717155;color:(--red);border-radius:8px;padding:8px 12px;font-size:5px;margin-top:14px;text-align:center}
.back{text-align:center;margin-top:16px}
.back a{color:(--accent);text-decoration:none;font-size:5px}
</style>
</head>
<body>
<div ="login-card">
  <h1>🔐 GOST 分发源</h1>
  <div ="sub">登录后可使用编辑 · 重命名 · 删除</div>
  <form id="f" onsubmit=" doLogin()">
    <label>用户名</label>
    <input id="username" autocomplete="username" required>
    <label>密码</label>
    <input id="password" type="password" autocomplete="current-password" required>
    <button type="submit">登 录</button>
    <div ="err" id="err">用户名或密码错误</div>
  </form>
  <div ="back"><a href="/">← 返回列表</a></div>
</div>
<script>
 doLogin(){
   u=document.getElementById('username').value;
   p=document.getElementById('password').value;
   xhr= XMLHttpRequest();
  xhr.open('POST','/login');
  xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
  xhr.onload=(){
    (xhr.status===){ location.href='/'; }
    { document.getElementById('err').style.display='block'; }
  };
  xhr.send('username='+encodeURIComponent(u)+'&password='+encodeURIComponent(p));
   false;
}
</script>
</body>
</html>
"""

 _hl(code_text):
    """轻量语法高亮: python/bash/js 通用"""
     re  _re
    esc = html.escape(code_text)
    # . 注释整行先提取为占位符(避免内部被其他规则二次处理)
    comments = []
     _stash_cmt(m):
        comments.append('<span ="cmt">' + m.group() + '</span>')
         m.group() + "\x00CMT" + str(len(comments)-) + "\x00"
    esc = _re.sub(r'(^|\n)(#[^\n]*)', _stash_cmt, esc)
    # . 关键字
    kws = ("||||||||||||||||||||||||||||||||||")
    esc = _re.sub(r'\b(' + kws + r')\b', '<span ="kw">\</span>', esc)
    # . 数字
    esc = _re.sub(r'\b(\d+\.?\d*)\b', '<span ="num">\</span>', esc)
    # . 还原注释占位
    esc = _re.sub(r"\x00CMT(\d+)\x00",  m: comments[int(m.group())], esc)
     esc


 H(BaseHTTPRequestHandler):
     do_GET(self):
        path = urllib.parse.unquote(self.path)

         path.startswith("/filedesc/"):
            fn = path[len("/filedesc/"):]
            DJ = "/tmp/pub_real/desc.json"
             json  _J
             self.command == "GET":
                :
                     open(DJ)  f:
                        data = _J.load(f).get(fn, ["",""])
                 Exception:
                    data = ["",""]
                body = _J.dumps({"title": data[], "sub": data[]}).encode()
                self.send_response()
                self.send_header("Content-Type","application/json")
                self.send_header("Content-Length",str(len(body)))
                self.end_headers(); self.wfile.write(body); 
             self.command == "POST":
                n = int(self.headers.get("Content-Length","")  )
                :
                    d = _J.loads(self.rfile.read(n))
                    :
                         open(DJ)  f: all_d = _J.load(f)
                     Exception: all_d = {}
                    all_d[fn] = [str(d.get("title",""))[:], str(d.get("sub",""))[:]]
                     open(DJ,"w")  f: f.write(_J.dumps(all_d, ensure_ascii=, indent=))
                    r = b'{"ok":true}'
                    self.send_response()
                 Exception  e:
                    r = str(e).encode()
                    self.send_response()
                self.send_header("Content-Type","application/json")
                self.send_header("Content-Length",str(len(r)))
                self.end_headers(); self.wfile.write(r); 
            self.send_response(); self.end_headers(); 

         path == "/serverip":
            ip = detect_ip()
            self.send_response()
            self.send_header("Content-Type","text/plain")
            self.send_header("Access-Control-Allow-Origin","*")
            self.end_headers(); self.wfile.write(ip.encode()); 

        # 编辑页
         path == "/login":
            # 已登录则跳首页
            tok = _get_cookie(self)
             tok  tok  SESSIONS:
                self.send_response(); self.send_header("Location","/"); self.end_headers(); 
            page = LOGIN_PAGE
            body = page.encode("utf-")
            self.send_response()
            self.send_header("Content-Type","text/html; charset=utf-")
            self.send_header("Cache-Control","no-cache")
            self.send_header("Content-Length",str(len(body)))
            self.end_headers(); self.wfile.write(body); 

         path.startswith("/edit/"):
            fn = path[len("/edit/"):]
            fpath = safe_path(fn)
              fpath   os.path.isfile(fpath):
                self.send_response(); self.end_headers(); 
              _is_logged_in(self):
                self.send_response()
                self.send_header("Location","/login")
                self.end_headers(); 
             open(fpath, "rb")  f:
                content = f.read()
            : text = content.decode("utf-")
             Exception: text = content.decode("utf-", errors="replace")
            esc = html.escape(text, quote=)
            qfn = urllib.parse.quote(fn)
            page = (f'''<!doctype html><html lang="zh"><head><meta charset="utf-">
<title>编辑 {html.escape(fn)}</title><style>{CSS}
.vpage textarea{{width:%;box-sizing:border-box;min-height:70vh;background:#1e293b;border:1px solid #;border-radius:10px;padding:16px;font-size:13px;line-height:;color:#7dd3fc;font-family:Consolas,monospace;white-space:pre;overflow:auto;tab-size:;outline:none;resize:vertical}}
.vpage textarea:focus{{border-color:#38bdf8}}
.vpage .top{{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}}
.vpage .top b{{font-size:15px}}
.vpage .top a,.vpage .top button{{color:#38bdf8;text-decoration:none;cursor:pointer;background:none;border:1px solid #;font-size:13px;padding:6px 14px;border-radius:8px}}
.vpage .top button.save{{background:linear-gradient(135deg,#10b981,#);color:#fff;border:none;font-weight:}}
.msg{{display:none;padding:8px 16px;border-radius:8px;font-size:13px;position:fixed;top:16px;right:16px;z-index:}}
.msg.ok{{background:#10b98122;border:1px solid #10b98155;color:#34d399;display:block}}
.msg.bad{{background:#f8717122;border:1px solid #f8717155;color:#f87171;display:block}}
</style></head><body ="vpage">
<div ="stars"></div><div ="stars2"></div><div ="stars3"></div>
<div ="top">
  <b>📝 编辑: {html.escape(fn)}</b>
  <button ="save" onclick="saveFile()">💾 保存 (Ctrl+S)</button>
  <a href="/view/{qfn}">👁 只读查看</a>
  <a href="/dl/{qfn}" download>⬇ 下载</a>
  <a href="/">← 返回列表</a>
</div>
<textarea id="editor" spellcheck="false">{esc}</textarea>
<div ="msg" id="msg"></div>
<script>
 FN={json.dumps(fn)};
 saveFile(){{
   t=document.getElementById('editor').value;
   xhr= XMLHttpRequest();
  xhr.open('POST','/saveedit/'+encodeURIComponent(FN));
  xhr.setRequestHeader('Content-Type','text/plain; charset=utf-');
  xhr.setRequestHeader('Authorization','Basic '+btoa('{AUTH_USER}:{AUTH_PASS}'));
  xhr.onload=()=>{{
     m=document.getElementById('msg');
    (xhr.status===){{m.className='msg ok';m.textContent='✓ 已保存 ('+FN+')';}}
    {{m.className='msg bad';m.textContent='保存失败: '+xhr.status;}}
    m.style.display='block';
    setTimeout(()=>m.style.display='none',);
  }};
  xhr.onerror=()=>{{m.className='msg bad';m.textContent='网络错误';m.style.display='block';}};
  xhr.send(t);
}}
document.addEventListener('keydown',e=>{{
  ((e.ctrlKey||e.metaKey)&&e.key==='s'){{e.preventDefault();saveFile()}}
}});
</script></body></html>''').encode("utf-")
            self.send_response()
            self.send_header("Content-Type","text/html; charset=utf-")
            self.send_header("Content-Length",str(len(page)))
            self.end_headers(); self.wfile.write(page); 

        # 只读查看页
         path.startswith("/view/"):
            fn = path[len("/view/"):]
            fpath = safe_path(fn)
              fpath   os.path.isfile(fpath):
                self.send_response(); self.end_headers(); 
             open(fpath, "rb")  f:
                content = f.read()
            : text = content.decode("utf-")
             Exception: text = content.decode("utf-", errors="replace")
            esc = html.escape(text)
            qfn = urllib.parse.quote(fn)
            page = (f'''<!doctype html><html lang="zh"><head><meta charset="utf-">
<title>View {html.escape(fn)}</title><style>{CSS}
.vpage pre{{background:rgba(,,,.);border:1px solid rgba(,,,.);border-radius:14px;padding:20px;overflow:auto;max-height:72vh;font-size:13px;line-height:;white-space:pre-wrap;word-:-all;margin:;color:#d5f0ff;font-family:Consolas,monospace;backdrop-filter:blur(18px) saturate();box-shadow: 8px 32px rgba(,,,.)}}
.vpage .top{{margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}}
.vpage .top b{{font-size:15px}}
.vpage .top a{{color:#38bdf8;text-decoration:none;background:none;border:1px solid #;padding:6px 14px;border-radius:8px}}
</style></head><body ="vpage">
<div ="stars"></div><div ="stars2"></div><div ="stars3"></div>
<div ="top"><b>📄 {html.escape(fn)}</b>
 <a href="/edit/{qfn}">📝 编辑</a>
 <a href="/dl/{qfn}" download>⬇ 下载</a>
 <a href="/">← 返回列表</a></div>
<pre>{_hl(text)}</pre></body></html>''').encode("utf-")
            self.send_response()
            self.send_header("Content-Type","text/html; charset=utf-")
            self.send_header("Content-Length",str(len(page)))
            self.end_headers(); self.wfile.write(page); 

        # 目录页
         path == "/"  path == "/index.html":
            body = build_list_page()
            self.send_response()
            self.send_header("Content-Type","text/html; charset=utf-")
            self.send_header("Content-Length",str(len(body)))
            self.end_headers(); self.wfile.write(body); 

        # 下载
        rel = path.lstrip("/")
         rel.startswith("dl/"): rel = rel[:]
        fpath = safe_path(rel)
          fpath:
            self.send_response()
            self.send_header("Content-Type","text/plain; charset=utf-")
            self.end_headers(); self.wfile.write("".encode()); 
         os.path.isdir(fpath):
            self.send_response(); self.send_header("Location","/"); self.end_headers(); 
         os.path.isfile(fpath):
             open(fpath,"rb")  f: body=f.read()
            ctype = "text/html; charset=utf-"  fpath.endswith(".html")  "text/plain; charset=utf-"
            self.send_response()
            self.send_header("Content-Type",ctype)
            self.send_header("Content-Length",str(len(body)))
            self.end_headers(); self.wfile.write(body); 
        self.send_response(); self.end_headers(); self.wfile.write("".encode())

     do_POST(self):
        path = urllib.parse.unquote(self.path)

         path == "/login":
            n = int(self.headers.get("Content-Length","")  )
            body = self.rfile.read(n).decode("utf-", errors="replace")
             urllib.parse  UP
            params = UP.parse_qs(body)
            u = params.get("username",[""])[]
            pw = params.get("password",[""])[]
             u == AUTH_USER  pw == AUTH_PASS:
                tok = _new_token()
                SESSIONS.add(tok)
                _save_sessions()
                body2 = b"OK"
                self.send_response()
                self.send_header("Set-Cookie","gsess=" + tok + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=")
                self.send_header("Content-Type","text/plain; charset=utf-")
                self.send_header("Content-Length",str(len(body2)))
                self.end_headers(); self.wfile.write(body2)
            :
                body2 = b"BAD"
                self.send_response()
                self.send_header("Content-Type","text/plain; charset=utf-")
                self.send_header("Content-Length",str(len(body2)))
                self.end_headers(); self.wfile.write(body2)
            

         path.startswith("/filedesc/"):
            fn = path[len("/filedesc/"):]
              _is_logged_in(self):
                self.send_response(); self.end_headers(); 
            DJ = "/tmp/pub_real/desc.json"
             json  _J
            n = int(self.headers.get("Content-Length","")  )
            :
                d = _J.loads(self.rfile.read(n))
                :
                     open(DJ)  f: all_d = _J.load(f)
                 Exception: all_d = {}
                all_d[fn] = [str(d.get("title",""))[:], str(d.get("sub",""))[:]]
                 open(DJ,"w")  f: f.write(_J.dumps(all_d, ensure_ascii=, indent=))
                r = b'{"ok":true}'
                self.send_response()
             Exception  e:
                r = str(e).encode()
                self.send_response()
            self.send_header("Content-Type","application/json")
            self.send_header("Content-Length",str(len(r)))
            self.end_headers(); self.wfile.write(r); 

         path.startswith("/saveedit/"):
            fn = path[len("/saveedit/"):]
            fpath = safe_path(fn)
              fpath   os.path.isfile(fpath):
                self.send_response(); self.end_headers(); 
              _is_logged_in(self):
                self.send_response(); self.end_headers(); 
            n = int(self.headers.get("Content-Length","")  )
            new_content = self.rfile.read(n)
             open(fpath, "wb")  f:
                f.write(new_content)
            self.send_response()
            self.send_header("Content-Type","text/plain; charset=utf-")
            self.end_headers()
            self.wfile.write(f"OK, saved {fn} ({len(new_content)}B)".encode()); 

         path == "/rename":
              _is_logged_in(self):
                self.send_response(); self.end_headers(); 
            n = int(self.headers.get("Content-Length","")  )
            :
                 json  J
                data = J.loads(self.rfile.read(n))
                old_rel = data.get("old","")
                new_rel = os.path.basename(data.get("","").strip())
                old_link = os.path.join(PUB_DIR, old_rel.strip("/"))
                old_fp = safe_path(old_rel)
                  old_fp   os.path.islink(old_link):
                    self.send_response(); self.end_headers(); 
                new_link = os.path.join(PUB_DIR, new_rel)
                 os.path.exists(new_link)  os.path.islink(new_link):
                    self.send_response(); self.end_headers(); self.wfile.write(b"exists"); 
                 os.path.islink(old_link):
                    # 软链接: 只重命名链接本身
                    os.rename(old_link, new_link)
                :
                    os.rename(old_fp, new_link)
                self.send_response(); self.end_headers(); self.wfile.write(b"OK")
             Exception  e:
                self.send_response(); self.end_headers(); self.wfile.write(str(e).encode())
            
         path == "/delete":
              _is_logged_in(self):
                self.send_response(); self.end_headers(); 
            n = int(self.headers.get("Content-Length","")  )
            :
                 json  J
                 shutil
                data = J.loads(self.rfile.read(n))
                rel = data.get("path","").strip("/")
                link_fp = os.path.join(PUB_DIR, rel)
                fp = safe_path(rel)
                  fp   os.path.islink(link_fp):
                    self.send_response(); self.end_headers(); 
                 os.path.islink(link_fp):
                    # 软链接: 只删链接本身, 不动源文件
                    os.unlink(link_fp)
                 os.path.isdir(fp):
                    shutil.rmtree(fp)
                :
                    os.remove(fp)
                self.send_response(); self.end_headers(); self.wfile.write(b"OK")
             Exception  e:
                self.send_response(); self.end_headers(); self.wfile.write(str(e).encode())
            
         path == "/upload":
            ct = self.headers.get("Content-Type","")
            n = int(self.headers.get("Content-Length","")  )
             n > ***:
                self.send_response(); self.end_headers(); 
            body = self.rfile.read(n)
            m = re.search(r'boundary="?([^";\s]+)"?', ct)
              m:
                self.send_response(); self.end_headers(); self.wfile.write(b"no boundary"); 
            boundary = ("--"+m.group()).encode()
            parts = body.split(boundary)
            saved = []
             part  parts:
                 b"filename="   part: 
                fnm = re.search(r'filename="([^"]+)"', part.decode("utf-",errors="replace"))
                  fnm: 
                fname = os.path.basename(fnm.group())
                fname = re.sub(r'[^\w.\-()]','_',fname)
                  fname: 
                didx = part.find(b"\r\n\r\n")
                 didx < : 
                data = part[didx+:]
                 data.endswith(b"\r\n"): data = data[:-]
                dest = os.path.join(PUB_DIR, fname)
                 open(dest,"wb")  f: f.write(data)
                saved.append((fname,len(data)))
             saved:
                names = ", ".join(f"{html.escape(s[])}({s[]//}KB)"  s  saved)
                page = f'''<!doctype html><html><head><meta charset="utf-"><meta http-equiv="refresh" content=";url=/"><style>body{{font-family:sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:}}div{{text-align:center;background:#1e293b;padding:40px;border-radius:14px;border:1px solid #}}a{{color:#38bdf8}}h2{{color:#34d399}}</style></head><body><div><h2>✅ 上传成功</h2><p>{names}</p><p><a href="/">← 返回列表 (2秒后自动跳转)</a></p></div></body></html>'''
            :
                page = '''<!doctype html><html><head><meta charset="utf-"><meta http-equiv="refresh" content=";url=/"><style>body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:}div{text-align:center;background:#1e293b;padding:40px;border-radius:14px;border:1px solid #}a{color:#38bdf8}</style></head><body><div><h2>❌ 上传失败</h2><p><a href="/">返回</a></p></div></body></html>'''
            body = page.encode("utf-")
            self.send_response()
            self.send_header("Content-Type","text/html; charset=utf-")
            self.send_header("Content-Length",str(len(body)))
            self.end_headers(); self.wfile.write(body); 

        self.send_response(); self.end_headers()
     log_message(self,*a): 

ThreadingHTTPServer((".",),H).serve_forever()