Import a questionably refactored version of file-service
This commit is contained in:
parent
356395a503
commit
81aa7410de
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "file-service"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["savanni@luminescent-dreams.com"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
hex-string = "0.1.0"
|
||||||
|
iron = "0.6.1"
|
||||||
|
logger = "*"
|
||||||
|
mime = "0.3.16"
|
||||||
|
mime_guess = "2.0.3"
|
||||||
|
mustache = "0.9.0"
|
||||||
|
orizentic = "1.0.0"
|
||||||
|
params = "*"
|
||||||
|
router = "*"
|
||||||
|
serde_json = "*"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
sha2 = "0.8.2"
|
||||||
|
uuid = { version = "0.4", features = ["serde", "v4"] }
|
||||||
|
thiserror = "1.0.20"
|
||||||
|
image = "0.23.5"
|
|
@ -0,0 +1 @@
|
||||||
|
[{"jti":"ac3a46c6-3fa1-4d0a-af12-e7d3fefdc878","aud":"savanni","exp":1621351436,"iss":"savanni","iat":1589729036,"sub":"https://savanni.luminescent-dreams.com/file-service/","perms":["admin"]}]
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1,61 @@
|
||||||
|
use iron::headers;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Cookie {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Cookie {
|
||||||
|
fn from(s: &str) -> Cookie {
|
||||||
|
let parts: Vec<&str> = s.split("=").collect();
|
||||||
|
Cookie {
|
||||||
|
name: String::from(parts[0]),
|
||||||
|
value: String::from(parts[1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&String> for Cookie {
|
||||||
|
fn from(s: &String) -> Cookie {
|
||||||
|
Cookie::from(s.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Cookie {
|
||||||
|
fn from(s: String) -> Cookie {
|
||||||
|
Cookie::from(s.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CookieJar(HashMap<String, Cookie>);
|
||||||
|
|
||||||
|
impl CookieJar {
|
||||||
|
pub fn new() -> CookieJar {
|
||||||
|
CookieJar(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_cookie(&mut self, name: String, value: Cookie) {
|
||||||
|
self.0.insert(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup(&self, name: &str) -> Option<&Cookie> {
|
||||||
|
self.0.get(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some(Cookie(["auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhYzNhNDZjNi0zZmExLTRkMGEtYWYxMi1lN2QzZmVmZGM4NzgiLCJhdWQiOiJzYXZhbm5pIiwiZXhwIjoxNjIxMzUxNDM2LCJpc3MiOiJzYXZhbm5pIiwiaWF0IjoxNTg5NzI5MDM2LCJzdWIiOiJodHRwczovL3NhdmFubmkubHVtaW5lc2NlbnQtZHJlYW1zLmNvbS9maWxlLXNlcnZpY2UvIiwicGVybXMiOlsiYWRtaW4iXX0.8zjAbZ7Ut0d6EcDeyik39GKhXvH4qkMDdaiQVNKWiuM"]))
|
||||||
|
impl From<&headers::Cookie> for CookieJar {
|
||||||
|
fn from(c: &headers::Cookie) -> CookieJar {
|
||||||
|
let jar = CookieJar::new();
|
||||||
|
|
||||||
|
let headers::Cookie(cs) = c;
|
||||||
|
cs.iter().fold(jar, |mut jar, c_| {
|
||||||
|
let cookie = Cookie::from(c_);
|
||||||
|
jar.add_cookie(cookie.name.clone(), cookie);
|
||||||
|
jar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("not implemented")]
|
||||||
|
NotImplemented,
|
||||||
|
|
||||||
|
#[error("file not found: `{0}`")]
|
||||||
|
FileNotFound(PathBuf),
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<A> = std::result::Result<A, Error>;
|
|
@ -0,0 +1,151 @@
|
||||||
|
use super::error::{Error, Result};
|
||||||
|
use super::fileinfo::FileInfo;
|
||||||
|
use super::thumbnail::Thumbnail;
|
||||||
|
use std::fs::{copy, read_dir, remove_file};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct File {
|
||||||
|
info: FileInfo,
|
||||||
|
tn: Thumbnail,
|
||||||
|
root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub fn new(
|
||||||
|
id: &str,
|
||||||
|
root: &Path,
|
||||||
|
temp_path: &PathBuf,
|
||||||
|
filename: &Option<PathBuf>,
|
||||||
|
) -> Result<File> {
|
||||||
|
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(File {
|
||||||
|
info,
|
||||||
|
tn,
|
||||||
|
root: PathBuf::from(root),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(id: &str, root: &Path) -> Result<File> {
|
||||||
|
let mut file_path = PathBuf::from(root);
|
||||||
|
file_path.push(id.clone());
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err(Error::FileNotFound(file_path));
|
||||||
|
}
|
||||||
|
if !file_path.is_file() {
|
||||||
|
return Err(Error::NotAFile(file_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = match FileInfo::open(id, root) {
|
||||||
|
Ok(i) => Ok(i),
|
||||||
|
Err(Error::FileNotFound(_)) => {
|
||||||
|
let info = FileInfo::from_path(&file_path)?;
|
||||||
|
info.save(&root)?;
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let tn = Thumbnail::open(id, root)?;
|
||||||
|
|
||||||
|
Ok(File {
|
||||||
|
info,
|
||||||
|
tn,
|
||||||
|
root: PathBuf::from(root),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(root: &Path) -> Vec<Result<File>> {
|
||||||
|
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();
|
||||||
|
File::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> {
|
||||||
|
let mut path = self.root.clone();
|
||||||
|
path.push(self.info.id.clone());
|
||||||
|
std::fs::File::open(path).map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self) -> Result<()> {
|
||||||
|
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::lib::utils::FileCleanup;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[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::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_return_a_thumbnail() {
|
||||||
|
let f = File::open("rawr.png", Path::new("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::open("rawr.png", Path::new("fixtures/")).expect("to succeed");
|
||||||
|
f.stream().expect("to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_raises_an_error_when_file_not_found() {
|
||||||
|
match File::open("garbage", Path::new("fixtures/")) {
|
||||||
|
Err(Error::FileNotFound(_)) => assert!(true),
|
||||||
|
_ => assert!(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
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 super::error::{Error, Result};
|
||||||
|
use super::utils::append_extension;
|
||||||
|
|
||||||
|
#[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<()> {
|
||||||
|
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(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(id: &str, root: &Path) -> Result<FileInfo> {
|
||||||
|
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 => Error::FileNotFound(md_path),
|
||||||
|
_ => Error::IOError(err),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let str_repr = std::str::from_utf8(&buf)?;
|
||||||
|
|
||||||
|
serde_json::from_str(&str_repr).map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(path: &Path) -> Result<FileInfo> {
|
||||||
|
match (path.is_file(), path.is_dir()) {
|
||||||
|
(false, false) => Err(Error::FileNotFound(PathBuf::from(path))),
|
||||||
|
(false, true) => Err(Error::NotAFile(PathBuf::from(path))),
|
||||||
|
(true, _) => Ok(()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let metadata = path.metadata().map_err(Error::IOError)?;
|
||||||
|
let id = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| String::from(s.to_string_lossy()))
|
||||||
|
.ok_or(Error::NotAFile(PathBuf::from(path)))?;
|
||||||
|
let created = metadata
|
||||||
|
.created()
|
||||||
|
.map(|m| DateTime::from(m))
|
||||||
|
.map_err(|err| Error::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: created,
|
||||||
|
file_type,
|
||||||
|
hash: hash.as_string(),
|
||||||
|
root: PathBuf::from(path.parent().unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_file(path: &Path) -> Result<HexString> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut file = std::fs::File::open(path).map_err(Error::from)?;
|
||||||
|
|
||||||
|
file.read_to_end(&mut buf).map_err(Error::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<()> {
|
||||||
|
let path = FileInfo::metadata_path(&self.id, &self.root);
|
||||||
|
remove_file(path).map_err(Error::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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod file;
|
||||||
|
mod fileinfo;
|
||||||
|
mod thumbnail;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use error::{Error, Result};
|
||||||
|
pub use file::File;
|
||||||
|
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>> {
|
||||||
|
File::list(&self.files_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file(&mut self, temp_path: &PathBuf, filename: &Option<PathBuf>) -> Result<File> {
|
||||||
|
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<()> {
|
||||||
|
let f = File::open(&id, &self.files_root)?;
|
||||||
|
f.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata(&self, id: String) -> Result<FileInfo> {
|
||||||
|
FileInfo::open(&id, &self.files_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File)> {
|
||||||
|
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)> {
|
||||||
|
let f = File::open(id, &self.files_root)?;
|
||||||
|
let stream = f.thumbnail().stream()?;
|
||||||
|
Ok((f.info(), stream))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use std::fs::remove_file;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::error::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Thumbnail {
|
||||||
|
pub id: String,
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thumbnail {
|
||||||
|
pub fn open(id: &str, root: &Path) -> Result<Thumbnail> {
|
||||||
|
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> {
|
||||||
|
let id = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| String::from(s.to_string_lossy()))
|
||||||
|
.ok_or(Error::NotAnImage(PathBuf::from(path)))?;
|
||||||
|
|
||||||
|
let root = path
|
||||||
|
.parent()
|
||||||
|
.ok_or(Error::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> {
|
||||||
|
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 {
|
||||||
|
Error::FileNotFound(thumbnail_path)
|
||||||
|
} else {
|
||||||
|
Error::from(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self) -> Result<()> {
|
||||||
|
let path = Thumbnail::thumbnail_path(&self.id, &self.root);
|
||||||
|
remove_file(path).map_err(Error::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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub struct FileCleanup(pub PathBuf);
|
||||||
|
|
||||||
|
impl Drop for FileCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_file(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append_extension(path: &Path, extra_ext: &str) -> PathBuf {
|
||||||
|
let ext_ = match path.extension() {
|
||||||
|
None => String::from(extra_ext),
|
||||||
|
Some(ext) => [ext.to_string_lossy(), std::borrow::Cow::from(extra_ext)].join("."),
|
||||||
|
};
|
||||||
|
path.with_extension(ext_)
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
use iron::headers;
|
||||||
|
use iron::middleware::Handler;
|
||||||
|
use iron::modifiers::{Header, Redirect};
|
||||||
|
use iron::prelude::*;
|
||||||
|
use iron::response::BodyReader;
|
||||||
|
use iron::status;
|
||||||
|
use mustache::{compile_path, Template};
|
||||||
|
use orizentic::{Permissions, ResourceName, Secret};
|
||||||
|
use params::{Params, Value};
|
||||||
|
use router::Router;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
mod cookies;
|
||||||
|
mod lib;
|
||||||
|
mod middleware;
|
||||||
|
|
||||||
|
use lib::{App, FileInfo};
|
||||||
|
use middleware::{Authentication, RestForm};
|
||||||
|
|
||||||
|
fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool {
|
||||||
|
let Permissions(perms) = permissions;
|
||||||
|
ResourceName(String::from(
|
||||||
|
"https://savanni.luminescent-dreams.com/file-service/",
|
||||||
|
)) == *resource
|
||||||
|
&& perms.contains(&String::from("admin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_etags(info: FileInfo, etag_list: &headers::IfNoneMatch) -> bool {
|
||||||
|
let current_etag = headers::EntityTag::new(false, info.hash);
|
||||||
|
match etag_list {
|
||||||
|
headers::IfNoneMatch::Any => false,
|
||||||
|
headers::IfNoneMatch::Items(lst) => lst.iter().any(|etag| etag.weak_eq(¤t_etag)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod files {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub struct IndexHandler {
|
||||||
|
pub app: Arc<RwLock<App>>,
|
||||||
|
pub template: Template,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub enum TemplateFile {
|
||||||
|
#[serde(rename = "error")]
|
||||||
|
Error { error: String },
|
||||||
|
#[serde(rename = "file")]
|
||||||
|
File {
|
||||||
|
id: String,
|
||||||
|
size: u64,
|
||||||
|
date: String,
|
||||||
|
type_: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct IndexTemplateParams {
|
||||||
|
files: Vec<TemplateFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for IndexHandler {
|
||||||
|
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||||
|
let app = self.app.read().unwrap();
|
||||||
|
let m_token = req.extensions.get::<Authentication>();
|
||||||
|
match m_token {
|
||||||
|
Some(token) => {
|
||||||
|
if token.check_authorizations(is_admin) {
|
||||||
|
let files: Vec<TemplateFile> = app
|
||||||
|
.list_files()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| match entry {
|
||||||
|
Ok(file) => TemplateFile::File {
|
||||||
|
id: file.info().id,
|
||||||
|
size: file.info().size,
|
||||||
|
date: format!(
|
||||||
|
"{}",
|
||||||
|
file.info().created.format("%Y-%m-%d %H:%M:%S")
|
||||||
|
),
|
||||||
|
type_: file.info().file_type,
|
||||||
|
},
|
||||||
|
Err(err) => TemplateFile::Error {
|
||||||
|
error: format!("{}", err),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Response::with((
|
||||||
|
status::Ok,
|
||||||
|
Header(headers::ContentType::html()),
|
||||||
|
Header(headers::SetCookie(vec![format!("auth={}", token.text)])),
|
||||||
|
self.template
|
||||||
|
.render_to_string(&IndexTemplateParams { files })
|
||||||
|
.expect("the template to render"),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(Response::with(status::Forbidden))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(Response::with(status::Forbidden)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GetHandler {
|
||||||
|
pub app: Arc<RwLock<App>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for GetHandler {
|
||||||
|
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||||
|
let app = self.app.read().unwrap();
|
||||||
|
let capture = req.extensions.get::<Router>().unwrap().clone();
|
||||||
|
let old_etags = req.headers.get::<headers::IfNoneMatch>();
|
||||||
|
match capture.find("id") {
|
||||||
|
Some(id) => {
|
||||||
|
let info = app.get_metadata(String::from(id));
|
||||||
|
match (info, old_etags) {
|
||||||
|
(Ok(info_), Some(if_none_match)) => {
|
||||||
|
if compare_etags(info_, if_none_match) {
|
||||||
|
return Ok(Response::with(status::NotModified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
match app.get_file(String::from(id)) {
|
||||||
|
Ok((info, stream)) => Ok(Response::with((
|
||||||
|
status::Ok,
|
||||||
|
Header(headers::ContentType(
|
||||||
|
info.file_type.parse::<iron::mime::Mime>().unwrap(),
|
||||||
|
)),
|
||||||
|
Header(headers::ETag(headers::EntityTag::new(false, info.hash))),
|
||||||
|
BodyReader(stream),
|
||||||
|
))),
|
||||||
|
Err(_err) => Ok(Response::with(status::NotFound)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(Response::with(status::BadRequest)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GetThumbnailHandler {
|
||||||
|
pub app: Arc<RwLock<App>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for GetThumbnailHandler {
|
||||||
|
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||||
|
let app = self.app.read().unwrap();
|
||||||
|
let capture = req.extensions.get::<Router>().unwrap().clone();
|
||||||
|
let old_etags = req.headers.get::<headers::IfNoneMatch>();
|
||||||
|
match capture.find("id") {
|
||||||
|
Some(id) => {
|
||||||
|
let info = app.get_metadata(String::from(id));
|
||||||
|
match (info, old_etags) {
|
||||||
|
(Ok(info_), Some(if_none_match)) => {
|
||||||
|
if compare_etags(info_, if_none_match) {
|
||||||
|
return Ok(Response::with(status::NotModified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
match app.get_thumbnail(id) {
|
||||||
|
Ok((info, stream)) => Ok(Response::with((
|
||||||
|
status::Ok,
|
||||||
|
Header(headers::ContentType(
|
||||||
|
info.file_type.parse::<iron::mime::Mime>().unwrap(),
|
||||||
|
)),
|
||||||
|
Header(headers::ETag(headers::EntityTag::new(false, info.hash))),
|
||||||
|
BodyReader(stream),
|
||||||
|
))),
|
||||||
|
Err(_err) => Ok(Response::with(status::NotFound)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(Response::with(status::BadRequest)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct PostHandler {
|
||||||
|
pub app: Arc<RwLock<App>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for PostHandler {
|
||||||
|
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||||
|
let mut app = self.app.write().unwrap();
|
||||||
|
let m_token = req.extensions.get::<Authentication>();
|
||||||
|
match m_token {
|
||||||
|
Some(token) => {
|
||||||
|
if token.check_authorizations(is_admin) {
|
||||||
|
let params = req.get_ref::<Params>().unwrap();
|
||||||
|
if let Value::File(f_info) = params.get("file").unwrap() {
|
||||||
|
match app.add_file(
|
||||||
|
&f_info.path,
|
||||||
|
&f_info.filename.clone().map(|fname| PathBuf::from(fname)),
|
||||||
|
) {
|
||||||
|
Ok(_) => Ok(Response::with((
|
||||||
|
status::MovedPermanently,
|
||||||
|
Redirect(router::url_for(req, "index", HashMap::new())),
|
||||||
|
))),
|
||||||
|
Err(_) => Ok(Response::with(status::InternalServerError)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Response::with(status::BadRequest))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Response::with(status::Forbidden))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(Response::with(status::Forbidden)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeleteHandler {
|
||||||
|
pub app: Arc<RwLock<App>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for DeleteHandler {
|
||||||
|
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||||
|
let mut app = self.app.write().unwrap();
|
||||||
|
let capture = req.extensions.get::<Router>().unwrap().clone();
|
||||||
|
let m_token = req.extensions.get::<Authentication>();
|
||||||
|
match m_token {
|
||||||
|
Some(token) => {
|
||||||
|
if token.check_authorizations(is_admin) {
|
||||||
|
match capture.find("id") {
|
||||||
|
Some(id) => match app.delete_file(String::from(id)) {
|
||||||
|
Ok(()) => Ok(Response::with((
|
||||||
|
status::MovedPermanently,
|
||||||
|
Redirect(router::url_for(req, "index", HashMap::new())),
|
||||||
|
))),
|
||||||
|
Err(_) => Ok(Response::with(status::InternalServerError)),
|
||||||
|
},
|
||||||
|
None => Ok(Response::with(status::BadRequest)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Response::with(status::Forbidden))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(Response::with(status::Forbidden)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn css(_: &mut Request) -> IronResult<Response> {
|
||||||
|
let mut css: String = String::from("");
|
||||||
|
File::open("templates/style.css")
|
||||||
|
.unwrap()
|
||||||
|
.read_to_string(&mut css)
|
||||||
|
.unwrap();
|
||||||
|
Ok(Response::with((
|
||||||
|
status::Ok,
|
||||||
|
Header(headers::ContentType(iron::mime::Mime(
|
||||||
|
iron::mime::TopLevel::Text,
|
||||||
|
iron::mime::SubLevel::Css,
|
||||||
|
vec![],
|
||||||
|
))),
|
||||||
|
css,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(_: &mut Request) -> IronResult<Response> {
|
||||||
|
let mut js: String = String::from("");
|
||||||
|
File::open("templates/script.js")
|
||||||
|
.unwrap()
|
||||||
|
.read_to_string(&mut js)
|
||||||
|
.unwrap();
|
||||||
|
Ok(Response::with((
|
||||||
|
status::Ok,
|
||||||
|
Header(headers::ContentType(iron::mime::Mime(
|
||||||
|
iron::mime::TopLevel::Text,
|
||||||
|
iron::mime::SubLevel::Javascript,
|
||||||
|
vec![],
|
||||||
|
))),
|
||||||
|
js,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let auth_db_path = std::env::var("ORIZENTIC_DB").unwrap();
|
||||||
|
let secret = Secret(Vec::from(
|
||||||
|
std::env::var("ORIZENTIC_SECRET").unwrap().as_bytes(),
|
||||||
|
));
|
||||||
|
let auth_middleware = Authentication::new(secret, auth_db_path);
|
||||||
|
|
||||||
|
let app = Arc::new(RwLock::new(App::new(Path::new(
|
||||||
|
&std::env::var("FILE_SHARE_DIR").unwrap(),
|
||||||
|
))));
|
||||||
|
|
||||||
|
let mut router = Router::new();
|
||||||
|
router.get(
|
||||||
|
"/",
|
||||||
|
files::IndexHandler {
|
||||||
|
app: app.clone(),
|
||||||
|
template: compile_path("templates/index.html").expect("the template to compile"),
|
||||||
|
},
|
||||||
|
"index",
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:id",
|
||||||
|
files::GetFileHandler {
|
||||||
|
app: app.clone(),
|
||||||
|
template: compile_path("templates/file.html").expect("the template to compile"),
|
||||||
|
},
|
||||||
|
"get-file-page",
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:id/raw",
|
||||||
|
files::GetHandler { app: app.clone() },
|
||||||
|
"get-file",
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:id/tn",
|
||||||
|
files::GetThumbnailHandler { app: app.clone() },
|
||||||
|
"get-thumbnail",
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post("/", files::PostHandler { app: app.clone() }, "upload-file");
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/:id",
|
||||||
|
files::DeleteHandler { app: app.clone() },
|
||||||
|
"delete-file",
|
||||||
|
);
|
||||||
|
router.get("/css", css, "styles");
|
||||||
|
router.get("/script", script, "script");
|
||||||
|
|
||||||
|
let mut chain = Chain::new(router);
|
||||||
|
chain.link_before(auth_middleware);
|
||||||
|
chain.link_before(RestForm {});
|
||||||
|
|
||||||
|
Iron::new(chain).http("0.0.0.0:3000").unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
use iron::headers;
|
||||||
|
use iron::middleware::BeforeMiddleware;
|
||||||
|
use iron::prelude::*;
|
||||||
|
use iron::typemap::Key;
|
||||||
|
use orizentic::{filedb, OrizenticCtx, Secret};
|
||||||
|
use params::{FromValue, Params};
|
||||||
|
|
||||||
|
use crate::cookies::{Cookie, CookieJar};
|
||||||
|
|
||||||
|
pub struct Authentication {
|
||||||
|
pub auth: OrizenticCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Key for Authentication {
|
||||||
|
type Value = orizentic::VerifiedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Authentication {
|
||||||
|
pub fn new(secret: Secret, auth_db_path: String) -> Authentication {
|
||||||
|
let claims = filedb::load_claims_from_file(&auth_db_path).expect("claims did not load");
|
||||||
|
let orizentic = OrizenticCtx::new(secret, claims);
|
||||||
|
Authentication { auth: orizentic }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate_user(
|
||||||
|
&self,
|
||||||
|
token_str: String,
|
||||||
|
) -> Result<orizentic::VerifiedToken, orizentic::Error> {
|
||||||
|
self.auth.decode_and_validate_text(&token_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeforeMiddleware for Authentication {
|
||||||
|
fn before(&self, req: &mut Request) -> IronResult<()> {
|
||||||
|
let params = req.get_ref::<Params>().unwrap();
|
||||||
|
let token = match params.get("auth").and_then(|v| String::from_value(v)) {
|
||||||
|
Some(token_str) => self.authenticate_user(token_str).ok(),
|
||||||
|
None => {
|
||||||
|
let m_jar = req
|
||||||
|
.headers
|
||||||
|
.get::<headers::Cookie>()
|
||||||
|
.map(|cookies| CookieJar::from(cookies));
|
||||||
|
m_jar
|
||||||
|
.and_then(|jar| jar.lookup("auth").cloned())
|
||||||
|
.and_then(|Cookie { value, .. }| self.authenticate_user(value.clone()).ok())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
token.map(|t| req.extensions.insert::<Authentication>(t));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use iron::middleware::{AfterMiddleware, BeforeMiddleware};
|
||||||
|
use iron::prelude::*;
|
||||||
|
|
||||||
|
pub struct Logging {}
|
||||||
|
|
||||||
|
impl BeforeMiddleware for Logging {
|
||||||
|
fn before(&self, _: &mut Request) -> IronResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AfterMiddleware for Logging {
|
||||||
|
fn after(&self, _: &mut Request, res: Response) -> IronResult<Response> {
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
mod authentication;
|
||||||
|
mod logging;
|
||||||
|
mod restform;
|
||||||
|
|
||||||
|
pub use authentication::Authentication;
|
||||||
|
pub use restform::RestForm;
|
|
@ -0,0 +1,34 @@
|
||||||
|
use iron::method::Method;
|
||||||
|
use iron::middleware::BeforeMiddleware;
|
||||||
|
use iron::prelude::*;
|
||||||
|
use params::{Params, Value};
|
||||||
|
|
||||||
|
pub struct RestForm {}
|
||||||
|
|
||||||
|
impl RestForm {
|
||||||
|
fn method(&self, v: &Value) -> Option<Method> {
|
||||||
|
match v {
|
||||||
|
Value::String(method_str) => match method_str.as_str() {
|
||||||
|
"delete" => Some(Method::Delete),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeforeMiddleware for RestForm {
|
||||||
|
fn before(&self, req: &mut Request) -> IronResult<()> {
|
||||||
|
if req.method == Method::Post {
|
||||||
|
let method = {
|
||||||
|
let params = req.get_ref::<Params>().unwrap();
|
||||||
|
params
|
||||||
|
.get("_method")
|
||||||
|
.and_then(|m| self.method(m))
|
||||||
|
.unwrap_or(Method::Post)
|
||||||
|
};
|
||||||
|
req.method = method;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title> {{title}} </title>
|
||||||
|
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||||
|
<script src="/script"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title> Admin list of files </title>
|
||||||
|
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||||
|
<script src="/script"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1> Admin list of files </h1>
|
||||||
|
|
||||||
|
<div class="uploadform">
|
||||||
|
<form action="/" method="post" enctype="multipart/form-data">
|
||||||
|
<div id="file-selector">
|
||||||
|
<input type="file" name="file" id="file-selector-input" />
|
||||||
|
<label for="file-selector-input" onclick="selectFile('file-selector')">Select a file</label>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Upload file" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="files">
|
||||||
|
{{#files}}
|
||||||
|
<div class="file">
|
||||||
|
{{#error}}
|
||||||
|
<div>
|
||||||
|
<p> {{error}} </p>
|
||||||
|
</div>
|
||||||
|
{{/error}}
|
||||||
|
|
||||||
|
{{#file}}
|
||||||
|
<div class="thumbnail">
|
||||||
|
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li> {{date}} </li>
|
||||||
|
<li> {{type_}} </li>
|
||||||
|
<li> {{size}} </li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<form action="/{{id}}" method="post">
|
||||||
|
<input type="hidden" name="_method" value="delete" />
|
||||||
|
<input type="submit" value="Delete" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/file}}
|
||||||
|
</div>
|
||||||
|
{{/files}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
const selectFile = (selectorId) => {
|
||||||
|
console.log("wide arrow functions work: " + selectorId);
|
||||||
|
const input = document.querySelector("#" + selectorId + " input[type='file']")
|
||||||
|
const label = document.querySelector("#" + selectorId + " label")
|
||||||
|
input.addEventListener("change", (e) => {
|
||||||
|
if (input.files.length > 0) {
|
||||||
|
label.innerHTML = input.files[0].name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
body {
|
||||||
|
font-family: 'Ariel', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
margin: 1em;
|
||||||
|
border: 1px solid #449dfc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="submit"] {
|
||||||
|
border-radius: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadform {
|
||||||
|
display: flex;
|
||||||
|
margin: 1em;
|
||||||
|
background-color: #e5f0fc;
|
||||||
|
border: 1px solid #449dfc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* https://benmarshall.me/styling-file-inputs/ */
|
||||||
|
[type="file"] {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"] + label {
|
||||||
|
background-color: rgb(0, 86, 112);
|
||||||
|
border-radius: 1em;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label,
|
||||||
|
[type="file"] + label:hover {
|
||||||
|
background-color: #67b0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label {
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 980px) { /* This is the screen width of a OnePlus 5t */
|
||||||
|
body {
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="submit"] {
|
||||||
|
font-size: xx-large;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadform {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"] + label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue