2023-09-23 23:15:56 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2023-10-04 19:57:18 +00:00
|
|
|
use std::{collections::HashSet, ops::Deref, path::PathBuf};
|
2023-09-23 00:03:58 +00:00
|
|
|
use thiserror::Error;
|
2023-09-19 22:55:53 +00:00
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
mod filehandle;
|
2023-09-19 22:55:53 +00:00
|
|
|
mod fileinfo;
|
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
pub use filehandle::FileHandle;
|
2023-09-19 22:55:53 +00:00
|
|
|
pub use fileinfo::FileInfo;
|
|
|
|
|
2023-09-23 00:03:58 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum WriteFileError {
|
|
|
|
#[error("root file path does not exist")]
|
|
|
|
RootNotFound,
|
|
|
|
|
|
|
|
#[error("permission denied")]
|
|
|
|
PermissionDenied,
|
|
|
|
|
2023-09-23 19:17:49 +00:00
|
|
|
#[error("invalid path")]
|
|
|
|
InvalidPath,
|
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
#[error("no metadata available")]
|
|
|
|
NoMetadata,
|
|
|
|
|
|
|
|
#[error("file could not be loaded")]
|
|
|
|
LoadError(#[from] ReadFileError),
|
|
|
|
|
2023-09-23 19:17:49 +00:00
|
|
|
#[error("image conversion failed")]
|
|
|
|
ImageError(#[from] image::ImageError),
|
|
|
|
|
2023-09-23 00:03:58 +00:00
|
|
|
#[error("JSON error")]
|
|
|
|
JSONError(#[from] serde_json::error::Error),
|
|
|
|
|
|
|
|
#[error("IO error")]
|
|
|
|
IOError(#[from] std::io::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ReadFileError {
|
|
|
|
#[error("file not found")]
|
2023-09-23 23:15:56 +00:00
|
|
|
FileNotFound(PathBuf),
|
2023-09-23 00:03:58 +00:00
|
|
|
|
|
|
|
#[error("path is not a file")]
|
|
|
|
NotAFile,
|
|
|
|
|
|
|
|
#[error("permission denied")]
|
|
|
|
PermissionDenied,
|
|
|
|
|
|
|
|
#[error("JSON error")]
|
|
|
|
JSONError(#[from] serde_json::error::Error),
|
|
|
|
|
|
|
|
#[error("IO error")]
|
|
|
|
IOError(#[from] std::io::Error),
|
|
|
|
}
|
|
|
|
|
2023-10-26 02:54:05 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum DeleteFileError {
|
|
|
|
#[error("file not found")]
|
|
|
|
FileNotFound(PathBuf),
|
|
|
|
|
|
|
|
#[error("metadata path is not a file")]
|
|
|
|
NotAFile,
|
|
|
|
|
|
|
|
#[error("cannot read metadata")]
|
|
|
|
PermissionDenied,
|
|
|
|
|
|
|
|
#[error("invalid metadata path")]
|
|
|
|
MetadataParseError(serde_json::error::Error),
|
|
|
|
|
|
|
|
#[error("IO error")]
|
|
|
|
IOError(#[from] std::io::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ReadFileError> for DeleteFileError {
|
|
|
|
fn from(err: ReadFileError) -> Self {
|
|
|
|
match err {
|
|
|
|
ReadFileError::FileNotFound(path) => DeleteFileError::FileNotFound(path),
|
|
|
|
ReadFileError::NotAFile => DeleteFileError::NotAFile,
|
|
|
|
ReadFileError::PermissionDenied => DeleteFileError::PermissionDenied,
|
|
|
|
ReadFileError::JSONError(err) => DeleteFileError::MetadataParseError(err),
|
|
|
|
ReadFileError::IOError(err) => DeleteFileError::IOError(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
2023-09-23 00:03:58 +00:00
|
|
|
pub struct FileId(String);
|
|
|
|
|
|
|
|
impl From<String> for FileId {
|
|
|
|
fn from(s: String) -> Self {
|
|
|
|
Self(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&str> for FileId {
|
|
|
|
fn from(s: &str) -> Self {
|
|
|
|
Self(s.to_owned())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-24 00:20:35 +00:00
|
|
|
impl From<FileId> for PathBuf {
|
|
|
|
fn from(s: FileId) -> Self {
|
|
|
|
Self::from(&s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&FileId> for PathBuf {
|
|
|
|
fn from(s: &FileId) -> Self {
|
|
|
|
let FileId(s) = s;
|
|
|
|
Self::from(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 00:03:58 +00:00
|
|
|
impl Deref for FileId {
|
|
|
|
type Target = String;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-19 18:44:20 +00:00
|
|
|
/*
|
2023-09-23 00:03:58 +00:00
|
|
|
pub trait FileRoot {
|
|
|
|
fn root(&self) -> PathBuf;
|
|
|
|
}
|
2025-01-19 18:44:20 +00:00
|
|
|
*/
|
2023-09-23 00:03:58 +00:00
|
|
|
|
2025-01-19 18:44:20 +00:00
|
|
|
// pub struct Context(PathBuf);
|
2023-09-23 00:03:58 +00:00
|
|
|
|
2025-01-19 18:44:20 +00:00
|
|
|
/*
|
2023-09-23 00:03:58 +00:00
|
|
|
impl FileRoot for Context {
|
|
|
|
fn root(&self) -> PathBuf {
|
|
|
|
self.0.clone()
|
|
|
|
}
|
|
|
|
}
|
2025-01-19 18:44:20 +00:00
|
|
|
*/
|
2023-09-23 00:03:58 +00:00
|
|
|
|
2023-09-23 03:43:45 +00:00
|
|
|
pub struct Store {
|
2023-09-19 22:55:53 +00:00
|
|
|
files_root: PathBuf,
|
|
|
|
}
|
|
|
|
|
2023-09-23 03:43:45 +00:00
|
|
|
impl Store {
|
|
|
|
pub fn new(files_root: PathBuf) -> Self {
|
|
|
|
Self { files_root }
|
2023-09-19 22:55:53 +00:00
|
|
|
}
|
|
|
|
|
2023-09-25 03:52:29 +00:00
|
|
|
pub fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
|
|
|
|
let paths = std::fs::read_dir(&self.files_root)?;
|
2023-09-25 04:17:34 +00:00
|
|
|
let info_files = paths
|
2023-09-25 03:52:29 +00:00
|
|
|
.into_iter()
|
2023-09-25 04:17:34 +00:00
|
|
|
.filter_map(|path| {
|
|
|
|
let path_ = path.unwrap().path();
|
|
|
|
if path_.extension().and_then(|s| s.to_str()) == Some("json") {
|
|
|
|
let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap();
|
2023-10-04 19:57:18 +00:00
|
|
|
Some(FileId::from(stem))
|
2023-09-25 04:17:34 +00:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2023-09-25 03:52:29 +00:00
|
|
|
})
|
2023-09-25 04:17:34 +00:00
|
|
|
.collect::<HashSet<FileId>>();
|
|
|
|
Ok(info_files)
|
2023-09-19 22:55:53 +00:00
|
|
|
}
|
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
pub fn add_file(
|
|
|
|
&mut self,
|
|
|
|
filename: String,
|
|
|
|
content: Vec<u8>,
|
|
|
|
) -> Result<FileHandle, WriteFileError> {
|
|
|
|
let mut file = FileHandle::new(filename, self.files_root.clone())?;
|
2023-09-23 00:03:58 +00:00
|
|
|
file.set_content(content)?;
|
|
|
|
Ok(file)
|
2023-09-19 22:55:53 +00:00
|
|
|
}
|
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
|
|
|
|
FileHandle::load(id, &self.files_root)
|
|
|
|
}
|
|
|
|
|
2023-10-26 02:54:05 +00:00
|
|
|
pub fn delete_file(&mut self, id: &FileId) -> Result<(), DeleteFileError> {
|
2023-09-24 16:08:09 +00:00
|
|
|
let handle = FileHandle::load(id, &self.files_root)?;
|
|
|
|
handle.delete();
|
|
|
|
Ok(())
|
2023-09-19 22:55:53 +00:00
|
|
|
}
|
|
|
|
|
2023-09-24 00:20:35 +00:00
|
|
|
pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
|
2023-09-23 23:15:56 +00:00
|
|
|
let mut path = self.files_root.clone();
|
2023-09-24 00:20:35 +00:00
|
|
|
path.push(PathBuf::from(id));
|
2023-09-23 23:15:56 +00:00
|
|
|
path.set_extension("json");
|
|
|
|
FileInfo::load(path)
|
2023-09-19 22:55:53 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-23 01:56:43 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2023-09-25 04:58:35 +00:00
|
|
|
use super::*;
|
2023-09-23 23:15:56 +00:00
|
|
|
use cool_asserts::assert_matches;
|
2023-09-24 16:08:09 +00:00
|
|
|
use std::{collections::HashSet, io::Read};
|
2023-09-25 04:58:35 +00:00
|
|
|
use tempdir::TempDir;
|
2023-09-23 01:56:43 +00:00
|
|
|
|
2023-09-24 00:20:35 +00:00
|
|
|
fn with_file<F>(test_fn: F)
|
|
|
|
where
|
2023-09-25 04:58:35 +00:00
|
|
|
F: FnOnce(Store, FileId, TempDir),
|
2023-09-24 00:20:35 +00:00
|
|
|
{
|
2023-09-25 04:58:35 +00:00
|
|
|
let tmp = TempDir::new("var").unwrap();
|
2023-09-25 03:52:29 +00:00
|
|
|
|
2023-09-24 00:20:35 +00:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
|
|
|
|
file.read_to_end(&mut buf).unwrap();
|
|
|
|
|
2023-09-25 04:58:35 +00:00
|
|
|
let mut store = Store::new(PathBuf::from(tmp.path()));
|
2023-09-24 00:20:35 +00:00
|
|
|
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
|
|
|
|
2023-09-25 04:58:35 +00:00
|
|
|
test_fn(store, file_record.id, tmp);
|
2023-09-24 00:20:35 +00:00
|
|
|
}
|
|
|
|
|
2023-09-23 03:43:45 +00:00
|
|
|
#[test]
|
|
|
|
fn adds_files() {
|
2023-09-25 04:58:35 +00:00
|
|
|
with_file(|store, id, tmp| {
|
2023-09-24 16:08:09 +00:00
|
|
|
let file = store.get_file(&id).expect("to retrieve the file");
|
2023-09-23 03:43:45 +00:00
|
|
|
|
2023-09-24 00:20:35 +00:00
|
|
|
assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
|
2023-09-23 23:15:56 +00:00
|
|
|
|
2023-09-25 04:58:35 +00:00
|
|
|
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
|
|
|
|
assert!(tmp.path().join(&(*id)).with_extension("json").exists());
|
|
|
|
assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists());
|
2023-09-24 00:20:35 +00:00
|
|
|
});
|
2023-09-23 03:43:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2023-09-23 23:15:56 +00:00
|
|
|
fn sets_up_metadata_for_file() {
|
2023-09-25 04:58:35 +00:00
|
|
|
with_file(|store, id, tmp| {
|
|
|
|
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
|
2023-09-24 00:20:35 +00:00
|
|
|
let info = store.get_metadata(&id).expect("to retrieve the metadata");
|
|
|
|
|
|
|
|
assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
|
|
|
|
assert_eq!(size, 23777);
|
|
|
|
assert_eq!(file_type, "image/png");
|
2023-09-25 04:58:35 +00:00
|
|
|
assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned());
|
2023-09-24 00:20:35 +00:00
|
|
|
assert_eq!(extension, "png".to_owned());
|
|
|
|
});
|
2023-09-23 23:15:56 +00:00
|
|
|
});
|
|
|
|
}
|
2023-09-23 03:43:45 +00:00
|
|
|
|
2023-09-24 16:08:09 +00:00
|
|
|
/*
|
2023-09-23 03:43:45 +00:00
|
|
|
#[test]
|
2023-09-24 00:20:35 +00:00
|
|
|
fn sets_up_thumbnail_for_file() {
|
|
|
|
with_file(|store, id| {
|
|
|
|
let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail");
|
|
|
|
assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869);
|
|
|
|
});
|
|
|
|
}
|
2023-09-24 16:08:09 +00:00
|
|
|
*/
|
2023-09-23 03:43:45 +00:00
|
|
|
|
|
|
|
#[test]
|
2023-09-24 00:20:35 +00:00
|
|
|
fn deletes_associated_files() {
|
2023-09-25 04:58:35 +00:00
|
|
|
with_file(|mut store, id, tmp| {
|
2023-09-24 00:20:35 +00:00
|
|
|
store.delete_file(&id).expect("file to be deleted");
|
|
|
|
|
2023-09-25 04:58:35 +00:00
|
|
|
assert!(!tmp.path().join(&(*id)).with_extension("png").exists());
|
|
|
|
assert!(!tmp.path().join(&(*id)).with_extension("json").exists());
|
|
|
|
assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists());
|
2023-09-24 00:20:35 +00:00
|
|
|
});
|
|
|
|
}
|
2023-09-23 03:43:45 +00:00
|
|
|
|
|
|
|
#[test]
|
2023-09-24 16:08:09 +00:00
|
|
|
fn lists_files_in_the_db() {
|
2023-09-25 04:58:35 +00:00
|
|
|
with_file(|store, id, _| {
|
2023-09-24 16:08:09 +00:00
|
|
|
let resolvers = store.list_files().expect("file listing to succeed");
|
|
|
|
let ids = resolvers.into_iter().collect::<HashSet<FileId>>();
|
|
|
|
|
|
|
|
assert_eq!(ids.len(), 1);
|
2023-09-25 04:17:34 +00:00
|
|
|
assert!(ids.contains(&id));
|
2023-09-24 16:08:09 +00:00
|
|
|
});
|
|
|
|
}
|
2023-09-23 01:56:43 +00:00
|
|
|
}
|