feat: 文件快传新增共享剪贴板(跨客户端粘贴、查看与一键复制)
- 后端:新增 /api/clips(列表/新建)、/api/clips/<id>(取全文/删除)、/api/clips/<id>/copy(复制计数),列表仅返回 240 字符预览 - 存储:SQLite 新增 clips 表(含 expires_at 索引),启动自动建表,线上老库无需手工迁移 - 前端:剪贴板卡片支持读取本机剪贴板、来源备注、查看全文、一键复制到本机剪贴板、单条删除与复制次数 - 清理:systemd 定时任务与 /api/cleanup 同时清理过期剪贴板 - 配置:新增 max_clip_length / max_clips / clip_default_expires_hours - 部署:deploy_transfer.py 新增 --keep-credentials(不重置登录密码与会话密钥)与 SSH 连接重试 - 文档:README / setup.md / CHANGELOG 同步更新
This commit is contained in:
+75
-26
@@ -4,12 +4,16 @@
|
||||
|
||||
LZWLAB_APP_PASSWORD='你的应用登录密码' python deploy_transfer.py --username admin
|
||||
|
||||
仅更新代码、不改动登录密码时(保留服务器上已有的 secret_key 与密码哈希):
|
||||
|
||||
python deploy_transfer.py --keep-credentials
|
||||
|
||||
服务器端安装:
|
||||
- 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 分钟清理过期文件
|
||||
- systemd 定时器每 10 分钟清理过期文件与过期剪贴板
|
||||
- 更新 nginx 反代配置与首页链接
|
||||
"""
|
||||
import argparse
|
||||
@@ -57,21 +61,36 @@ def parse_auth() -> tuple[str, str, str]:
|
||||
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 connect(ip: str, user: str, pw: str, attempts: int = 3) -> paramiko.SSHClient:
|
||||
"""连接 SSH;服务器偶发拒绝握手(Error reading SSH protocol banner)时自动重试。"""
|
||||
last_exc = None
|
||||
for attempt in range(1, attempts + 1):
|
||||
cli = paramiko.SSHClient()
|
||||
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
cli.connect(ip, port=22, username=user, password=pw, timeout=20,
|
||||
look_for_keys=False, allow_agent=False,
|
||||
banner_timeout=30, auth_timeout=30)
|
||||
return cli
|
||||
except (paramiko.SSHException, OSError) as exc:
|
||||
last_exc = exc
|
||||
cli.close()
|
||||
if attempt < attempts:
|
||||
print(" [retry] SSH 连接失败(%s),2 秒后重试(%d/%d)…"
|
||||
% (exc, attempt, attempts))
|
||||
time.sleep(2)
|
||||
raise SystemExit("SSH 连接失败:%s" % last_exc)
|
||||
|
||||
|
||||
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> tuple[int, str, str]:
|
||||
print(" $ " + cmd)
|
||||
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600,
|
||||
quiet: bool = False) -> tuple[int, str, str]:
|
||||
if not quiet:
|
||||
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():
|
||||
if not quiet and out_s.strip():
|
||||
print(out_s.strip())
|
||||
if err_s.strip():
|
||||
print("[stderr]", err_s.strip())
|
||||
@@ -111,11 +130,15 @@ def main() -> None:
|
||||
ap.add_argument("--password-env", default="LZWLAB_APP_PASSWORD",
|
||||
help="应用登录密码所在的环境变量名")
|
||||
ap.add_argument("--skip-apt", action="store_true", help="跳过 apt 安装依赖")
|
||||
ap.add_argument("--keep-credentials", action="store_true",
|
||||
help="保留服务器上已有的 secret_key 与用户密码哈希(只更新代码与新增配置项,"
|
||||
"不改登录密码;此时无需提供应用密码)")
|
||||
args = ap.parse_args()
|
||||
|
||||
password = os.environ.get(args.password_env, "")
|
||||
if len(password) < 8:
|
||||
sys.exit("请通过环境变量 %s 提供至少 8 位的应用登录密码" % args.password_env)
|
||||
if not args.keep_credentials and len(password) < 8:
|
||||
sys.exit("请通过环境变量 %s 提供至少 8 位的应用登录密码"
|
||||
"(仅更新代码可用 --keep-credentials 保留现有密码)" % args.password_env)
|
||||
|
||||
ip, ssh_user, ssh_pw = parse_auth()
|
||||
print("目标服务器:%s@%s" % (ssh_user, ip))
|
||||
@@ -150,19 +173,42 @@ def main() -> None:
|
||||
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)
|
||||
# 4. 生成配置(密码只以 scrypt 哈希保存;--keep-credentials 时沿用服务器上已有凭据)
|
||||
existing = None
|
||||
if args.keep_credentials:
|
||||
code, out, _err = run(ssh, "sudo -n cat %s" % CONFIG_PATH, quiet=True)
|
||||
if code != 0 or not out.strip():
|
||||
sys.exit("--keep-credentials 需要服务器上已存在 %s" % CONFIG_PATH)
|
||||
existing = json.loads(out)
|
||||
print(" [config] 保留服务器上已有的 secret_key 与用户密码哈希")
|
||||
|
||||
if existing is not None:
|
||||
cfg = dict(existing)
|
||||
cfg.setdefault("cookie_secure", True)
|
||||
cfg.setdefault("max_content_length", 5 * 1024 ** 3)
|
||||
cfg.setdefault("data_dir", DATA_DIR)
|
||||
cfg.setdefault("database_path", DATA_DIR + "/files.db")
|
||||
cfg.setdefault("session_days", 7)
|
||||
cfg.setdefault("max_clip_length", 256 * 1024) # 单条剪贴板文本上限(字符)
|
||||
cfg.setdefault("max_clips", 200) # 剪贴板保留条数上限
|
||||
cfg.setdefault("clip_default_expires_hours", 24) # 剪贴板默认有效期
|
||||
print(" [generate] 应用配置(保留用户:%s)" % "、".join(sorted(cfg.get("users", {}))))
|
||||
else:
|
||||
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,
|
||||
"max_clip_length": 256 * 1024, # 单条剪贴板文本上限(字符)
|
||||
"max_clips": 200, # 剪贴板保留条数上限
|
||||
"clip_default_expires_hours": 24,
|
||||
"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:
|
||||
@@ -203,7 +249,10 @@ def main() -> None:
|
||||
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))
|
||||
if args.keep_credentials:
|
||||
print("登录用户:%s(沿用服务器上已有的密码,未重置)" % "、".join(sorted(cfg.get("users", {}))))
|
||||
else:
|
||||
print("登录用户:%s(密码为环境变量 %s 中提供的值)" % (args.username, args.password_env))
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user