116 lines
4.0 KiB
Rust
116 lines
4.0 KiB
Rust
use rusqlite::{params, Connection};
|
|
use crate::config::Config;
|
|
use crate::error::AppResult;
|
|
use crate::models::{AnnounceRequest, FileRecord};
|
|
|
|
pub fn open(cfg: &Config) -> AppResult<Connection> {
|
|
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<String> {
|
|
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<FileRecord> {
|
|
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<Vec<FileRecord>> {
|
|
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<Box<dyn rusqlite::ToSql>> = Vec::new();
|
|
let mut clauses: Vec<String> = 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()
|
|
} |