Make rpid.paskiadb a folder containing the database and the files in one. Migrates existing old format rpid.paskiadb file to main.db.

This commit is contained in:
2026-05-22 01:22:35 +00:00
parent cc938dd306
commit d31c09084e
9 changed files with 55 additions and 26 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ paskia [options]
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
| --save | Save current options to database | (only --rp-id required on further invocations) |
To clear a stored setting, pass an empty value like `--auth-host=`. The database is stored in `{rp-id}.paskiadb` in current directory. This can be overridden by environment `PASKIA_DB` if needed.
To clear a stored setting, pass an empty value like `--auth-host=`. The database is stored in `{rp-id}.paskiadb` folder in current directory. This can be overridden by environment `PASKIA_DB` if needed.
## Tutorial: From Local Testing to Production
+3 -2
View File
@@ -9,6 +9,7 @@ from fastapi_vue.hostutil import parse_endpoints
from paskia._version import __version__
from paskia.db.jsonl import load_readonly
from paskia.db.paths import db_file_path
from paskia.util import startupbox
from paskia.util.hostutil import (
normalize_auth_host_and_origins,
@@ -75,9 +76,9 @@ def main():
args = parser.parse_args()
# Load stored config (read-only, no writes, no global state)
db_path = os.environ.get("PASKIA_DB", f"{args.rp_id}.paskiadb")
db_path = db_file_path(rp_id=args.rp_id, create_root=True)
try:
config = load_readonly(db_path, rp_id=args.rp_id).config
config = load_readonly(str(db_path), rp_id=args.rp_id).config
except SystemExit as e:
print(f"🛑 Paskia {__version__} could not load")
sys.exit(str(e))
+4 -4
View File
@@ -3,13 +3,13 @@ Database lifecycle: initialization and maintenance.
"""
import logging
import os
from datetime import UTC, datetime
import paskia.db.operations as _ops
from paskia import oidc_notify
from paskia.authsession import EXPIRES
from paskia.db.jsonl import JsonlStore
from paskia.db.paths import db_file_path
_logger = logging.getLogger(__name__)
@@ -19,9 +19,9 @@ async def init(rp_id: str, *args, **kwargs):
if _ops._db._store:
_logger.debug("Database already initialized, skipping reload")
return
db_path = os.environ.get("PASKIA_DB", f"{rp_id}.paskiadb")
store = JsonlStore(_ops._db, db_path)
await store.load(db_path, rp_id=rp_id)
db_path = db_file_path(rp_id=rp_id, create_root=True)
store = JsonlStore(_ops._db, str(db_path))
await store.load(str(db_path), rp_id=rp_id)
_ops._db = store.db
_ops._db._store = store
# Request a snapshot after successful startup
+1 -3
View File
@@ -460,9 +460,7 @@ def update_session(
s.validated = validated
def set_session_host(
key: str, host: str, *, ctx: SessionContext | None = None
) -> None:
def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None:
"""Set the host for a session (first-time binding)."""
update_session(key, host=host, ctx=ctx)
+1 -1
View File
@@ -21,7 +21,6 @@ from paskia.fastapi import authz, session, user
from paskia.fastapi.response import MsgspecResponse
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
from paskia.globals import passkey as global_passkey
from paskia.util.crypto import hash_secret
from paskia.util import hostutil, htmlutil, passphrase, permutil, userinfo
from paskia.util.apistructs import (
ApiCheckUserResponse,
@@ -33,6 +32,7 @@ from paskia.util.apistructs import (
ApiUserContext,
ApiValidateResponse,
)
from paskia.util.crypto import hash_secret
bearer_auth = HTTPBearer(auto_error=False)
+3 -1
View File
@@ -134,7 +134,9 @@ def format_access_log(
# Format: "IP STATUS METHOD host path [extra] TIMING"
extra_str = f" {_TIMING}{extra}{_RESET}" if extra else ""
return f"{ip} {status_str} {method_str} {host_str}{path_str}{extra_str} {timing_str}"
return (
f"{ip} {status_str} {method_str} {host_str}{path_str}{extra_str} {timing_str}"
)
# WebSocket connection counter (mod 100)
+8 -2
View File
@@ -109,11 +109,17 @@ async def authenticate_and_login(
cred, new_sign_count = await authenticate_chat(ws, credential_ids)
# Use overrides if provided, otherwise use websocket metadata
login_host = hostutil.normalize_host(session_host) if session_host is not None else normalized_host
login_host = (
hostutil.normalize_host(session_host)
if session_host is not None
else normalized_host
)
if not login_host:
raise ValueError("Host required for session creation")
login_ip = session_ip if session_ip is not None else metadata["ip"]
login_user_agent = session_user_agent if session_user_agent is not None else metadata["user_agent"]
login_user_agent = (
session_user_agent if session_user_agent is not None else metadata["user_agent"]
)
# Create session and update user/credential
secret = db.login(
+3 -6
View File
@@ -4,12 +4,12 @@ from __future__ import annotations
import contextlib
import hashlib
import os
from pathlib import Path
from uuid import UUID
from fastapi import HTTPException, UploadFile
from paskia.db.paths import users_root_path
from paskia.util import hostutil
MAX_UPLOAD_BYTES = 10 * 1024 * 1024
@@ -17,15 +17,12 @@ MAX_UPLOAD_BYTES = 10 * 1024 * 1024
def media_root() -> Path:
"""Return the filesystem root for auxiliary media files."""
db_path = Path(os.environ.get("PASKIA_DB", "localhost.paskiadb")).resolve()
db_name = db_path.name
hostname = db_name.removesuffix(".paskiadb") or db_path.stem
return db_path.parent / f"{hostname}.data"
return users_root_path(create_root=True)
def avatars_root() -> Path:
"""Return the filesystem root for stored avatar images."""
return media_root() / "user"
return media_root()
def avatar_path(user_uuid: UUID) -> Path:
+31 -6
View File
@@ -15,6 +15,7 @@ from urllib.parse import urlsplit
import httpx
import pytest
from paskia.db.paths import db_file_path, users_root_path
from tests.conftest import auth_headers, create_test_image_bytes
@@ -178,6 +179,13 @@ class TestUserAvatar:
)
assert response.status_code == 200
info = await client.get(
"/auth/api/user-info",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert info.status_code == 200
assert info.json()["user"].get("avatar_url") is None
@pytest.mark.asyncio
async def test_regular_user_cannot_upload_another_users_avatar(
self,
@@ -194,12 +202,29 @@ class TestUserAvatar:
)
assert response.status_code == 403
info = await client.get(
"/auth/api/user-info",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert info.status_code == 200
assert info.json()["user"].get("avatar_url") is None
def test_paskia_db_legacy_file_is_migrated_to_root_dir(tmp_path, monkeypatch):
legacy_path = tmp_path / "legacy.paskiadb"
legacy_bytes = b'{"v":0}\n'
legacy_path.write_bytes(legacy_bytes)
monkeypatch.setenv("PASKIA_DB", str(legacy_path))
db_path = db_file_path(create_root=True)
assert legacy_path.is_dir()
assert db_path == legacy_path / "main.db"
assert db_path.read_bytes() == legacy_bytes
def test_paskia_db_root_uses_users_directory(tmp_path, monkeypatch):
root_path = tmp_path / "instance-root"
monkeypatch.setenv("PASKIA_DB", str(root_path))
users_path = users_root_path(create_root=True)
assert users_path == root_path / "users"
assert users_path.parent == root_path
class TestUserLogoutAll: