slack status without the slack status.zzstoatzz.io/
quickslice

style: rustfmt after emoji runtime refactor

+4 -1
.env.template
··· 7 7 # Dev Mode Configuration 8 8 DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled. 9 9 10 - 10 + # Custom Emojis 11 + # Directory to read/write custom emoji image files at runtime. 12 + # For local dev, keep under the repo: 13 + EMOJI_DIR="static/emojis"
+28 -1
README.md
··· 25 25 # navigate to http://127.0.0.1:8080 26 26 ``` 27 27 28 + ### custom emojis (no redeploys) 29 + 30 + Emojis are now served from a runtime directory configured by `EMOJI_DIR` (defaults to `static/emojis` locally; set to `/data/emojis` on Fly.io). On startup, if the runtime emoji directory is empty, it will be seeded from the bundled `static/emojis`. 31 + 32 + - Local dev: add image files to `static/emojis/` (or set `EMOJI_DIR` in `.env`). 33 + - Production (Fly.io): upload files directly into the mounted volume at `/data/emojis` — no rebuild or redeploy needed. 34 + 35 + Examples with Fly CLI: 36 + 37 + ```bash 38 + # Open an SSH console to the machine 39 + fly ssh console -a zzstoatzz-status 40 + 41 + # Inside the VM, copy or fetch files into /data/emojis 42 + mkdir -p /data/emojis 43 + curl -L -o /data/emojis/my_new_emoji.png https://example.com/my_new_emoji.png 44 + ``` 45 + 46 + Or from your machine using SFTP: 47 + 48 + ```bash 49 + fly ssh sftp -a zzstoatzz-status 50 + sftp> put ./static/emojis/my_new_emoji.png /data/emojis/ 51 + ``` 52 + 53 + The app serves them at `/emojis/<filename>` and lists them via `/api/custom-emojis`. 54 + 28 55 ### available commands 29 56 30 57 we use [just](https://github.com/casey/just) for common tasks: ··· 43 70 - [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium)) 44 71 - [sqlite](https://www.sqlite.org/) for local storage 45 72 - [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption 46 - - [fly.io](https://fly.io/) for hosting 73 + - [fly.io](https://fly.io/) for hosting
+2 -1
fly.toml
··· 10 10 DATABASE_URL = "sqlite:///data/status.db" 11 11 ENABLE_FIREHOSE = "true" 12 12 OAUTH_REDIRECT_BASE = "https://status.zzstoatzz.io" 13 + EMOJI_DIR = "/data/emojis" 13 14 14 15 [http_service] 15 16 internal_port = 8080 ··· 30 31 [[vm]] 31 32 cpu_kind = "shared" 32 33 cpus = 1 33 - memory_mb = 512 34 + memory_mb = 512
+4 -3
src/api/status.rs
··· 1 + use crate::config::Config; 1 2 use crate::resolver::HickoryDnsTxtResolver; 2 3 use crate::{ 3 4 api::auth::OAuthClientType, ··· 729 730 730 731 /// Get all custom emojis available on the site 731 732 #[get("/api/custom-emojis")] 732 - pub async fn get_custom_emojis() -> Result<impl Responder> { 733 + pub async fn get_custom_emojis(app_config: web::Data<Config>) -> Result<impl Responder> { 733 734 use std::fs; 734 735 735 736 #[derive(Serialize)] ··· 738 739 filename: String, 739 740 } 740 741 741 - let emojis_dir = "static/emojis"; 742 + let emojis_dir = app_config.emoji_dir.clone(); 742 743 let mut emojis = Vec::new(); 743 744 744 - if let Ok(entries) = fs::read_dir(emojis_dir) { 745 + if let Ok(entries) = fs::read_dir(&emojis_dir) { 745 746 for entry in entries.flatten() { 746 747 if let Some(filename) = entry.file_name().to_str() { 747 748 // Only include image files
+5
src/config.rs
··· 31 31 32 32 /// Dev mode for testing with dummy data 33 33 pub dev_mode: bool, 34 + 35 + /// Directory to serve and manage custom emojis from 36 + pub emoji_dir: String, 34 37 } 35 38 36 39 impl Config { ··· 60 63 .unwrap_or_else(|_| "false".to_string()) 61 64 .parse() 62 65 .unwrap_or(false), 66 + // Default to static/emojis for local dev; override in prod to /data/emojis 67 + emoji_dir: env::var("EMOJI_DIR").unwrap_or_else(|_| "static/emojis".to_string()), 63 68 }) 64 69 } 65 70 }
+58
src/emoji.rs
··· 1 + use std::{fs, path::Path}; 2 + 3 + use crate::config::Config; 4 + 5 + /// Ensure the runtime emoji directory exists, and seed it from the bundled 6 + /// `static/emojis` on first run if the runtime directory is empty. 7 + pub fn init_runtime_dir(config: &Config) { 8 + let runtime_emoji_dir = &config.emoji_dir; 9 + let bundled_emoji_dir = "static/emojis"; 10 + 11 + if let Err(e) = fs::create_dir_all(runtime_emoji_dir) { 12 + log::warn!( 13 + "Failed to ensure emoji directory exists at {}: {}", 14 + runtime_emoji_dir, 15 + e 16 + ); 17 + return; 18 + } 19 + 20 + let should_seed = runtime_emoji_dir != bundled_emoji_dir 21 + && fs::read_dir(runtime_emoji_dir) 22 + .map(|mut it| it.next().is_none()) 23 + .unwrap_or(false); 24 + 25 + if !should_seed { 26 + return; 27 + } 28 + 29 + if !Path::new(bundled_emoji_dir).exists() { 30 + return; 31 + } 32 + 33 + match fs::read_dir(bundled_emoji_dir) { 34 + Ok(entries) => { 35 + for entry in entries.flatten() { 36 + let path = entry.path(); 37 + if let Some(name) = path.file_name() { 38 + let dest = Path::new(runtime_emoji_dir).join(name); 39 + if path.is_file() { 40 + if let Err(err) = fs::copy(&path, &dest) { 41 + log::warn!("Failed to seed emoji {:?} -> {:?}: {}", path, dest, err); 42 + } 43 + } 44 + } 45 + } 46 + log::info!( 47 + "Seeded emoji directory {} from {}", 48 + runtime_emoji_dir, 49 + bundled_emoji_dir 50 + ); 51 + } 52 + Err(err) => log::warn!( 53 + "Failed to read bundled emoji directory {}: {}", 54 + bundled_emoji_dir, 55 + err 56 + ), 57 + } 58 + }
+17 -2
src/main.rs
··· 31 31 mod config; 32 32 mod db; 33 33 mod dev_utils; 34 + mod emoji; 34 35 mod error_handler; 35 36 mod ingester; 36 37 #[allow(dead_code)] ··· 190 191 // Create rate limiter - 30 requests per minute per IP 191 192 let rate_limiter = web::Data::new(RateLimiter::new(30, Duration::from_secs(60))); 192 193 194 + // Initialize runtime emoji directory (kept out of main for clarity) 195 + emoji::init_runtime_dir(&config); 196 + 193 197 log::debug!("starting HTTP server at http://{host}:{port}"); 194 198 HttpServer::new(move || { 195 199 App::new() ··· 210 214 .build(), 211 215 ) 212 216 .service(Files::new("/static", "static").show_files_listing()) 213 - .service(Files::new("/emojis", "static/emojis").show_files_listing()) 217 + .service( 218 + Files::new("/emojis", app_config.emoji_dir.clone()) 219 + .use_last_modified(true) 220 + .use_etag(true) 221 + .show_files_listing(), 222 + ) 214 223 .configure(api::configure_routes) 215 224 }) 216 225 .bind((host.as_str(), port))? ··· 233 242 #[actix_web::test] 234 243 async fn test_custom_emojis_endpoint() { 235 244 // Test that the custom emojis endpoint returns JSON 236 - let app = test::init_service(App::new().service(get_custom_emojis)).await; 245 + let cfg = crate::config::Config::from_env().expect("load config"); 246 + let app = test::init_service( 247 + App::new() 248 + .app_data(web::Data::new(cfg)) 249 + .service(get_custom_emojis), 250 + ) 251 + .await; 237 252 238 253 let req = test::TestRequest::get() 239 254 .uri("/api/custom-emojis")