serverus: DockerHost - установка Docker и ingress Caddy, deploy приложения с проверкой compose и caddy.conf до изменений и пересозданием только изменившихся сервисов, status, control, logs, remove. nftables перезапускается только при изменении конфига, Docker следом восстанавливает свои правила; release восстанавливает конфиг Debian вместо остановки nftables. Telegram: прием документов и отправка файлов, раздача файлов своего Bot API сервера через nginx (telegram-files). App: общая модель Managed для Point и Platform, DockerApp и версии файлов AppVersion, разбор архивов (uploads), сценарии платформы (platforms), задачи и меню docker-серверов в боте. Проверено на стенде Debian 13 с local_certs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Приводит папку приложения к присланной копии: app_sync.py <stage> <app_dir>.
|
|
|
|
Файлы переписываются по месту (тот же inode): смонтированный в контейнер файл остается тем же файлом.
|
|
Лишние файлы удаляются, пустые папки за ними тоже.
|
|
Печатает JSON: {"changed": [...], "removed": [...], "created": true, если папки приложения не было}.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import sys
|
|
|
|
|
|
def files(root):
|
|
result = set()
|
|
for directory, _, names in os.walk(root):
|
|
for name in names:
|
|
result.add(os.path.relpath(os.path.join(directory, name), root))
|
|
return result
|
|
|
|
|
|
def same(src, dst):
|
|
if not os.path.isfile(dst) or os.path.islink(dst):
|
|
return False
|
|
if stat.S_IMODE(os.stat(src).st_mode) != stat.S_IMODE(os.stat(dst).st_mode):
|
|
return False
|
|
with open(src, 'rb') as a, open(dst, 'rb') as b:
|
|
return a.read() == b.read()
|
|
|
|
|
|
def clear(path):
|
|
"""Убирает то, что мешает положить файл: папку или ссылку на его месте."""
|
|
if os.path.islink(path) or (os.path.exists(path) and not os.path.isdir(path)):
|
|
os.remove(path)
|
|
elif os.path.isdir(path):
|
|
shutil.rmtree(path)
|
|
|
|
|
|
def ensure_dir(app_dir, relative):
|
|
"""Создает папки до файла; файл или ссылка на месте папки удаляется."""
|
|
current = app_dir
|
|
for part in relative.split(os.sep)[:-1]:
|
|
current = os.path.join(current, part)
|
|
if os.path.islink(current) or (os.path.exists(current) and not os.path.isdir(current)):
|
|
os.remove(current)
|
|
os.makedirs(current, exist_ok=True)
|
|
|
|
|
|
def main(stage, app_dir):
|
|
created = not os.path.isdir(app_dir)
|
|
os.makedirs(app_dir, mode=0o700, exist_ok=True)
|
|
new, old = files(stage), files(app_dir)
|
|
changed, removed = [], []
|
|
for relative in sorted(old - new):
|
|
os.remove(os.path.join(app_dir, relative))
|
|
removed.append(relative)
|
|
for relative in sorted(new):
|
|
src, dst = os.path.join(stage, relative), os.path.join(app_dir, relative)
|
|
if same(src, dst):
|
|
continue
|
|
ensure_dir(app_dir, relative)
|
|
if os.path.isdir(dst) or os.path.islink(dst):
|
|
clear(dst)
|
|
with open(src, 'rb') as source, open(dst, 'wb') as target:
|
|
shutil.copyfileobj(source, target)
|
|
os.chmod(dst, stat.S_IMODE(os.stat(src).st_mode))
|
|
changed.append(relative)
|
|
for directory, _, _ in os.walk(app_dir, topdown=False):
|
|
if directory != app_dir and not os.listdir(directory):
|
|
os.rmdir(directory)
|
|
print(json.dumps({'changed': changed, 'removed': removed, 'created': created}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1], sys.argv[2])
|