Files
dedinit.ru/tools/deploy_sftp.py
T
estorozhenko d426aa39ed chore(docs): уборка структуры — вся документация в docs/, краткий README, добавлен AGENTS.md
- AGENTS.md: правила проекта (структура доков, стек, статьи, деплой, git, openspec, проверки)
- README.md: сокращён до введения + указателей на docs/
- docs/: DEPLOY_INSTRUCTIONS, ROADMAP, PAPERMOD_PARAMS, QWEN, gitea-setup,
  Telegram_frontend, ssh_setup, archive/PROJECT_SUMMARY (из .qwen/)
- tools/: winscp_script.txt переехал из корня к deploy_sftp.py
- Makefile и deploy_sftp.py: ссылки на docs/DEPLOY_INSTRUCTIONS.md
2026-09-12 09:58:09 +00:00

102 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Deploy public/ -> /dedinit.ru on Jino (SFTP-only, password auth).
Используется командой `make deploy` (единственный способ деплоя сайта).
Jino-аккаунт kpa39l_dedinit — SFTP-only: удалённый exec (rsync/ssh) запрещён,
поэтому заливаем через SFTP с рекурсивным mirror и --delete (как rsync).
Пароль — из env SSHPASS (см. README.md / docs/DEPLOY_INSTRUCTIONS.md).
"""
import os
import stat as stmod
import sys
try:
import paramiko
except ImportError:
print("ОШИБКА: нет paramiko. Установите: pip install paramiko", file=sys.stderr)
sys.exit(1)
HOST = os.environ.get("DEDINIT_HOST", "kpa39l.myjino.ru")
PORT = int(os.environ.get("DEDINIT_PORT", "2222"))
USER = os.environ.get("DEDINIT_USER", "kpa39l_dedinit")
PASS = os.environ.get("SSHPASS", "")
LOCAL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "public")
REMOTE = os.environ.get("DEDINIT_REMOTE", "/dedinit.ru")
DRYRUN = "--dry-run" in sys.argv
if not PASS:
print("ОШИБКА: переменная SSHPASS (пароль SFTP) не задана. См. README.md / docs/DEPLOY_INSTRUCTIONS.md", file=sys.stderr)
sys.exit(1)
LOCAL = os.path.abspath(LOCAL)
if not os.path.isdir(LOCAL):
print(f"ОШИБКА: нет каталога {LOCAL}. Сначала: make build", file=sys.stderr)
sys.exit(1)
t = paramiko.Transport((HOST, PORT))
t.connect(username=USER, password=PASS)
sftp = paramiko.SFTPClient.from_transport(t)
def rlist(path):
out = []
try:
entries = sftp.listdir_attr(path)
except IOError:
return out
for e in entries:
full = os.path.join(path, e.filename)
rel = os.path.relpath(full, REMOTE)
isdir = stmod.S_ISDIR(e.st_mode)
out.append((rel, isdir))
if isdir:
out.extend(rlist(full))
return out
print(f"Копирую {LOCAL} -> sftp://{USER}@{HOST}:{PORT}{REMOTE}/"
+ (" (DRY-RUN)" if DRYRUN else ""))
local_files = []
for root, dirs, files in os.walk(LOCAL):
for f in files:
local_files.append(os.path.relpath(os.path.join(root, f), LOCAL))
if not DRYRUN:
remote_entries = rlist(REMOTE)
remote_files = {r for r, d in remote_entries if not d}
for rel in sorted(local_files):
if DRYRUN:
print(" +", rel)
continue
lpath = os.path.join(LOCAL, rel)
rpath = os.path.join(REMOTE, rel)
rdir = os.path.dirname(rpath)
cur = REMOTE
for seg in os.path.relpath(rdir, REMOTE).split(os.sep):
if not seg:
continue
cur = os.path.join(cur, seg)
try:
sftp.stat(cur)
except IOError:
try:
sftp.mkdir(cur)
except IOError:
pass
sftp.put(lpath, rpath)
print(" +", rel)
if not DRYRUN:
to_delete = remote_files - set(local_files)
for rel in sorted(to_delete):
try:
sftp.remove(os.path.join(REMOTE, rel))
print(" -", rel)
except Exception as e:
print(" !", rel, e)
sftp.close()
t.close()
print("Готово." if not DRYRUN else "DRY-RUN завершён: показаны изменения без заливки.")