mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 22:05:20 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b293bee5f4 | ||
|
|
122a3633c2 | ||
|
|
0e2580030b |
+3
-4
@@ -49,8 +49,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
# UID/GID will be handled by entrypoint script, but TZ/Locale are still needed
|
||||
LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US:en \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
APP_ENV=prod
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
# Set ARG for build-time expansion (FLASK_PORT), ENV for runtime access
|
||||
ENV FLASK_PORT=8084
|
||||
@@ -108,9 +107,9 @@ RUN mkdir -p /var/log/cwa-book-downloader /cwa-book-ingest && \
|
||||
EXPOSE ${FLASK_PORT}
|
||||
|
||||
# Add healthcheck for container status
|
||||
# This will run as root initially, but check localhost which should work if the app binds correctly.
|
||||
# Uses /api/health which doesn't require authentication
|
||||
HEALTHCHECK --interval=60s --timeout=60s --start-period=60s --retries=3 \
|
||||
CMD curl -s http://localhost:${FLASK_PORT}/api/status > /dev/null || exit 1
|
||||
CMD curl -s http://localhost:${FLASK_PORT}/api/health > /dev/null || exit 1
|
||||
|
||||
# Use dumb-init as the entrypoint to handle signals properly
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
@@ -16,7 +16,7 @@ import typing
|
||||
|
||||
from logger import setup_logger
|
||||
from config import _SUPPORTED_BOOK_LANGUAGE, BOOK_LANGUAGE, SUPPORTED_FORMATS
|
||||
from env import FLASK_HOST, FLASK_PORT, APP_ENV, CWA_DB_PATH, DEBUG, USING_EXTERNAL_BYPASSER, BUILD_VERSION, RELEASE_VERSION, CALIBRE_WEB_URL
|
||||
from env import FLASK_HOST, FLASK_PORT, CWA_DB_PATH, DEBUG, USING_EXTERNAL_BYPASSER, BUILD_VERSION, RELEASE_VERSION, CALIBRE_WEB_URL
|
||||
import backend
|
||||
|
||||
from models import SearchFilters
|
||||
@@ -28,13 +28,10 @@ app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching
|
||||
app.config['APPLICATION_ROOT'] = '/'
|
||||
|
||||
# Determine async mode based on environment
|
||||
# In production with Gunicorn + gevent worker, use 'gevent'
|
||||
# In development with Flask dev server, use 'threading'
|
||||
if APP_ENV == 'prod':
|
||||
async_mode = 'gevent'
|
||||
else:
|
||||
async_mode = 'threading'
|
||||
# Socket.IO async mode.
|
||||
# We run this app under Gunicorn with a gevent websocket worker (even when DEBUG=true),
|
||||
# so Socket.IO should always use gevent here.
|
||||
async_mode = 'gevent'
|
||||
|
||||
# Initialize Flask-SocketIO with reverse proxy support
|
||||
socketio = SocketIO(
|
||||
@@ -152,16 +149,9 @@ werkzeug_logger.addFilter(StatusEndpointFilter())
|
||||
# The secret key will reset every time we restart, which will
|
||||
# require users to authenticate again
|
||||
|
||||
# Secure cookie handling (HTTP vs HTTPS)
|
||||
# Can be overridden with SESSION_COOKIE_SECURE environment variable
|
||||
session_cookie_secure_env = os.getenv('SESSION_COOKIE_SECURE', 'auto').lower()
|
||||
if session_cookie_secure_env in ['true', 'yes', '1']:
|
||||
SESSION_COOKIE_SECURE = True
|
||||
elif session_cookie_secure_env in ['false', 'no', '0']:
|
||||
SESSION_COOKIE_SECURE = False
|
||||
else:
|
||||
# Auto mode: align with deployment environment
|
||||
SESSION_COOKIE_SECURE = APP_ENV == 'prod'
|
||||
# Session cookie security - set to 'true' if exclusively using HTTPS
|
||||
session_cookie_secure_env = os.getenv('SESSION_COOKIE_SECURE', 'false').lower()
|
||||
SESSION_COOKIE_SECURE = session_cookie_secure_env in ['true', 'yes', '1']
|
||||
|
||||
app.config.update(
|
||||
SECRET_KEY = os.urandom(64),
|
||||
@@ -382,7 +372,6 @@ def api_config() -> Union[Response, Tuple[Response, int]]:
|
||||
config = {
|
||||
"calibre_web_url": CALIBRE_WEB_URL,
|
||||
"debug": DEBUG,
|
||||
"app_env": APP_ENV,
|
||||
"build_version": BUILD_VERSION,
|
||||
"release_version": RELEASE_VERSION,
|
||||
"book_languages": _SUPPORTED_BOOK_LANGUAGE,
|
||||
@@ -394,6 +383,17 @@ def api_config() -> Union[Response, Tuple[Response, int]]:
|
||||
logger.error_trace(f"Config error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def api_health() -> Union[Response, Tuple[Response, int]]:
|
||||
"""
|
||||
Health check endpoint for container orchestration.
|
||||
No authentication required.
|
||||
|
||||
Returns:
|
||||
flask.Response: JSON with status "ok".
|
||||
"""
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
@login_required
|
||||
def api_status() -> Union[Response, Tuple[Response, int]]:
|
||||
@@ -812,7 +812,7 @@ def handle_status_request():
|
||||
logger.log_resource_usage()
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info(f"Starting Flask application with WebSocket support on {FLASK_HOST}:{FLASK_PORT} IN {APP_ENV} mode")
|
||||
logger.info(f"Starting Flask application with WebSocket support on {FLASK_HOST}:{FLASK_PORT} (debug={DEBUG})")
|
||||
socketio.run(
|
||||
app,
|
||||
host=FLASK_HOST,
|
||||
|
||||
@@ -9,7 +9,6 @@ services:
|
||||
target: cwa-bd
|
||||
environment:
|
||||
DEBUG: true
|
||||
APP_ENV: dev
|
||||
USE_DOH: true
|
||||
CUSTOM_DNS: cloudflare
|
||||
volumes:
|
||||
|
||||
@@ -10,7 +10,6 @@ services:
|
||||
target: cwa-bd-extbp
|
||||
environment:
|
||||
DEBUG: true
|
||||
APP_ENV: dev
|
||||
USE_DOH: true
|
||||
CUSTOM_DNS: cloudflare
|
||||
USE_CF_BYPASS: true # Enable Cloudflare bypass (default: true)
|
||||
|
||||
@@ -7,9 +7,11 @@ services:
|
||||
BOOK_LANGUAGE: en
|
||||
USE_BOOK_TITLE: true
|
||||
TZ: America/New_York
|
||||
APP_ENV: prod
|
||||
UID: 1000
|
||||
GID: 100
|
||||
# CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication
|
||||
# SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS
|
||||
# DEBUG: 'true' # Enable debug mode (debug button, verbose logging)
|
||||
EXT_BYPASSER_URL: http://flaresolverr:8191
|
||||
ports:
|
||||
- 8084:8084
|
||||
@@ -19,7 +21,7 @@ services:
|
||||
# the same as whatever you gave in "calibre-web-automated"
|
||||
- /tmp/data/calibre-web/ingest:/cwa-book-ingest
|
||||
# This is the location of CWA's app.db, which contains authentication
|
||||
# details
|
||||
# details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above)
|
||||
#- /cwa/config/path/app.db:/auth/app.db:ro
|
||||
flaresolverr:
|
||||
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||
|
||||
@@ -9,7 +9,6 @@ services:
|
||||
target: cwa-bd-tor
|
||||
environment:
|
||||
DEBUG: true
|
||||
APP_ENV: dev
|
||||
volumes:
|
||||
- /tmp/cwa-book-downloader:/tmp/cwa-book-downloader
|
||||
- /tmp/cwa-book-downloader-log:/var/log/cwa-book-downloader
|
||||
|
||||
@@ -8,7 +8,9 @@ services:
|
||||
USE_BOOK_TITLE: true
|
||||
TZ: America/New_York
|
||||
USING_TOR: true
|
||||
APP_ENV: prod
|
||||
# CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication
|
||||
# SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS
|
||||
# DEBUG: 'true' # Enable debug mode (debug button, verbose logging)
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
@@ -16,6 +18,9 @@ services:
|
||||
- 8084:8084
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# This is where the books will be downloaded to, usually it would be
|
||||
# the same as whatever you gave in "calibre-web-automated"
|
||||
# This is where the books will be downloaded to, usually it would be
|
||||
# the same as whatever you gave in "calibre-web-automated"
|
||||
- /tmp/data/calibre-web/ingest:/cwa-book-ingest
|
||||
# This is the location of CWA's app.db, which contains authentication
|
||||
# details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above)
|
||||
#- /cwa/config/path/app.db:/auth/app.db:ro
|
||||
|
||||
+5
-3
@@ -11,10 +11,12 @@ services:
|
||||
BOOK_LANGUAGE: en
|
||||
USE_BOOK_TITLE: true
|
||||
TZ: America/New_York
|
||||
APP_ENV: prod
|
||||
UID: 1000
|
||||
GID: 100
|
||||
# CWA_DB_PATH: /auth/app.db # Comment out to disable authentication
|
||||
# CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication (also uncomment volume below)
|
||||
# CALIBRE_WEB_URL: http://localhost:8080 # Uncomment and add your custom library URL to enable "Go To Library" button in the Web UI
|
||||
# SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS
|
||||
# DEBUG: 'true' # Enable debug mode (debug button, verbose logging)
|
||||
# Queue management settings
|
||||
MAX_CONCURRENT_DOWNLOADS: 3
|
||||
DOWNLOAD_PROGRESS_UPDATE_INTERVAL: 5
|
||||
@@ -26,5 +28,5 @@ services:
|
||||
# the same as whatever you gave in "calibre-web-automated"
|
||||
- /tmp/data/calibre-web/ingest:/cwa-book-ingest
|
||||
# This is the location of CWA's app.db, which contains authentication
|
||||
# details. Comment out to disable authentication
|
||||
# details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above)
|
||||
#- /cwa/config/path/app.db:/auth/app.db:ro
|
||||
|
||||
+6
-12
@@ -105,17 +105,11 @@ change_ownership /tmp/cwa-book-downloader
|
||||
# Test write to all folders
|
||||
make_writable /cwa-book-ingest
|
||||
|
||||
# Set the command to run based on the environment
|
||||
is_prod=$(echo "$APP_ENV" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$is_prod" = "prod" ]; then
|
||||
# Use geventwebsocket worker for SocketIO + WebSocket compatibility
|
||||
# This special worker class handles WebSocket upgrades properly
|
||||
# --workers 1: SocketIO requires sticky sessions, use 1 worker or configure sticky sessions
|
||||
# -t 300: 300 second timeout for long-running requests
|
||||
command="gunicorn --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} app:app"
|
||||
else
|
||||
command="python3 app.py"
|
||||
fi
|
||||
# Always run Gunicorn (even when DEBUG=true) to ensure Socket.IO WebSocket
|
||||
# upgrades work reliably on customer machines.
|
||||
# Map app LOG_LEVEL (often DEBUG/INFO/...) to gunicorn's --log-level (lowercase).
|
||||
gunicorn_loglevel=$([ "$DEBUG" = "true" ] && echo debug || echo "${LOG_LEVEL:-info}" | tr '[:upper:]' '[:lower:]')
|
||||
command="gunicorn --log-level ${gunicorn_loglevel} --access-logfile - --error-logfile - --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} app:app"
|
||||
|
||||
# If DEBUG and not using an external bypass
|
||||
if [ "$DEBUG" = "true" ] && [ "$USING_EXTERNAL_BYPASSER" != "true" ]; then
|
||||
@@ -179,7 +173,7 @@ sum=$(python3 -c "print(sum(int(l.strip()) for l in open('/tmp/test.cwa-bd').rea
|
||||
[ "$sum" == 11250075000 ] && echo "Success: /tmp is writable" || (echo "Failure: /tmp is not writable" && exit 1)
|
||||
rm /tmp/test.cwa-bd
|
||||
|
||||
echo "Running command: '$command' as '$USERNAME' in '$APP_ENV' mode"
|
||||
echo "Running command: '$command' as '$USERNAME' (debug=$is_debug)"
|
||||
|
||||
# Stop logging
|
||||
exec 1>&3 2>&4
|
||||
|
||||
@@ -5,11 +5,7 @@ def string_to_bool(s: str) -> bool:
|
||||
return s.lower() in ["true", "yes", "1", "y"]
|
||||
|
||||
# Authentication and session settings
|
||||
# SESSION_COOKIE_SECURE: Controls whether session cookies are marked as secure (HTTPS only)
|
||||
# - 'auto' (default): Uses False in dev, True in prod, can be overridden with environment variable
|
||||
# - 'true'/'yes'/'1': Always use secure cookies (recommended for production with HTTPS)
|
||||
# - 'false'/'no'/'0': Never use secure cookies (only for local HTTP)
|
||||
SESSION_COOKIE_SECURE_ENV = os.getenv("SESSION_COOKIE_SECURE", "auto")
|
||||
SESSION_COOKIE_SECURE_ENV = os.getenv("SESSION_COOKIE_SECURE", "false")
|
||||
|
||||
CWA_DB = os.getenv("CWA_DB_PATH")
|
||||
CWA_DB_PATH = Path(CWA_DB) if CWA_DB else None
|
||||
@@ -54,7 +50,6 @@ _CUSTOM_SCRIPT = os.getenv("CUSTOM_SCRIPT", "").strip()
|
||||
FLASK_HOST = os.getenv("FLASK_HOST", "0.0.0.0")
|
||||
FLASK_PORT = int(os.getenv("FLASK_PORT", "8084"))
|
||||
DEBUG = string_to_bool(os.getenv("DEBUG", "false"))
|
||||
APP_ENV = os.getenv("APP_ENV", "N/A").lower()
|
||||
PRIORITIZE_WELIB = string_to_bool(os.getenv("PRIORITIZE_WELIB", "false"))
|
||||
ALLOW_USE_WELIB = string_to_bool(os.getenv("ALLOW_USE_WELIB", "true"))
|
||||
|
||||
|
||||
@@ -63,10 +63,14 @@ An intuitive web interface for searching and requesting book downloads, designed
|
||||
| `CWA_DB_PATH` | Calibre-Web's database | None |
|
||||
| `ENABLE_LOGGING` | Enable log file | `true` |
|
||||
| `LOG_LEVEL` | Log level to use | `info` |
|
||||
| `SESSION_COOKIE_SECURE` | Secure cookie enforcement - Use for HTTPS connections only | `false` |
|
||||
| `CALIBRE_WEB_URL` | Custom WebUI library link | None |
|
||||
|
||||
If you wish to enable authentication, you must set `CWA_DB_PATH` to point to Calibre-Web's `app.db`, in order to match the username and password.
|
||||
|
||||
If logging is enabld, log folder default location is `/var/log/cwa-book-downloader`
|
||||
Set `CALIBRE_WEB_URL` to your Calibre-Web / Booklore base URL. A ‘Go to library’ button will appear in the Web UI for quick access while downloading, and it also provides library access when CWA-BD is installed as a mobile PWA.
|
||||
|
||||
If logging is enabled, log folder default location is `/var/log/cwa-book-downloader`
|
||||
Available log levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Higher levels show fewer messages.
|
||||
|
||||
Note that if using TOR, the TZ will be calculated automatically based on IP.
|
||||
|
||||
@@ -490,9 +490,9 @@ function App() {
|
||||
</main>
|
||||
|
||||
<Footer
|
||||
buildVersion={config?.build_version || 'dev'}
|
||||
releaseVersion={config?.release_version || 'dev'}
|
||||
appEnv={config?.app_env || 'development'}
|
||||
buildVersion={config?.build_version}
|
||||
releaseVersion={config?.release_version}
|
||||
debug={config?.debug}
|
||||
/>
|
||||
<ToastContainer toasts={toasts} />
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
interface FooterProps {
|
||||
buildVersion?: string;
|
||||
releaseVersion?: string;
|
||||
appEnv?: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export const Footer = ({ buildVersion, releaseVersion, appEnv }: FooterProps) => {
|
||||
export const Footer = ({ buildVersion, releaseVersion, debug }: FooterProps) => {
|
||||
// Determine version display - show "dev" if no version is set
|
||||
const versionDisplay = releaseVersion && releaseVersion !== 'N/A'
|
||||
? releaseVersion
|
||||
: 'dev';
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="mt-8 border-t py-6"
|
||||
@@ -17,8 +22,9 @@ export const Footer = ({ buildVersion, releaseVersion, appEnv }: FooterProps) =>
|
||||
<div className="min-w-0 text-left">
|
||||
<p className="text-sm opacity-80">Calibre Web Book Downloader</p>
|
||||
<p className="text-xs opacity-60 mt-1">
|
||||
Build: {buildVersion || 'dev'} • Release: {releaseVersion || 'dev'} • Env:{' '}
|
||||
{appEnv || 'development'}
|
||||
Version: {versionDisplay}
|
||||
{buildVersion && buildVersion !== 'N/A' && ` (${buildVersion})`}
|
||||
{debug && ' • Debug Mode'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
|
||||
@@ -71,7 +71,6 @@ export interface Toast {
|
||||
export interface AppConfig {
|
||||
calibre_web_url: string;
|
||||
debug: boolean;
|
||||
app_env: string;
|
||||
build_version: string;
|
||||
release_version: string;
|
||||
book_languages: Language[];
|
||||
|
||||
Reference in New Issue
Block a user