use rusqlite::{params, Connection}; use crate::config::Config; use crate::error::AppResult; use crate::models::{AnnounceRequest, FileRecord}; pub fn open(cfg: &Config) -> AppResult { if let Some(parent) = std::path::Path::new(&cfg.db_path).parent() { std::fs::create_dir_all(parent).ok(); } let db = Connection::open(&cfg.db_path)?; db.pragma_update(None, "journal_mode", "WAL")?; db.pragma_update(None, "synchronous", "NORMAL")?; migrate(&db)?; Ok(db) } pub fn migrate(db: &Connection) -> rusqlite::Result<()> { db.execute_batch( r#" CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, peer_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', ftype TEXT NOT NULL, extension TEXT NOT NULL DEFAULT '', size_bytes INTEGER NOT NULL, hash TEXT NOT NULL UNIQUE, license TEXT NOT NULL DEFAULT 'authored', tags TEXT NOT NULL DEFAULT '[]', created_at INTEGER NOT NULL ); CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5( name, description, tags, content='files', content_rowid='rowid', tokenize='unicode61' ); CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN INSERT INTO files_fts(rowid, name, description, tags) VALUES (new.rowid, new.name, new.description, new.tags); END; CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN INSERT INTO files_fts(files_fts, rowid, name, description, tags) VALUES ('delete', old.rowid, old.name, old.description, old.tags); END; "#, ) } pub fn insert_file(db: &Connection, req: &AnnounceRequest) -> rusqlite::Result { let id = format!("{}::{}", req.peer_id, &req.hash[..12]); let tags_json = serde_json::to_string(&req.tags).unwrap_or_else(|_| "[]".into()); db.execute( "INSERT INTO files (id, peer_id, name, description, ftype, extension, size_bytes, hash, license, tags, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ id, req.peer_id, req.name, req.description, req.ftype, req.extension, req.size_bytes, req.hash, req.license, tags_json, chrono::Utc::now().timestamp(), ], )?; Ok(id) } pub fn get_file(db: &Connection, file_id: &str) -> rusqlite::Result { db.query_row( "SELECT id, peer_id, name, description, ftype, extension, size_bytes, hash, license, tags, created_at FROM files WHERE id = ?1", [file_id], FileRecord::from_row, ) } pub fn search_files( db: &Connection, q: &str, ftype: Option<&str>, limit: i64, offset: i64, ) -> rusqlite::Result> { let mut sql = String::from( "SELECT f.id, f.peer_id, f.name, f.description, f.ftype, f.extension, f.size_bytes, f.hash, f.license, f.tags, f.created_at FROM files f", ); let mut params: Vec> = Vec::new(); let mut clauses: Vec = Vec::new(); if let Some(query) = crate::search::fts_query(q) { sql.push_str(" JOIN files_fts ON files_fts.rowid = f.rowid"); clauses.push("files_fts MATCH ?".to_string()); params.push(Box::new(query)); } if let Some(ft) = ftype { params.push(Box::new(ft.to_string())); clauses.push(format!("f.ftype = ?{}", params.len())); } if !clauses.is_empty() { sql.push_str(" WHERE "); sql.push_str(&clauses.join(" AND ")); } params.push(Box::new(limit)); sql.push_str(&format!(" ORDER BY f.created_at DESC LIMIT ?{}", params.len())); params.push(Box::new(offset)); sql.push_str(&format!(" OFFSET ?{}", params.len())); let mut stmt = db.prepare(&sql)?; let rows = stmt.query_map( rusqlite::params_from_iter(params.iter().map(|p| p.as_ref())), FileRecord::from_row, )?; rows.collect() }