Compare commits
4 Commits
bda0caff95
...
6c1fa7cc2f
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 6c1fa7cc2f | |
Savanni D'Gerinel | b0641bf5e9 | |
Savanni D'Gerinel | 98a07ce03e | |
Savanni D'Gerinel | 809861a38e |
|
@ -674,6 +674,7 @@ dependencies = [
|
||||||
"build_html",
|
"build_html",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"cool_asserts",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex-string",
|
"hex-string",
|
||||||
"http",
|
"http",
|
||||||
|
|
|
@ -31,4 +31,5 @@ pretty_env_logger = { version = "0.5" }
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
bytes = { version = "1" }
|
bytes = { version = "1" }
|
||||||
futures-util = { version = "0.3" }
|
futures-util = { version = "0.3" }
|
||||||
|
cool_asserts = { version = "2" }
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ mod middleware;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
pub use store::{FileInfo, 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,24 +228,24 @@ fn script(_: &mut Request) -> IronResult<Response> {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fn serve_file(
|
fn serve_file(
|
||||||
info: FileInfo,
|
file: FileHandle,
|
||||||
mut file: std::fs::File,
|
|
||||||
old_etags: Option<String>,
|
old_etags: Option<String>,
|
||||||
) -> http::Result<http::Response<Vec<u8>>> {
|
) -> http::Result<http::Response<Vec<u8>>> {
|
||||||
let mut content = Vec::new();
|
|
||||||
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(content),
|
.body(vec![]),
|
||||||
_ => {
|
_ => match file.content() {
|
||||||
let _ = file.read_to_end(&mut content);
|
Ok(content) => warp::http::Response::builder()
|
||||||
warp::http::Response::builder()
|
.header("content-type", file.info.file_type)
|
||||||
.header("content-type", info.file_type)
|
.header("etag", file.info.hash)
|
||||||
.header("etag", info.hash)
|
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.body(content)
|
.body(content),
|
||||||
}
|
Err(_) => warp::http::Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(vec![]),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,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()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -359,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(&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![]),
|
||||||
|
@ -373,8 +386,12 @@ pub async fn main() {
|
||||||
.and(warp::header::optional::<String>("if-none-match"))
|
.and(warp::header::optional::<String>("if-none-match"))
|
||||||
.map({
|
.map({
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
move |id: String, old_etags: Option<String>| match app.read().unwrap().get_file(&id) {
|
move |id: String, old_etags: Option<String>| match app
|
||||||
Ok((info, file)) => serve_file(info, file, old_etags),
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get_file(&FileId::from(id))
|
||||||
|
{
|
||||||
|
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![]),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
use super::{
|
|
||||||
fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, 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();
|
|
||||||
path.push(filename.clone());
|
|
||||||
let path = PathResolver::try_from(path).map_err(|_| WriteFileError::InvalidPath)?;
|
|
||||||
|
|
||||||
let file_type = mime_guess::from_ext(&filename)
|
|
||||||
.first_or_text_plain()
|
|
||||||
.essence_str()
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let info = FileInfo {
|
|
||||||
size: 0,
|
|
||||||
created: Utc::now(),
|
|
||||||
file_type,
|
|
||||||
hash: "".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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> {
|
|
||||||
/*
|
|
||||||
Ok(Self {
|
|
||||||
id: FileId::from(
|
|
||||||
resolver
|
|
||||||
.file_path()
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_owned(),
|
|
||||||
),
|
|
||||||
path: resolver,
|
|
||||||
info: FileInfo::load(resolver.metadata_path())?,
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)?)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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("fixtures/")),
|
|
||||||
)
|
|
||||||
.expect("to succeed");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_return_a_thumbnail() {
|
|
||||||
let f = File::new(
|
|
||||||
"rawr.png".to_owned(),
|
|
||||||
FileContext(PathBuf::from("fixtures/")),
|
|
||||||
)
|
|
||||||
.expect("to succeed");
|
|
||||||
/*
|
|
||||||
assert_eq!(
|
|
||||||
f.thumbnail(),
|
|
||||||
Thumbnail {
|
|
||||||
id: String::from("rawr.png"),
|
|
||||||
root: PathBuf::from("fixtures/"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_return_a_file_stream() {
|
|
||||||
let f = File::new(
|
|
||||||
"rawr.png".to_owned(),
|
|
||||||
FileContext(PathBuf::from("fixtures/")),
|
|
||||||
)
|
|
||||||
.expect("to succeed");
|
|
||||||
// f.stream().expect("to succeed");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_raises_an_error_when_file_not_found() {
|
|
||||||
let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path");
|
|
||||||
match File::load(resolver) {
|
|
||||||
Err(ReadFileError::FileNotFound) => assert!(true),
|
|
||||||
_ => assert!(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::FileId;
|
||||||
|
|
||||||
use super::{ReadFileError, WriteFileError};
|
use super::{ReadFileError, WriteFileError};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -9,16 +11,19 @@ use std::{
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
|
pub id: FileId,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub file_type: String,
|
pub file_type: String,
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
|
pub extension: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileInfo {
|
impl FileInfo {
|
||||||
pub fn load(path: PathBuf) -> Result<Self, ReadFileError> {
|
pub fn load(path: PathBuf) -> Result<Self, ReadFileError> {
|
||||||
let mut content: Vec<u8> = Vec::new();
|
let mut content: Vec<u8> = Vec::new();
|
||||||
let mut file = std::fs::File::open(path)?;
|
let mut file =
|
||||||
|
std::fs::File::open(path.clone()).map_err(|_| ReadFileError::FileNotFound(path))?;
|
||||||
file.read_to_end(&mut content)?;
|
file.read_to_end(&mut content)?;
|
||||||
let js = serde_json::from_slice(&content)?;
|
let js = serde_json::from_slice(&content)?;
|
||||||
|
|
||||||
|
@ -31,87 +36,29 @@ 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, PathResolver};
|
use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_saves_and_loads_metadata() {
|
fn it_saves_and_loads_metadata() {
|
||||||
let resolver = PathResolver::try_from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607.png")
|
let resolver = PathResolver::try_from("var/1617654d-a588-4714-b4fa-e00ed0a8a607.png")
|
||||||
.expect("a valid path");
|
.expect("a valid path");
|
||||||
let _cleanup = FileCleanup(resolver.metadata_path());
|
let _cleanup = FileCleanup(resolver.metadata_path());
|
||||||
|
|
||||||
let created = Utc::now();
|
let created = Utc::now();
|
||||||
|
|
||||||
let info = FileInfo {
|
let info = FileInfo {
|
||||||
|
id: FileId("temp-id".to_owned()),
|
||||||
size: 23777,
|
size: 23777,
|
||||||
created,
|
created,
|
||||||
file_type: "image/png".to_owned(),
|
file_type: "image/png".to_owned(),
|
||||||
hash: "abcdefg".to_owned(),
|
hash: "abcdefg".to_owned(),
|
||||||
|
extension: "png".to_owned(),
|
||||||
};
|
};
|
||||||
info.save(resolver.metadata_path()).unwrap();
|
info.save(resolver.metadata_path()).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -26,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),
|
||||||
|
|
||||||
|
@ -39,7 +44,7 @@ pub enum WriteFileError {
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ReadFileError {
|
pub enum ReadFileError {
|
||||||
#[error("file not found")]
|
#[error("file not found")]
|
||||||
FileNotFound,
|
FileNotFound(PathBuf),
|
||||||
|
|
||||||
#[error("path is not a file")]
|
#[error("path is not a file")]
|
||||||
NotAFile,
|
NotAFile,
|
||||||
|
@ -57,93 +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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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)]
|
|
||||||
pub struct FileId(String);
|
pub struct FileId(String);
|
||||||
|
|
||||||
impl From<String> for FileId {
|
impl From<String> for FileId {
|
||||||
|
@ -158,6 +77,19 @@ impl From<&str> for FileId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for FileId {
|
impl Deref for FileId {
|
||||||
type Target = String;
|
type Target = String;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -186,77 +118,49 @@ 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 delete_file(&mut self, id: String) -> Result<(), WriteFileError> {
|
pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
|
||||||
/*
|
FileHandle::load(id, &self.files_root)
|
||||||
let f = File::open(&id, &self.files_root)?;
|
|
||||||
f.delete()
|
|
||||||
*/
|
|
||||||
unimplemented!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_metadata(&self, id: String) -> Result<FileInfo, ReadFileError> {
|
pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> {
|
||||||
// FileInfo::open(&id, &self.files_root)
|
let handle = FileHandle::load(id, &self.files_root)?;
|
||||||
unimplemented!()
|
handle.delete();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> {
|
pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
|
||||||
/*
|
let mut path = self.files_root.clone();
|
||||||
let f = File::open(&id, &self.files_root)?;
|
path.push(PathBuf::from(id));
|
||||||
let info = f.info();
|
path.set_extension("json");
|
||||||
let stream = f.stream()?;
|
FileInfo::load(path)
|
||||||
Ok((info, stream))
|
|
||||||
*/
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> {
|
|
||||||
/*
|
|
||||||
let f = File::open(id, &self.files_root)?;
|
|
||||||
let stream = f.thumbnail().stream()?;
|
|
||||||
Ok((f.info(), stream))
|
|
||||||
*/
|
|
||||||
unimplemented!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::store::utils::FileCleanup;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Read;
|
use crate::store::utils::FileCleanup;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use std::{collections::HashSet, io::Read};
|
||||||
|
|
||||||
#[test]
|
fn with_file<F>(test_fn: F)
|
||||||
fn paths() {
|
where
|
||||||
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
|
F: FnOnce(Store, FileId),
|
||||||
.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]
|
|
||||||
fn adds_files() {
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
|
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
|
||||||
file.read_to_end(&mut buf).unwrap();
|
file.read_to_end(&mut buf).unwrap();
|
||||||
|
@ -264,24 +168,68 @@ mod test {
|
||||||
let mut store = Store::new(PathBuf::from("var/"));
|
let mut store = Store::new(PathBuf::from("var/"));
|
||||||
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
||||||
|
|
||||||
let _file = FileCleanup(PathBuf::from(format!("var/{}", *file_record.id)));
|
let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id)));
|
||||||
let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id)));
|
let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id)));
|
||||||
let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id)));
|
let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn.png", *file_record.id)));
|
||||||
|
|
||||||
assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists());
|
test_fn(store, file_record.id);
|
||||||
assert!(PathBuf::from(format!("var/{}.png.json", *file_record.id)).exists());
|
|
||||||
assert!(PathBuf::from(format!("var/{}.png.tn", *file_record.id)).exists());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sets_up_metadata_for_file() {}
|
fn adds_files() {
|
||||||
|
with_file(|store, id| {
|
||||||
|
let file = store.get_file(&id).expect("to retrieve the file");
|
||||||
|
|
||||||
#[test]
|
assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
|
||||||
fn sets_up_thumbnail_for_file() {}
|
|
||||||
|
|
||||||
#[test]
|
assert!(PathBuf::from(format!("var/{}.png", *id)).exists());
|
||||||
fn deletes_associated_files() {}
|
assert!(PathBuf::from(format!("var/{}.json", *id)).exists());
|
||||||
|
assert!(PathBuf::from(format!("var/{}.tn.png", *id)).exists());
|
||||||
#[test]
|
});
|
||||||
fn lists_files_in_the_db() {}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sets_up_metadata_for_file() {
|
||||||
|
with_file(|store, id| {
|
||||||
|
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");
|
||||||
|
assert_eq!(hash, "".to_owned());
|
||||||
|
assert_eq!(extension, "png".to_owned());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[test]
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deletes_associated_files() {
|
||||||
|
with_file(|mut store, id| {
|
||||||
|
store.delete_file(&id).expect("file to be deleted");
|
||||||
|
|
||||||
|
assert!(!PathBuf::from(format!("var/{}.png", *id)).exists());
|
||||||
|
assert!(!PathBuf::from(format!("var/{}.json", *id)).exists());
|
||||||
|
assert!(!PathBuf::from(format!("var/{}.tn.png", *id)).exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use image::imageops::FilterType;
|
|
||||||
use std::fs::remove_file;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use super::{ReadFileError, WriteFileError};
|
use super::{ReadFileError, WriteFileError};
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use std::{
|
||||||
|
fs::remove_file,
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Thumbnail {
|
pub struct Thumbnail {
|
||||||
|
@ -27,6 +29,16 @@ impl Thumbnail {
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load(path: PathBuf) -> Result<Thumbnail, ReadFileError> {
|
||||||
|
let s = Thumbnail { path: path.clone() };
|
||||||
|
|
||||||
|
if !s.path.exists() {
|
||||||
|
return Err(ReadFileError::FileNotFound(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
|
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
|
||||||
let id = path
|
let id = path
|
||||||
|
@ -43,14 +55,6 @@ impl Thumbnail {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn thumbnail_path(id: &str, root: &Path) -> PathBuf {
|
|
||||||
let mut path = PathBuf::from(root);
|
|
||||||
path.push(".thumbnails");
|
|
||||||
path.push(id.clone());
|
|
||||||
path
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn stream(&self) -> Result<std::fs::File, ReadFileError> {
|
pub fn stream(&self) -> Result<std::fs::File, ReadFileError> {
|
||||||
std::fs::File::open(self.path.clone()).map_err(|err| {
|
std::fs::File::open(self.path.clone()).map_err(|err| {
|
||||||
if err.kind() == std::io::ErrorKind::NotFound {
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
@ -60,10 +64,13 @@ impl Thumbnail {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
pub fn delete(self) -> Result<(), WriteFileError> {
|
pub fn delete(self) -> Result<(), WriteFileError> {
|
||||||
remove_file(self.path).map_err(WriteFileError::from)
|
remove_file(self.path).map_err(WriteFileError::from)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in New Issue