mirror of
https://github.com/TeFiLeDo/listload.git
synced 2024-11-23 04:46:15 +01:00
very basic list management
This commit is contained in:
parent
79788ae5d2
commit
b1c1b4f551
@ -14,7 +14,10 @@ keywords = ["download", "http", "https"]
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
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"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.20", features = ["brotli", "deflate", "gzip"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
|
11
src/cli.rs
11
src/cli.rs
@ -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
33
src/cli/list.rs
Normal 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
22
src/cli/mod.rs
Normal 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
28
src/data.rs
Normal 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;
|
||||
}
|
||||
}
|
67
src/main.rs
67
src/main.rs
@ -1,11 +1,72 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
|
||||
use crate::store::DownloadListStore;
|
||||
|
||||
mod cli;
|
||||
mod data;
|
||||
mod store;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
human_panic::setup_panic!();
|
||||
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
103
src/store.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user