-
-
Notifications
You must be signed in to change notification settings - Fork 132
Expand file tree
/
Copy pathmain.py
More file actions
177 lines (145 loc) · 5.2 KB
/
Copy pathmain.py
File metadata and controls
177 lines (145 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""
Gmail Cleaner - FastAPI Application
-----------------------------------
Main application factory and configuration.
"""
import hashlib
import subprocess
import time
import sys
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.core import settings
from app.api import status_router, actions_router, setup_router
def resource_path(relative_path):
"""Get absolute path to resource, works for dev and for PyInstaller"""
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
templates = Jinja2Templates(directory=resource_path("templates"))
def get_cache_bust_value() -> str:
"""
Get cache-busting value using a robust strategy:
1. Try git commit hash (most specific)
2. If uncommitted changes exist in static files, append a hash of those changes
3. Fall back to app version from settings
4. Fall back to timestamp if both unavailable
"""
# Skip git checks if frozen (PyInstaller)
if getattr(sys, "frozen", False):
return settings.app_version or str(int(time.time()))
base_value = None
# Try git commit hash first
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
timeout=5,
)
commit_hash = result.stdout.strip()
if commit_hash:
base_value = commit_hash
except (
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
FileNotFoundError,
):
pass
# Check for uncommitted changes in static files
if base_value:
try:
# Check for uncommitted changes in static directory
diff_result = subprocess.run(
["git", "diff", "--name-only", "static/"],
capture_output=True,
text=True,
check=False,
timeout=5,
)
# Also check for untracked files in static directory
untracked_result = subprocess.run(
["git", "ls-files", "--others", "--exclude-standard", "static/"],
capture_output=True,
text=True,
check=False,
timeout=5,
)
changed_files = diff_result.stdout.strip().splitlines()
untracked_files = untracked_result.stdout.strip().splitlines()
all_changed = [f for f in changed_files + untracked_files if f]
if all_changed:
# Compute hash of changed file names and their content
hasher = hashlib.sha256()
for file_path in sorted(all_changed):
# Validate file path is in static directory (security check)
if not file_path.startswith("static/"):
continue
hasher.update(file_path.encode())
try:
with open(file_path, "rb") as f:
hasher.update(f.read())
except OSError:
# Skip files that can't be read
pass
change_hash = hasher.hexdigest()[:8]
return f"{base_value}-{change_hash}"
except (
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
FileNotFoundError,
):
pass
# If we have a base value (commit hash), use it
if base_value:
return base_value
# Fall back to app version
if settings.app_version:
return settings.app_version
# Final fallback to timestamp
return str(int(time.time()))
# Cache-busting value generated at app startup
STARTUP_CACHE_BUST = get_cache_bust_value()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan - startup and shutdown events."""
# Startup
print(f"{settings.app_name} v{settings.app_version} starting...")
yield
# Shutdown
print("Shutting down...")
def create_app() -> FastAPI:
"""Application factory - creates and configures the FastAPI app."""
app = FastAPI(
title=settings.app_name,
description="Bulk unsubscribe and email management tool for Gmail",
version=settings.app_version,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
)
# Mount static files
app.mount("/static", StaticFiles(directory=resource_path("static")), name="static")
# Include API routers
app.include_router(status_router)
app.include_router(actions_router)
app.include_router(setup_router)
# HTML routes
@app.get("/", include_in_schema=False)
async def root(request: Request):
"""Serve the main HTML page."""
return templates.TemplateResponse(
request,
"index.html",
{"cache_bust": STARTUP_CACHE_BUST, "version": settings.app_version},
)
return app
# Create app instance
app = create_app()