diff --git a/src/downloader/mod.rs b/src/downloader/mod.rs new file mode 100644 index 0000000..610a386 --- /dev/null +++ b/src/downloader/mod.rs @@ -0,0 +1 @@ +pub mod pd_file; diff --git a/src/downloader/pd_file/enums.rs b/src/downloader/pd_file/enums.rs new file mode 100644 index 0000000..c4ecdc3 --- /dev/null +++ b/src/downloader/pd_file/enums.rs @@ -0,0 +1,45 @@ +use int_enum::IntEnum; + +/// The status of the downloaded file. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +pub enum PdFileStatus { + /// The download is already started but the target size is unknown. + Started = 0, + /// The download is started and the tagret size is known. + Downloading = 1, + /// The download is completed. + Downloaded = 2, +} + +impl PdFileStatus { + #[inline] + /// Returns true if the download is completed. + pub fn is_completed(&self) -> bool { + *self == PdFileStatus::Downloaded + } +} + +/// The type of the downloader. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +pub enum PdFileType { + /// Download in single thread mode. + SingleThread = 0, + /// Download in multiple thread mode. + MultiThread = 1, +} + +impl PdFileType { + #[inline] + /// Returns true if is multiple thread mode. + pub fn is_multi(&self) -> bool { + *self == PdFileType::MultiThread + } +} + +#[test] +fn test_enums() { + assert_eq!(PdFileStatus::Downloading.int_value().to_le_bytes(), [1]); + assert_eq!(PdFileType::MultiThread.int_value().to_le_bytes(), [1]); +} diff --git a/src/downloader/pd_file/error.rs b/src/downloader/pd_file/error.rs new file mode 100644 index 0000000..f1bfa43 --- /dev/null +++ b/src/downloader/pd_file/error.rs @@ -0,0 +1,30 @@ +use crate::gettext; +use std::convert::From; +use std::fmt::Display; + +#[derive(Debug, derive_more::From)] +pub enum PdFileError { + IoError(std::io::Error), + String(String), +} + +impl Display for PdFileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => { + f.write_str(gettext("Errors occured when operating files: "))?; + e.fmt(f)?; + } + Self::String(e) => { + f.write_str(e)?; + } + } + Ok(()) + } +} + +impl From<&str> for PdFileError { + fn from(value: &str) -> Self { + PdFileError::String(String::from(value)) + } +} diff --git a/src/downloader/pd_file/file.rs b/src/downloader/pd_file/file.rs new file mode 100644 index 0000000..8eb1d69 --- /dev/null +++ b/src/downloader/pd_file/file.rs @@ -0,0 +1,222 @@ +use crate::downloader::pd_file::error::PdFileError; +use crate::downloader::pd_file::enums::PdFileStatus; +use crate::downloader::pd_file::enums::PdFileType; +use crate::downloader::pd_file::version::PdFileVersion; +use crate::ext::rw_lock::GetRwLock; +use crate::ext::try_err::TryErr; +use crate::ext::try_err::TryErr2; +use crate::gettext; +use int_enum::IntEnum; +use std::convert::AsRef; +use std::fs::File; +#[cfg(test)] +use std::fs::metadata; +use std::fs::remove_file; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::ops::Drop; +use std::path::Path; +use std::path::PathBuf; +use std::sync::RwLock; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; + +lazy_static! { + #[doc(hidden)] + static ref MAGIC_WORDS: Vec = vec![0x50, 0x44, 0xff, 0xff]; +} + +/// The pd file +pub struct PdFile { + /// The version of the current file. + version: PdFileVersion, + /// Needed to save to file. + need_saved: AtomicBool, + /// The file handle of the pd file. + file: RwLock>, + /// The file path of the pd file. + file_path: RwLock>, + /// The file name + file_name: RwLock>, + /// The status of the downloaded file. + status: RwLock, + /// The type of the downloader. + ftype: RwLock, + /// The target size of the file. If unknown, set this to 0. + file_size: AtomicU64, + /// The size of the downloaded data. + downloaded_file_size: AtomicU64, + /// The size of the each part. Ignored in single thread mode. + part_size: AtomicU32, +} + +impl PdFile { + /// Create a new instance of the [PdFile] + pub fn new() -> Self { + Self { + version: PdFileVersion::new(1, 0), + need_saved: AtomicBool::new(false), + file: RwLock::new(None), + file_path: RwLock::new(None), + file_name: RwLock::new(None), + status: RwLock::new(PdFileStatus::Started), + ftype: RwLock::new(PdFileType::SingleThread), + file_size: AtomicU64::new(0), + downloaded_file_size: AtomicU64::new(0), + part_size: AtomicU32::new(0), + } + } + + /// Close the file. + /// This function will return error if write failed. + /// If you want to force close the file. Please use [Self::force_close()]. + pub fn close(&self) -> Result<(), PdFileError> { + if self.is_need_saved() { + self.write()?; + } + self.force_close(); + Ok(()) + } + + /// Force close the file + pub fn force_close(&self) { + let mut f = self.file.get_mut(); + match f.as_mut() { + Some(_) => { f.take(); } + None => {} + } + } + + #[inline] + /// Returns true if the download is completed. + pub fn is_completed(&self) -> bool { + self.status.get_ref().is_completed() + } + + #[inline] + /// Returns true if needed to save to file. + fn is_need_saved(&self) -> bool { + self.need_saved.load(Ordering::Relaxed) + } + + /// Create a new file and prepare to write data to it. + /// If file alreay exists, will remove it first. + /// * `path` - The path to the pd file. + pub fn open_with_create_file + ?Sized>(&self, path: &P) -> Result<(), PdFileError> { + let p = path.as_ref(); + if p.exists() { + remove_file(p)?; + } + let f = File::create(p)?; + self.file.get_mut().replace(f); + self.file_path.get_mut().replace(PathBuf::from(p)); + self.need_saved.store(true, Ordering::Relaxed); + Ok(()) + } + + /// Remove file in [Self::file_path] + fn remove_pd_file(&self) -> Result<(), PdFileError> { + match self.file_path.get_ref().as_ref() { + Some(pb) => { + if pb.exists() { + remove_file(pb)?; + } + Ok(()) + } + None => { Ok(()) } + } + } + + /// Remove file in [Self::file_path] and if error occered print that error. + fn remove_pd_file_with_err_msg(&self) { + match self.remove_pd_file() { + Ok(_) => {} + Err(e) => { + println!("{} {}", gettext("Failed to remove file: "), e); + } + } + } + + /// Set the file name + /// * `file_name` - The file name. Should not be empty. + pub fn set_file_name + ?Sized>(&self, file_name: &S) -> Result<(), PdFileError> { + let fname = file_name.as_ref(); + if fname.is_empty() { + Err(gettext("File name should not be empty."))? + } else { + self.file_name.get_mut().replace(String::from(fname)); + self.need_saved.store(true, Ordering::Relaxed); + // Rewrite all datas. + self.write()?; + Ok(()) + } + } + + /// Write all data to the file. + pub fn write(&self) -> Result<(), PdFileError> { + let mut f = self.file.get_mut(); + let mut f = f.as_mut().try_err(gettext("The file is not opened."))?; + f.seek(SeekFrom::Start(0))?; + f.write_all(&MAGIC_WORDS)?; + self.version.write_to(&mut f)?; + let file_name = self.file_name.get_ref().try_err2(gettext("File name is not set."))?; + let file_name = file_name.as_bytes(); + f.write_all(&(file_name.len() as u32).to_le_bytes())?; + f.write_all(&self.status.get_ref().int_value().to_le_bytes())?; + let ftype = self.ftype.get_ref(); + f.write_all(&ftype.int_value().to_le_bytes())?; + f.write_all(&self.file_size.load(Ordering::Relaxed).to_le_bytes())?; + f.write_all(&self.downloaded_file_size.load(Ordering::Relaxed).to_le_bytes())?; + let part_size = if ftype.is_multi() { + self.part_size.load(Ordering::Relaxed) + } else { + 0 + }; + f.write_all(&part_size.to_le_bytes())?; + f.write_all(file_name)?; + self.need_saved.store(false, Ordering::Relaxed); + Ok(()) + } +} + +impl Drop for PdFile { + fn drop(&mut self) { + if self.is_completed() { + self.force_close(); + self.remove_pd_file_with_err_msg(); + return; + } + if self.is_need_saved() { + match self.write() { + Ok(_) => {} + Err(e) => { + println!("{}", e); + self.force_close(); + self.remove_pd_file_with_err_msg(); + } + } + }; + self.force_close(); + } +} + +#[cfg(test)] +fn check_file_size + ?Sized>(path: &P, size: u64) -> Result<(), PdFileError> { + let m = metadata(path)?; + assert!(m.len() == size); + Ok(()) +} + +#[test] +fn test_pd_file() -> Result<(), PdFileError> { + { + let f = PdFile::new(); + f.open_with_create_file("test/a.pd")?; + f.set_file_name("a")?; + } + check_file_size("test/a.pd", 33)?; + Ok(()) +} diff --git a/src/downloader/pd_file/mod.rs b/src/downloader/pd_file/mod.rs new file mode 100644 index 0000000..ff1b856 --- /dev/null +++ b/src/downloader/pd_file/mod.rs @@ -0,0 +1,8 @@ +/// The error type of the pd file. +pub mod error; +/// The pd file +pub mod file; +/// The enums of the pd file. +pub mod enums; +/// Version of the pd file +pub mod version; diff --git a/src/downloader/pd_file/version.rs b/src/downloader/pd_file/version.rs new file mode 100644 index 0000000..0b7a82d --- /dev/null +++ b/src/downloader/pd_file/version.rs @@ -0,0 +1,129 @@ +use std::cmp::Ord; +use std::cmp::Ordering; +use std::cmp::PartialEq; +use std::cmp::PartialOrd; +use std::convert::AsRef; +use std::convert::TryFrom; +use std::io::Write; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Version of the pd file +pub struct PdFileVersion { + /// Major verson + major: u8, + /// Minor version + minor: u8, +} + +impl PdFileVersion { + /// Create a new instance of the [PdFileVersion] + /// * `major` - major version + /// * `minor` - minor version + pub fn new(major: u8, minor: u8) -> Self { + Self { major, minor } + } + + /// Create a new instance of the [PdFileVersion] from bytes. + /// * `bytes` - The data + /// * `offset` - The offset of the needed data + /// + /// Returns a new instance if succeed otherwise a Error because the data is less than 2 bytes. + pub fn from_bytes + ?Sized>(bytes: &T, offset: usize) -> Result { + let value = bytes.as_ref(); + if (value.len() - offset) < 2 { + Err(()) + } else { + Ok(Self::new(value[offset], value[offset + 1])) + } + } + + /// Get version bytes + pub fn to_bytes(&self) -> Vec { + let mut data = Vec::new(); + data.push(self.major); + data.push(self.minor); + data + } + + /// Write version bytes to writer. + /// * `writer` - The writer which implement the [Write] trait + /// + /// Returns io Result. + pub fn write_to(&self, writer: &mut W) -> std::io::Result<()> { + writer.write_all(&self.to_bytes()) + } +} + +impl TryFrom<&[u8]> for PdFileVersion { + type Error = (); + fn try_from(value: &[u8]) -> Result { + Self::from_bytes(value, 0) + } +} + +impl Ord for PdFileVersion { + fn cmp(&self, other: &Self) -> Ordering { + let r = self.major.cmp(&other.major); + if r.is_eq() { + self.minor.cmp(&other.minor) + } else { + r + } + } +} + +impl PartialOrd for PdFileVersion { + fn partial_cmp(&self, other: &Self) -> Option { + let r = self.major.cmp(&other.major); + if r.is_eq() { + Some(self.minor.cmp(&other.minor)) + } else { + Some(r) + } + } +} + +/// # Note +/// If [Self::from_bytes()] returns error, assert it to false. +impl + ?Sized> PartialEq for PdFileVersion { + fn eq(&self, other: &T) -> bool { + match Self::from_bytes(other, 0) { + Ok(v) => { v == *self } + Err(_) => { false } + } + } +} + +/// # Note +/// If [Self::from_bytes()] returns error, assert it to false. +impl + ?Sized> PartialOrd for PdFileVersion { + fn partial_cmp(&self, other: &T) -> Option { + match Self::from_bytes(other, 0) { + Ok(v) => { Some(self.cmp(&v)) } + Err(_) => { None } + } + } +} + +#[test] +fn test_pd_file_version() { + assert!(PdFileVersion::new(1, 0) != PdFileVersion::new(0, 1)); + assert!(PdFileVersion::new(1, 10) != PdFileVersion::new(11, 0)); + assert!(PdFileVersion::new(1, 10) == PdFileVersion::new(1, 10)); + assert!(PdFileVersion::from_bytes(&vec![1, 10], 0).unwrap() == PdFileVersion::new(1, 10)); + assert_eq!(PdFileVersion::new(1, 10).to_bytes(), vec![1, 10]); + assert!(PdFileVersion::new(2, 1) > PdFileVersion::new(1, 3)); + assert!(PdFileVersion::new(1, 3) > PdFileVersion::new(1, 2)); + assert!(PdFileVersion::new(3, 2) >= PdFileVersion::new(3, 2)); + assert!(PdFileVersion::new(1, 4) < PdFileVersion::new(1, 11)); + assert!(PdFileVersion::new(1, 3) < PdFileVersion::new(2, 1)); + assert!(PdFileVersion::new(1, 2) == [1u8, 2]); + assert!(PdFileVersion::new(1, 3) == vec![1u8, 3, 2]); + assert!(PdFileVersion::new(1, 2) == [1, 2, 3]); + assert!(PdFileVersion::new(1, 3) == vec![1, 3, 2]); + assert!(PdFileVersion::new(1, 2) != [1]); + assert!(PdFileVersion::new(2, 3) > [1, 10]); + assert!(PdFileVersion::new(3, 1) < [10, 1]); + assert!(PdFileVersion::new(3, 3) >= [3, 3]); + assert!(!(PdFileVersion::new(2, 3) > [1])) +} diff --git a/src/ext/try_err.rs b/src/ext/try_err.rs index f098aa6..13a1c80 100644 --- a/src/ext/try_err.rs +++ b/src/ext/try_err.rs @@ -1,14 +1,29 @@ +/// Try with custom error message +pub trait TryErr2 { + /// try with custom error message + fn try_err2(&self, err: E) -> Result; +} + /// Try with custom error message pub trait TryErr { /// try with custom error message - fn try_err(&self, err: E) -> Result; + fn try_err(self, err: E) -> Result; } -impl, E> TryErr for Option { - fn try_err(&self, v: E) -> Result { +impl, E> TryErr2 for Option { + fn try_err2(&self, v: E) -> Result { match self { Some(r) => { Ok(r.to_owned()) } None => { Err(v) } } } } + +impl TryErr for Option { + fn try_err(self, err: E) -> Result { + match self { + Some(v) => { Ok(v) } + None => { Err(err) } + } + } +} diff --git a/src/main.rs b/src/main.rs index 76804a6..6aab259 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ mod avdict; mod cookies; mod data; mod download; +mod downloader; mod dur; #[cfg(feature = "exif")] /// Used to read/modify image's exif data