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
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:
411
Cargo.lock
generated
411
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")));
|
||||||
|
|||||||
49
src/main.rs
49
src/main.rs
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user