velocimeter/src/main.rs

237 lines
6.4 KiB
Rust

use std::{
fs::ReadDir,
path::{Path, PathBuf},
process::ExitCode,
};
use clap::{Parser, Subcommand};
use device::DeviceList;
use local::LocalServer;
use tracing::{debug, error, info, warn};
mod db;
mod device;
mod local;
mod web;
pub const CONFIG_DIR_NAME: &str = env!("CARGO_PKG_NAME");
#[derive(Parser)]
struct Args {
#[command(subcommand)]
cmd: Command,
}
#[derive(Subcommand)]
enum Command {
/// Lists saved devices
ListSaved {
/// Filter for device name
name: Option<String>,
},
/// Deletes a saved device
RmSaved {
/// Name of the device to delete
name: String,
},
/// Performs the sync
Run {
/// Don't show the QR code in the console
#[arg(short, long)]
no_qr_code: bool,
/// Use saved device for transfer
#[arg(short, long)]
device: Option<String>,
/// Directory to sync
sync_dir: PathBuf,
},
}
/// list-devices entrypoint
fn list_devices(list: &DeviceList, name: Option<String>) {
if let Some(name) = name {
if let Some(device) = list.find(name) {
println!("{}", device);
}
} else {
for device in list.iter() {
println!("{}", device);
}
}
}
/// Wrapper function for appending children of a `ReadDir`
async fn append_children(paths: &mut Vec<PathBuf>, dir: ReadDir) -> std::io::Result<()> {
let mut children = dir
.map(|f| f.map(|f| f.path()))
.collect::<std::io::Result<Vec<PathBuf>>>()?;
paths.append(&mut children);
Ok(())
}
/// Performs the actual sync process with the device.
async fn sync_dir(dir: impl AsRef<Path>, server: &mut LocalServer) -> bool {
// To avoid recursing, which seems to be a mess with how we're using async,
// we continue to add children to our vec as we run into them. So to start,
// let's get all children of the "root" directory.
let iter = match dir.as_ref().read_dir() {
Ok(iter) => iter,
Err(err) => {
error!("couldn't read {}: {}", dir.as_ref().display(), err);
return false;
}
};
let mut paths: Vec<PathBuf> = Vec::new();
let mut count = 0u32;
// If we can't get all children of the root dir, assume something's wrong.
if let Err(err) = append_children(&mut paths, iter).await {
error!(
"couldn't read paths from {}: {}",
dir.as_ref().display(),
err
);
return false;
}
// Loop to find all valid files
while let Some(path) = paths.pop() {
if path.is_dir() {
match path.read_dir() {
Ok(iter) => {
if let Err(err) = append_children(&mut paths, iter).await {
warn!("couldn't get paths from {}: {}", path.display(), err);
}
}
Err(err) => {
warn!("couldn't read {}: {}", path.display(), err);
continue;
}
};
} else if path.exists() && server.should_upload(&path) {
debug!("Adding {} to queue", path.display());
// Add the download to the queue
if let Err(err) = server.queue_upload(&path).await {
warn!("couldn't send {}: {err}", path.display());
} else {
count += 1;
}
}
}
info!(
"Processing {} {}.",
count,
if count == 1 { "song" } else { "songs" }
);
// Wait for the queue to complete, or for an error to occur
if let Err(err) = server.wait_on_queue().await {
error!("Error processing uploads: {err}");
false
} else {
true
}
}
async fn start_sync(
list: &mut DeviceList,
qr_code: bool,
dir: PathBuf,
device: Option<String>,
) -> ExitCode {
if !dir.is_dir() {
error!("can't open {} as a directory", dir.display());
return ExitCode::FAILURE;
}
let mut web_conn = match web::connect().await {
Ok(conn) => conn,
Err(err) => {
error!("unable to connect with Doppler web service: {err}");
return ExitCode::FAILURE;
}
};
if let Some(device) = device {
if let Some(device) = list.find(&device) {
if let Err(err) = web_conn.request_device(device).await {
error!("requesting device {device} failed: {err}");
return ExitCode::FAILURE;
}
} else {
error!("device {device} not found in saved list");
return ExitCode::FAILURE;
}
} else {
if qr_code {
let qrcode = qrencode::QrCode::new(web_conn.code()).unwrap();
let encoded = qrcode.render::<char>().module_dimensions(2, 1).build();
println!("{}", encoded);
}
println!("Use code {} to connect your device.", web_conn.code());
}
let (dev_id, dev_uri) = match web_conn.wait_for_device(list).await {
Ok(uri) => uri,
Err(err) => {
error!("error getting device URI: {err}");
return ExitCode::FAILURE;
}
};
let cache = db::FileCache::open_for_device(dev_id).await;
if let Err(err) = list.commit().await {
warn!("can't save device: {err}");
}
debug!("Got device URI {}", &dev_uri);
let mut local_server = match local::LocalServer::new(dev_uri.to_string(), cache).await {
Ok(serv) => serv,
Err(err) => {
error!("couldn't connect to {dev_uri}: {err}");
return ExitCode::FAILURE;
}
};
if sync_dir(dir, &mut local_server).await {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
#[tokio::main]
async fn main() -> ExitCode {
let mut device_list = match DeviceList::load().await {
Ok(list) => list,
Err(err) => {
eprintln!("error loading saved device list: {err}");
return ExitCode::FAILURE;
}
};
match Args::parse().cmd {
Command::ListSaved { name } => {
list_devices(&device_list, name);
}
Command::RmSaved { name } => {
device_list.drop_by_name(name);
}
Command::Run {
no_qr_code,
sync_dir,
device,
} => {
tracing_subscriber::fmt().init();
return start_sync(&mut device_list, !no_qr_code, sync_dir, device).await;
}
}
ExitCode::SUCCESS
}