Initial commit: lzwlab.cn server project (docs, nginx, gitea, rustdesk, file transfer app)

This commit is contained in:
2026-08-26 22:59:47 +08:00
commit 7da61a0456
18 changed files with 2703 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
"""将 lzwlab.cn 文件快传部署到远程服务器(幂等,可重复执行)。
用法(凭据从 Authentication.md 解析;应用登录密码通过环境变量传入,不落盘):
LZWLAB_APP_PASSWORD='你的应用登录密码' python deploy_transfer.py --username admin
服务器端安装:
- apt 安装 python3-flask、gunicorn
- 应用代码安装到 /opt/lzwlab-transferroot 所有)
- 运行用户 lzwlabsystemd 服务,监听 127.0.0.1:8090
- 数据目录 /var/lib/lzwlab-transfer,配置 /etc/lzwlab-transfer/config.json(密码仅存 scrypt 哈希)
- systemd 定时器每 10 分钟清理过期文件
- 更新 nginx 反代配置与首页链接
"""
import argparse
import json
import os
import re
import sys
import time
import paramiko
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(ROOT, "transfer"))
import auth # noqa: E402
REMOTE_TMP = "/tmp/lzwlab-transfer-deploy"
APP_DIR = "/opt/lzwlab-transfer"
DATA_DIR = "/var/lib/lzwlab-transfer"
CONFIG_PATH = "/etc/lzwlab-transfer/config.json"
LOG_DIR = "/var/log/lzwlab-transfer"
SERVICE_USER = "lzwlab"
PORT = 8090
LOCAL_FILES = [
("transfer/app.py", "app.py"),
("transfer/auth.py", "auth.py"),
("transfer/storage.py", "storage.py"),
("transfer/cleanup.py", "cleanup.py"),
("transfer/templates/index.html", "templates/index.html"),
]
SYSTEMD_FILES = [
("transfer/systemd/lzwlab-transfer.service", "lzwlab-transfer.service"),
("transfer/systemd/lzwlab-transfer-cleanup.service", "lzwlab-transfer-cleanup.service"),
("transfer/systemd/lzwlab-transfer-cleanup.timer", "lzwlab-transfer-cleanup.timer"),
]
def parse_auth() -> tuple[str, str, str]:
text = open(os.path.join(ROOT, "Authentication.md"), encoding="utf-8").read()
def find(label: str) -> str:
m = re.search(label + r"\s*[:]\s*(\S+)", text)
if not m:
sys.exit("无法解析 Authentication.md 中的 " + label)
return m.group(1)
return find("IP地址"), find("用户名"), find("密码")
def connect(ip: str, user: str, pw: str) -> paramiko.SSHClient:
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(ip, port=22, username=user, password=pw, timeout=20,
look_for_keys=False, allow_agent=False, banner_timeout=30)
return cli
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> tuple[int, str, str]:
print(" $ " + cmd)
_in, out, err = ssh.exec_command(cmd, timeout=timeout)
out_s = out.read().decode("utf-8", "replace")
err_s = err.read().decode("utf-8", "replace")
code = out.channel.recv_exit_status()
if out_s.strip():
print(out_s.strip())
if err_s.strip():
print("[stderr]", err_s.strip())
return code, out_s, err_s
def must(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> str:
code, out, err = run(ssh, cmd, timeout)
if code != 0:
sys.exit("命令失败(exit=%d):%s" % (code, cmd))
return out
def sftp_mkdir_p(sftp: paramiko.SFTPClient, remote_dir: str) -> None:
try:
sftp.stat(remote_dir)
except IOError:
parent = os.path.dirname(remote_dir.rstrip("/"))
if parent and parent != remote_dir:
sftp_mkdir_p(sftp, parent)
sftp.mkdir(remote_dir)
def upload(ssh: paramiko.SSHClient, local: str, remote: str) -> None:
print(" [upload] %s -> %s" % (local, remote))
sftp = ssh.open_sftp()
try:
sftp_mkdir_p(sftp, os.path.dirname(remote))
sftp.put(os.path.join(ROOT, local), remote)
finally:
sftp.close()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--username", default="admin", help="应用登录用户名(默认 admin")
ap.add_argument("--password-env", default="LZWLAB_APP_PASSWORD",
help="应用登录密码所在的环境变量名")
ap.add_argument("--skip-apt", action="store_true", help="跳过 apt 安装依赖")
args = ap.parse_args()
password = os.environ.get(args.password_env, "")
if len(password) < 8:
sys.exit("请通过环境变量 %s 提供至少 8 位的应用登录密码" % args.password_env)
ip, ssh_user, ssh_pw = parse_auth()
print("目标服务器:%s@%s" % (ssh_user, ip))
ssh = connect(ip, ssh_user, ssh_pw)
try:
# 1. 依赖
if not args.skip_apt:
code, _, err = run(ssh, "sudo -n env DEBIAN_FRONTEND=noninteractive "
"apt-get install -y python3-flask gunicorn", timeout=900)
if code != 0:
print("直接安装失败,尝试先 apt-get update …")
must(ssh, "sudo -n apt-get update -qq", timeout=900)
must(ssh, "sudo -n env DEBIAN_FRONTEND=noninteractive "
"apt-get install -y python3-flask gunicorn", timeout=900)
# 2. 目录、运行用户与数据目录
must(ssh, "sudo -n mkdir -p %s %s/templates %s/files %s /etc/lzwlab-transfer"
% (APP_DIR, APP_DIR, DATA_DIR, LOG_DIR))
code, _, _ = run(ssh, "id -u %s >/dev/null 2>&1" % SERVICE_USER)
if code != 0:
must(ssh, "sudo -n useradd --system --home-dir %s --shell /usr/sbin/nologin %s"
% (APP_DIR, SERVICE_USER))
must(ssh, "sudo -n chown -R %s:%s %s %s" % (SERVICE_USER, SERVICE_USER, DATA_DIR, LOG_DIR))
must(ssh, "sudo -n chmod 750 %s" % LOG_DIR)
must(ssh, "sudo -n rm -rf %s && mkdir -p %s" % (REMOTE_TMP, REMOTE_TMP))
# 3. 上传应用文件
for local, name in LOCAL_FILES:
upload(ssh, local, "%s/%s" % (REMOTE_TMP, name))
for local, name in SYSTEMD_FILES:
upload(ssh, local, "%s/systemd/%s" % (REMOTE_TMP, name))
upload(ssh, "index.html", "%s/index.html" % REMOTE_TMP)
upload(ssh, "lzwlab.nginx.conf", "%s/lzwlab.cn.conf" % REMOTE_TMP)
# 4. 生成配置(仅包含密码哈希,不保存明文)
cfg = {
"secret_key": auth.random_secret(),
"cookie_secure": True,
"max_content_length": 5 * 1024 ** 3,
"data_dir": DATA_DIR,
"database_path": DATA_DIR + "/files.db", # 远端为 Linux 路径,不能用 Windows 的 os.path.join
"session_days": 7,
"users": {
args.username: {"password_hash": auth.hash_password(password)}
},
}
print(" [generate] 应用配置(用户:%s" % args.username)
sftp = ssh.open_sftp()
try:
with sftp.open("%s/config.json" % REMOTE_TMP, "w") as fh:
fh.write(json.dumps(cfg, ensure_ascii=False, indent=2))
finally:
sftp.close()
# 5. 安装应用、配置与 systemd
for _, name in LOCAL_FILES:
must(ssh, "sudo -n install -m 644 -o root -g root %s/%s %s/%s"
% (REMOTE_TMP, name, APP_DIR, name))
must(ssh, "sudo -n install -m 640 -o root -g %s %s/config.json %s"
% (SERVICE_USER, REMOTE_TMP, CONFIG_PATH))
for _, name in SYSTEMD_FILES:
must(ssh, "sudo -n install -m 644 -o root -g root %s/systemd/%s /etc/systemd/system/%s"
% (REMOTE_TMP, name, name))
must(ssh, "sudo -n install -m 644 -o root -g root %s/index.html /var/www/lzwlab.cn/index.html"
% REMOTE_TMP)
must(ssh, "sudo -n install -m 644 -o root -g root %s/lzwlab.cn.conf "
"/etc/nginx/sites-available/lzwlab.cn" % REMOTE_TMP)
# 6. 启动服务与定时器
must(ssh, "sudo -n systemctl daemon-reload")
must(ssh, "sudo -n systemctl enable --now lzwlab-transfer.service lzwlab-transfer-cleanup.timer")
must(ssh, "sudo -n systemctl restart lzwlab-transfer.service")
must(ssh, "sudo -n systemctl enable lzwlab-transfer-cleanup.timer && "
"sudo -n systemctl start lzwlab-transfer-cleanup.timer")
time.sleep(1)
# 7. 校验并重载 nginx
must(ssh, "sudo -n nginx -t")
must(ssh, "sudo -n systemctl reload nginx")
must(ssh, "sudo -n rm -rf %s" % REMOTE_TMP)
print()
print("== 部署完成,服务状态 ==")
run(ssh, "systemctl is-active lzwlab-transfer.service lzwlab-transfer-cleanup.timer")
run(ssh, "curl -s -o /dev/null -w 'local https /transfer/ => %{http_code}\n' "
"-k https://127.0.0.1/transfer/ -H 'Host: lzwlab.cn'")
print("应用入口:https://lzwlab.cn/transfer/")
print("登录用户:%s(密码为环境变量 %s 中提供的值)" % (args.username, args.password_env))
finally:
ssh.close()
if __name__ == "__main__":
main()