Files
minmal-file-cloud-email-pim…/backend/app/__init__.py
T
Stefan Hacker 3f0d823dbf fix: CalDAV fuer DAVx5 - Well-Known intern dispatchen, mehr Properties
Aenderungen fuer besseren DAVx5-Support:

* /.well-known/caldav reagiert jetzt direkt auf PROPFIND/OPTIONS
  ohne Redirect-Zickerei. GET/HEAD redirecten weiterhin auf /dav/
  als visuelle Fallback.
* strict_slashes app-weit aus: /dav und /dav/ sind gleichwertig,
  ebenso die Unterpfade. DAVx5 nutzt beides gemischt.
* Jede DAV-Response traegt jetzt den DAV-Header (1, 2, 3,
  calendar-access), nicht nur OPTIONS.
* Kalender-Response enthaelt jetzt supported-report-set mit
  calendar-query + calendar-multiget (DAVx5 prueft das).
* current-user-privilege-set wird mit konkreten Privilegien gefuellt
  (read, write, write-properties, write-content, bind, unbind)
  statt leer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:50:50 +02:00

152 lines
5.7 KiB
Python

import os
from pathlib import Path
from flask import Flask, redirect, send_from_directory
from flask_cors import CORS
from app.config import Config
from app.extensions import db, bcrypt, migrate
def _auto_migrate(db):
"""Add missing columns to existing tables by comparing model definitions
with actual database schema. This handles the case where new columns are
added to models but db.create_all() only creates new tables."""
import sqlalchemy
with db.engine.connect() as conn:
inspector = sqlalchemy.inspect(db.engine)
for table_name, table in db.metadata.tables.items():
if not inspector.has_table(table_name):
continue
existing_cols = {col['name'] for col in inspector.get_columns(table_name)}
for column in table.columns:
if column.name not in existing_cols:
# Determine SQL type
col_type = column.type.compile(db.engine.dialect)
# Determine default value
default = ''
if column.default is not None:
if hasattr(column.default, 'arg'):
val = column.default.arg
if callable(val):
default = ''
elif isinstance(val, str):
default = f" DEFAULT '{val}'"
elif isinstance(val, bool):
default = f" DEFAULT {1 if val else 0}"
elif isinstance(val, (int, float)):
default = f" DEFAULT {val}"
elif column.nullable:
default = ' DEFAULT NULL'
elif col_type.upper().startswith(('VARCHAR', 'TEXT')):
default = " DEFAULT ''"
elif col_type.upper().startswith(('INTEGER', 'BIGINT', 'FLOAT')):
default = " DEFAULT 0"
elif col_type.upper() == 'BOOLEAN':
default = " DEFAULT 0"
sql = f'ALTER TABLE "{table_name}" ADD COLUMN "{column.name}" {col_type}{default}'
try:
conn.execute(db.text(sql))
print(f'[Auto-Migrate] Added column {table_name}.{column.name} ({col_type})')
except Exception as e:
print(f'[Auto-Migrate] Failed to add {table_name}.{column.name}: {e}')
conn.commit()
def create_app(config_class=Config):
# Check if static frontend build exists (Docker production mode)
static_dir = Path(__file__).resolve().parent.parent / 'static'
if static_dir.exists():
app = Flask(__name__, static_folder=str(static_dir), static_url_path='')
else:
app = Flask(__name__)
app.config.from_object(config_class)
# DAV-Clients setzen Trailing-Slashes uneinheitlich - daher deaktivieren
# wir die strikte Pruefung app-weit. Betrifft alle Blueprints.
app.url_map.strict_slashes = False
# Ensure data directories exist
Path(app.config['UPLOAD_PATH']).mkdir(parents=True, exist_ok=True)
db_dir = Path(app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '')).parent
db_dir.mkdir(parents=True, exist_ok=True)
# Initialize extensions
db.init_app(app)
bcrypt.init_app(app)
migrate.init_app(app, db)
# CORS
CORS(app, resources={r'/api/*': {'origins': app.config['FRONTEND_URL']}},
supports_credentials=True)
# Register blueprints
from app.api import api_bp
app.register_blueprint(api_bp)
from app.dav import dav_bp
app.register_blueprint(dav_bp)
# Well-known URLs for CalDAV/CardDAV auto-discovery (iOS, DAVx5, etc.).
# 301-Redirect bei PROPFIND ist bei einigen Clients zickig, deshalb
# delegieren wir intern direkt an die DAV-Handler, statt zu redirecten.
from app.dav.caldav import propfind as dav_propfind, options as dav_options
@app.route('/.well-known/caldav', methods=['GET', 'HEAD'])
@app.route('/.well-known/carddav', methods=['GET', 'HEAD'])
def wellknown_redirect():
return redirect('/dav/', code=301)
@app.route('/.well-known/caldav', methods=['PROPFIND'])
@app.route('/.well-known/carddav', methods=['PROPFIND'])
def wellknown_propfind():
return dav_propfind(subpath='')
@app.route('/.well-known/caldav', methods=['OPTIONS'])
@app.route('/.well-known/carddav', methods=['OPTIONS'])
def wellknown_options():
return dav_options()
# iCal export (public, no auth)
@app.route('/ical/<token>')
def ical_export_route(token):
from app.api.calendar import ical_export as _ical_export
return _ical_export(token)
# Serve frontend SPA for all non-API routes (production/Docker)
if static_dir.exists():
@app.route('/')
def serve_index():
return send_from_directory(str(static_dir), 'index.html')
@app.errorhandler(404)
def serve_spa(e):
return send_from_directory(str(static_dir), 'index.html')
# Create tables + auto-migrate missing columns
with app.app_context():
from app import models # noqa: F401
db.create_all()
# Auto-migrate: add missing columns to existing tables
_auto_migrate(db)
# Enable WAL mode for SQLite
with db.engine.connect() as conn:
conn.execute(db.text('PRAGMA journal_mode=WAL'))
conn.commit()
# Start backup scheduler (only in main process, not reloader)
if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
from app.services.backup_scheduler import start_backup_scheduler
start_backup_scheduler(app)
return app