Add Move File functionality and endpoint #23

Merged
cameron merged 1 commits from feature/file-move-endpoint into master 2024-01-22 02:14:34 +00:00
3 changed files with 106 additions and 30 deletions

View File

@@ -1,17 +1,19 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::read_dir; use std::fs::read_dir;
use std::io; use std::io;
use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
use ::anyhow; use ::anyhow;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use actix_web::web::Data;
use actix_web::{ use actix_web::{
web::{self, Query}, web::{self, Query},
HttpResponse, HttpResponse,
}; };
use log::{debug, error}; use log::{debug, error, info};
use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse}; use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse};
use crate::AppState; use crate::AppState;
@@ -19,6 +21,7 @@ use crate::AppState;
use crate::error::IntoHttpError; use crate::error::IntoHttpError;
use crate::tags::TagDao; use crate::tags::TagDao;
use path_absolutize::*; use path_absolutize::*;
use serde::Deserialize;
pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>( pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
_: Claims, _: Claims,
@@ -182,8 +185,49 @@ fn is_path_above_base_dir<P: AsRef<Path> + Debug>(
) )
} }
pub async fn move_file<FS: FileSystemAccess>(
_: Claims,
file_system: web::Data<FS>,
app_state: Data<AppState>,
request: web::Json<MoveFileRequest>,
) -> 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 { pub trait FileSystemAccess {
fn get_files_for_path(&self, path: &str) -> anyhow::Result<Vec<PathBuf>>; fn get_files_for_path(&self, path: &str) -> anyhow::Result<Vec<PathBuf>>;
fn move_file<P: AsRef<Path>>(&self, from: P, destination: P) -> anyhow::Result<()>;
} }
pub struct RealFileSystem { pub struct RealFileSystem {
@@ -205,6 +249,17 @@ impl FileSystemAccess for RealFileSystem {
}) })
.context("Invalid path") .context("Invalid path")
} }
fn move_file<P: AsRef<Path>>(&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)] #[cfg(test)]
@@ -248,6 +303,10 @@ mod tests {
} }
} }
} }
fn move_file<P: AsRef<Path>>(&self, from: P, destination: P) -> anyhow::Result<()> {
todo!()
}
} }
mod api { mod api {

View File

@@ -36,7 +36,7 @@ use log::{debug, error, info, warn};
use crate::auth::login; use crate::auth::login;
use crate::data::*; use crate::data::*;
use crate::database::*; 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::service::ServiceBuilder;
use crate::state::AppState; use crate::state::AppState;
use crate::tags::*; use crate::tags::*;
@@ -497,6 +497,7 @@ fn main() -> std::io::Result<()> {
web::resource("/photos") web::resource("/photos")
.route(web::get().to(files::list_photos::<SqliteTagDao, RealFileSystem>)), .route(web::get().to(files::list_photos::<SqliteTagDao, RealFileSystem>)),
) )
.service(web::resource("/file/move").post(move_file::<RealFileSystem>))
.service(get_image) .service(get_image)
.service(upload_image) .service(upload_image)
.service(generate_video) .service(generate_video)

View File

@@ -1,16 +1,16 @@
use crate::data::GetTagsRequest;
use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest}; use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest};
use actix_web::dev::{ServiceFactory, ServiceRequest}; use actix_web::dev::{ServiceFactory, ServiceRequest};
use actix_web::{web, App, HttpResponse, Responder}; use actix_web::{web, App, HttpResponse, Responder};
use anyhow::Context; use anyhow::Context;
use chrono::Utc; use chrono::Utc;
use diesel::dsl::{count_star}; use diesel::dsl::count_star;
use diesel::prelude::*; use diesel::prelude::*;
use log::{debug, info}; use log::{debug, info};
use schema::{tagged_photo, tags}; use schema::{tagged_photo, tags};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::sync::Mutex; use std::sync::Mutex;
use crate::data::GetTagsRequest;
pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T> pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T>
where where
@@ -61,16 +61,24 @@ async fn get_tags<D: TagDao>(
.into_http_internal_err() .into_http_internal_err()
} }
async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>, query: web::Query<GetTagsRequest>) -> impl Responder { async fn get_all_tags<D: TagDao>(
_: Claims,
tag_dao: web::Data<Mutex<D>>,
query: web::Query<GetTagsRequest>,
) -> impl Responder {
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao"); let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao tag_dao
.get_all_tags(query.path.clone()) .get_all_tags(query.path.clone())
.map(|tags| HttpResponse::Ok().json(tags.iter().map(|(tag_count, tag)| .map(|tags| {
TagWithTagCount { HttpResponse::Ok().json(
tags.iter()
.map(|(tag_count, tag)| TagWithTagCount {
tag: tag.clone(), tag: tag.clone(),
tag_count: *tag_count, tag_count: *tag_count,
} })
).collect::<Vec<TagWithTagCount>>())) .collect::<Vec<TagWithTagCount>>(),
)
})
.into_http_internal_err() .into_http_internal_err()
} }
@@ -149,7 +157,6 @@ pub struct Tag {
pub created_time: i64, pub created_time: i64,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct TagWithTagCount { pub struct TagWithTagCount {
pub tag_count: i64, pub tag_count: i64,
@@ -223,7 +230,9 @@ impl TagDao for SqliteTagDao {
.filter(tagged_photo::photo_name.like(path)) .filter(tagged_photo::photo_name.like(path))
.get_results(&mut self.connection) .get_results(&mut self.connection)
.map::<Vec<(i64, Tag)>, _>(|tags_with_count: Vec<(i64, i32, String, i64)>| { .map::<Vec<(i64, Tag)>, _>(|tags_with_count: Vec<(i64, i32, String, i64)>| {
tags_with_count.iter().map(|tup| { tags_with_count
.iter()
.map(|tup| {
( (
tup.0, tup.0,
Tag { Tag {
@@ -232,7 +241,8 @@ impl TagDao for SqliteTagDao {
created_time: tup.3, created_time: tup.3,
}, },
) )
}).collect() })
.collect()
}) })
.with_context(|| "Unable to get all tags") .with_context(|| "Unable to get all tags")
} }
@@ -372,7 +382,13 @@ mod tests {
impl TagDao for TestTagDao { impl TagDao for TestTagDao {
fn get_all_tags(&mut self, _option: Option<String>) -> anyhow::Result<Vec<(i64, Tag)>> { fn get_all_tags(&mut self, _option: Option<String>) -> anyhow::Result<Vec<(i64, Tag)>> {
Ok(self.tags.borrow().iter().map(|t| (1, t.clone())).collect::<Vec<(i64, Tag)>>().clone()) Ok(self
.tags
.borrow()
.iter()
.map(|t| (1, t.clone()))
.collect::<Vec<(i64, Tag)>>()
.clone())
} }
fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> { fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> {