monorepo/file-service/src/store/filehandle.rs

299 lines
8.3 KiB
Rust
Raw Normal View History

2023-09-24 16:08:09 +00:00
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> {
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;
2023-09-25 03:52:29 +00:00
self.info.hash = self.hash_content(&content).as_string();
2023-09-24 16:08:09 +00:00
let mut md_file = std::fs::File::create(self.path.metadata_path())?;
md_file.write(&serde_json::to_vec(&self.info)?)?;
2023-09-25 03:52:29 +00:00
self.write_thumbnail()?;
2023-09-24 16:08:09 +00:00
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()?)
}
2023-09-25 03:52:29 +00:00
fn hash_content(&self, data: &Vec<u8>) -> HexString {
HexString::from_bytes(&Sha256::digest(data).to_vec())
2023-09-24 16:08:09 +00:00
}
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(())
}
2023-09-25 03:52:29 +00:00
pub fn delete(self) {
match self.path.thumbnail_path() {
Ok(path) => {
let _ = std::fs::remove_file(path);
}
Err(_) => {}
};
match self.path.file_path() {
Ok(path) => {
let _ = std::fs::remove_file(path);
}
Err(_) => {}
};
let _ = std::fs::remove_file(self.path.metadata_path());
2023-09-24 16:08:09 +00:00
}
}
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::*;
2023-09-25 03:52:29 +00:00
use crate::store::utils::DirCleanup;
2023-09-24 16:08:09 +00:00
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");
2023-09-25 03:52:29 +00:00
2023-09-24 16:08:09 +00:00
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() {
2023-09-25 03:52:29 +00:00
let _cleanup = DirCleanup(PathBuf::from("var/"));
2023-09-24 16:08:09 +00:00
FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
}
#[test]
2023-09-25 03:52:29 +00:00
fn it_deletes_a_file() {
let _cleanup = DirCleanup(PathBuf::from("var/"));
2023-09-24 16:08:09 +00:00
let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
2023-09-25 03:52:29 +00:00
f.delete();
}
#[test]
fn it_can_return_a_thumbnail() {
let _cleanup = DirCleanup(PathBuf::from("var/"));
let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
2023-09-24 16:08:09 +00:00
/*
assert_eq!(
f.thumbnail(),
Thumbnail {
id: String::from("rawr.png"),
root: PathBuf::from("var/"),
},
);
*/
}
#[test]
fn it_can_return_a_file_stream() {
2023-09-25 03:52:29 +00:00
let _cleanup = DirCleanup(PathBuf::from("var/"));
let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed");
2023-09-24 16:08:09 +00:00
// f.stream().expect("to succeed");
}
#[test]
fn it_raises_an_error_when_file_not_found() {
2023-09-25 03:52:29 +00:00
let _cleanup = DirCleanup(PathBuf::from("var/"));
2023-09-24 16:08:09 +00:00
match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) {
Err(ReadFileError::FileNotFound(_)) => assert!(true),
_ => assert!(false),
}
}
}