Files
trackpull/docs/database.md

105 lines
3.1 KiB
Markdown

# Database
## Overview
Trackpull uses a single SQLite database at `/config/trackpull.db`. All access goes through `db.py`, which uses thread-local connections to stay safe under multi-threaded Gunicorn workers.
---
## Schema
### `users`
| Column | Type | Notes |
|--------|------|-------|
| `id` | TEXT PK | UUID |
| `username` | TEXT UNIQUE | Login name |
| `password_hash` | TEXT | werkzeug PBKDF2 hash |
| `role` | TEXT | `admin` or `user` |
| `created_at` | TEXT | ISO datetime |
| `last_login` | TEXT | ISO datetime, nullable |
### `jobs`
| Column | Type | Notes |
|--------|------|-------|
| `id` | TEXT PK | UUID |
| `user_id` | TEXT FK → users | Cascading delete |
| `urls` | TEXT | JSON array of download URLs |
| `options` | TEXT | JSON object of download parameters |
| `status` | TEXT | `queued`, `running`, `completed`, `failed`, `cancelled` |
| `output` | TEXT | JSON array of log lines (max 500) |
| `command` | TEXT | Full CLI command string (Votify jobs) |
| `return_code` | INTEGER | Process exit code |
| `created_at` | TEXT | ISO datetime |
| `updated_at` | TEXT | ISO datetime |
### `app_settings`
| Column | Type | Notes |
|--------|------|-------|
| `key` | TEXT PK | Setting name |
| `value` | TEXT | Setting value (always a string) |
Current settings keys: `fallback_quality`, `job_expiry_days`.
---
## Threading Model
`db.py` creates a new connection per thread using `threading.local()`. Each call opens a connection, runs the query, and closes it. This avoids SQLite's "objects created in a thread can only be used in that same thread" limitation.
Foreign keys are enabled on every connection via `PRAGMA foreign_keys = ON`.
---
## Key Functions
### Users
| Function | Purpose |
|----------|---------|
| `create_user(username, password, role)` | Hashes password and inserts row |
| `get_user_by_username(username)` | Lookup for login |
| `get_user_by_id(user_id)` | Lookup for session validation |
| `list_users()` | Admin user list |
| `delete_user(user_id)` | Cascades to jobs |
| `verify_password(username, password)` | Returns user row or None |
| `update_user_password(user_id, new_password)` | Re-hashes and saves |
| `update_last_login(user_id)` | Stamps `last_login` |
### Jobs
| Function | Purpose |
|----------|---------|
| `upsert_job(job_dict)` | Insert or replace job record |
| `get_job(job_id)` | Fetch single job |
| `list_jobs_for_user(user_id)` | All jobs for a user |
| `delete_job(job_id)` | Remove single job |
| `delete_jobs_older_than(days)` | Expiry cleanup |
### Settings
| Function | Purpose |
|----------|---------|
| `get_setting(key, default)` | Fetch value with fallback |
| `set_setting(key, value)` | Upsert a setting |
| `get_all_settings()` | Return all as dict |
---
## Initialization
`db.py` calls `init_db()` on import, which:
1. Creates all tables if they don't exist.
2. Seeds the first admin user from `ADMIN_USERNAME` / `ADMIN_PASSWORD` env vars if the `users` table is empty.
---
## Key Files
| File | Relevance |
|------|-----------|
| [db.py](../db.py) | Entire database layer |
| [app.py](../app.py) | Calls db functions for all CRUD operations |