295 lines
8.3 KiB
Rust
295 lines
8.3 KiB
Rust
|
use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError};
|
||
|
use chrono::prelude::*;
|
||
|
use hex_string::HexString;
|
||
|
use image::imageops::FilterType;
|
||
|
use sha2::{Digest, Sha256};
|
||
|
use std::{
|
||
|
convert::TryFrom,
|
||
|
io::{Read, Write},
|
||
|
path::{Path, PathBuf},
|
||
|
};
|
||
|
use thiserror::Error;
|
||
|
use uuid::Uuid;
|
||
|
|
||
|
#[derive(Debug, Error)]
|
||
|
pub enum PathError {
|
||
|
#[error("path cannot be derived from input")]
|
||
|
InvalidPath,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct PathResolver {
|
||
|
base: PathBuf,
|
||
|
id: FileId,
|
||
|
}
|
||
|
|
||
|
impl PathResolver {
|
||
|
pub fn new(base: &Path, id: FileId) -> Self {
|
||
|
Self {
|
||
|
base: base.to_owned(),
|
||
|
id,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn id(&self) -> FileId {
|
||
|
self.id.clone()
|
||
|
}
|
||
|
|
||
|
pub fn file_path(&self) -> Result<PathBuf, ReadFileError> {
|
||
|
let info = FileInfo::load(self.metadata_path())?;
|
||
|
|
||
|
let mut path = self.base.clone();
|
||
|
path.push(PathBuf::from(self.id.clone()));
|
||
|
path.set_extension(info.extension);
|
||
|
Ok(path)
|
||
|
}
|
||
|
|
||
|
pub fn metadata_path(&self) -> PathBuf {
|
||
|
let mut path = self.base.clone();
|
||
|
path.push(PathBuf::from(self.id.clone()));
|
||
|
path.set_extension("json");
|
||
|
path
|
||
|
}
|
||
|
|
||
|
pub fn thumbnail_path(&self) -> Result<PathBuf, ReadFileError> {
|
||
|
let info = FileInfo::load(self.metadata_path())?;
|
||
|
|
||
|
let mut path = self.base.clone();
|
||
|
path.push(PathBuf::from(self.id.clone()));
|
||
|
path.set_extension(format!("tn.{}", info.extension));
|
||
|
Ok(path)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl TryFrom<String> for PathResolver {
|
||
|
type Error = PathError;
|
||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||
|
PathResolver::try_from(s.as_str())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl TryFrom<&str> for PathResolver {
|
||
|
type Error = PathError;
|
||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||
|
let path = Path::new(s);
|
||
|
PathResolver::try_from(Path::new(s))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl TryFrom<PathBuf> for PathResolver {
|
||
|
type Error = PathError;
|
||
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||
|
PathResolver::try_from(path.as_path())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl TryFrom<&Path> for PathResolver {
|
||
|
type Error = PathError;
|
||
|
fn try_from(path: &Path) -> Result<Self, Self::Error> {
|
||
|
Ok(Self {
|
||
|
base: path
|
||
|
.parent()
|
||
|
.map(|s| s.to_owned())
|
||
|
.ok_or(PathError::InvalidPath)?,
|
||
|
id: path
|
||
|
.file_stem()
|
||
|
.and_then(|s| s.to_str().map(|s| FileId::from(s)))
|
||
|
.ok_or(PathError::InvalidPath)?,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// One file in the database, complete with the path of the file and information about the
|
||
|
/// thumbnail of the file.
|
||
|
#[derive(Debug)]
|
||
|
pub struct FileHandle {
|
||
|
pub id: FileId,
|
||
|
pub path: PathResolver,
|
||
|
pub info: FileInfo,
|
||
|
}
|
||
|
|
||
|
impl FileHandle {
|
||
|
/// Create a new entry in the database
|
||
|
pub fn new(filename: String, root: PathBuf) -> Result<Self, WriteFileError> {
|
||
|
let id = FileId::from(Uuid::new_v4().hyphenated().to_string());
|
||
|
|
||
|
let extension = PathBuf::from(filename)
|
||
|
.extension()
|
||
|
.and_then(|s| s.to_str().map(|s| s.to_owned()))
|
||
|
.ok_or(WriteFileError::InvalidPath)?;
|
||
|
let path = PathResolver {
|
||
|
base: root.clone(),
|
||
|
id: id.clone(),
|
||
|
};
|
||
|
|
||
|
let file_type = mime_guess::from_ext(&extension)
|
||
|
.first_or_text_plain()
|
||
|
.essence_str()
|
||
|
.to_owned();
|
||
|
|
||
|
let info = FileInfo {
|
||
|
id: id.clone(),
|
||
|
size: 0,
|
||
|
created: Utc::now(),
|
||
|
file_type,
|
||
|
hash: "".to_owned(),
|
||
|
extension,
|
||
|
};
|
||
|
|
||
|
let mut md_file = std::fs::File::create(path.metadata_path())?;
|
||
|
md_file.write(&serde_json::to_vec(&info)?)?;
|
||
|
|
||
|
Ok(Self { id, path, info })
|
||
|
}
|
||
|
|
||
|
pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> {
|
||
|
let resolver = PathResolver::new(root, id.clone());
|
||
|
let info = FileInfo::load(resolver.metadata_path())?;
|
||
|
Ok(Self {
|
||
|
id: info.id.clone(),
|
||
|
path: resolver,
|
||
|
info,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> {
|
||
|
let mut content_file = std::fs::File::create(
|
||
|
self.path
|
||
|
.file_path()
|
||
|
.map_err(|_| WriteFileError::NoMetadata)?,
|
||
|
)?;
|
||
|
let byte_count = content_file.write(&content)?;
|
||
|
self.info.size = byte_count;
|
||
|
|
||
|
let mut md_file = std::fs::File::create(self.path.metadata_path())?;
|
||
|
md_file.write(&serde_json::to_vec(&self.info)?)?;
|
||
|
|
||
|
self.write_thumbnail();
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
|
||
|
load_content(&self.path.file_path()?)
|
||
|
}
|
||
|
|
||
|
pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> {
|
||
|
load_content(&self.path.thumbnail_path()?)
|
||
|
}
|
||
|
|
||
|
fn hash_content(&self) -> Result<HexString, ReadFileError> {
|
||
|
let mut buf = Vec::new();
|
||
|
let mut file = std::fs::File::open(self.path.file_path()?)?;
|
||
|
file.read_to_end(&mut buf).map_err(ReadFileError::from)?;
|
||
|
|
||
|
Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec()))
|
||
|
}
|
||
|
|
||
|
fn write_thumbnail(&self) -> Result<(), WriteFileError> {
|
||
|
let img = image::open(
|
||
|
&self
|
||
|
.path
|
||
|
.file_path()
|
||
|
.map_err(|_| WriteFileError::NoMetadata)?,
|
||
|
)?;
|
||
|
let tn = img.resize(640, 640, FilterType::Nearest);
|
||
|
tn.save(
|
||
|
&self
|
||
|
.path
|
||
|
.thumbnail_path()
|
||
|
.map_err(|_| WriteFileError::NoMetadata)?,
|
||
|
)?;
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn delete(self) -> Result<(), WriteFileError> {
|
||
|
std::fs::remove_file(
|
||
|
self.path
|
||
|
.thumbnail_path()
|
||
|
.map_err(|_| WriteFileError::NoMetadata)?,
|
||
|
)?;
|
||
|
std::fs::remove_file(self.path.metadata_path())?;
|
||
|
std::fs::remove_file(
|
||
|
self.path
|
||
|
.file_path()
|
||
|
.map_err(|_| WriteFileError::NoMetadata)?,
|
||
|
)?;
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn load_content(path: &Path) -> Result<Vec<u8>, ReadFileError> {
|
||
|
let mut buf = Vec::new();
|
||
|
let mut file = std::fs::File::open(&path)?;
|
||
|
file.read_to_end(&mut buf)?;
|
||
|
Ok(buf)
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod test {
|
||
|
use super::*;
|
||
|
use crate::store::utils::FileCleanup;
|
||
|
use cool_asserts::assert_matches;
|
||
|
use std::{convert::TryFrom, path::PathBuf};
|
||
|
|
||
|
#[test]
|
||
|
fn paths() {
|
||
|
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
|
||
|
.expect("to have a valid path");
|
||
|
assert_matches!(
|
||
|
resolver.file_path(),
|
||
|
Ok(path) => assert_eq!(path, PathBuf::from(
|
||
|
"path/82420255-d3c8-4d90-a582-f94be588c70c.png"
|
||
|
))
|
||
|
);
|
||
|
assert_eq!(
|
||
|
resolver.metadata_path(),
|
||
|
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json")
|
||
|
);
|
||
|
assert_matches!(
|
||
|
resolver.thumbnail_path(),
|
||
|
Ok(path) => assert_eq!(path, PathBuf::from(
|
||
|
"path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png"
|
||
|
))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn it_opens_a_file() {
|
||
|
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json"));
|
||
|
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
|
||
|
|
||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn it_can_return_a_thumbnail() {
|
||
|
let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
|
||
|
/*
|
||
|
assert_eq!(
|
||
|
f.thumbnail(),
|
||
|
Thumbnail {
|
||
|
id: String::from("rawr.png"),
|
||
|
root: PathBuf::from("var/"),
|
||
|
},
|
||
|
);
|
||
|
*/
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn it_can_return_a_file_stream() {
|
||
|
let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
|
||
|
// f.stream().expect("to succeed");
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn it_raises_an_error_when_file_not_found() {
|
||
|
let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path");
|
||
|
match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) {
|
||
|
Err(ReadFileError::FileNotFound(_)) => assert!(true),
|
||
|
_ => assert!(false),
|
||
|
}
|
||
|
}
|
||
|
}
|