229 lines
6 KiB
Rust
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;
|