use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use std::os::unix::ffi::OsStrExt;
use crate::{
CacheRootProblem, PersonalOriginalUri, Result, SharedRelativeOriginalUri, ThumbnailError,
ThumbnailSize, validate_mime_type,
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UnixMtimeSeconds {
seconds: u64,
}
impl UnixMtimeSeconds {
#[must_use]
pub const fn new(seconds: u64) -> Self {
Self { seconds }
}
pub const fn try_from_i64(seconds: i64) -> Result<Self> {
if seconds < 0 {
return Err(ThumbnailError::invalid_metadata(
"mtime is before the Unix epoch",
));
}
Ok(Self {
seconds: seconds as u64,
})
}
pub fn from_system_time(time: SystemTime) -> Result<Self> {
let duration = time
.duration_since(UNIX_EPOCH)
.map_err(|_| ThumbnailError::invalid_metadata("mtime is before the Unix epoch"))?;
Ok(Self {
seconds: duration.as_secs(),
})
}
#[must_use]
pub const fn as_u64(self) -> u64 {
self.seconds
}
}
impl TryFrom<i64> for UnixMtimeSeconds {
type Error = ThumbnailError;
fn try_from(seconds: i64) -> Result<Self> {
Self::try_from_i64(seconds)
}
}
impl From<u64> for UnixMtimeSeconds {
fn from(seconds: u64) -> Self {
Self::new(seconds)
}
}
impl From<UnixMtimeSeconds> for u64 {
fn from(mtime: UnixMtimeSeconds) -> Self {
mtime.as_u64()
}
}
impl TryFrom<SystemTime> for UnixMtimeSeconds {
type Error = ThumbnailError;
fn try_from(time: SystemTime) -> Result<Self> {
Self::from_system_time(time)
}
}
impl fmt::Display for UnixMtimeSeconds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.seconds)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PersonalOriginalIdentity {
uri: PersonalOriginalUri,
mtime: UnixMtimeSeconds,
original_byte_size: Option<u64>,
mime_type: Option<String>,
}
impl PersonalOriginalIdentity {
#[must_use]
pub fn new(uri: PersonalOriginalUri, mtime: UnixMtimeSeconds) -> Self {
Self {
uri,
mtime,
original_byte_size: None,
mime_type: None,
}
}
#[must_use]
pub fn with_original_byte_size(mut self, size: u64) -> Self {
self.original_byte_size = Some(size);
self
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Result<Self> {
let mime_type = mime_type.into();
validate_mime_type(&mime_type)?;
self.mime_type = Some(mime_type);
Ok(self)
}
#[must_use]
pub fn uri(&self) -> &PersonalOriginalUri {
&self.uri
}
#[must_use]
pub const fn mtime(&self) -> UnixMtimeSeconds {
self.mtime
}
#[must_use]
pub const fn original_byte_size(&self) -> Option<u64> {
self.original_byte_size
}
#[must_use]
pub fn mime_type(&self) -> Option<&str> {
self.mime_type.as_deref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReadablePersonalOriginalIdentity {
identity: PersonalOriginalIdentity,
}
impl ReadablePersonalOriginalIdentity {
#[must_use]
pub fn assume_readable(identity: PersonalOriginalIdentity) -> Self {
Self { identity }
}
#[must_use]
pub fn into_identity(self) -> PersonalOriginalIdentity {
self.identity
}
pub fn from_local_path(path: impl AsRef<Path>) -> Result<Self> {
Self::from_local_path_inner(path.as_ref(), None)
}
pub fn from_local_path_with_mime_type(
path: impl AsRef<Path>,
mime_type: impl Into<String>,
) -> Result<Self> {
Self::from_local_path_inner(path.as_ref(), Some(mime_type.into()))
}
fn from_local_path_inner(path: &Path, mime_type: Option<String>) -> Result<Self> {
if !path.is_absolute() {
return Err(ThumbnailError::invalid_uri("local path must be absolute"));
}
let file = File::open(path).map_err(|source| {
ThumbnailError::io("open original for reading", Some(path.to_owned()), source)
})?;
let metadata = file.metadata().map_err(|source| {
ThumbnailError::io("read original metadata", Some(path.to_owned()), source)
})?;
let uri = PersonalOriginalUri::from_absolute_path_bytes(path.as_os_str().as_bytes())?;
let mtime = UnixMtimeSeconds::from_system_time(metadata.modified().map_err(|source| {
ThumbnailError::io(
"read original modification time",
Some(path.to_owned()),
source,
)
})?)?;
let identity =
PersonalOriginalIdentity::new(uri, mtime).with_original_byte_size(metadata.len());
let identity = if let Some(mime_type) = mime_type {
identity.with_mime_type(mime_type)?
} else {
identity
};
Ok(Self { identity })
}
#[must_use]
pub const fn identity(&self) -> &PersonalOriginalIdentity {
&self.identity
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SharedRepositoryContext {
repository_root: PathBuf,
original_child_name: OsString,
shared_uri: SharedRelativeOriginalUri,
}
impl SharedRepositoryContext {
pub fn new(
repository_root: impl AsRef<Path>,
original_child_name: impl AsRef<OsStr>,
) -> Result<Self> {
let repository_root = repository_root.as_ref();
let original_child_name = original_child_name.as_ref();
if !repository_root.is_absolute() {
return Err(ThumbnailError::InvalidCacheRoot {
path: repository_root.to_owned(),
problem: CacheRootProblem::NotAbsolute,
});
}
let shared_uri =
SharedRelativeOriginalUri::from_raw_child_name(original_child_name.as_bytes())?;
Ok(Self {
repository_root: repository_root.to_owned(),
original_child_name: original_child_name.to_owned(),
shared_uri,
})
}
#[must_use]
pub fn repository_root(&self) -> &Path {
&self.repository_root
}
#[must_use]
pub fn original_child_name(&self) -> &OsStr {
&self.original_child_name
}
#[must_use]
pub const fn shared_uri(&self) -> &SharedRelativeOriginalUri {
&self.shared_uri
}
#[must_use]
pub fn cache_entry_path(&self, size: ThumbnailSize) -> PathBuf {
self.repository_root
.join(".sh_thumbnails")
.join(size.directory_name())
.join(self.shared_uri.thumbnail_file_name())
}
}