diff --git a/gpt_interface/app.py b/gpt_interface/app.py new file mode 100644 index 0000000..ef0e39c --- /dev/null +++ b/gpt_interface/app.py @@ -0,0 +1,11 @@ +from flask import Flask +from file_serv import bp as uploader_bp +from waitress import serve + +app = Flask(__name__) +app.register_blueprint(uploader_bp, url_prefix="/api") +HOST_ADDRESS = "192.168.1.15" +PORT_ADDRESS = 65408 + +if __name__ == "__main__": + serve(app, host=HOST_ADDRESS, port=PORT_ADDRESS) \ No newline at end of file diff --git a/gpt_interface/file_serv.py b/gpt_interface/file_serv.py new file mode 100644 index 0000000..c042098 --- /dev/null +++ b/gpt_interface/file_serv.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import os +from pathlib import Path +from typing import List, Dict, Any, Optional + +from flask import Blueprint, request, jsonify, send_file, abort, current_app +from werkzeug.utils import secure_filename + +# ===== Konfiguracja przez ENV ===== + +if API_KEY := os.getenv("API_KEY") is None: + with open("/home/pi/gpt_cont_api_key", "r") as f: + API_KEY = f.read().strip() +else: + API_KEY = os.getenv("API_KEY", "") # np. openssl rand -hex 32 + +UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/home/pi/tmp_git/")) # katalog na dysku +MAX_CONTENT_MB = int(os.getenv("MAX_CONTENT_MB", "200")) + +# Google Drive (opcjonalnie) +GDRIVE_ENABLE = os.getenv("GDRIVE_ENABLE", "0") == "1" +GDRIVE_SA_JSON = os.getenv("GDRIVE_SA_JSON", "") # ścieżka do pliku .json konta serwisowego +GDRIVE_FOLDER_ID = os.getenv("GDRIVE_FOLDER_ID", "") # ID folderu na Drive + +bp = Blueprint("uploader", __name__) + +# Inicjalizacja katalogu +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + +def _check_auth() -> bool: + """Proste Bearer auth. Jeśli API_KEY puste -> bez auth (niezalecane).""" + if not API_KEY: + return True + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + token = auth[7:].strip() + return token == API_KEY + return False + +def _safe_join(base: Path, *parts: str) -> Path: + """Zapobiega ../ — wymusza pozostanie w katalogu bazowym.""" + p = (base.joinpath(*parts)).resolve() + if not str(p).startswith(str(base.resolve())): + abort(400, description="Invalid path") + return p + +# ===== Google Drive helper ===== +_drive_client_cached = None + +def _get_drive(): + global _drive_client_cached + if _drive_client_cached is not None: + return _drive_client_cached + + if not (GDRIVE_ENABLE and GDRIVE_SA_JSON and os.path.exists(GDRIVE_SA_JSON)): + return None + + from google.oauth2 import service_account + from googleapiclient.discovery import build + + scopes = ["https://www.googleapis.com/auth/drive.file"] + creds = service_account.Credentials.from_service_account_file(GDRIVE_SA_JSON, scopes=scopes) + _drive_client_cached = build("drive", "v3", credentials=creds, cache_discovery=False) + return _drive_client_cached + +def upload_to_drive(local_path: Path, filename: str) -> Optional[Dict[str, Any]]: + drv = _get_drive() + if drv is None: + return None + from googleapiclient.http import MediaFileUpload + file_metadata = {"name": filename} + if GDRIVE_FOLDER_ID: + file_metadata["parents"] = [GDRIVE_FOLDER_ID] + media = MediaFileUpload(str(local_path), resumable=False) + created = drv.files().create(body=file_metadata, media_body=media, fields="id,webViewLink,webContentLink").execute() + file_id = created.get("id") + # Przydatne linki: + return { + "file_id": file_id, + "webViewLink": created.get("webViewLink"), + "webContentLink": created.get("webContentLink"), + "direct_view": f"https://drive.google.com/file/d/{file_id}/view", + "direct_download": f"https://drive.google.com/uc?export=download&id={file_id}", + } + +@bp.get("/health") +def health(): + return jsonify(ok=True) + +@bp.post("/upload") +def upload(): + """Przyjmuje: + - multipart/form-data z jednym plikiem ('file') lub wieloma ('files') + - opcjonalnie: form 'subdir' (podkatalog), 'drive' (1/0) żeby wymusić wysyłkę na Drive + """ + if not _check_auth(): + return jsonify(error="Unauthorized"), 401 + + # Limit ciała żądania po stronie Flaska: + request.max_content_length = MAX_CONTENT_MB * 1024 * 1024 + + files = [] + if "file" in request.files: + files = [request.files["file"]] + elif "files" in request.files: + files = request.files.getlist("files") + else: + return jsonify(error="No file provided (use 'file' or 'files')"), 400 + + subdir = (request.form.get("subdir") or "").strip() + target_dir = _safe_join(UPLOAD_DIR, subdir) if subdir else UPLOAD_DIR + target_dir.mkdir(parents=True, exist_ok=True) + + want_drive = (request.form.get("drive") == "1") or (request.args.get("drive") == "1") + saved: List[Dict[str, Any]] = [] + + for f in files: + if not f or not f.filename: + continue + fname = secure_filename(f.filename) + dest = _safe_join(target_dir, fname) + f.save(dest) + + item = { + "filename": fname, + "size": dest.stat().st_size, + "path": str(dest.relative_to(UPLOAD_DIR)), + "url_hint": f"/api/files/{dest.relative_to(UPLOAD_DIR)}", + } + + if want_drive and GDRIVE_ENABLE: + try: + gd = upload_to_drive(dest, fname) + if gd: + item["gdrive"] = gd + except Exception as e: + # log i idziemy dalej + current_app.logger.exception("Drive upload failed: %s", e) + item["gdrive_error"] = str(e) + + saved.append(item) + + if not saved: + return jsonify(error="No valid files"), 400 + return jsonify(ok=True, saved=saved) + +@bp.get("/files/") +def get_file(relpath: str): + if not _check_auth(): + return jsonify(error="Unauthorized"), 401 + target = _safe_join(UPLOAD_DIR, relpath) + if not target.exists() or not target.is_file(): + return jsonify(error="Not found"), 404 + return send_file(target, as_attachment=False)