Clean up the filehandle logic

This commit is contained in:
Savanni D'Gerinel 2023-09-24 12:08:09 -04:00
parent da6bf3bfea
commit 68b62464f0
7 changed files with 366 additions and 499 deletions

View File

@ -30,7 +30,7 @@ mod middleware;
mod pages; mod pages;
mod store; mod store;
pub use store::{File, FileId, FileInfo, HasContent, Store}; pub use store::{FileHandle, FileId, FileInfo, ReadFileError, Store};
/* /*
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
@ -228,19 +228,18 @@ fn script(_: &mut Request) -> IronResult<Response> {
*/ */
fn serve_file( fn serve_file(
info: FileInfo, file: FileHandle,
file: impl HasContent,
old_etags: Option<String>, old_etags: Option<String>,
) -> http::Result<http::Response<Vec<u8>>> { ) -> http::Result<http::Response<Vec<u8>>> {
match old_etags { match old_etags {
Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() Some(old_etags) if old_etags != file.info.hash => warp::http::Response::builder()
.header("content-type", info.file_type) .header("content-type", file.info.file_type)
.status(StatusCode::NOT_MODIFIED) .status(StatusCode::NOT_MODIFIED)
.body(vec![]), .body(vec![]),
_ => match file.content() { _ => match file.content() {
Ok(content) => warp::http::Response::builder() Ok(content) => warp::http::Response::builder()
.header("content-type", info.file_type) .header("content-type", file.info.file_type)
.header("etag", info.hash) .header("etag", file.info.hash)
.status(StatusCode::OK) .status(StatusCode::OK)
.body(content), .body(content),
Err(_) => warp::http::Response::builder() Err(_) => warp::http::Response::builder()
@ -331,10 +330,23 @@ pub async fn main() {
let app = app.clone(); let app = app.clone();
move || { move || {
info!("root handler"); info!("root handler");
let app = app.read().unwrap();
match app.list_files() {
Ok(ids) => {
let files = ids
.into_iter()
.map(|id| app.get_file(&id))
.collect::<Vec<Result<FileHandle, ReadFileError>>>();
warp::http::Response::builder() warp::http::Response::builder()
.header("content-type", "text/html") .header("content-type", "text/html")
.status(StatusCode::OK) .status(StatusCode::OK)
.body(pages::index(app.read().unwrap().list_files()).to_html_string()) .body(pages::index(files).to_html_string())
}
Err(_) => warp::http::Response::builder()
.header("content-type", "text/html")
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("".to_owned()),
}
} }
}); });
@ -360,9 +372,9 @@ pub async fn main() {
move |id: String, old_etags: Option<String>| match app move |id: String, old_etags: Option<String>| match app
.read() .read()
.unwrap() .unwrap()
.get_thumbnail(&FileId::from(id)) .get_file(&FileId::from(id))
{ {
Ok((info, file)) => serve_file(info, file, old_etags), Ok(file) => serve_file(file, old_etags),
Err(_err) => warp::http::Response::builder() Err(_err) => warp::http::Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body(vec![]), .body(vec![]),
@ -379,7 +391,7 @@ pub async fn main() {
.unwrap() .unwrap()
.get_file(&FileId::from(id)) .get_file(&FileId::from(id))
{ {
Ok((info, file)) => serve_file(info, file, old_etags), Ok(file) => serve_file(file, old_etags),
Err(_err) => warp::http::Response::builder() Err(_err) => warp::http::Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body(vec![]), .body(vec![]),

View File

@ -1,10 +1,10 @@
use crate::{ use crate::{
html::*, html::*,
store::{File, ReadFileError}, store::{FileHandle, FileId, ReadFileError, Thumbnail},
}; };
use build_html::{self, Container, ContainerType, Html, HtmlContainer}; use build_html::{self, Container, ContainerType, Html, HtmlContainer};
pub fn index(files: Vec<Result<File, ReadFileError>>) -> build_html::HtmlPage { pub fn index(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
let mut page = build_html::HtmlPage::new() let mut page = build_html::HtmlPage::new()
.with_title("Admin list of files") .with_title("Admin list of files")
.with_header(1, "Admin list of files") .with_header(1, "Admin list of files")
@ -20,11 +20,11 @@ pub fn index(files: Vec<Result<File, ReadFileError>>) -> build_html::HtmlPage {
.with_html(Button::new("Upload file")), .with_html(Button::new("Upload file")),
); );
for file in files { for handle in handles {
let container = match file { let container = match handle {
Ok(ref file) => thumbnail(file).with_html( Ok(ref handle) => thumbnail(&handle.id).with_html(
Form::new() Form::new()
.with_path(&format!("/{}", *file.id)) .with_path(&format!("/{}", *handle.id))
.with_method("post") .with_method("post")
.with_html(Input::new("hidden", "_method").with_value("delete")) .with_html(Input::new("hidden", "_method").with_value("delete"))
.with_html(Button::new("Delete")), .with_html(Button::new("Delete")),
@ -39,13 +39,13 @@ pub fn index(files: Vec<Result<File, ReadFileError>>) -> build_html::HtmlPage {
page page
} }
pub fn thumbnail(file: &File) -> Container { pub fn thumbnail(id: &FileId) -> Container {
let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]);
let tn = Container::new(ContainerType::Div) let tn = Container::new(ContainerType::Div)
.with_attributes(vec![("class", "thumbnail")]) .with_attributes(vec![("class", "thumbnail")])
.with_link( .with_link(
format!("/{}", *file.id), format!("/{}", **id),
Image::new(&format!("{}/tn", *file.id)).to_html_string(), Image::new(&format!("{}/tn", **id)).to_html_string(),
); );
container.add_html(tn); container.add_html(tn);
container container

View File

@ -1,252 +0,0 @@
use super::{
fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, HasContent, PathResolver,
ReadFileError, WriteFileError,
};
use chrono::prelude::*;
use hex_string::HexString;
use sha2::{Digest, Sha256};
use std::{
convert::TryFrom,
io::{Read, Write},
path::{Path, PathBuf},
};
use uuid::Uuid;
/// One file in the database, complete with the path of the file and information about the
/// thumbnail of the file.
#[derive(Debug)]
pub struct File {
pub id: FileId,
pub path: PathResolver,
pub info: FileInfo,
}
impl File {
/// Create a new entry in the database
pub fn new<CTX: FileRoot>(filename: String, context: CTX) -> Result<Self, WriteFileError> {
let id = FileId::from(Uuid::new_v4().hyphenated().to_string());
let mut path = context.root();
let extension = PathBuf::from(filename)
.extension()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(WriteFileError::InvalidPath)?;
path.push((*id).clone());
let path = PathResolver {
base: context.root().clone(),
id: (*id).clone(),
extension: extension.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(resolver: PathResolver) -> Result<Self, ReadFileError> {
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())?;
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)?)?;
Thumbnail::open(self.path.file_path(), self.path.thumbnail_path())?;
Ok(())
}
pub 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()))
}
/*
pub fn new(
id: &str,
root: &Path,
filename: &Option<PathBuf>,
) -> Result<Self, FileError> {
let mut dest_path = PathBuf::from(root);
dest_path.push(id);
match filename {
Some(fname) => match fname.extension() {
Some(ext) => {
dest_path.set_extension(ext);
()
}
None => (),
},
None => (),
};
copy(temp_path, dest_path.clone())?;
let info = FileInfo::from_path(&dest_path)?;
let tn = Thumbnail::from_path(&dest_path)?;
Ok(Self {
info,
tn,
root: PathBuf::from(root),
})
}
pub fn open(id: &str, root: &Path) -> Result<Self, FileError> {
let mut file_path = PathBuf::from(root);
file_path.push(id.clone());
if !file_path.exists() {
return Err(FileError::FileNotFound(file_path));
}
if !file_path.is_file() {
return Err(FileError::NotAFile(file_path));
}
let info = match FileInfo::open(id, root) {
Ok(i) => Ok(i),
Err(FileError::FileNotFound(_)) => {
let info = FileInfo::from_path(&file_path)?;
info.save(&root)?;
Ok(info)
}
Err(err) => Err(err),
}?;
let tn = Thumbnail::open(id, root)?;
Ok(Self {
info,
tn,
root: PathBuf::from(root),
})
}
pub fn list(root: &Path) -> Vec<Result<Self, FileError>> {
let dir_iter = read_dir(&root).unwrap();
dir_iter
.filter(|entry| {
let entry_ = entry.as_ref().unwrap();
let filename = entry_.file_name();
!(filename.to_string_lossy().starts_with("."))
})
.map(|entry| {
let entry_ = entry.unwrap();
let id = entry_.file_name().into_string().unwrap();
Self::open(&id, root)
})
.collect()
}
pub fn info(&self) -> FileInfo {
self.info.clone()
}
pub fn thumbnail(&self) -> Thumbnail {
self.tn.clone()
}
pub fn stream(&self) -> Result<std::fs::File, FileError> {
let mut path = self.root.clone();
path.push(self.info.id.clone());
std::fs::File::open(path).map_err(FileError::from)
}
pub fn delete(&self) -> Result<(), FileError> {
let mut path = self.root.clone();
path.push(self.info.id.clone());
remove_file(path)?;
self.tn.delete()?;
self.info.delete()
}
*/
}
impl HasContent for File {
fn content(&self) -> Result<Vec<u8>, ReadFileError> {
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(self.path.file_path())?;
file.read_to_end(&mut content)?;
Ok(content)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::utils::FileCleanup;
use std::{convert::TryFrom, path::PathBuf};
struct FileContext(PathBuf);
impl FileRoot for FileContext {
fn root(&self) -> PathBuf {
self.0.clone()
}
}
#[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"));
File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))).expect("to succeed");
}
#[test]
fn it_can_return_a_thumbnail() {
let f = File::new("rawr.png".to_owned(), FileContext(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 = File::new("rawr.png".to_owned(), FileContext(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 File::load(resolver) {
Err(ReadFileError::FileNotFound(_)) => assert!(true),
_ => assert!(false),
}
}
}

View File

@ -0,0 +1,294 @@
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),
}
}
}

View File

@ -36,72 +36,12 @@ impl FileInfo {
file.write(ser.as_bytes())?; file.write(ser.as_bytes())?;
Ok(()) Ok(())
} }
/*
pub fn from_path(path: &Path) -> Result<FileInfo, ReadFileError> {
match (path.is_file(), path.is_dir()) {
(false, false) => Err(ReadFileError::FileNotFound),
(false, true) => Err(ReadFileError::NotAFile),
(true, _) => Ok(()),
}?;
let metadata = path.metadata().map_err(ReadFileError::IOError)?;
let id = path
.file_name()
.map(|s| String::from(s.to_string_lossy()))
.ok_or(ReadFileError::NotAFile)?;
let created = metadata
.created()
.map(|m| DateTime::from(m))
.map_err(|err| ReadFileError::IOError(err))?;
let file_type = String::from(
mime_guess::from_path(path)
.first_or_octet_stream()
.essence_str(),
);
let hash = FileInfo::hash_file(path)?;
Ok(FileInfo {
id,
size: metadata.len(),
created,
file_type,
hash: hash.as_string(),
root: PathBuf::from(path.parent().unwrap()),
})
}
*/
/*
fn hash_file(path: &Path) -> Result<HexString, ReadFileError> {
let mut buf = Vec::new();
let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?;
file.read_to_end(&mut buf).map_err(ReadFileError::from)?;
let mut vec = Vec::new();
vec.extend_from_slice(Sha256::digest(&buf).as_slice());
Ok(HexString::from_bytes(&vec))
}
*/
/*
fn metadata_path(id: &str, root: &Path) -> PathBuf {
let mut path = PathBuf::from(root);
path.push(".metadata");
path.push(id.clone());
append_extension(&path, "json")
}
pub fn delete(&self) -> Result<(), FileError> {
let path = FileInfo::metadata_path(&self.id, &self.root);
remove_file(path).map_err(FileError::from)
}
*/
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::store::{utils::FileCleanup, FileId, PathResolver}; use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId};
use std::convert::TryFrom; use std::convert::TryFrom;
#[test] #[test]

View File

@ -1,18 +1,16 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
convert::TryFrom,
ops::Deref, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use thiserror::Error; use thiserror::Error;
use uuid::Uuid;
mod file; mod filehandle;
mod fileinfo; mod fileinfo;
mod thumbnail; mod thumbnail;
pub mod utils; pub mod utils;
pub use file::File; pub use filehandle::FileHandle;
pub use fileinfo::FileInfo; pub use fileinfo::FileInfo;
pub use thumbnail::Thumbnail; pub use thumbnail::Thumbnail;
@ -27,6 +25,12 @@ pub enum WriteFileError {
#[error("invalid path")] #[error("invalid path")]
InvalidPath, InvalidPath,
#[error("no metadata available")]
NoMetadata,
#[error("file could not be loaded")]
LoadError(#[from] ReadFileError),
#[error("image conversion failed")] #[error("image conversion failed")]
ImageError(#[from] image::ImageError), ImageError(#[from] image::ImageError),
@ -58,97 +62,7 @@ pub enum ReadFileError {
IOError(#[from] std::io::Error), IOError(#[from] std::io::Error),
} }
#[derive(Debug, Error)] #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub enum PathError {
#[error("path cannot be derived from input")]
InvalidPath,
}
pub trait HasContent {
fn content(&self) -> Result<Vec<u8>, ReadFileError>;
}
#[derive(Clone, Debug)]
pub struct PathResolver {
base: PathBuf,
id: String,
extension: String,
}
impl PathResolver {
fn new(base: &Path, id: &str, extension: &str) -> Self {
Self {
base: base.to_owned(),
id: id.to_owned(),
extension: extension.to_owned(),
}
}
fn file_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(self.id.clone());
path.set_extension(self.extension.clone());
path
}
fn metadata_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(self.id.clone());
path.set_extension("json");
path
}
fn thumbnail_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(self.id.clone());
path.set_extension(format!("tn.{}", self.extension));
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| s.to_owned()))
.ok_or(PathError::InvalidPath)?,
extension: path
.extension()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(PathError::InvalidPath)?,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileId(String); pub struct FileId(String);
impl From<String> for FileId { impl From<String> for FileId {
@ -204,23 +118,28 @@ impl Store {
Self { files_root } Self { files_root }
} }
pub fn list_files(&self) -> Vec<Result<File, ReadFileError>> { pub fn list_files(&self) -> Result<Vec<FileId>, ReadFileError> {
unimplemented!() unimplemented!()
} }
pub fn add_file(&mut self, filename: String, content: Vec<u8>) -> Result<File, WriteFileError> { pub fn add_file(
let context = Context(self.files_root.clone()); &mut self,
let mut file = File::new(filename, context)?; filename: String,
content: Vec<u8>,
) -> Result<FileHandle, WriteFileError> {
let mut file = FileHandle::new(filename, self.files_root.clone())?;
file.set_content(content)?; file.set_content(content)?;
Ok(file) Ok(file)
} }
pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
FileHandle::load(id, &self.files_root)
}
pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> { pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> {
/* let handle = FileHandle::load(id, &self.files_root)?;
let f = File::open(&id, &self.files_root)?; handle.delete();
f.delete() Ok(())
*/
unimplemented!()
} }
pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> { pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
@ -229,32 +148,6 @@ impl Store {
path.set_extension("json"); path.set_extension("json");
FileInfo::load(path) FileInfo::load(path)
} }
pub fn get_file(&self, id: &FileId) -> Result<(FileInfo, File), ReadFileError> {
let info = self.get_metadata(id)?;
let resolver = PathResolver {
base: self.files_root.clone(),
id: (**id).clone(),
extension: info.extension.clone(),
};
let f = File::load(resolver)?;
Ok((info, f))
}
pub fn get_thumbnail(&self, id: &FileId) -> Result<(FileInfo, Thumbnail), ReadFileError> {
let info = self.get_metadata(id)?;
let resolver = PathResolver {
base: self.files_root.clone(),
id: (**id).clone(),
extension: info.extension.clone(),
};
let f = Thumbnail::load(resolver.thumbnail_path())?;
Ok((info, f))
}
} }
#[cfg(test)] #[cfg(test)]
@ -262,7 +155,7 @@ mod test {
use super::*; use super::*;
use crate::store::utils::FileCleanup; use crate::store::utils::FileCleanup;
use cool_asserts::assert_matches; use cool_asserts::assert_matches;
use std::io::Read; use std::{collections::HashSet, io::Read};
fn with_file<F>(test_fn: F) fn with_file<F>(test_fn: F)
where where
@ -282,28 +175,10 @@ mod test {
test_fn(store, file_record.id); test_fn(store, file_record.id);
} }
#[test]
fn paths() {
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
.expect("to have a valid path");
assert_eq!(
resolver.file_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_eq!(
resolver.thumbnail_path(),
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png")
);
}
#[test] #[test]
fn adds_files() { fn adds_files() {
with_file(|store, id| { with_file(|store, id| {
let (_, file) = store.get_file(&id).expect("to retrieve the file"); let file = store.get_file(&id).expect("to retrieve the file");
assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
@ -327,6 +202,7 @@ mod test {
}); });
} }
/*
#[test] #[test]
fn sets_up_thumbnail_for_file() { fn sets_up_thumbnail_for_file() {
with_file(|store, id| { with_file(|store, id| {
@ -334,6 +210,7 @@ mod test {
assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869); assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869);
}); });
} }
*/
#[test] #[test]
fn deletes_associated_files() { fn deletes_associated_files() {
@ -347,5 +224,12 @@ mod test {
} }
#[test] #[test]
fn lists_files_in_the_db() {} fn lists_files_in_the_db() {
with_file(|store, id| {
let resolvers = store.list_files().expect("file listing to succeed");
let ids = resolvers.into_iter().collect::<HashSet<FileId>>();
assert_eq!(ids.len(), 1);
});
}
} }

View File

@ -1,4 +1,4 @@
use super::{HasContent, ReadFileError, WriteFileError}; use super::{ReadFileError, WriteFileError};
use image::imageops::FilterType; use image::imageops::FilterType;
use std::{ use std::{
fs::remove_file, fs::remove_file,
@ -73,17 +73,6 @@ impl Thumbnail {
*/ */
} }
impl HasContent for Thumbnail {
fn content(&self) -> Result<Vec<u8>, ReadFileError> {
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(&self.path)?;
file.read_to_end(&mut content)?;
Ok(content)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;