refactor: organize Rust modules and simplify deployment README

This commit is contained in:
junior
2026-10-17 22:59:25 +08:00
parent 68c972dadd
commit ceb3e5e2b5
37 changed files with 1029 additions and 1225 deletions
+153
View File
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-2.0-only
use super::{now, Store};
use crate::{
domain::DownloadJob,
error::{Error, Result},
media::*,
};
impl Store {
pub async fn destinations(&self) -> Result<Vec<Destination>> {
Ok(sqlx::query_as("SELECT d.*, COALESCE(s.destination_id = d.id, 0) AS is_active FROM media_destinations d CROSS JOIN destination_selection s ORDER BY d.rowid")
.fetch_all(&self.pool).await?)
}
pub async fn destination(&self, id: &str) -> Result<Destination> {
self.destinations()
.await?
.into_iter()
.find(|d| d.id == id)
.ok_or(Error::NotFound)
}
pub async fn create_destination(&self, mut input: DestinationInput) -> Result<Destination> {
input.validate()?;
let id = uuid::Uuid::new_v4().to_string();
sqlx::query(
"INSERT INTO media_destinations(id, name, kind, root_path) VALUES (?, ?, 'local', ?)",
)
.bind(&id)
.bind(input.name)
.bind(input.root_path)
.execute(&self.pool)
.await?;
self.destination(&id).await
}
pub async fn active_destination_id(&self) -> Result<Option<String>> {
Ok(sqlx::query_scalar(
"SELECT destination_id FROM destination_selection WHERE singleton = 1",
)
.fetch_one(&self.pool)
.await?)
}
pub async fn activate_destination(&self, id: &str) -> Result<()> {
let mut tx = self.pool.begin().await?;
let exists: bool =
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM media_destinations WHERE id = ?)")
.bind(id)
.fetch_one(&mut *tx)
.await?;
if !exists {
return Err(Error::NotFound);
}
let mapped: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM destination_mappings m JOIN downloader_selection s ON s.downloader_id = m.downloader_id WHERE m.destination_id = ?)")
.bind(id).fetch_one(&mut *tx).await?;
if !mapped {
return Err(Error::Invalid(
"select a downloader and configure its mapping before activating this destination"
.into(),
));
}
sqlx::query("UPDATE destination_selection SET destination_id = ? WHERE singleton = 1")
.bind(id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
pub async fn mappings(&self, id: &str) -> Result<Vec<Mapping>> {
self.destination(id).await?;
Ok(sqlx::query_as("SELECT * FROM destination_mappings WHERE destination_id = ? ORDER BY version DESC, downloader_id")
.bind(id).fetch_all(&self.pool).await?)
}
pub async fn create_mapping(
&self,
destination_id: &str,
input: MappingInput,
) -> Result<Mapping> {
self.destination(destination_id).await?;
self.downloader_config(&input.downloader_id).await?;
let root = normalize_download_root(&input.download_root)?;
let id = uuid::Uuid::new_v4().to_string();
let mut tx = self.pool.begin().await?;
let version: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(version), 0) + 1 FROM destination_mappings WHERE destination_id = ? AND downloader_id = ?")
.bind(destination_id).bind(&input.downloader_id).fetch_one(&mut *tx).await?;
sqlx::query("INSERT INTO destination_mappings(id, destination_id, downloader_id, version, download_root) VALUES (?, ?, ?, ?, ?)")
.bind(&id).bind(destination_id).bind(&input.downloader_id).bind(version).bind(&root).execute(&mut *tx).await?;
tx.commit().await?;
Ok(Mapping {
id,
destination_id: destination_id.into(),
downloader_id: input.downloader_id,
version,
download_root: root,
})
}
pub async fn job(&self, id: &str) -> Result<DownloadJob> {
sqlx::query_as("SELECT * FROM download_jobs WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await?
.ok_or(Error::NotFound)
}
pub async fn file_report(&self, id: &str) -> Result<FileReport> {
let job = self.job(id).await?;
let files = sqlx::query_as("SELECT relative_path, expected_size, observed_size, status, error FROM media_files WHERE job_id = ? ORDER BY relative_path")
.bind(id).fetch_all(&self.pool).await?;
Ok(FileReport {
job_id: id.into(),
destination_id: job.destination_id,
status: job.file_status,
checked_at: job.file_checked_at,
last_success_at: job.file_last_success_at,
error: job.file_error,
files,
})
}
pub(crate) async fn save_manifest(&self, id: &str, files: &[MediaFile]) -> Result<()> {
let mut tx = self.pool.begin().await?;
for file in files {
validate_relative(&file.relative_path, false)?;
sqlx::query("INSERT INTO media_files(job_id, relative_path, expected_size) VALUES (?, ?, ?) ON CONFLICT(job_id, relative_path) DO NOTHING")
.bind(id).bind(&file.relative_path).bind(file.expected_size).execute(&mut *tx).await?;
}
tx.commit().await?;
Ok(())
}
pub(crate) async fn save_file_check(
&self,
id: &str,
status: &str,
error: Option<&str>,
files: &[MediaFile],
) -> Result<()> {
let mut tx = self.pool.begin().await?;
let checked_at = now();
for file in files {
sqlx::query("UPDATE media_files SET observed_size = ?, status = ?, error = ? WHERE job_id = ? AND relative_path = ?")
.bind(file.observed_size).bind(&file.status).bind(&file.error).bind(id).bind(&file.relative_path).execute(&mut *tx).await?;
}
sqlx::query("UPDATE download_jobs SET file_status = ?, file_error = ?, file_checked_at = ?, file_last_success_at = CASE WHEN ? IN ('present', 'absent') THEN ? ELSE file_last_success_at END WHERE id = ?")
.bind(status).bind(error).bind(checked_at).bind(status).bind(checked_at).bind(id).execute(&mut *tx).await?;
tx.commit().await?;
Ok(())
}
}