Merge pull request 'Create file metadata endpoint' (#14) from feature/file-info-api into master
All checks were successful
Core Repos/ImageApi/pipeline/head This commit looks good

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2021-07-08 21:28:15 +00:00
6 changed files with 303 additions and 209 deletions

411
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,12 @@ jsonwebtoken = "7.2.0"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
diesel = { version = "1.4.5", features = ["sqlite"] } diesel = { version = "1.4.5", features = ["sqlite"] }
hmac = "0.10" hmac = "0.11"
sha2 = "0.9" sha2 = "0.9"
chrono = "0.4" chrono = "0.4"
dotenv = "0.15" dotenv = "0.15"
bcrypt = "0.9" bcrypt = "0.9"
image = { version = "0.23.7", default-features = false, features = ["jpeg", "png", "jpeg_rayon"] } image = { version = "0.23", default-features = false, features = ["jpeg", "png", "jpeg_rayon"] }
walkdir = "2" walkdir = "2"
rayon = "1.3" rayon = "1.3"
notify = "4.0" notify = "4.0"

View File

@@ -1,5 +1,6 @@
use std::str::FromStr; use std::{fs, str::FromStr};
use chrono::{DateTime, Utc};
use log::error; use log::error;
use actix_web::error::ErrorUnauthorized; use actix_web::error::ErrorUnauthorized;
@@ -68,6 +69,12 @@ impl FromRequest for Claims {
} }
} }
#[derive(Serialize)]
pub struct PhotosResponse<'a> {
pub photos: &'a [String],
pub dirs: &'a [String],
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ThumbnailRequest { pub struct ThumbnailRequest {
pub path: String, pub path: String,
@@ -92,6 +99,29 @@ pub struct AddFavoriteRequest {
pub path: String, pub path: String,
} }
#[derive(Debug, Serialize)]
pub struct MetadataResponse {
pub created: Option<i64>,
pub modified: Option<i64>,
pub size: u64,
}
impl From<fs::Metadata> for MetadataResponse {
fn from(metadata: fs::Metadata) -> Self {
MetadataResponse {
created: metadata.created().ok().map(|created| {
let utc: DateTime<Utc> = created.into();
utc.timestamp()
}),
modified: metadata.modified().ok().map(|modified| {
let utc: DateTime<Utc> = modified.into();
utc.timestamp()
}),
size: metadata.len(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Claims; use super::Claims;

View File

@@ -44,15 +44,12 @@ impl UserDao for SqliteUserDao {
.execute(&self.connection) .execute(&self.connection)
.unwrap(); .unwrap();
match users users
.filter(username.eq(username)) .filter(username.eq(username))
.load::<User>(&self.connection) .load::<User>(&self.connection)
.unwrap() .unwrap()
.first() .first()
{ .cloned()
Some(u) => Some(u.clone()),
None => None,
}
} else { } else {
None None
} }

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use path_absolutize::*; use path_absolutize::*;
pub fn list_files(dir: PathBuf) -> io::Result<Vec<PathBuf>> { pub fn list_files(dir: &Path) -> io::Result<Vec<PathBuf>> {
let files = read_dir(dir)? let files = read_dir(dir)?
.map(|res| res.unwrap()) .map(|res| res.unwrap())
.filter(|entry| is_image_or_video(&entry.path()) || entry.file_type().unwrap().is_dir()) .filter(|entry| is_image_or_video(&entry.path()) || entry.file_type().unwrap().is_dir())
@@ -151,6 +151,13 @@ mod tests {
assert!(is_image_or_video(Path::new("image.MoV"))); assert!(is_image_or_video(Path::new("image.MoV")));
} }
#[test]
fn nef_valid_extension_test() {
assert!(is_image_or_video(Path::new("image.nef")));
assert!(is_image_or_video(Path::new("image.NEF")));
assert!(is_image_or_video(Path::new("image.NeF")));
}
#[test] #[test]
fn hidden_file_not_valid_test() { fn hidden_file_not_valid_test() {
assert!(!is_image_or_video(Path::new(".DS_store"))); assert!(!is_image_or_video(Path::new(".DS_store")));

View File

@@ -2,17 +2,17 @@
extern crate diesel; extern crate diesel;
extern crate rayon; extern crate rayon;
use crate::auth::login;
use actix_web_prom::PrometheusMetrics; use actix_web_prom::PrometheusMetrics;
use database::{DbError, DbErrorKind, FavoriteDao, SqliteFavoriteDao, SqliteUserDao, UserDao};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prometheus::{self, IntGauge}; use prometheus::{self, IntGauge};
use std::path::{Path, PathBuf}; use std::sync::{mpsc::channel, Arc};
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::{collections::HashMap, io::prelude::*}; use std::{collections::HashMap, io::prelude::*};
use std::{env, fs::File}; use std::{env, fs::File};
use std::{
io::ErrorKind,
path::{Path, PathBuf},
};
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use actix::prelude::*; use actix::prelude::*;
@@ -27,12 +27,12 @@ use actix_web::{
}; };
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use rayon::prelude::*; use rayon::prelude::*;
use serde::Serialize;
use data::{AddFavoriteRequest, ThumbnailRequest};
use log::{debug, error, info}; use log::{debug, error, info};
use crate::data::Claims; use crate::auth::login;
use crate::data::*;
use crate::database::*;
use crate::files::{is_image_or_video, is_valid_path, list_files}; use crate::files::{is_image_or_video, is_valid_path, list_files};
use crate::video::*; use crate::video::*;
@@ -61,17 +61,17 @@ async fn list_photos(_claims: Claims, req: Query<ThumbnailRequest>) -> impl Resp
let path = &req.path; let path = &req.path;
if let Some(path) = is_valid_path(path) { if let Some(path) = is_valid_path(path) {
let files = list_files(path).unwrap_or_default(); let files = list_files(&path).unwrap_or_default();
let photos = &files let photos = &files
.iter() .iter()
.filter(|f| !f.extension().unwrap_or_default().is_empty()) .filter(|&f| f.metadata().map_or(false, |md| md.is_file()))
.map(|f| f.to_str().unwrap().to_string()) .map(|f| f.to_str().unwrap().to_string())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let dirs = &files let dirs = &files
.iter() .iter()
.filter(|f| f.extension().unwrap_or_default().is_empty()) .filter(|&f| f.metadata().map_or(false, |md| md.is_dir()))
.map(|f| f.to_str().unwrap().to_string()) .map(|f| f.to_str().unwrap().to_string())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
@@ -82,12 +82,6 @@ async fn list_photos(_claims: Claims, req: Query<ThumbnailRequest>) -> impl Resp
} }
} }
#[derive(Serialize)]
struct PhotosResponse<'a> {
photos: &'a [String],
dirs: &'a [String],
}
#[get("/image")] #[get("/image")]
async fn get_image( async fn get_image(
_claims: Claims, _claims: Claims,
@@ -119,6 +113,24 @@ async fn get_image(
} }
} }
#[get("/image/metadata")]
async fn get_file_metadata(_: Claims, path: web::Query<ThumbnailRequest>) -> impl Responder {
match is_valid_path(&path.path)
.ok_or_else(|| ErrorKind::InvalidData.into())
.and_then(File::open)
.and_then(|file| file.metadata())
{
Ok(metadata) => {
let response: MetadataResponse = metadata.into();
HttpResponse::Ok().json(response)
}
Err(e) => {
error!("Error getting metadata for file '{}': {:?}", path.path, e);
HttpResponse::InternalServerError().finish()
}
}
}
#[post("/image")] #[post("/image")]
async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder { async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
let mut file_content: BytesMut = BytesMut::new(); let mut file_content: BytesMut = BytesMut::new();
@@ -370,7 +382,7 @@ fn create_thumbnails() {
update_media_counts(&images); update_media_counts(&images);
} }
fn update_media_counts(media_dir: &PathBuf) { fn update_media_counts(media_dir: &Path) {
let mut image_count = 0; let mut image_count = 0;
let mut video_count = 0; let mut video_count = 0;
for ref entry in WalkDir::new(media_dir).into_iter().filter_map(|e| e.ok()) { for ref entry in WalkDir::new(media_dir).into_iter().filter_map(|e| e.ok()) {
@@ -485,6 +497,7 @@ fn main() -> std::io::Result<()> {
.service(favorites) .service(favorites)
.service(put_add_favorite) .service(put_add_favorite)
.service(delete_favorite) .service(delete_favorite)
.service(get_file_metadata)
.app_data(app_data.clone()) .app_data(app_data.clone())
.data::<Box<dyn UserDao>>(Box::new(user_dao)) .data::<Box<dyn UserDao>>(Box::new(user_dao))
.data::<Box<dyn FavoriteDao>>(Box::new(favorites_dao)) .data::<Box<dyn FavoriteDao>>(Box::new(favorites_dao))