diff --git a/src/files.rs b/src/files.rs index b161776..a46b026 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,17 +1,19 @@ use std::fmt::Debug; use std::fs::read_dir; use std::io; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::sync::Mutex; use ::anyhow; use anyhow::{anyhow, Context}; +use actix_web::web::Data; use actix_web::{ web::{self, Query}, HttpResponse, }; -use log::{debug, error}; +use log::{debug, error, info}; use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse}; use crate::AppState; @@ -19,6 +21,7 @@ use crate::AppState; use crate::error::IntoHttpError; use crate::tags::TagDao; use path_absolutize::*; +use serde::Deserialize; pub async fn list_photos( _: Claims, @@ -182,8 +185,49 @@ fn is_path_above_base_dir + Debug>( ) } +pub async fn move_file( + _: Claims, + file_system: web::Data, + app_state: Data, + request: web::Json, +) -> HttpResponse { + match is_valid_full_path(&app_state.base_path, &request.source, false) + .and_then(|source| { + is_valid_full_path(&app_state.base_path, &request.destination, true) + .map(|dest| (source, dest)) + }) + .ok_or(ErrorKind::InvalidData) + .map(|(source, dest)| file_system.move_file(source, dest)) + { + Ok(_) => { + info!("Moved file: {} -> {}", request.source, request.destination,); + + HttpResponse::Ok().finish() + } + Err(e) => { + error!( + "Error moving file: {} to: {}. {}", + request.source, request.destination, e + ); + + if e == ErrorKind::InvalidData { + HttpResponse::BadRequest().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + } +} + +#[derive(Deserialize)] +pub struct MoveFileRequest { + source: String, + destination: String, +} + pub trait FileSystemAccess { fn get_files_for_path(&self, path: &str) -> anyhow::Result>; + fn move_file>(&self, from: P, destination: P) -> anyhow::Result<()>; } pub struct RealFileSystem { @@ -205,6 +249,17 @@ impl FileSystemAccess for RealFileSystem { }) .context("Invalid path") } + + fn move_file>(&self, from: P, destination: P) -> anyhow::Result<()> { + let name = from + .as_ref() + .file_name() + .map(|n| n.to_str().unwrap_or_default().to_string()) + .unwrap_or_default(); + + std::fs::rename(from, destination) + .with_context(|| format!("Failed to move file: {:?}", name)) + } } #[cfg(test)] @@ -248,6 +303,10 @@ mod tests { } } } + + fn move_file>(&self, from: P, destination: P) -> anyhow::Result<()> { + todo!() + } } mod api { diff --git a/src/main.rs b/src/main.rs index 6b07143..045e2cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ use log::{debug, error, info, warn}; use crate::auth::login; use crate::data::*; use crate::database::*; -use crate::files::{is_image_or_video, is_valid_full_path, RealFileSystem}; +use crate::files::{is_image_or_video, is_valid_full_path, move_file, RealFileSystem}; use crate::service::ServiceBuilder; use crate::state::AppState; use crate::tags::*; @@ -497,6 +497,7 @@ fn main() -> std::io::Result<()> { web::resource("/photos") .route(web::get().to(files::list_photos::)), ) + .service(web::resource("/file/move").post(move_file::)) .service(get_image) .service(upload_image) .service(generate_video) diff --git a/src/tags.rs b/src/tags.rs index 8b478b9..eb2cb7a 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,20 +1,20 @@ +use crate::data::GetTagsRequest; use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest}; use actix_web::dev::{ServiceFactory, ServiceRequest}; use actix_web::{web, App, HttpResponse, Responder}; use anyhow::Context; use chrono::Utc; -use diesel::dsl::{count_star}; +use diesel::dsl::count_star; use diesel::prelude::*; use log::{debug, info}; use schema::{tagged_photo, tags}; use serde::{Deserialize, Serialize}; use std::borrow::BorrowMut; use std::sync::Mutex; -use crate::data::GetTagsRequest; pub fn add_tag_services(app: App) -> App - where - T: ServiceFactory, +where + T: ServiceFactory, { app.service( web::resource("image/tags") @@ -22,8 +22,8 @@ pub fn add_tag_services(app: App) -> App .route(web::get().to(get_tags::)) .route(web::delete().to(remove_tagged_photo::)), ) - .service(web::resource("image/tags/all").route(web::get().to(get_all_tags::))) - .service(web::resource("image/tags/batch").route(web::post().to(update_tags::))) + .service(web::resource("image/tags/all").route(web::get().to(get_all_tags::))) + .service(web::resource("image/tags/batch").route(web::post().to(update_tags::))) } async fn add_tag( @@ -61,16 +61,24 @@ async fn get_tags( .into_http_internal_err() } -async fn get_all_tags(_: Claims, tag_dao: web::Data>, query: web::Query) -> impl Responder { +async fn get_all_tags( + _: Claims, + tag_dao: web::Data>, + query: web::Query, +) -> impl Responder { let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao"); tag_dao .get_all_tags(query.path.clone()) - .map(|tags| HttpResponse::Ok().json(tags.iter().map(|(tag_count, tag)| - TagWithTagCount { - tag: tag.clone(), - tag_count: *tag_count, - } - ).collect::>())) + .map(|tags| { + HttpResponse::Ok().json( + tags.iter() + .map(|(tag_count, tag)| TagWithTagCount { + tag: tag.clone(), + tag_count: *tag_count, + }) + .collect::>(), + ) + }) .into_http_internal_err() } @@ -149,7 +157,6 @@ pub struct Tag { pub created_time: i64, } - #[derive(Serialize, Debug)] pub struct TagWithTagCount { pub tag_count: i64, @@ -223,16 +230,19 @@ impl TagDao for SqliteTagDao { .filter(tagged_photo::photo_name.like(path)) .get_results(&mut self.connection) .map::, _>(|tags_with_count: Vec<(i64, i32, String, i64)>| { - tags_with_count.iter().map(|tup| { - ( - tup.0, - Tag { - id: tup.1, - name: tup.2.clone(), - created_time: tup.3, - }, - ) - }).collect() + tags_with_count + .iter() + .map(|tup| { + ( + tup.0, + Tag { + id: tup.1, + name: tup.2.clone(), + created_time: tup.3, + }, + ) + }) + .collect() }) .with_context(|| "Unable to get all tags") } @@ -289,9 +299,9 @@ impl TagDao for SqliteTagDao { .filter(tagged_photo::tag_id.eq(tag.id)) .filter(tagged_photo::photo_name.eq(path)), ) - .execute(&mut self.connection) - .with_context(|| format!("Unable to delete tag: '{}'", &tag.name)) - .map(|_| Some(())) + .execute(&mut self.connection) + .with_context(|| format!("Unable to delete tag: '{}'", &tag.name)) + .map(|_| Some(())) } else { info!("No tag found with name '{}'", tag_name); Ok(None) @@ -372,7 +382,13 @@ mod tests { impl TagDao for TestTagDao { fn get_all_tags(&mut self, _option: Option) -> anyhow::Result> { - Ok(self.tags.borrow().iter().map(|t| (1, t.clone())).collect::>().clone()) + Ok(self + .tags + .borrow() + .iter() + .map(|t| (1, t.clone())) + .collect::>() + .clone()) } fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result> {