0
0
mirror of https://github.com/TeFiLeDo/listload.git synced 2024-11-23 12:46:17 +01:00

very basic list management

This commit is contained in:
Adrian Wannenmacher 2023-09-30 02:35:18 +02:00
parent 79788ae5d2
commit b1c1b4f551
Signed by: tfld
GPG Key ID: 19D986ECB1E492D5
7 changed files with 253 additions and 14 deletions

View File

@ -14,7 +14,10 @@ keywords = ["download", "http", "https"]
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
clap = { version = "4.4.6", features = ["env", "derive", "wrap_help", "cargo", "unicode"] } clap = { version = "4.4.6", features = ["env", "derive", "wrap_help", "cargo", "unicode"] }
directories = "5.0.1"
fs4 = "0.6.6"
human-panic = "1.2.1" human-panic = "1.2.1"
rand = "0.8.5"
reqwest = { version = "0.11.20", features = ["brotli", "deflate", "gzip"] } reqwest = { version = "0.11.20", features = ["brotli", "deflate", "gzip"] }
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"

View File

@ -1,11 +0,0 @@
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[clap(author, version, about)]
pub struct CLI {
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {}

33
src/cli/list.rs Normal file
View File

@ -0,0 +1,33 @@
use clap::Subcommand;
#[derive(Debug, Subcommand)]
pub enum ListCommand {
/// Create a new download list.
#[clap(visible_alias = "new", visible_alias = "c")]
Create {
/// A unique name, which will be used to refer to the list.
name: String,
/// A short description of the lists purpose.
#[clap(long, short, default_value = "", hide_default_value = true)]
description: String,
},
/// Delete a download list.
#[clap(visible_alias = "rm", visible_alias = "d")]
Delete {
/// The name of the list to remove.
name: String,
},
/// Update an existing list.
///
/// Only values specified for this command are changed.
#[clap(visible_alias = "u")]
Update {
/// The name of the list to change.
name: String,
/// A short description of the lists purpose.
///
/// Set this to an empty string (e.g. pass "" as value) to remove the current description.
#[clap(long, short)]
description: Option<String>,
},
}

22
src/cli/mod.rs Normal file
View File

@ -0,0 +1,22 @@
use clap::{Parser, Subcommand};
mod list;
pub use list::*;
#[derive(Debug, Parser)]
#[clap(author, version, about)]
pub struct CLI {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Manage entire download lists.
#[clap(visible_alias = "l")]
List {
#[clap(subcommand)]
command: ListCommand,
},
}

28
src/data.rs Normal file
View File

@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct DownloadList {
name: String,
description: String,
}
impl DownloadList {
pub fn new(name: String) -> Self {
Self {
name,
description: String::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
}

View File

@ -1,11 +1,72 @@
use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{ensure, Context};
use clap::Parser; use clap::Parser;
use directories::ProjectDirs;
use crate::store::DownloadListStore;
mod cli; mod cli;
mod data;
mod store;
#[tokio::main] fn main() -> anyhow::Result<()> {
async fn main() -> anyhow::Result<()> {
human_panic::setup_panic!(); human_panic::setup_panic!();
let cli = cli::CLI::parse(); let cli = cli::CLI::parse();
let (state, data) = prepare_directories()?;
Ok(()) match cli.command {
cli::Command::List { command } => match command {
cli::ListCommand::Update { name, description } => {
let mut list = DownloadListStore::load(&name, &data)?;
if let Some(description) = description {
list.set_description(description);
}
list.save()
}
cli::ListCommand::Create { name, description } => {
let mut list = DownloadListStore::new(name, &data)?;
list.set_description(description);
list.save()
}
cli::ListCommand::Delete { name } => DownloadListStore::delete(&name, &data),
},
}
}
fn prepare_directories() -> anyhow::Result<(PathBuf, PathBuf)> {
let dirs = ProjectDirs::from("dev", "TFLD", "listload")
.context("failed to discover project directories")?;
let state = dirs.state_dir().unwrap_or_else(|| dirs.data_local_dir());
prepare_directory(state)?;
let data = dirs.data_dir();
prepare_directory(data)?;
Ok((state.to_path_buf(), data.to_path_buf()))
}
fn prepare_directory(path: &Path) -> anyhow::Result<()> {
if path.is_dir() {
return Ok(());
}
ensure!(
!path.exists(),
"required directory is not a directory: {}",
path.display()
);
fs::create_dir_all(path).context(format!(
"failed to create required directory: {}",
path.display()
))
} }

103
src/store.rs Normal file
View File

@ -0,0 +1,103 @@
use std::{
fs::{self, File, OpenOptions},
io::{BufReader, BufWriter, Seek},
ops::{Deref, DerefMut},
path::Path,
};
use anyhow::{ensure, Context};
use fs4::FileExt;
use crate::data::DownloadList;
#[derive(Debug)]
pub struct DownloadListStore {
list: DownloadList,
file: File,
}
impl DownloadListStore {
pub fn new(name: String, directory: &Path) -> anyhow::Result<Self> {
let path = Self::path_in_directory(directory, &name);
let file = OpenOptions::new()
.create_new(true)
.read(true)
.write(true)
.open(&path)
.context("failed to create list file")?;
file.lock_exclusive()
.context("failed to acquire list file lock")?;
Ok(Self {
list: DownloadList::new(name),
file,
})
}
pub fn load(name: &str, directory: &Path) -> anyhow::Result<Self> {
let path = Self::path_in_directory(directory, name);
let file = OpenOptions::new()
.create(false)
.read(true)
.write(true)
.open(&path)
.context("failed to open list file")?;
file.lock_exclusive()
.context("failed to acquire list file lock")?;
let reader = BufReader::new(&file);
let list: DownloadList =
serde_json::from_reader(reader).context("failed to deserialize list")?;
ensure!(
name == list.name(),
"list name mismatch: found {name} instead of {}",
list.name()
);
Ok(Self { list, file })
}
pub fn save(&mut self) -> anyhow::Result<()> {
self.file.set_len(0).context("failed to clear list file")?;
self.file
.seek(std::io::SeekFrom::Start(0))
.context("failed to find list file start")?;
let writer = BufWriter::new(&self.file);
if cfg!(debug_assertions) {
serde_json::to_writer_pretty(writer, &self.list)
} else {
serde_json::to_writer(writer, &self.list)
}
.context("failed to write list file")
}
pub fn delete(name: &str, directory: &Path) -> anyhow::Result<()> {
fs::remove_file(Self::path_in_directory(directory, name))
.context("failed to remove list file")
}
fn path_in_directory(directory: &Path, name: &str) -> std::path::PathBuf {
directory.join(name).with_extension("json")
}
}
impl Deref for DownloadListStore {
type Target = DownloadList;
fn deref(&self) -> &Self::Target {
&self.list
}
}
impl DerefMut for DownloadListStore {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.list
}
}