Browse Source

Update packages; refactor

ghorsington 2 years ago
parent
commit
1b9a8ac3c1
12 changed files with 117 additions and 88 deletions
  1. 1 0
      .gitignore
  2. 16 0
      .vscode/launch.json
  3. 3 3
      Dockerfile
  4. 1 1
      README.md
  5. 7 8
      docker-compose.yml
  6. 4 4
      requirements.txt
  7. 75 66
      src/app.py
  8. 1 1
      src/debug.py
  9. 4 0
      src/gunicorn.conf.py
  10. 2 2
      src/templates/base.html
  11. 2 2
      src/templates/main.html
  12. 1 1
      src/wsgi.py

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+venv/
 builds/
 ftp.env
 src/static/all.css

+ 16 - 0
.vscode/launch.json

@@ -0,0 +1,16 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Python: Current File",
+            "type": "python",
+            "request": "launch",
+            "program": "${file}",
+            "cwd": "${workspaceFolder}/src",
+            "console": "integratedTerminal"
+        }
+    ]
+}

+ 3 - 3
Dockerfile

@@ -2,12 +2,12 @@ FROM python:3-alpine
 
 RUN apk add build-base
 
-WORKDIR /var/www/bepisbuilds
+WORKDIR /var/www/app
 
 COPY requirements.txt ./
 RUN pip install --no-cache-dir -r requirements.txt
 
 COPY . .
 
-WORKDIR /var/www/bepisbuilds/src
-CMD ["gunicorn", "-w 4", "-b 0.0.0.0:4000", "wsgi:app"]
+WORKDIR /var/www/app/src
+CMD ["gunicorn", "wsgi:app"]

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# Bepisbuilds
+# BepInbuilds
 
 Builds distribution website for BepInEx and other distributions
 

+ 7 - 8
docker-compose.yml

@@ -1,31 +1,30 @@
 version: '3'
 
 services:
-  bepisbuilds:
+  builds-server:
     build:
       context: .
       dockerfile: Dockerfile
     volumes:
-      - "builds-volume:/var/www/bepisbuilds/builds"
-    restart: always
+      - "builds-volume:/var/www/app/builds"
+    restart: unless-stopped
     ports:
       - "4000:4000"
 
-  ftpd-server:
+  builds-ftp:
     image: stilliard/pure-ftpd:hardened
     ports:
       - "21:21"
       - "20:20"
       - "30000-30009:30000-30009"
     volumes:
-      - "builds-volume:/home/bepisbuilds"
+      - "builds-volume:/home/builds"
     env_file:
       - ./ftp.env
     environment:
-      FTP_USER_HOME: "/home/bepisbuilds"
+      FTP_USER_HOME: "/home/builds"
       FTP_PASSIVE_PORTS: "30000:30009"
-    restart: on-failure
-
+    restart: unless-stopped
 
 volumes:
   builds-volume:

+ 4 - 4
requirements.txt

@@ -1,4 +1,4 @@
-flask==1.1.2
-gunicorn==20.0.4
-markdown==3.3.3
-dataclasses-json==0.5.2
+flask==2.0.2
+gunicorn==20.1.0
+markdown==3.3.6
+dataclasses-json==0.5.6

+ 75 - 66
src/app.py

@@ -1,13 +1,16 @@
-from flask import Flask, render_template, abort, send_file, jsonify
-import os
-import json
-from typing import List
+from pathlib import Path
+from flask import Flask, Response, render_template, abort, send_file, jsonify
 import markdown as md
 from dataclasses import dataclass
 from dataclasses_json import DataClassJsonMixin
 
 app = Flask(__name__)
 
+BUILDS_PATH = Path("../builds")
+ARTIFACTS_FOLDER = "artifacts"
+INFO_FILE = "info.json"
+
+
 @dataclass
 class ArtifactItem(DataClassJsonMixin):
     file: str
@@ -19,9 +22,9 @@ class Artifact(DataClassJsonMixin):
     id: str
     date: str
     changelog: str
-    artifacts: List[ArtifactItem]
-    hash: str = None
-    short_hash: str = None
+    artifacts: list[ArtifactItem]
+    hash: str | None = None
+    short_hash: str | None = None
 
 
 @dataclass
@@ -30,50 +33,47 @@ class ProjectInfo(DataClassJsonMixin):
     commit_url: str
 
 
+@dataclass
 class Project:
     id: str
     info: ProjectInfo
 
+    def _get_artifact_paths(self) -> list[Path]:
+        artifacts_path = BUILDS_PATH / self.id / ARTIFACTS_FOLDER
+        if not artifacts_path.exists():
+            return []
 
-    def get_artifacts(self) -> List[Artifact]:
-        result = []
-        artifacts_path = os.path.join("../builds", self.id, "artifacts")
-        artifact_folders = sorted([folder.path for folder in os.scandir(
-            artifacts_path) if folder.is_dir()], reverse=True)
+        return sorted(
+            [p for p in artifacts_path.iterdir() if p.is_dir()],
+            reverse=True,
+        )
 
-        for artifact_folder in artifact_folders:
-            info_file_path = os.path.join(artifact_folder, "info.json")
+    def get_artifacts(self) -> list[Artifact]:
+        result = []
+        for artifact_folder in self._get_artifact_paths():
+            info_file_path = artifact_folder / INFO_FILE
             artifact = self._parse_artifact(info_file_path)
             if artifact:
                 result.append(artifact)
         return result
 
-
-    def get_lastest_artifact(self) -> Artifact:
-        artifacts_path = os.path.join("../builds", self.id, "artifacts")
-        artifact_folders = sorted([folder.path for folder in os.scandir(
-            artifacts_path) if folder.is_dir()], reverse=True)
-        if not artifact_folders:
+    def get_lastest_artifact(self) -> Artifact | None:
+        latest_artifact_folder = max(self._get_artifact_paths(), default=None)
+        if not latest_artifact_folder:
             return None
-        
-        latest_artifact_folder = max(artifact_folders)
-        info_file_path = os.path.join(latest_artifact_folder, "info.json")
-        return self._parse_artifact(info_file_path)
-
+        return self._parse_artifact(latest_artifact_folder / INFO_FILE)
 
-    def get_artifact(self, artifact_id) -> Artifact:
-        artifact_folder = os.path.join("../builds", self.id, "artifacts", artifact_id)
-        if not os.path.isdir(artifact_folder):
+    def get_artifact(self, artifact_id: str) -> Artifact | None:
+        artifact_folder = BUILDS_PATH / self.id / ARTIFACTS_FOLDER / artifact_id
+        if not artifact_folder.is_dir():
             return None
-        info_file_path = os.path.join(artifact_folder, "info.json")
-        return self._parse_artifact(info_file_path)
-
+        return self._parse_artifact(artifact_folder / INFO_FILE)
 
-    def _parse_artifact(self, info_file_path):
-        if not os.path.exists(info_file_path):
+    def _parse_artifact(self, info_file_path: Path) -> Artifact | None:
+        if not info_file_path.exists():
             return None
         try:
-            with open(info_file_path, "r", encoding="utf-8") as f:
+            with info_file_path.open("r", encoding="utf-8") as f:
                 artifact = Artifact.from_json(f.read())
                 artifact.date = artifact.date.strip()
                 return artifact
@@ -81,93 +81,102 @@ class Project:
             return None
 
 
-def get_projects() -> List[Project]:
+def get_projects() -> list[Project]:
     result = []
-    projects = [f.path for f in os.scandir("../builds") if f.is_dir()]
+    projects = [p for p in BUILDS_PATH.iterdir() if p.is_dir()]
     for project in projects:
-        info_path = os.path.join(project, "info.json")
-        if not os.path.exists(info_path):
+        info_path = project / INFO_FILE
+        if not info_path.exists():
             continue
         try:
-            proj = Project()
-            _, proj.id = os.path.split(project)
-            with open(info_path, "r", encoding="utf-8") as f:
-                proj.info = ProjectInfo.from_json(f.read())
-            result.append(proj)
+            *_, proj_id = project.parts
+            with info_path.open("r", encoding="utf-8") as f:
+                proj_info = ProjectInfo.from_json(f.read())
+            result.append(Project(proj_id, proj_info))
         except:
             continue
     return result
 
 
-@app.route("/api/projects/<string:project_id>/artifacts/latest")
-def get_latest_artifact(project_id):
+@app.get("/api/projects/<string:project_id>/artifacts/latest")
+def get_latest_artifact(project_id: str) -> Response:
     projects = get_projects()
 
     selected_project = next(
-        (project for project in projects if project.id == project_id), None)
+        (project for project in projects if project.id == project_id), None
+    )
     if not selected_project:
         return abort(404)
 
     artifact = selected_project.get_lastest_artifact()
     if not artifact:
         return abort(404)
-    
+
     return jsonify(artifact)
 
 
