Compare commits
4 Commits
8c099d0586
...
bda0caff95
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | bda0caff95 | |
Savanni D'Gerinel | 90f9b80dd9 | |
Savanni D'Gerinel | 163e1e1de1 | |
Savanni D'Gerinel | d658747202 |
|
@ -158,18 +158,6 @@ version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
|
|
||||||
dependencies = [
|
|
||||||
"block-padding",
|
|
||||||
"byte-tools",
|
|
||||||
"byteorder",
|
|
||||||
"generic-array 0.12.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -179,15 +167,6 @@ dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-padding"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
|
|
||||||
dependencies = [
|
|
||||||
"byte-tools",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bodyparser"
|
name = "bodyparser"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -223,12 +202,6 @@ version = "3.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byte-tools"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
|
@ -550,22 +523,13 @@ dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array 0.12.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -678,12 +642,6 @@ dependencies = [
|
||||||
"zune-inflate",
|
"zune-inflate",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fake-simd"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -714,7 +672,9 @@ name = "file-service"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build_html",
|
"build_html",
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"futures-util",
|
||||||
"hex-string",
|
"hex-string",
|
||||||
"http",
|
"http",
|
||||||
"image 0.23.14",
|
"image 0.23.14",
|
||||||
|
@ -1015,15 +975,6 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
|
@ -2323,12 +2274,6 @@ version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "opaque-debug"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.57"
|
version = "0.10.57"
|
||||||
|
@ -3307,19 +3252,18 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"digest 0.10.7",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.8.2"
|
version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
|
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.7.3",
|
"cfg-if",
|
||||||
"digest 0.8.1",
|
"cpufeatures",
|
||||||
"fake-simd",
|
"digest",
|
||||||
"opaque-debug",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
fixtures
|
|
@ -0,0 +1,2 @@
|
||||||
|
fixtures
|
||||||
|
var
|
File diff suppressed because it is too large
Load Diff
|
@ -22,10 +22,13 @@ params = "*"
|
||||||
router = "*"
|
router = "*"
|
||||||
serde_json = "*"
|
serde_json = "*"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = "0.8.2"
|
sha2 = "0.10"
|
||||||
thiserror = "1.0.20"
|
thiserror = "1.0.20"
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
warp = { version = "0.3" }
|
warp = { version = "0.3" }
|
||||||
pretty_env_logger = { version = "0.5" }
|
pretty_env_logger = { version = "0.5" }
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
|
bytes = { version = "1" }
|
||||||
|
futures-util = { version = "0.3" }
|
||||||
|
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
use chrono::prelude::*;
|
|
||||||
use hex_string::HexString;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::fs::remove_file;
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::{append_extension, FileError};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct FileInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub size: u64,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
pub file_type: String,
|
|
||||||
pub hash: String,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
root: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileInfo {
|
|
||||||
pub fn save(&self, root: &Path) -> Result<(), FileError> {
|
|
||||||
let ser = serde_json::to_string(self).unwrap();
|
|
||||||
std::fs::File::create(FileInfo::metadata_path(&self.id, root))
|
|
||||||
.and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (())))
|
|
||||||
.map_err(FileError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open(id: &str, root: &Path) -> Result<FileInfo, FileError> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
let md_path = FileInfo::metadata_path(id, root);
|
|
||||||
std::fs::File::open(md_path.clone())
|
|
||||||
.and_then(|mut stream| stream.read_to_end(&mut buf))
|
|
||||||
.map_err(move |err| match err.kind() {
|
|
||||||
std::io::ErrorKind::NotFound => FileError::FileNotFound(md_path),
|
|
||||||
_ => FileError::IOError(err),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let str_repr = std::str::from_utf8(&buf)?;
|
|
||||||
|
|
||||||
serde_json::from_str(&str_repr).map_err(FileError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_path(path: &Path) -> Result<FileInfo, FileError> {
|
|
||||||
match (path.is_file(), path.is_dir()) {
|
|
||||||
(false, false) => Err(FileError::FileNotFound(PathBuf::from(path))),
|
|
||||||
(false, true) => Err(FileError::NotAFile(PathBuf::from(path))),
|
|
||||||
(true, _) => Ok(()),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let metadata = path.metadata().map_err(FileError::IOError)?;
|
|
||||||
let id = path
|
|
||||||
.file_name()
|
|
||||||
.map(|s| String::from(s.to_string_lossy()))
|
|
||||||
.ok_or(FileError::NotAFile(PathBuf::from(path)))?;
|
|
||||||
let created = metadata
|
|
||||||
.created()
|
|
||||||
.map(|m| DateTime::from(m))
|
|
||||||
.map_err(|err| FileError::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, FileError> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
let mut file = std::fs::File::open(path).map_err(FileError::from)?;
|
|
||||||
|
|
||||||
file.read_to_end(&mut buf).map_err(FileError::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)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use crate::lib::utils::FileCleanup;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_generates_information_from_file_path() {
|
|
||||||
let path = Path::new("fixtures/rawr.png");
|
|
||||||
match FileInfo::from_path(&path) {
|
|
||||||
Ok(FileInfo {
|
|
||||||
id,
|
|
||||||
size,
|
|
||||||
file_type,
|
|
||||||
hash,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
assert_eq!(id, "rawr.png");
|
|
||||||
assert_eq!(size, 23777);
|
|
||||||
assert_eq!(file_type, "image/png");
|
|
||||||
assert_eq!(
|
|
||||||
hash,
|
|
||||||
"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("error loading file path: {}", err);
|
|
||||||
assert!(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_saves_and_loads_metadata() {
|
|
||||||
let path = Path::new("fixtures/rawr.png");
|
|
||||||
let _ = FileCleanup(append_extension(path, "json"));
|
|
||||||
let info = FileInfo::from_path(&path).unwrap();
|
|
||||||
info.save(Path::new("fixtures")).unwrap();
|
|
||||||
|
|
||||||
assert!(Path::new("fixtures/.metadata/rawr.png.json").is_file());
|
|
||||||
|
|
||||||
let info_ = FileInfo::open("rawr.png", Path::new("fixtures")).unwrap();
|
|
||||||
assert_eq!(info_.id, "rawr.png");
|
|
||||||
assert_eq!(info_.size, 23777);
|
|
||||||
assert_eq!(info_.created, info.created);
|
|
||||||
assert_eq!(info_.file_type, "image/png");
|
|
||||||
assert_eq!(info_.hash, info.hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_extends_a_file_extension() {
|
|
||||||
assert_eq!(
|
|
||||||
append_extension(Path::new("fixtures/rawr.png"), "json"),
|
|
||||||
Path::new("fixtures/rawr.png.json")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod file;
|
|
||||||
mod fileinfo;
|
|
||||||
mod thumbnail;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
pub use file::{File, FileError};
|
|
||||||
pub use fileinfo::FileInfo;
|
|
||||||
pub use thumbnail::Thumbnail;
|
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
files_root: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn new(files_root: &Path) -> App {
|
|
||||||
App {
|
|
||||||
files_root: PathBuf::from(files_root),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_files(&self) -> Vec<Result<File, FileError>> {
|
|
||||||
File::list(&self.files_root)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_file(
|
|
||||||
&mut self,
|
|
||||||
temp_path: &PathBuf,
|
|
||||||
filename: &Option<PathBuf>,
|
|
||||||
) -> Result<File, FileError> {
|
|
||||||
let id = Uuid::new_v4().hyphenated().to_string();
|
|
||||||
File::new(&id, &self.files_root, temp_path, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_file(&mut self, id: String) -> Result<(), FileError> {
|
|
||||||
let f = File::open(&id, &self.files_root)?;
|
|
||||||
f.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_metadata(&self, id: String) -> Result<FileInfo, FileError> {
|
|
||||||
FileInfo::open(&id, &self.files_root)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> {
|
|
||||||
let f = File::open(&id, &self.files_root)?;
|
|
||||||
let info = f.info();
|
|
||||||
let stream = f.stream()?;
|
|
||||||
Ok((info, stream))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> {
|
|
||||||
let f = File::open(id, &self.files_root)?;
|
|
||||||
let stream = f.thumbnail().stream()?;
|
|
||||||
Ok((f.info(), stream))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
use image::imageops::FilterType;
|
|
||||||
use std::fs::remove_file;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::FileError;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct Thumbnail {
|
|
||||||
pub id: String,
|
|
||||||
pub root: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Thumbnail {
|
|
||||||
pub fn open(id: &str, root: &Path) -> Result<Thumbnail, FileError> {
|
|
||||||
let mut source_path = PathBuf::from(root);
|
|
||||||
source_path.push(id);
|
|
||||||
|
|
||||||
let self_ = Thumbnail {
|
|
||||||
id: String::from(id),
|
|
||||||
root: PathBuf::from(root),
|
|
||||||
};
|
|
||||||
|
|
||||||
let thumbnail_path = Thumbnail::thumbnail_path(id, root);
|
|
||||||
if !thumbnail_path.exists() {
|
|
||||||
let img = image::open(source_path)?;
|
|
||||||
let tn = img.resize(640, 640, FilterType::Nearest);
|
|
||||||
tn.save(thumbnail_path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self_)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_path(path: &Path) -> Result<Thumbnail, FileError> {
|
|
||||||
let id = path
|
|
||||||
.file_name()
|
|
||||||
.map(|s| String::from(s.to_string_lossy()))
|
|
||||||
.ok_or(FileError::NotAnImage(PathBuf::from(path)))?;
|
|
||||||
|
|
||||||
let root = path
|
|
||||||
.parent()
|
|
||||||
.ok_or(FileError::FileNotFound(PathBuf::from(path)))?;
|
|
||||||
|
|
||||||
Thumbnail::open(&id, root)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, FileError> {
|
|
||||||
let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root);
|
|
||||||
std::fs::File::open(thumbnail_path.clone()).map_err(|err| {
|
|
||||||
if err.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
FileError::FileNotFound(thumbnail_path)
|
|
||||||
} else {
|
|
||||||
FileError::from(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self) -> Result<(), FileError> {
|
|
||||||
let path = Thumbnail::thumbnail_path(&self.id, &self.root);
|
|
||||||
remove_file(path).map_err(FileError::from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::lib::utils::FileCleanup;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_creates_a_thumbnail_if_one_does_not_exist() {
|
|
||||||
let _ = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
|
|
||||||
let _ =
|
|
||||||
Thumbnail::open("rawr.png", Path::new("fixtures")).expect("thumbnail open must work");
|
|
||||||
assert!(Path::new("fixtures/.thumbnails/rawr.png").is_file());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,22 +13,24 @@ use http::status::StatusCode;
|
||||||
// use mustache::{compile_path, Template};
|
// use mustache::{compile_path, Template};
|
||||||
// use orizentic::{Permissions, ResourceName, Secret};
|
// use orizentic::{Permissions, ResourceName, Secret};
|
||||||
use build_html::Html;
|
use build_html::Html;
|
||||||
|
use bytes::Buf;
|
||||||
|
use futures_util::{Stream, StreamExt};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::Read,
|
io::Read,
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
path::Path,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use warp::Filter;
|
use warp::{filters::multipart::Part, Filter};
|
||||||
|
|
||||||
mod cookies;
|
mod cookies;
|
||||||
mod html;
|
mod html;
|
||||||
mod lib;
|
|
||||||
mod middleware;
|
mod middleware;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
mod store;
|
||||||
|
|
||||||
use lib::{utils::append_extension, App, File, FileError, FileInfo};
|
pub use store::{FileInfo, Store};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
|
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
|
||||||
|
@ -247,6 +249,32 @@ fn serve_file(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn collect_content(mut part: Part) -> Result<(Option<String>, Vec<u8>), String> {
|
||||||
|
let mut content: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(Ok(data)) = part.data().await {
|
||||||
|
let mut reader = data.reader();
|
||||||
|
reader.read_to_end(&mut content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((part.filename().map(|s| s.to_owned()), content))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_multipart(
|
||||||
|
mut stream: warp::filters::multipart::FormData,
|
||||||
|
) -> Result<Vec<(Option<String>, Vec<u8>)>, warp::Error> {
|
||||||
|
let mut content: Vec<(Option<String>, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(part) = stream.next().await {
|
||||||
|
match part {
|
||||||
|
Ok(part) => content.push(collect_content(part).await.unwrap()),
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
/*
|
/*
|
||||||
|
@ -292,7 +320,7 @@ pub async fn main() {
|
||||||
|
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let app = Arc::new(RwLock::new(App::new(Path::new(
|
let app = Arc::new(RwLock::new(Store::new(PathBuf::from(
|
||||||
&std::env::var("FILE_SHARE_DIR").unwrap(),
|
&std::env::var("FILE_SHARE_DIR").unwrap(),
|
||||||
))));
|
))));
|
||||||
|
|
||||||
|
@ -309,6 +337,7 @@ pub async fn main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
let post_handler = warp::path!(String)
|
let post_handler = warp::path!(String)
|
||||||
.and(warp::post())
|
.and(warp::post())
|
||||||
.and(warp::filters::body::form())
|
.and(warp::filters::body::form())
|
||||||
|
@ -320,6 +349,7 @@ pub async fn main() {
|
||||||
.status(StatusCode::SEE_OTHER)
|
.status(StatusCode::SEE_OTHER)
|
||||||
.body(vec![])
|
.body(vec![])
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
let thumbnail = warp::path!(String / "tn")
|
let thumbnail = warp::path!(String / "tn")
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
|
@ -351,8 +381,20 @@ pub async fn main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let upload = warp::path!().and(warp::post()).map(|| {
|
let upload = warp::path!()
|
||||||
println!("upload");
|
.and(warp::post())
|
||||||
|
.and(warp::filters::multipart::form().max_length(1024 * 1024 * 32))
|
||||||
|
.then(|form: warp::filters::multipart::FormData| async move {
|
||||||
|
let files = collect_multipart(form).await;
|
||||||
|
/*
|
||||||
|
for (filename, content) in files {
|
||||||
|
app.write()
|
||||||
|
.unwrap()
|
||||||
|
.add_file(Some(filename), content)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
println!("file length: {:?}", files.map(|f| f.len()));
|
||||||
warp::reply()
|
warp::reply()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -361,6 +403,7 @@ pub async fn main() {
|
||||||
warp::reply()
|
warp::reply()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
let server = warp::serve(
|
let server = warp::serve(
|
||||||
root.or(post_handler)
|
root.or(post_handler)
|
||||||
.or(file)
|
.or(file)
|
||||||
|
@ -369,6 +412,8 @@ pub async fn main() {
|
||||||
.or(delete)
|
.or(delete)
|
||||||
.with(log),
|
.with(log),
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
let server = warp::serve(root.or(upload).with(log));
|
||||||
server
|
server
|
||||||
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
|
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use crate::{html::*, File, FileError};
|
use crate::{
|
||||||
|
html::*,
|
||||||
|
store::{File, ReadFileError},
|
||||||
|
};
|
||||||
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
|
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
|
||||||
|
|
||||||
pub fn index(files: Vec<Result<File, FileError>>) -> build_html::HtmlPage {
|
pub fn index(files: Vec<Result<File, 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")
|
||||||
|
@ -21,7 +24,7 @@ pub fn index(files: Vec<Result<File, FileError>>) -> build_html::HtmlPage {
|
||||||
let container = match file {
|
let container = match file {
|
||||||
Ok(ref file) => thumbnail(file).with_html(
|
Ok(ref file) => thumbnail(file).with_html(
|
||||||
Form::new()
|
Form::new()
|
||||||
.with_path(&format!("/{}", file.info().id))
|
.with_path(&format!("/{}", *file.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")),
|
||||||
|
@ -41,8 +44,8 @@ pub fn thumbnail(file: &File) -> Container {
|
||||||
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.info().id),
|
format!("/{}", *file.id),
|
||||||
Image::new(&format!("{}/tn", file.info().id)).to_html_string(),
|
Image::new(&format!("{}/tn", *file.id)).to_html_string(),
|
||||||
);
|
);
|
||||||
container.add_html(tn);
|
container.add_html(tn);
|
||||||
container
|
container
|
||||||
|
|
|
@ -1,50 +1,98 @@
|
||||||
use super::fileinfo::FileInfo;
|
use super::{
|
||||||
use super::thumbnail::Thumbnail;
|
fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, PathResolver, ReadFileError,
|
||||||
use std::fs::{copy, read_dir, remove_file};
|
WriteFileError,
|
||||||
use std::path::{Path, PathBuf};
|
};
|
||||||
use thiserror::Error;
|
use chrono::prelude::*;
|
||||||
|
use hex_string::HexString;
|
||||||
#[derive(Error, Debug)]
|
use sha2::{Digest, Sha256};
|
||||||
pub enum FileError {
|
use std::{
|
||||||
#[error("not implemented")]
|
convert::TryFrom,
|
||||||
NotImplemented,
|
io::{Read, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
#[error("file not found: `{0}`")]
|
};
|
||||||
FileNotFound(PathBuf),
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[error("file is not an image: `{0}`")]
|
|
||||||
NotAnImage(PathBuf),
|
|
||||||
|
|
||||||
#[error("path is not a file: `{0}`")]
|
|
||||||
NotAFile(PathBuf),
|
|
||||||
|
|
||||||
#[error("Image loading error")]
|
|
||||||
ImageError(#[from] image::ImageError),
|
|
||||||
|
|
||||||
#[error("IO error")]
|
|
||||||
IOError(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("JSON error")]
|
|
||||||
JSONError(#[from] serde_json::error::Error),
|
|
||||||
|
|
||||||
#[error("UTF8 Error")]
|
|
||||||
UTF8Error(#[from] std::str::Utf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One file in the database, complete with the path of the file and information about the
|
/// One file in the database, complete with the path of the file and information about the
|
||||||
/// thumbnail of the file.
|
/// thumbnail of the file.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
info: FileInfo,
|
pub id: FileId,
|
||||||
tn: Thumbnail,
|
pub path: PathResolver,
|
||||||
root: PathBuf,
|
pub info: FileInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl File {
|
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(
|
pub fn new(
|
||||||
id: &str,
|
id: &str,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
temp_path: &PathBuf,
|
|
||||||
filename: &Option<PathBuf>,
|
filename: &Option<PathBuf>,
|
||||||
) -> Result<Self, FileError> {
|
) -> Result<Self, FileError> {
|
||||||
let mut dest_path = PathBuf::from(root);
|
let mut dest_path = PathBuf::from(root);
|
||||||
|
@ -136,25 +184,43 @@ impl File {
|
||||||
self.tn.delete()?;
|
self.tn.delete()?;
|
||||||
self.info.delete()
|
self.info.delete()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::lib::utils::FileCleanup;
|
use crate::store::utils::FileCleanup;
|
||||||
use std::path::{Path, PathBuf};
|
use std::{convert::TryFrom, path::PathBuf};
|
||||||
|
|
||||||
|
struct FileContext(PathBuf);
|
||||||
|
|
||||||
|
impl FileRoot for FileContext {
|
||||||
|
fn root(&self) -> PathBuf {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_opens_a_file() {
|
fn it_opens_a_file() {
|
||||||
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json"));
|
let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json"));
|
||||||
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
|
let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png"));
|
||||||
|
|
||||||
File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
|
File::new(
|
||||||
|
"rawr.png".to_owned(),
|
||||||
|
FileContext(PathBuf::from("fixtures/")),
|
||||||
|
)
|
||||||
|
.expect("to succeed");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_return_a_thumbnail() {
|
fn it_can_return_a_thumbnail() {
|
||||||
let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
|
let f = File::new(
|
||||||
|
"rawr.png".to_owned(),
|
||||||
|
FileContext(PathBuf::from("fixtures/")),
|
||||||
|
)
|
||||||
|
.expect("to succeed");
|
||||||
|
/*
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f.thumbnail(),
|
f.thumbnail(),
|
||||||
Thumbnail {
|
Thumbnail {
|
||||||
|
@ -162,18 +228,24 @@ mod test {
|
||||||
root: PathBuf::from("fixtures/"),
|
root: PathBuf::from("fixtures/"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_return_a_file_stream() {
|
fn it_can_return_a_file_stream() {
|
||||||
let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
|
let f = File::new(
|
||||||
f.stream().expect("to succeed");
|
"rawr.png".to_owned(),
|
||||||
|
FileContext(PathBuf::from("fixtures/")),
|
||||||
|
)
|
||||||
|
.expect("to succeed");
|
||||||
|
// f.stream().expect("to succeed");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_raises_an_error_when_file_not_found() {
|
fn it_raises_an_error_when_file_not_found() {
|
||||||
match File::open("garbage", Path::new("fixtures/")) {
|
let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path");
|
||||||
Err(Error::FileNotFound(_)) => assert!(true),
|
match File::load(resolver) {
|
||||||
|
Err(ReadFileError::FileNotFound) => assert!(true),
|
||||||
_ => assert!(false),
|
_ => assert!(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
use super::{ReadFileError, WriteFileError};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FileInfo {
|
||||||
|
pub size: usize,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub file_type: String,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInfo {
|
||||||
|
pub fn load(path: PathBuf) -> Result<Self, ReadFileError> {
|
||||||
|
let mut content: Vec<u8> = Vec::new();
|
||||||
|
let mut file = std::fs::File::open(path)?;
|
||||||
|
file.read_to_end(&mut content)?;
|
||||||
|
let js = serde_json::from_slice(&content)?;
|
||||||
|
|
||||||
|
Ok(js)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: PathBuf) -> Result<(), WriteFileError> {
|
||||||
|
let ser = serde_json::to_string(self).unwrap();
|
||||||
|
let mut file = std::fs::File::create(path)?;
|
||||||
|
file.write(ser.as_bytes())?;
|
||||||
|
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)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::store::{utils::FileCleanup, PathResolver};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_saves_and_loads_metadata() {
|
||||||
|
let resolver = PathResolver::try_from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607.png")
|
||||||
|
.expect("a valid path");
|
||||||
|
let _cleanup = FileCleanup(resolver.metadata_path());
|
||||||
|
|
||||||
|
let created = Utc::now();
|
||||||
|
|
||||||
|
let info = FileInfo {
|
||||||
|
size: 23777,
|
||||||
|
created,
|
||||||
|
file_type: "image/png".to_owned(),
|
||||||
|
hash: "abcdefg".to_owned(),
|
||||||
|
};
|
||||||
|
info.save(resolver.metadata_path()).unwrap();
|
||||||
|
|
||||||
|
let info_ = FileInfo::load(resolver.metadata_path()).unwrap();
|
||||||
|
assert_eq!(info_.size, 23777);
|
||||||
|
assert_eq!(info_.created, info.created);
|
||||||
|
assert_eq!(info_.file_type, "image/png");
|
||||||
|
assert_eq!(info_.hash, info.hash);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
|
ops::Deref,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
mod file;
|
||||||
|
mod fileinfo;
|
||||||
|
mod thumbnail;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use file::File;
|
||||||
|
pub use fileinfo::FileInfo;
|
||||||
|
pub use thumbnail::Thumbnail;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WriteFileError {
|
||||||
|
#[error("root file path does not exist")]
|
||||||
|
RootNotFound,
|
||||||
|
|
||||||
|
#[error("permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("invalid path")]
|
||||||
|
InvalidPath,
|
||||||
|
|
||||||
|
#[error("image conversion failed")]
|
||||||
|
ImageError(#[from] image::ImageError),
|
||||||
|
|
||||||
|
#[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")]
|
||||||
|
FileNotFound,
|
||||||
|
|
||||||
|
#[error("path is not a file")]
|
||||||
|
NotAFile,
|
||||||
|
|
||||||
|
#[error("permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("invalid path")]
|
||||||
|
InvalidPath,
|
||||||
|
|
||||||
|
#[error("JSON error")]
|
||||||
|
JSONError(#[from] serde_json::error::Error),
|
||||||
|
|
||||||
|
#[error("IO error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
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);
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for FileId {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FileRoot {
|
||||||
|
fn root(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context(PathBuf);
|
||||||
|
|
||||||
|
impl FileRoot for Context {
|
||||||
|
fn root(&self) -> PathBuf {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Store {
|
||||||
|
files_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn new(files_root: PathBuf) -> Self {
|
||||||
|
Self { files_root }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_files(&self) -> Vec<Result<File, ReadFileError>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file(&mut self, filename: String, content: Vec<u8>) -> Result<File, WriteFileError> {
|
||||||
|
let context = Context(self.files_root.clone());
|
||||||
|
let mut file = File::new(filename, context)?;
|
||||||
|
file.set_content(content)?;
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_file(&mut self, id: String) -> Result<(), WriteFileError> {
|
||||||
|
/*
|
||||||
|
let f = File::open(&id, &self.files_root)?;
|
||||||
|
f.delete()
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata(&self, id: String) -> Result<FileInfo, ReadFileError> {
|
||||||
|
// FileInfo::open(&id, &self.files_root)
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> {
|
||||||
|
/*
|
||||||
|
let f = File::open(&id, &self.files_root)?;
|
||||||
|
let info = f.info();
|
||||||
|
let stream = f.stream()?;
|
||||||
|
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)]
|
||||||
|
mod test {
|
||||||
|
use crate::store::utils::FileCleanup;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
#[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]
|
||||||
|
fn adds_files() {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
|
||||||
|
file.read_to_end(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let mut store = Store::new(PathBuf::from("var/"));
|
||||||
|
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
||||||
|
|
||||||
|
let _file = FileCleanup(PathBuf::from(format!("var/{}", *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)));
|
||||||
|
|
||||||
|
assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists());
|
||||||
|
assert!(PathBuf::from(format!("var/{}.png.json", *file_record.id)).exists());
|
||||||
|
assert!(PathBuf::from(format!("var/{}.png.tn", *file_record.id)).exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sets_up_metadata_for_file() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sets_up_thumbnail_for_file() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deletes_associated_files() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_files_in_the_db() {}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use std::fs::remove_file;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::{ReadFileError, WriteFileError};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Thumbnail {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thumbnail {
|
||||||
|
pub fn open(
|
||||||
|
origin_path: PathBuf,
|
||||||
|
thumbnail_path: PathBuf,
|
||||||
|
) -> Result<Thumbnail, WriteFileError> {
|
||||||
|
let s = Thumbnail {
|
||||||
|
path: PathBuf::from(thumbnail_path),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !s.path.exists() {
|
||||||
|
let img = image::open(&origin_path)?;
|
||||||
|
let tn = img.resize(640, 640, FilterType::Nearest);
|
||||||
|
tn.save(&s.path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
|
||||||
|
let id = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| String::from(s.to_string_lossy()))
|
||||||
|
.ok_or(ReadFileError::NotAnImage(PathBuf::from(path)))?;
|
||||||
|
|
||||||
|
let path = path
|
||||||
|
.parent()
|
||||||
|
.ok_or(ReadFileError::FileNotFound(PathBuf::from(path)))?;
|
||||||
|
|
||||||
|
Thumbnail::open(&id, root)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
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> {
|
||||||
|
std::fs::File::open(self.path.clone()).map_err(|err| {
|
||||||
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
ReadFileError::FileNotFound
|
||||||
|
} else {
|
||||||
|
ReadFileError::from(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self) -> Result<(), WriteFileError> {
|
||||||
|
remove_file(self.path).map_err(WriteFileError::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::store::utils::FileCleanup;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_a_thumbnail_if_one_does_not_exist() {
|
||||||
|
let _ = FileCleanup(PathBuf::from("var/rawr.tn.png"));
|
||||||
|
let _ = Thumbnail::open(
|
||||||
|
PathBuf::from("fixtures/rawr.png"),
|
||||||
|
PathBuf::from("var/rawr.tn.png"),
|
||||||
|
)
|
||||||
|
.expect("thumbnail open must work");
|
||||||
|
assert!(Path::new("var/rawr.tn.png").is_file());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue