Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,16 @@ jobs:
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
# Enable CI feature so prod() spawns backend + sync in release builds
args: "--target aarch64-apple-darwin --features ci"
server-artifact: "PictoPy-MacOS"
sync-artifact: "PictoPy-Sync-MacOS"
- platform: "ubuntu-22.04"
args: ""
args: "--features ci"
server-artifact: "PictoPy-Ubuntu"
sync-artifact: "PictoPy-Sync-Ubuntu"
- platform: "windows-latest"
args: ""
args: "--features ci"
server-artifact: "PictoPy-Windows"
sync-artifact: "PictoPy-Sync-Windows"
runs-on: ${{ matrix.platform }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr-check-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
args: "--target aarch64-apple-darwin --features ci"
- platform: "ubuntu-22.04"
args: ""
args: "--features ci"
- platform: "windows-latest"
args: ""
args: "--features ci"
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
Expand Down
10 changes: 8 additions & 2 deletions backend/app/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from platformdirs import user_data_dir
import os

# Model Exports Path
MODEL_EXPORTS_PATH = "app/models/ONNX_Exports"

Expand All @@ -20,6 +23,9 @@

TEST_INPUT_PATH = "tests/inputs"
TEST_OUTPUT_PATH = "tests/outputs"
DATABASE_PATH = "app/database/PictoPy.db"
THUMBNAIL_IMAGES_PATH = "./images/thumbnails"
if os.getenv("GITHUB_ACTIONS") == "true":
DATABASE_PATH = os.path.join(os.getcwd(), "test_db.sqlite3")
else:
DATABASE_PATH = os.path.join(user_data_dir("PictoPy"), "database", "PictoPy.db")
THUMBNAIL_IMAGES_PATH = os.path.join(user_data_dir("PictoPy"), "thumbnails")
IMAGES_PATH = "./images"
66 changes: 66 additions & 0 deletions backend/app/routes/shutdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import asyncio
import os
import platform
import signal
from fastapi import APIRouter
from pydantic import BaseModel
from app.logging.setup_logging import get_logger

logger = get_logger(__name__)

router = APIRouter(tags=["Shutdown"])


class ShutdownResponse(BaseModel):
"""Response model for shutdown endpoint."""

status: str
message: str


async def _delayed_shutdown(delay: float = 0.5):
"""
Shutdown the server after a short delay to allow the response to be sent.

Args:
delay: Seconds to wait before signaling shutdown
"""
await asyncio.sleep(delay)
logger.info("Backend shutdown initiated, exiting process...")

if platform.system() == "Windows":
# Windows: SIGTERM doesn't work reliably with uvicorn subprocesses
os._exit(0)
else:
# Unix (Linux/macOS): SIGTERM allows cleanup handlers to run
os.kill(os.getpid(), signal.SIGTERM)


@router.post("/shutdown", response_model=ShutdownResponse)
async def shutdown():
"""
Gracefully shutdown the PictoPy backend.

This endpoint schedules backend server termination after response is sent.
The frontend is responsible for shutting down the sync service separately.

Returns:
ShutdownResponse with status and message
"""
logger.info("Shutdown request received for PictoPy backend")

# Define callback to handle potential exceptions in the background task
def _handle_shutdown_exception(task: asyncio.Task):
try:
task.result()
except Exception as e:
logger.error(f"Shutdown task failed: {e}")

# Schedule backend shutdown after response is sent
shutdown_task = asyncio.create_task(_delayed_shutdown())
shutdown_task.add_done_callback(_handle_shutdown_exception)

return ShutdownResponse(
status="shutting_down",
message="PictoPy backend shutdown initiated.",
)
10 changes: 9 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import json

from app.config.settings import DATABASE_PATH, THUMBNAIL_IMAGES_PATH
from uvicorn import Config, Server
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
Expand All @@ -25,6 +26,7 @@
from app.routes.images import router as images_router
from app.routes.face_clusters import router as face_clusters_router
from app.routes.user_preferences import router as user_preferences_router
from app.routes.shutdown import router as shutdown_router
from fastapi.openapi.utils import get_openapi
from app.logging.setup_logging import (
configure_uvicorn_logging,
Expand All @@ -38,6 +40,12 @@
# Configure Uvicorn logging to use our custom formatter
configure_uvicorn_logging("backend")

path = os.path.dirname(DATABASE_PATH)
os.makedirs(path, exist_ok=True)

thumbnail_path = os.path.dirname(THUMBNAIL_IMAGES_PATH)
os.makedirs(thumbnail_path, exist_ok=True)
Comment on lines +46 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Thumbnail directory won't be created correctly.

THUMBNAIL_IMAGES_PATH is already the target directory path (e.g., ~/.local/share/PictoPy/thumbnails). Using os.path.dirname() returns its parent directory (~/.local/share/PictoPy), so os.makedirs only creates the parent, not the actual thumbnails directory.

🐛 Fix: Create the thumbnail directory directly
-thumbnail_path = os.path.dirname(THUMBNAIL_IMAGES_PATH)
-os.makedirs(thumbnail_path, exist_ok=True)
+os.makedirs(THUMBNAIL_IMAGES_PATH, exist_ok=True)
🤖 Prompt for AI Agents
In `@backend/main.py` around lines 46 - 47, The code uses
os.path.dirname(THUMBNAIL_IMAGES_PATH) so thumbnail_path points to the parent
directory instead of the actual thumbnails directory; update the creation to use
the target path itself by replacing the dirname usage and call os.makedirs on
THUMBNAIL_IMAGES_PATH (or set thumbnail_path = THUMBNAIL_IMAGES_PATH) with
exist_ok=True so the thumbnails directory is created correctly where expected
(refer to the THUMBNAIL_IMAGES_PATH variable and the os.makedirs call).



@asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down Expand Up @@ -130,6 +138,7 @@ async def root():
app.include_router(
user_preferences_router, prefix="/user-preferences", tags=["User Preferences"]
)
app.include_router(shutdown_router, tags=["Shutdown"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate "Shutdown" tag will appear in OpenAPI spec.

The shutdown_router is already defined with tags=["Shutdown"] (in shutdown.py line 11), and here it's included again with tags=["Shutdown"]. This causes the tag to appear twice in the generated OpenAPI spec (visible in openapi.json lines 1306-1308).

Remove the tag from one location:

♻️ Fix: Remove duplicate tag
-app.include_router(shutdown_router, tags=["Shutdown"])
+app.include_router(shutdown_router)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.include_router(shutdown_router, tags=["Shutdown"])
app.include_router(shutdown_router)
🤖 Prompt for AI Agents
In `@backend/main.py` at line 141, The OpenAPI spec shows duplicate "Shutdown" tag
because shutdown_router is tagged twice; remove the redundant tags=["Shutdown"]
on one side—either delete the tags argument where shutdown_router is defined
(shutdown.py, the router declaration) or remove tags=["Shutdown"] from the
app.include_router call in main.py (app.include_router(shutdown_router, ...));
keep exactly one tags declaration associated with the shutdown_router to prevent
duplicate tag entries.



# Entry point for running with: python3 main.py
Expand All @@ -138,7 +147,6 @@ async def root():
logger = get_logger(__name__)
logger.info("Starting PictoPy backend server...")

# Create a simple config with log_config=None to disable Uvicorn's default logging
config = Config(
app=app,
host="localhost",
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ ruff>=0.0.241
psutil>=5.9.5
pytest-asyncio>=1.0.0
setuptools==66.1.1
platformdirs==4.5.1
106 changes: 72 additions & 34 deletions docs/backend/backend_python/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1117,14 +1117,9 @@
"in": "query",
"required": false,
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/InputType"
}
],
"$ref": "#/components/schemas/InputType",
"description": "Choose input type: 'path' or 'base64'",
"default": "path",
"title": "Input Type"
"default": "path"
},
"description": "Choose input type: 'path' or 'base64'"
}
Expand Down Expand Up @@ -1237,7 +1232,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/app__schemas__user_preferences__ErrorResponse"
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
Expand Down Expand Up @@ -1277,7 +1272,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/app__schemas__user_preferences__ErrorResponse"
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
Expand All @@ -1287,7 +1282,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/app__schemas__user_preferences__ErrorResponse"
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
Expand All @@ -1304,6 +1299,29 @@
}
}
}
},
"/shutdown": {
"post": {
"tags": [
"Shutdown",
"Shutdown"
],
"summary": "Shutdown",
"description": "Gracefully shutdown the PictoPy backend.\n\nThis endpoint schedules backend server termination after response is sent.\nThe frontend is responsible for shutting down the sync service separately.\n\nReturns:\n ShutdownResponse with status and message",
"operationId": "shutdown_shutdown_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShutdownResponse"
}
}
}
}
}
}
}
Comment on lines +1303 to 1325
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate tag in the shutdown endpoint.

The tags array contains "Shutdown" twice (lines 1306-1308). This appears to be generated from the router definition. While not breaking, it's a minor inconsistency in the OpenAPI spec.

This likely stems from the router being defined with tags=["Shutdown"] and then included with tags=["Shutdown"] again in main.py line 138. Consider removing the tag from one location.

🤖 Prompt for AI Agents
In @docs/backend/backend_python/openapi.json around lines 1303 - 1325, OpenAPI
shows the "Shutdown" tag twice for the "/shutdown" POST because the Shutdown
router is tagged in both its router definition (e.g., the Shutdown router where
tags=["Shutdown"]) and again when mounted/included in main.py (the
include_router call around line 138 that also sets tags=["Shutdown"]); remove
the duplicate by keeping the tag in only one place—either delete the tags
parameter from the router definition or remove the tags argument from the
include_router call in main.py—so the shutdown route emits a single "Shutdown"
tag in the generated openapi.json.

},
"components": {
Expand Down Expand Up @@ -1619,6 +1637,30 @@
],
"title": "DeleteFoldersResponse"
},
"ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success"
},
"error": {
"type": "string",
"title": "Error"
},
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"success",
"error",
"message"
],
"title": "ErrorResponse",
"description": "Error response model"
},
"FaceSearchRequest": {
"properties": {
"path": {
Expand Down Expand Up @@ -2204,6 +2246,7 @@
"metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
Expand Down Expand Up @@ -2425,6 +2468,25 @@
],
"title": "RenameClusterResponse"
},
"ShutdownResponse": {
"properties": {
"status": {
"type": "string",
"title": "Status"
},
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"status",
"message"
],
"title": "ShutdownResponse",
"description": "Response model for shutdown endpoint."
},
"SuccessResponse": {
"properties": {
"success": {
Expand Down Expand Up @@ -2897,30 +2959,6 @@
"error"
],
"title": "ErrorResponse"
},
"app__schemas__user_preferences__ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success"
},
"error": {
"type": "string",
"title": "Error"
},
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"success",
"error",
"message"
],
"title": "ErrorResponse",
"description": "Error response model"
}
}
}
Expand Down
Loading
Loading