Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,131 @@ All notable changes to RustChan will be documented in this file.

## [1.0.13] — 2026-03-08

## WAL Mode + Connection Tuning
**`db/mod.rs`**

`cache_size` bumped from `-4096` (4 MiB) to `-32000` (32 MiB) in the pool's `with_init` pragma block. The `journal_mode=WAL` and `synchronous=NORMAL` pragmas were already present.

---

## Missing Indexes
**`db/mod.rs`**

Two new migrations added at the end of the migration table:

- **Migration 23:** `CREATE INDEX IF NOT EXISTS idx_posts_thread_id ON posts(thread_id)` — supplements the existing composite index for queries that filter on `thread_id` alone.
- **Migration 24:** `CREATE INDEX IF NOT EXISTS idx_posts_ip_hash ON posts(ip_hash)` — eliminates the full-table scan on the admin IP history page and per-IP cooldown checks.

---

## Prepared Statement Caching Audit
**`db/threads.rs` · `db/boards.rs` · `db/posts.rs`**

All remaining bare `conn.prepare(...)` calls on hot or repeated queries replaced with `conn.prepare_cached(...)`: `delete_thread`, `archive_old_threads`, `prune_old_threads` (outer `SELECT`) in `threads.rs`; `delete_board` in `boards.rs`; `search_posts` in `posts.rs`. Every query path is now consistently cached.

---

## Transaction Batching for Thread Prune
Already implemented in the codebase. Both `prune_old_threads` and `archive_old_threads` already use `unchecked_transaction()` / `tx.commit()` to batch all deletes/updates into a single atomic transaction. No changes needed.

---

## RETURNING Clause for Inserts
**`db/threads.rs` · `db/posts.rs`**

`create_thread_with_op` and `create_post_inner` now use `INSERT … RETURNING id` via `query_row`, replacing the `execute()` + `last_insert_rowid()` pattern. The new ID is returned atomically in the same statement, eliminating the implicit coupling to connection-local state.

---

## Scheduled VACUUM
**`config.rs` · `main.rs`**

Added `auto_vacuum_interval_hours = 24` to config. A background Tokio task now sleeps for the configured interval (staggered from startup), then calls `db::run_vacuum()` via `spawn_blocking` and logs the bytes reclaimed.

---

## Expired Poll Cleanup
**`config.rs` · `main.rs` · `db/posts.rs`**

Added `poll_cleanup_interval_hours = 72`. A new `cleanup_expired_poll_votes()` DB function deletes vote rows for polls whose `expires_at` is older than the retention window. A background task runs it on the configured interval, preserving poll questions and options.

---

## DB Size Warning
**`config.rs` · `handlers/admin.rs` · `templates/admin.rs`**

Added `db_warn_threshold_mb = 2048`. The admin panel handler reads the actual file size via `std::fs::metadata`, computes a boolean flag, and passes it to the template. The template renders a red warning banner in the database maintenance section when the threshold is exceeded.

---

## Job Queue Back-Pressure
**`config.rs` · `workers/mod.rs`**

Added `job_queue_capacity = 1000`. The `enqueue()` method now checks `pending_job_count()` before inserting — if the queue is at or over capacity, the job is dropped with a `warn!` log and a sentinel `-1` is returned, avoiding OOM under post floods.

---

## Coalesce Duplicate Media Jobs
**`workers/mod.rs`**

Added an `Arc<DashMap<String, bool>>` (`in_progress`) to `JobQueue`. Before dispatching a `VideoTranscode` or `AudioWaveform` job, `handle_job` checks if the `file_path` is already in the map — if so it skips and logs. The entry is removed on both success and failure.

---

## FFmpeg Timeout
**`config.rs` · `workers/mod.rs`**

Replaced hardcoded `FFMPEG_TRANSCODE_TIMEOUT` / `FFMPEG_WAVEFORM_TIMEOUT` constants with `CONFIG.ffmpeg_timeout_secs` (default: `120`). Both `transcode_video` and `generate_waveform` now read this value at runtime so operators can tune it in `settings.toml`.

---

## Auto-Archive Before Prune
**`workers/mod.rs` · `config.rs`**

`prune_threads` now evaluates `allow_archive || CONFIG.archive_before_prune`. The new global flag (default `true`) means no thread is ever silently hard-deleted on a board that has archiving enabled at the global level, even if the individual board didn't opt in.

---

## Waveform Cache Eviction
**`main.rs` · `config.rs`**

