use std::{ fs, path::{Path, PathBuf}, }; use eyre::Context; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub use upload_id::*; use crate::schemas::novel::{FullNovel, Novel}; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct State { pub posted_until: Option, } #[derive(Debug, Clone)] pub struct Db { novels: PathBuf, state_path: PathBuf, state: State, } #[derive(Debug, Serialize, Deserialize)] pub struct Entry { pub info: PathBuf, pub thumbnail: PathBuf, pub screenshots: PathBuf, pub files: PathBuf, pub upload_queue: PathBuf, } impl Entry { pub fn finish_upload(&self, id: UploadId, file_name: &str) -> eyre::Result<()> { let src = self.upload_queue.join(id.to_file_name()); let dest = match id.kind { UploadKind::Thumbnail => self.thumbnail.clone(), UploadKind::Screenshot => self.screenshots.join(file_name), UploadKind::File => self.files.join(file_name), }; fs::rename(src, dest).wrap_err("failed to mark upload as finished")?; Ok(()) } pub fn start_upload(&self, slug: impl Into, kind: UploadKind) -> (UploadId, PathBuf) { let id = UploadId { id: Uuid::new_v4(), kind, novel: slug.into(), }; let path = self.upload_queue.join(id.to_file_name()); (id, path) } } impl Entry { pub fn read_info(&self) -> eyre::Result { let raw_info = fs::read_to_string(&self.info).wrap_err("failed to read info")?; let info: Novel = toml::from_str(&raw_info).wrap_err("failed to deserialize TOML")?; Ok(info) } pub fn get_full_info(&self) -> eyre::Result { let info = self.read_info()?; Ok(FullNovel { data: info, upload_queue: self.upload_queue()?, files: self.list_files()?, screenshots: self.list_screenshots()?, }) } pub fn write_info(&self, info: &Novel) -> eyre::Result<()> { let s = toml::to_string_pretty(info).wrap_err("failed to serialize novel")?; fs::write(&self.info, &s)?; Ok(()) } pub fn screenshot(&self, name: &str) -> eyre::Result { let path = self.files.join(name); eyre::ensure!(path.exists(), "the screenshot does not exist"); Ok(path) } pub fn file(&self, name: &str) -> eyre::Result { let path = self.files.join(name); eyre::ensure!(path.exists(), "the file does not exist"); Ok(path) } fn listdir(dir: &Path) -> eyre::Result> { let mut out = Vec::new(); for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); let last = path.components().last().unwrap(); let last = last.as_os_str().to_string_lossy().to_string(); out.push(last); } Ok(out) } pub fn list_screenshots(&self) -> eyre::Result> { Self::listdir(&self.screenshots) } pub fn list_files(&self) -> eyre::Result> { Self::listdir(&self.files) } pub fn upload_queue(&self) -> eyre::Result> { Self::listdir(&self.upload_queue) } pub fn init(&self, info: &Novel) -> eyre::Result<()> { if self.info.exists() || self.files.exists() { eyre::bail!("this entry is already exists"); } fs::create_dir_all(&self.screenshots).wrap_err("failed to create screenshots dir")?; fs::create_dir_all(&self.files).wrap_err("failed to create files dir")?; fs::create_dir_all(&self.upload_queue).wrap_err("failed to create upload queue")?; self.write_info(info)?; Ok(()) } pub fn exists(&self) -> bool { self.info.exists() } } fn create_dir(p: impl AsRef) -> eyre::Result<()> { let p = p.as_ref(); if p.exists() { return Ok(()); } tracing::info!("creating directory {}", p.display()); fs::create_dir_all(p)?; Ok(()) } impl Db { pub fn state(&self) -> &State { &self.state } pub fn update_state(&mut self, f: impl FnOnce(&mut State) -> O) -> O { let out = f(&mut self.state); let serialized = toml::to_string_pretty(&self.state).unwrap(); fs::write(&self.state_path, serialized).unwrap(); out } pub fn from_root(root: impl AsRef) -> eyre::Result { let root = root.as_ref(); fs::create_dir_all(root)?; let novels = root.join("novels"); let state_path = root.join("state.toml"); if !state_path.exists() { fs::write( &state_path, toml::to_string_pretty(&State::default()).unwrap(), ) .wrap_err("failed to create state")?; } create_dir(root)?; create_dir(&novels)?; let state = toml::from_str(&fs::read_to_string(&state_path).unwrap()) .wrap_err("failed to read state")?; Ok(Self { novels, state, state_path, }) } } impl Db { pub fn enumerate(&self) -> Vec<(String, Entry)> { let read = fs::read_dir(&self.novels).unwrap(); let mut entries = vec![]; for entry in read.map(|e| e.unwrap().path()) { let slug = entry.file_stem().unwrap().to_str().unwrap(); entries.push((slug.to_owned(), self.entry(slug))); } entries } pub fn entry(&self, slug: &str) -> Entry { let root = self.novels.join(slug); let thumbnail = root.join("thumbnail"); let screenshots = root.join("screenshots"); let upload_queue = root.join("upload_queue"); let info = root.join("info.toml"); let files = root.join("files"); Entry { info, files, upload_queue, thumbnail, screenshots, } } } mod upload_id;