upload builds to the server

This commit is contained in:
Nordup 2025-08-30 23:03:51 +07:00
parent 6941bfe99a
commit ad1dd849d5
3 changed files with 152 additions and 18 deletions

5
.gitignore vendored
View file

@ -2,4 +2,7 @@
.vscode/*
# for ipc files (inter process communication)
app/sandbox
app/sandbox
# for deployment
deployment/upload_api.key

View file

@ -9,6 +9,7 @@ import shutil
import subprocess
import sys
from pathlib import Path
import argparse
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
@ -26,27 +27,33 @@ def parse_version(project_path: Path) -> str:
raise RuntimeError(f"Failed to parse version from {project_path}")
def open_folder_and_url(builds_dir: Path, url: str) -> None:
os_name = platform.system()
try:
if os_name == "Linux":
# fire-and-forget
subprocess.Popen(["xdg-open", str(builds_dir)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
elif os_name == "Darwin":
subprocess.Popen(["open", str(builds_dir)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
# Non-fatal if opening fails
pass
def build_expected_zip_paths(builds_dir: Path, version: str, os_name: str) -> list[Path]:
paths: list[Path] = []
if os_name == "Linux":
paths.append(builds_dir / "Linux" / f"TheGates_Linux_{version}.zip")
paths.append(builds_dir / "Windows" / f"TheGates_Windows_{version}.zip")
elif os_name == "Darwin":
paths.append(builds_dir / f"TheGates_MacOS_{version}.zip")
return paths
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Export, compress, and upload builds.")
parser.add_argument(
"--force",
action="store_true",
help="Overwrite existing compressed files (Linux compressor only).",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
script_dir = Path(__file__).resolve().parent
repo_root = script_dir.parent
app_dir = repo_root / "app"
export_script = repo_root / "deployment" / "export_project.py"
open_url = "https://devs.thegates.io/files/builds/"
uploader = repo_root / "deployment" / "upload_build.py"
os_name = platform.system()
print(f"==> Using repo root: {repo_root}")
@ -63,6 +70,8 @@ def main() -> int:
version = parse_version(app_dir / "project.godot")
print(f"==> App version: {version}")
uploaded: list[Path] = []
if os_name == "Linux":
builds_dir = Path("/media/common/Projects/thegates-folder/AppBuilds")
compress_src = repo_root / "deployment" / "compress_builds_linux.py"
@ -77,9 +86,12 @@ def main() -> int:
shutil.copy2(compress_src, compress_dst)
print(f"==> Compressing Linux/Windows builds with version {version}...")
run([sys.executable, str(compress_dst), version], cwd=builds_dir)
compress_cmd = [sys.executable, str(compress_dst), version]
if args.force:
compress_cmd.append("--force")
run(compress_cmd, cwd=builds_dir)
open_folder_and_url(builds_dir, open_url)
uploaded = build_expected_zip_paths(builds_dir, version, os_name)
elif os_name == "Darwin":
builds_dir = Path("/Users/nordup/Projects/thegates-folder/AppBuilds")
@ -91,7 +103,19 @@ def main() -> int:
print(f"==> Compressing macOS build with version {version}...")
run([sys.executable, str(compress_script), version], cwd=builds_dir)
open_folder_and_url(builds_dir, open_url)
uploaded = build_expected_zip_paths(builds_dir, version, os_name)
# Upload created zip files via uploader
existing = [p for p in uploaded if p.exists()]
if not existing:
print("No compressed build files found to upload.")
return 1
print("==> Uploading:")
for p in existing:
print(f" - {p}")
run([sys.executable, str(uploader), *[str(p) for p in existing]], cwd=repo_root)
print("==> Done.")
return 0

107
deployment/upload_build.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import sys
import mimetypes
import uuid
from pathlib import Path
from typing import Iterable
from urllib import request, error
DEFAULT_ENDPOINT = "https://app.thegates.io/api/upload_build"
def read_api_key(repo_root: Path) -> str:
# Allow override via env. Default to deployment/upload_api.key
key_file_env = os.environ.get("TG_UPLOAD_API_KEY_FILE")
if key_file_env:
key_path = Path(key_file_env).expanduser().resolve()
else:
key_path = (repo_root / "deployment" / "upload_api.key").resolve()
if not key_path.exists():
raise FileNotFoundError(
f"API key file not found: {key_path}. Set TG_UPLOAD_API_KEY_FILE or create the file."
)
key = key_path.read_text(encoding="utf-8").strip()
if not key:
raise ValueError(f"API key file {key_path} is empty")
return key
def build_multipart_body(field_name: str, file_path: Path, boundary: str) -> tuple[bytes, str]:
filename = file_path.name
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
file_bytes = file_path.read_bytes()
boundary_bytes = boundary.encode("utf-8")
crlf = b"\r\n"
body = []
body.append(b"--" + boundary_bytes + crlf)
body.append(
(
f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"' # noqa: E501
).encode("utf-8")
)
body.append((f"Content-Type: {content_type}").encode("utf-8") + crlf + crlf)
body.append(file_bytes + crlf)
body.append(b"--" + boundary_bytes + b"--" + crlf)
body_bytes = b"".join(body)
content_type_header = f"multipart/form-data; boundary={boundary}"
return body_bytes, content_type_header
def upload_file(endpoint: str, api_key: str, file_path: Path) -> int:
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
boundary = f"----TheGatesBoundary{uuid.uuid4().hex}"
body, content_type = build_multipart_body("file", file_path, boundary)
req = request.Request(endpoint, method="POST")
req.add_header("Content-Type", content_type)
req.add_header("Content-Length", str(len(body)))
req.add_header("X-API-Key", api_key)
try:
with request.urlopen(req, data=body, timeout=300) as resp:
status = resp.getcode()
print(f"Uploaded {file_path.name}: HTTP {status}")
return status
except error.HTTPError as e:
print(f"Upload failed for {file_path.name}: HTTP {e.code} - {e.read().decode(errors='ignore')}")
return e.code
except Exception as e:
print(f"Upload error for {file_path.name}: {e}")
return 1
def main(argv: list[str]) -> int:
if len(argv) < 2:
print("Usage: upload_build.py <file1> [<file2> ...]")
return 2
repo_root = Path(__file__).resolve().parent.parent
endpoint = os.environ.get("TG_UPLOAD_ENDPOINT", DEFAULT_ENDPOINT)
api_key = read_api_key(repo_root)
statuses: list[int] = []
for arg in argv[1:]:
file_path = Path(arg).expanduser().resolve()
statuses.append(upload_file(endpoint, api_key, file_path))
# Return non-zero if any upload failed (status >= 400 or ==1)
for s in statuses:
if isinstance(s, int) and (s >= 400 or s == 1):
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))