A background task runs every hour (after a 30-min startup stagger). It walks every `{board}/thumbs/` directory, sorts files oldest-first by mtime, and deletes until total size is under `waveform_cache_max_mb` (default 200 MiB). A new `evict_thumb_cache` function handles the scan-and-prune logic; originals are never touched.

---

## Streaming Multipart
**`handlers/mod.rs`**

The old `.bytes().await` (full in-memory buffering) is replaced by `read_field_bytes`, which streams via `.chunk()` and returns a `413 UploadTooLarge` the moment the running total exceeds the configured limit — before memory is exhausted.

---

## ETag / Conditional GET
**`handlers/board.rs` · `handlers/thread.rs`**

Both handlers now accept `HeaderMap`, derive an ETag (board index: `"{max_bump_ts}-{page}"`; thread: `"{bumped_at}"`), check `If-None-Match`, and return `304 Not Modified` on a hit. The ETag is included on all 200 responses too.

---

## Gzip / Brotli Compression
**`main.rs` · `Cargo.toml`**

`tower-http` features updated to `compression-full`. `CompressionLayer::new()` added to the middleware stack — it negotiates gzip, Brotli, or zstd based on the client's `Accept-Encoding` header.

---

## Blocking Pool Sizing
**`main.rs` · `config.rs`**

`#[tokio::main]` replaced with a manual `tokio::runtime::Builder` that calls `.max_blocking_threads(CONFIG.blocking_threads)`. Default is `logical_cpus × 4` (auto-detected); configurable via `blocking_threads` in `settings.toml` or `CHAN_BLOCKING_THREADS`.

---

## EXIF Orientation Correction
**`utils/files.rs` · `Cargo.toml`**

`kamadak-exif = "0.5"` added. `generate_image_thumb` now calls `read_exif_orientation` for JPEGs and passes the result to `apply_exif_orientation`, which dispatches to `imageops::rotate90/180/270` and `flip_horizontal/vertical` as needed. Non-JPEG formats skip the EXIF path entirely.

### ✨ Added
- **Backup system rewritten to stream instead of buffering in RAM** — all backup operations previously loaded entire zip files into memory, risking OOM on large instances. Downloads now stream from disk in 64 KiB chunks (browsers also get a proper progress bar). Backup creation now writes directly to disk via temp files with atomic rename on success, so partial backups never appear in the saved list. Individual file archiving now streams through an 8 KiB buffer instead of reading each file fully into memory. Peak RAM usage dropped from "entire backup size" to roughly 64 KiB regardless of instance size.
- **ChanClassic theme** — a new theme that mimics the classic 4chan aesthetic: light tan/beige background, maroon/red accents, blue post-number links, and the iconic post block styling. Available in the theme picker alongside existing themes.
Expand Down
126 changes: 126 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async-trait = "0.1"
axum = { version = "0.8", features = ["multipart"] }
axum-extra = { version = "0.12", features = ["cookie"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["fs", "set-header"] }
tower-http = { version = "0.6", features = ["fs", "set-header", "compression-full"] }
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"

Expand All @@ -54,13 +54,19 @@ rand_core = { version = "0.6", features = ["getrandom"] }

image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }

# EXIF orientation correction: read Orientation tag from JPEG uploads and
# apply the corresponding rotation before thumbnailing so that photos taken
# on phones display upright (4.1).
kamadak-exif = "0.5"

clap = { version = "4", features = ["derive"] }

uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
dashmap = "6"
anyhow = "1"
once_cell = "1"
dashmap = "6"
parking_lot = "0.12"
anyhow = "1"
once_cell = "1"
regex = "1"

# zip 8: SimpleFileOptions and core ZipWriter/ZipArchive API unchanged.
Expand All @@ -84,3 +90,9 @@ unwrap_used = "warn" # force .expect("reason") or proper ha
expect_used = "allow" # allow in tests; use with a message in production
indexing_slicing = "warn" # prefer .get() with bounds check
arithmetic_side_effects = "allow" # too noisy for general arithmetic
missing_errors_doc = "allow" # pedantic docs lint, too noisy for all Results
doc_markdown = "allow" # allow non-backticked identifiers in docs
doc_lazy_continuation = "allow" # don't require strict indentation for doc lists
items_after_statements = "allow" # permit helper items inside functions
wildcard_imports = "allow" # crate-level style choice for imports
uninlined_format_args = "allow" # allow older-style format! calls
Loading
Loading