Add Move File functionality and endpoint #23
61
src/files.rs
61
src/files.rs
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
72
src/tags.rs
72
src/tags.rs
@@ -1,20 +1,20 @@
|
|||||||
|
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
|
||||||
T: ServiceFactory<ServiceRequest, Config=(), Error=actix_web::Error, InitError=()>,
|
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
|
||||||
{
|
{
|
||||||
app.service(
|
app.service(
|
||||||
web::resource("image/tags")
|
web::resource("image/tags")
|
||||||
@@ -22,8 +22,8 @@ pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T>
|
|||||||
.route(web::get().to(get_tags::<TagD>))
|
.route(web::get().to(get_tags::<TagD>))
|
||||||
.route(web::delete().to(remove_tagged_photo::<TagD>)),
|
.route(web::delete().to(remove_tagged_photo::<TagD>)),
|
||||||
)
|
)
|
||||||
.service(web::resource("image/tags/all").route(web::get().to(get_all_tags::<TagD>)))
|
.service(web::resource("image/tags/all").route(web::get().to(get_all_tags::<TagD>)))
|
||||||
.service(web::resource("image/tags/batch").route(web::post().to(update_tags::<TagD>)))
|
.service(web::resource("image/tags/batch").route(web::post().to(update_tags::<TagD>)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_tag<D: TagDao>(
|
async fn add_tag<D: TagDao>(
|
||||||
@@ -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(
|
||||||
tag: tag.clone(),
|
tags.iter()
|
||||||
tag_count: *tag_count,
|
.map(|(tag_count, tag)| TagWithTagCount {
|
||||||
}
|
tag: tag.clone(),
|
||||||
).collect::<Vec<TagWithTagCount>>()))
|
tag_count: *tag_count,
|
||||||
|
})
|
||||||
|
.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,16 +230,19 @@ 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()
|
||||||
tup.0,
|
.map(|tup| {
|
||||||
Tag {
|
(
|
||||||
id: tup.1,
|
tup.0,
|
||||||
name: tup.2.clone(),
|
Tag {
|
||||||
created_time: tup.3,
|
id: tup.1,
|
||||||
},
|
name: tup.2.clone(),
|
||||||
)
|
created_time: tup.3,
|
||||||
}).collect()
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
})
|
})
|
||||||
.with_context(|| "Unable to get all tags")
|
.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::tag_id.eq(tag.id))
|
||||||
.filter(tagged_photo::photo_name.eq(path)),
|
.filter(tagged_photo::photo_name.eq(path)),
|
||||||
)
|
)
|
||||||
.execute(&mut self.connection)
|
.execute(&mut self.connection)
|
||||||
.with_context(|| format!("Unable to delete tag: '{}'", &tag.name))
|
.with_context(|| format!("Unable to delete tag: '{}'", &tag.name))
|
||||||
.map(|_| Some(()))
|
.map(|_| Some(()))
|
||||||
} else {
|
} else {
|
||||||
info!("No tag found with name '{}'", tag_name);
|
info!("No tag found with name '{}'", tag_name);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -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>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user