From 5f99a0460c7f61543e4a72b35ec5637ca848a530 Mon Sep 17 00:00:00 2001 From: Telegram Bot Date: Wed, 29 Apr 2026 01:28:59 +0300 Subject: [PATCH] infra: deploy script + webhook server + S3 uploader (saguaro test) --- infra/README.md | 31 ++++++ infra/deploy-ptp.sh | 9 ++ infra/uploader.py | 228 ++++++++++++++++++++++++++++++++++++++++ infra/webhook-server.py | 41 ++++++++ 4 files changed, 309 insertions(+) create mode 100644 infra/README.md create mode 100644 infra/deploy-ptp.sh create mode 100644 infra/uploader.py create mode 100644 infra/webhook-server.py diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..fd643ff --- /dev/null +++ b/infra/README.md @@ -0,0 +1,31 @@ +# PTP Infrastructure Scripts + +## deploy-ptp.sh +Deploy script: git pull mirror → hugo build → /var/www/html +Triggered by Gitea webhook on push to mirror/main. +Service: ptp-webhook (port 9000) + +## webhook-server.py +HTTP server на порту 9000. +Принимает POST /deploy от Gitea (HMAC-SHA256 подпись). +Secret: ptp-deploy-2026 + +## uploader.py +Flask-приложение на порту 9001. +Drag-and-drop загрузка фото → S3 reg.ru (sleeptrip-dev/images/). +Защищено Basic Auth через nginx. +Поддерживает: jpg, png, gif, webp, heic, mp4, mov. +HEIC автоматически конвертируется в JPG. + +## Systemd services +- ptp-webhook.service → webhook-server.py +- ptp-uploader.service → uploader.py + +## Nginx snippets +- /etc/nginx/snippets/ptp-webhook.conf +- /etc/nginx/snippets/ptp-uploader.conf + +## Gitea webhook +Repo: ptpadmin/ptp, hook id: 1 +URL: http://127.0.0.1:9000/deploy +Branch filter: mirror diff --git a/infra/deploy-ptp.sh b/infra/deploy-ptp.sh new file mode 100644 index 0000000..f4562b7 --- /dev/null +++ b/infra/deploy-ptp.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +LOG=/var/log/ptp-deploy.log +echo "[2026-04-29 00:05:28] Deploy started" >> $LOG +cd /var/www/hugo-source +git checkout mirror >> $LOG 2>&1 +git pull gitea mirror >> $LOG 2>&1 +hugo -d /var/www/html --baseURL 'https://ptp.saguaro-cactus.ru' >> $LOG 2>&1 +echo "[2026-04-29 00:05:28] Deploy done" >> $LOG diff --git a/infra/uploader.py b/infra/uploader.py new file mode 100644 index 0000000..d127081 --- /dev/null +++ b/infra/uploader.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +import os, tempfile +from flask import Flask, request, jsonify +import boto3 +from botocore.client import Config +from botocore.exceptions import ClientError +import pillow_heif +from PIL import Image +pillow_heif.register_heif_opener() + +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB + +S3_CONFIG = dict( + endpoint_url='https://s3.regru.cloud', + aws_access_key_id='TXBN3MRO1YCC2Y593URD', + aws_secret_access_key='GIMA9lJetvXgMtl8GCO4dNSpJDGHvKgGBVfiA9zV', + region_name='ru-1', + config=Config(signature_version='s3v4') +) +BUCKET = 'sleeptrip-dev' +PREFIX = 'images/' +ALLOWED = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'mov', 'heic', 'heif'} +MIME = { + 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', + 'gif': 'image/gif', 'webp': 'image/webp', + 'mp4': 'video/mp4', 'mov': 'video/quicktime', + 'heic': 'image/jpeg', 'heif': 'image/jpeg' +} + +PAGE = """ + + + + +Upload to S3 + + + +
+

Upload → S3

+

sleeptrip-dev · s3.regru.cloud/images/

+
+ +
+
Перетащи или выбери файл
+
+
+
+
Загружаем...
+
+
+
+
+
Нажми чтобы скопировать
+ +
+
+

История

+
+
+
+ + +""" + + +@app.route('/upload/') +def index(): + return PAGE + + +@app.route('/upload/api', methods=['POST']) +def upload_file(): + if 'file' not in request.files: + return jsonify({'error': 'Нет файла'}), 400 + + f = request.files['file'] + if not f.filename: + return jsonify({'error': 'Пустое имя файла'}), 400 + + ext = f.filename.rsplit('.', 1)[-1].lower() if '.' in f.filename else '' + if ext not in ALLOWED: + return jsonify({'error': 'Формат .{} не поддерживается'.format(ext)}), 400 + + original_name = os.path.splitext(f.filename)[0] + + # HEIC/HEIF → конвертируем в JPG + is_heic = ext in ('heic', 'heif') + out_ext = 'jpg' if is_heic else ext + s3_key = '{}{}.{}'.format(PREFIX, original_name, out_ext) + + tmp_path = None + try: + with tempfile.NamedTemporaryFile(delete=False, suffix='.{}'.format(ext)) as tmp: + f.save(tmp.name) + tmp_path = tmp.name + + if is_heic: + img = Image.open(tmp_path) + converted = tmp_path.replace('.{}'.format(ext), '.jpg') + img.convert('RGB').save(converted, 'JPEG', quality=92) + os.unlink(tmp_path) + tmp_path = converted + + import urllib3 + urllib3.disable_warnings() + + s3 = boto3.client('s3', verify=False, **S3_CONFIG) + s3.upload_file( + tmp_path, BUCKET, s3_key, + ExtraArgs={ + 'ContentType': MIME.get(ext, 'application/octet-stream'), + 'CacheControl': 'max-age=31536000' + } + ) + + url = 'https://s3.regru.cloud/{}/{}'.format(BUCKET, s3_key) + # hint: HEIC was converted to JPG + return jsonify({'url': url}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=9001, debug=False) diff --git a/infra/webhook-server.py b/infra/webhook-server.py new file mode 100644 index 0000000..6fc4e42 --- /dev/null +++ b/infra/webhook-server.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import http.server, subprocess, hashlib, hmac, json, os, sys + +SECRET = os.environ.get('WEBHOOK_SECRET', 'ptp-deploy-2026') +PORT = 9000 + +class Handler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + if self.path != '/deploy': + self.send_response(404); self.end_headers(); return + + length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(length) + + # Verify signature (Gitea sends X-Gitea-Signature) + sig = self.headers.get('X-Gitea-Signature', '') + expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest() + + if not hmac.compare_digest(sig, expected): + self.send_response(403); self.end_headers() + self.wfile.write(b'Bad signature'); return + + try: + data = json.loads(body) + ref = data.get('ref', '') + if ref not in ('refs/heads/mirror', 'refs/heads/main'): + self.send_response(200); self.end_headers() + self.wfile.write(b'Skipped (not mirror/main)'); return + except: pass + + subprocess.Popen(['/usr/local/bin/deploy-ptp.sh']) + self.send_response(200); self.end_headers() + self.wfile.write(b'Deploy triggered') + + def log_message(self, fmt, *args): + pass # silent + +if __name__ == '__main__': + httpd = http.server.HTTPServer(('127.0.0.1', PORT), Handler) + print(f'Webhook server on port {PORT}') + httpd.serve_forever()