Files
dedinit.ru/tools/deploy_sftp.py
T

136 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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).
Инкрементально: файлы, которые уже есть на сервере с тем же размером и mtime
(допуск 300с), пропускаются — заливаются только новые/изменённые.
Таймауты: соединение banner_timeout=30/timeout=30, операция — socket timeout 120с.
"""
import os
import socket
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
MTIME_TOLERANCE = int(os.environ.get("DEDINIT_MTIME_TOLERANCE", "300"))
if not PASS and not DRYRUN:
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)
# Защита от бесконечного зависания при обрыве соединения.
socket.setdefaulttimeout(120)
t = paramiko.Transport((HOST, PORT))
t.banner_timeout = 30
try:
t.connect(username=USER, password=PASS)
except Exception as e:
print(f"ОШИБКА: не удалось подключиться к {HOST}:{PORT}: {e}", file=sys.stderr)
sys.exit(1)
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
def same_file(lstat, rstat):
"""True, если файл на сервере совпадает с локальным (размер + mtime с допуском)."""
if rstat.st_size != lstat.st_size:
return False
rmt = int(rstat.st_mtime)
lmt = int(lstat.st_mtime)
# Некоторые SFTP-сервера отдают mtime=0 — в этом случае считаем файл изменённым.
if rmt <= 0:
return False
return abs(rmt - lmt) <= MTIME_TOLERANCE
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))
remote_entries = rlist(REMOTE) if not DRYRUN else []
remote_files = {r for r, d in remote_entries if not d}
uploaded = skipped = 0
for rel in sorted(local_files):
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
try:
rstat = sftp.stat(rpath)
except IOError:
rstat = None
if rstat is not None and same_file(os.stat(lpath), rstat):
skipped += 1
print(f" = {rel}")
continue
if not DRYRUN:
sftp.put(lpath, rpath)
uploaded += 1
print(f" + {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(f" - {rel}")
except Exception as e:
print(f" ! {rel} {e}")
sftp.close()
t.close()
print(f"Готово: {uploaded} залито, {skipped} пропущено."
if not DRYRUN else f"DRY-RUN завершён: будет залито {uploaded}, пропущено {skipped}.")