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 | | --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) | | --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 ## 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._version import __version__
from paskia.db.jsonl import load_readonly from paskia.db.jsonl import load_readonly
from paskia.db.paths import db_file_path
from paskia.util import startupbox from paskia.util import startupbox
from paskia.util.hostutil import ( from paskia.util.hostutil import (
normalize_auth_host_and_origins, normalize_auth_host_and_origins,
@@ -75,9 +76,9 @@ def main():
args = parser.parse_args() args = parser.parse_args()
# Load stored config (read-only, no writes, no global state) # 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: 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: except SystemExit as e:
print(f"🛑 Paskia {__version__} could not load") print(f"🛑 Paskia {__version__} could not load")
sys.exit(str(e)) sys.exit(str(e))
+4 -4
View File
@@ -3,13 +3,13 @@ Database lifecycle: initialization and maintenance.
""" """
import logging import logging
import os
from datetime import UTC, datetime from datetime import UTC, datetime
import paskia.db.operations as _ops import paskia.db.operations as _ops
from paskia import oidc_notify from paskia import oidc_notify
from paskia.authsession import EXPIRES from paskia.authsession import EXPIRES
from paskia.db.jsonl import JsonlStore from paskia.db.jsonl import JsonlStore
from paskia.db.paths import db_file_path
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -19,9 +19,9 @@ async def init(rp_id: str, *args, **kwargs):
if _ops._db._store: if _ops._db._store:
_logger.debug("Database already initialized, skipping reload") _logger.debug("Database already initialized, skipping reload")
return return
db_path = os.environ.get("PASKIA_DB", f"{rp_id}.paskiadb") db_path = db_file_path(rp_id=rp_id, create_root=True)
store = JsonlStore(_ops._db, db_path) store = JsonlStore(_ops._db, str(db_path))
await store.load(db_path, rp_id=rp_id) await store.load(str(db_path), rp_id=rp_id)
_ops._db = store.db _ops._db = store.db
_ops._db._store = store _ops._db._store = store
# Request a snapshot after successful startup # Request a snapshot after successful startup
+1 -3
View File
@@ -460,9 +460,7 @@ def update_session(
s.validated = validated s.validated = validated
def set_session_host( def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None:
key: str, host: str, *, ctx: SessionContext | None = None
) -> None:
"""Set the host for a session (first-time binding).""" """Set the host for a session (first-time binding)."""
update_session(key, host=host, ctx=ctx) 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.response import MsgspecResponse
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
from paskia.globals import passkey as global_passkey 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 import hostutil, htmlutil, passphrase, permutil, userinfo
from paskia.util.apistructs import ( from paskia.util.apistructs import (
ApiCheckUserResponse, ApiCheckUserResponse,
@@ -33,6 +32,7 @@ from paskia.util.apistructs import (
ApiUserContext, ApiUserContext,
ApiValidateResponse, ApiValidateResponse,
) )
from paskia.util.crypto import hash_secret
bearer_auth = HTTPBearer(auto_error=False) 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" # Format: "IP STATUS METHOD host path [extra] TIMING"
extra_str = f" {_TIMING}{extra}{_RESET}" if extra else "" 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) # 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) cred, new_sign_count = await authenticate_chat(ws, credential_ids)
# Use overrides if provided, otherwise use websocket metadata # 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: if not login_host:
raise ValueError("Host required for session creation") raise ValueError("Host required for session creation")
login_ip = session_ip if session_ip is not None else metadata["ip"] 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 # Create session and update user/credential
secret = db.login( secret = db.login(
+3 -6
View File
@@ -4,12 +4,12 @@ from __future__ import annotations
import contextlib import contextlib
import hashlib import hashlib
import os
from pathlib import Path from pathlib import Path
from uuid import UUID from uuid import UUID
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from paskia.db.paths import users_root_path
from paskia.util import hostutil from paskia.util import hostutil
MAX_UPLOAD_BYTES = 10 * 1024 * 1024 MAX_UPLOAD_BYTES = 10 * 1024 * 1024
@@ -17,15 +17,12 @@ MAX_UPLOAD_BYTES = 10 * 1024 * 1024
def media_root() -> Path: def media_root() -> Path:
"""Return the filesystem root for auxiliary media files.""" """Return the filesystem root for auxiliary media files."""
db_path = Path(os.environ.get("PASKIA_DB", "localhost.paskiadb")).resolve() return users_root_path(create_root=True)
db_name = db_path.name
hostname = db_name.removesuffix(".paskiadb") or db_path.stem
return db_path.parent / f"{hostname}.data"
def avatars_root() -> Path: def avatars_root() -> Path:
"""Return the filesystem root for stored avatar images.""" """Return the filesystem root for stored avatar images."""
return media_root() / "user" return media_root()
def avatar_path(user_uuid: UUID) -> Path: def avatar_path(user_uuid: UUID) -> Path:
+31 -6
View File
@@ -15,6 +15,7 @@ from urllib.parse import urlsplit
import httpx import httpx
import pytest import pytest
from paskia.db.paths import db_file_path, users_root_path
from tests.conftest import auth_headers, create_test_image_bytes from tests.conftest import auth_headers, create_test_image_bytes
@@ -178,6 +179,13 @@ class TestUserAvatar:
) )
assert response.status_code == 200 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 @pytest.mark.asyncio
async def test_regular_user_cannot_upload_another_users_avatar( async def test_regular_user_cannot_upload_another_users_avatar(
self, self,
@@ -194,12 +202,29 @@ class TestUserAvatar:
) )
assert response.status_code == 403 assert response.status_code == 403
info = await client.get(
"/auth/api/user-info", def test_paskia_db_legacy_file_is_migrated_to_root_dir(tmp_path, monkeypatch):
headers={**auth_headers(session_token), "Host": "localhost:4401"}, legacy_path = tmp_path / "legacy.paskiadb"
) legacy_bytes = b'{"v":0}\n'
assert info.status_code == 200 legacy_path.write_bytes(legacy_bytes)
assert info.json()["user"].get("avatar_url") is None
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: class TestUserLogoutAll: