Initial commit: lzwlab.cn server project (docs, nginx, gitea, rustdesk, file transfer app)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"""将 lzwlab.cn 文件快传部署到远程服务器(幂等,可重复执行)。
|
||||
|
||||
用法(凭据从 Authentication.md 解析;应用登录密码通过环境变量传入,不落盘):
|
||||
|
||||
LZWLAB_APP_PASSWORD='你的应用登录密码' python deploy_transfer.py --username admin
|
||||
|
||||
服务器端安装:
|
||||
- apt 安装 python3-flask、gunicorn
|
||||
- 应用代码安装到 /opt/lzwlab-transfer(root 所有)
|
||||
- 运行用户 lzwlab(systemd 服务,监听 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()
|
||||
Reference in New Issue
Block a user