vnj/src/db/mod.rs
2024-11-17 14:58:34 +03:00

229 lines
6 KiB
Rust

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<jiff::Zoned>,
}
#[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<String>, 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<Novel> {
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<FullNovel> {
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<PathBuf> {
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<PathBuf> {
let path = self.files.join(name);
eyre::ensure!(path.exists(), "the file does not exist");
Ok(path)
}
fn listdir(dir: &Path) -> eyre::Result<Vec<String>> {
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<Vec<String>> {
Self::listdir(&self.screenshots)
}
pub fn list_files(&self) -> eyre::Result<Vec<String>> {
Self::listdir(&self.files)
}
pub fn upload_queue(&self) -> eyre::Result<Vec<String>> {
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<Path>) -> 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<O>(&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<Path>) -> eyre::Result<Self> {
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;