-@app.route("/api/projects/<string:project_id>/artifacts/<string:artifact_id>")
-def get_artifact_by_id(project_id, artifact_id):
+@app.get("/api/projects/<string:project_id>/artifacts/<string:artifact_id>")
+def get_artifact_by_id(project_id: str, artifact_id: str) -> Response:
     projects = get_projects()
 
     selected_project = next(
-        (project for project in projects if project.id == project_id), None)
+        (project for project in projects if project.id == project_id), None
+    )
     if not selected_project:
         return abort(404)
 
     artifact = selected_project.get_artifact(artifact_id)
     if not artifact:
         return abort(404)
-    
+
     return jsonify(artifact)
 
 
-@app.route("/projects/<string:project_id>/<string:artifact_id>/<string:download_item>")
-def download_item(project_id, artifact_id, download_item):
-    file_path = os.path.join("../builds", project_id,
-                             "artifacts", artifact_id, download_item)
-    if not os.path.exists(file_path):
+@app.get("/projects/<string:project_id>/<string:artifact_id>/<string:download_item>")
+def download_item(project_id: str, artifact_id: str, download_item: str) -> Response:
+    file_path = (
+        BUILDS_PATH / project_id / ARTIFACTS_FOLDER / artifact_id / download_item
+    )
+    if not file_path.exists():
         return abort(404)
     return send_file(file_path)
 
 
-@app.route("/projects/<string:project_id>")
-def display_project(project_id):
+@app.get("/projects/<string:project_id>")
+def display_project(project_id: str) -> Response | str:
     projects = get_projects()
 
     selected_project = next(
-        (project for project in projects if project.id == project_id), None)
+        (project for project in projects if project.id == project_id), None
+    )
     if not selected_project:
         return abort(404)
 
-    info_path = os.path.join("../builds", selected_project.id, "info.md")
+    info_path = BUILDS_PATH / selected_project.id / "info.md"
     readme = None
     try:
-        if os.path.exists(info_path):
-            with open(info_path, "r", encoding="utf-8") as f:
+        if info_path.exists():
+            with info_path.open("r", encoding="utf-8") as f:
                 readme = md.markdown(f.read())
     except:
         readme = None
 
     artifacts = selected_project.get_artifacts()
 
-    return render_template("project_view.html", projects=projects, selected_project=selected_project, readme=readme, artifacts=artifacts)
+    return render_template(
+        "project_view.html",
+        projects=projects,
+        selected_project=selected_project,
+        readme=readme,
+        artifacts=artifacts,
+    )
 
 
-@app.route("/")
-def index():
+@app.get("/")
+def index() -> Response | str:
     projects = get_projects()
     return render_template("main.html", projects=projects)
 
 
 if __name__ == "__main__":
-    app.run(host='0.0.0.0')
+    app.run(host="0.0.0.0")

+ 1 - 1
src/debug.py

@@ -1,4 +1,4 @@
 from app import app
 
 if __name__ == "__main__":
-    app.run(debug=True)
+    app.run(debug=True)

+ 4 - 0
src/gunicorn.conf.py

@@ -0,0 +1,4 @@
+import multiprocessing
+
+bind = "0.0.0.0:4000"
+workers = multiprocessing.cpu_count() * 2 + 1

+ 2 - 2
src/templates/base.html

@@ -7,14 +7,14 @@
     <meta http-equiv="X-UA-Compatible" content="ie=edge">
     <link rel="stylesheet" href="{{ url_for('static', filename='style/style.min.css') }}">
     {%block head%}{%endblock%}
-    <title>BepisBuilds - {% block title %}{% endblock %}</title>
+    <title>BepInBuilds - {% block title %}{% endblock %}</title>
 </head>
 
 <body>
     <header>
         <a class="brand" href="{{ url_for('index') }}">
             <img src="{{ url_for('static', filename='img/logo.png') }}" alt="BepInEx logo">
-            <span>BepisBuilds</span>
+            <span>BepInBuilds</span>
         </a>
         <span>
             Powered with 🐴

+ 2 - 2
src/templates/main.html

@@ -5,9 +5,9 @@
 {%block content%}
 
 <div class="content big">
-    <h1>BepisBuilds</h1>
+    <h1>BepInBuilds</h1>
     <p>
-        Welcome to BepisBuilds! <br/>
+        Welcome to BepInBuilds! <br/>
         This page contains different artifacts produced by the CI service used by <a href="https://github.com/bepinex">BepInEx organization</a>.
     </p>
     <p>

+ 1 - 1
src/wsgi.py

@@ -1,4 +1,4 @@
 from app import app
 
 if __name__ == "__main__":
-    app.run()
+    app.run()