localsnd 0.5.31

CLI implementation of localsend
Documentation
use std::{
	collections::HashMap,
	path::{Path, PathBuf},
};

use crate::Result;
use crate::localsend_proto::dto::{FileDto, FileType};
use linked_hash_map::LinkedHashMap;
use uuid::Uuid;

#[derive(Debug, Clone, PartialEq)]
pub enum FileStatus {
	Queue,
	Skipped,
	Sending,
	Failed,
	Finished,
}

#[derive(Debug, Clone)]
pub struct SendingFile {
	pub index: usize,
	pub file: FileDto,
	pub status: FileStatus,
	pub path: Option<PathBuf>,
	pub token: Option<String>,
}

impl SendingFile {
	pub fn new(index: usize, file: FileDto, path: Option<PathBuf>) -> Self {
		Self { index, file, status: FileStatus::Queue, path, token: None }
	}
}

#[derive(Debug, Default, Clone)]
pub struct SendingFiles {
	pub files: LinkedHashMap<String, SendingFile>,
}

impl SendingFiles {
	pub fn add_text(&mut self, text: impl ToString, preview: bool) {
		let text = text.to_string();
		let id = Uuid::new_v4().to_string();
		let text_hash = format!("{:x}", md5::compute(&text));
		let file = FileDto {
			id: id.clone(),
			file_name: format!("{}.txt", text_hash),
			size: text.len() as u64,
			file_type: FileType::Text,
			hash: Some(text_hash),
			preview: if preview { Some(text) } else { None },
		};
		self.files.insert(id.clone(), SendingFile::new(self.files.len(), file, None));
	}

	pub fn add_dir(&mut self, path: impl AsRef<Path>) -> Result<()> {
		use super::SendError;

		let base = path.as_ref().parent().ok_or(SendError::NoPermission)?;

		for entry in walkdir::WalkDir::new(&path) {
			let entry = entry?;
			let entry_path = entry.path();
			if !entry_path.is_file() {
				continue;
			}

			let diff_path = pathdiff::diff_paths(entry_path, base).ok_or(SendError::NoPermission)?;
			let file_name = match diff_path.to_str() {
				Some(name) => name.replace("\\", "/"),
				None => {
					log::error!("ignore file: {:?}", entry_path);
					continue;
				}
			};

			log::debug!("add file {}", file_name);
			self.add_file(entry_path, Some(file_name))?;
		}

		Ok(())
	}

	pub fn add_file(&mut self, path: impl AsRef<Path>, file_name: Option<String>) -> Result<()> {
		fn get_file_name(path: &Path) -> Option<String> {
			Some(path.file_name()?.to_str()?.to_string())
		}

		fn file_type(file_name: &String) -> FileType {
			use mime_guess::mime::*;

			let mime = mime_guess::from_path(file_name).first_or_octet_stream();
			match (mime.type_(), mime.subtype()) {
				(IMAGE, _) => FileType::Image,
				(VIDEO, _) => FileType::Video,
				(APPLICATION, PDF) => FileType::Pdf,
				(TEXT, _) => FileType::Text,
				(APPLICATION, name) if name.as_str() == "vnd.android.package-archive" => FileType::Apk,
				_ => FileType::Other,
			}
		}

		let path = path.as_ref();

		let id = Uuid::new_v4().to_string();
		let size = std::fs::metadata(path)?.len();
		let file_name = file_name.unwrap_or(get_file_name(path).unwrap_or(id.clone()));
		let file_type = file_type(&file_name);

		let file = FileDto { id: id.clone(), file_name, size, file_type, hash: None, preview: None };
		self.files.insert(id.clone(), SendingFile::new(self.files.len(), file, Some(path.to_path_buf())));
		Ok(())
	}

	pub fn update_token(&mut self, token: HashMap<String, String>) {
		for (file_id, file) in &mut self.files {
			match token.get(file_id) {
				Some(token) => {
					file.status = FileStatus::Sending;
					file.token = Some(token.clone());
				}
				None => {
					file.status = FileStatus::Skipped;
				}
			}
		}
	}

	pub fn mark_finish_status(&mut self, file_id: String, success: bool) {
		if let Some(file) = self.files.get_mut(&file_id) {
			if success {
				file.status = FileStatus::Finished;
			} else {
				file.status = FileStatus::Failed;
			}
		}
	}

	pub fn to_dto_map(&self) -> HashMap<String, FileDto> {
		self.files.iter().map(|(id, file)| (id.clone(), file.file.clone())).collect()
	}
}