feature/tagging #16

Merged
cameron merged 22 commits from feature/tagging into master 2023-04-10 12:55:28 +00:00
3 changed files with 245 additions and 36 deletions
Showing only changes of commit 4ded708911 - Show all commits

View File

@@ -27,6 +27,24 @@ impl SqliteUserDao {
}
}
#[cfg(test)]
pub mod test {
use diesel::{Connection, SqliteConnection};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const DB_MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub fn in_memory_db_connection() -> SqliteConnection {
let mut connection = SqliteConnection::establish(":memory:")
.expect("Unable to create in-memory db connection");
connection
.run_pending_migrations(DB_MIGRATIONS)
.expect("Failure running DB migrations");
return connection;
}
}
impl UserDao for SqliteUserDao {
// TODO: Should probably use Result here
fn create_user(&mut self, user: &str, pass: &str) -> Option<User> {

View File

@@ -19,16 +19,17 @@ use crate::AppState;
use crate::tags::TagDao;
use path_absolutize::*;
pub async fn list_photos<TagD: TagDao>(
pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
_: Claims,
req: Query<FilesRequest>,
app_state: web::Data<AppState>,
file_system: web::Data<FS>,
tag_dao: web::Data<Mutex<TagD>>,
) -> HttpResponse {
let path = &req.path;
if let Some(path) = is_valid_full_path(&PathBuf::from(&app_state.base_path), path) {
if let Ok(files) = file_system.get_files_for_path(path) {
debug!("Valid path: {:?}", path);
let files = list_files(&path).unwrap_or_default();
let photos = files
.iter()
@@ -36,7 +37,7 @@ pub async fn list_photos<TagD: TagDao>(
f.metadata().map_or_else(
|e| {
error!("Failed getting file metadata: {:?}", e);
false
f.extension().is_some()
},
|md| md.is_file(),
)
@@ -110,10 +111,13 @@ pub fn is_image_or_video(path: &Path) -> bool {
|| extension == "nef"
}
pub fn is_valid_full_path<P: AsRef<Path> + Debug>(base: &P, path: &str) -> Option<PathBuf> {
debug!("Base: {:?}. Path: {}", base, path);
pub fn is_valid_full_path<P: AsRef<Path> + Debug + AsRef<std::ffi::OsStr>>(
base: &P,
path: &P,
) -> Option<PathBuf> {
debug!("Base: {:?}. Path: {:?}", base, path);
let path = PathBuf::from(path);
let path = PathBuf::from(&path);
let mut path = if path.is_relative() {
let mut full_path = PathBuf::new();
full_path.push(base);
@@ -153,31 +157,95 @@ fn is_path_above_base_dir<P: AsRef<Path> + Debug>(
)
}
pub trait FileSystemAccess {
fn get_files_for_path(&self, path: &str) -> anyhow::Result<Vec<PathBuf>>;
}
pub struct RealFileSystem {
base_path: String,
}
impl RealFileSystem {
pub(crate) fn new(base_path: String) -> RealFileSystem {
RealFileSystem { base_path }
}
}
impl FileSystemAccess for RealFileSystem {
fn get_files_for_path(&self, path: &str) -> anyhow::Result<Vec<PathBuf>> {
is_valid_full_path(&PathBuf::from(&self.base_path), &PathBuf::from(path))
.map(|path| {
debug!("Valid path: {:?}", path);
list_files(&path).unwrap_or_default()
})
.context("Invalid path")
}
}
#[cfg(test)]
mod tests {
use crate::database::connect;
use crate::tags::SqliteTagDao;
use actix_web::web::Data;
use diesel::{Connection, SqliteConnection};
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::fs::File;
use super::*;
struct FakeFileSystem {
files: HashMap<String, Vec<String>>,
err: bool,
}
impl FakeFileSystem {
fn with_error() -> FakeFileSystem {
FakeFileSystem {
files: HashMap::new(),
err: true,
}
}
fn new(files: HashMap<String, Vec<String>>) -> FakeFileSystem {
FakeFileSystem { files, err: false }
}
}
impl FileSystemAccess for FakeFileSystem {
fn get_files_for_path(&self, path: &str) -> anyhow::Result<Vec<PathBuf>> {
if self.err {
Err(anyhow!("Error for test"))
} else {
if let Some(files) = self.files.get(path) {
Ok(files
.iter()
.map(|p| PathBuf::from(p))
.collect::<Vec<PathBuf>>())
} else {
Ok(Vec::new())
}
}
}
}
mod api {
use super::*;
use actix::Actor;
use actix_web::{
web::{self, Query},
HttpResponse,
};
use actix_web::{web::Query, HttpResponse};
use crate::{
data::{Claims, PhotosResponse, ThumbnailRequest},
data::{Claims, PhotosResponse},
testhelpers::BodyReader,
video::StreamActor,
AppState,
};
use std::{fs, sync::Arc};
use actix_web::web::Data;
use crate::database::test::in_memory_db_connection;
use crate::tags::SqliteTagDao;
use actix_web::web::Data;
use std::{fs, sync::Arc};
fn setup() {
let _ = env_logger::builder().is_test(true).try_init();
@@ -194,7 +262,7 @@ mod tests {
let request: Query<FilesRequest> = Query::from_query("path=").unwrap();
let mut temp_photo = std::env::temp_dir();
let mut temp_photo = env::temp_dir();
let mut tmp = temp_photo.clone();
tmp.push("test-dir");
@@ -202,24 +270,25 @@ mod tests {
temp_photo.push("photo.jpg");
fs::File::create(temp_photo.clone()).unwrap();
File::create(temp_photo.clone()).unwrap();
let response: HttpResponse = list_photos(
claims,
request,
web::Data::new(AppState::new(
Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from("/tmp"),
String::from("/tmp/thumbs"),
)),
Data::new(RealFileSystem::new(String::from("/tmp"))),
Data::new(Mutex::new(SqliteTagDao::default())),
)
.await;
let status = response.status();
assert_eq!(status, 200);
let body: PhotosResponse = serde_json::from_str(&response.read_to_str()).unwrap();
assert_eq!(status, 200);
assert!(body.photos.contains(&String::from("photo.jpg")));
assert!(body.dirs.contains(&String::from("test-dir")));
assert!(body
@@ -246,28 +315,145 @@ mod tests {
let response = list_photos(
claims,
request,
web::Data::new(AppState::new(
Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from("/tmp"),
String::from("/tmp/thumbs"),
)),
Data::new(RealFileSystem::new(String::from("./"))),
Data::new(Mutex::new(SqliteTagDao::default())),
)
.await;
assert_eq!(response.status(), 400);
}
#[actix_rt::test]
async fn get_files_with_tag_any_filter() {
setup();
let claims = Claims {
sub: String::from("1"),
exp: 12345,
};
let request: Query<FilesRequest> = Query::from_query("path=&tag_ids=1,3").unwrap();
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
let tag1 = tag_dao.create_tag("tag1").unwrap();
let tag2 = tag_dao.create_tag("tag2").unwrap();
let tag3 = tag_dao.create_tag("tag3").unwrap();
&tag_dao.tag_file("test.jpg", tag1.id).unwrap();
&tag_dao.tag_file("test.jpg", tag3.id).unwrap();
let mut files = HashMap::new();
files.insert(
String::from(""),
vec![
String::from("file1.txt"),
String::from("test.jpg"),
String::from("some-other.jpg"),
],
);
let response: HttpResponse = list_photos(
claims,
request,
Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from(""),
String::from("/tmp/thumbs"),
)),
Data::new(FakeFileSystem::new(files)),
Data::new(Mutex::new(tag_dao)),
)
.await;
assert_eq!(200, response.status());
let body: PhotosResponse = serde_json::from_str(&response.read_to_str()).unwrap();
assert_eq!(1, body.photos.len());
assert!(body.photos.contains(&String::from("test.jpg")));
}
#[actix_rt::test]
async fn get_files_with_tag_all_filter() {
setup();
let claims = Claims {
sub: String::from("1"),
exp: 12345,
};
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
let tag1 = tag_dao.create_tag("tag1").unwrap();
let tag2 = tag_dao.create_tag("tag2").unwrap();
let tag3 = tag_dao.create_tag("tag3").unwrap();
&tag_dao.tag_file("test.jpg", tag1.id).unwrap();
&tag_dao.tag_file("test.jpg", tag3.id).unwrap();
// Should get filtered since it doesn't have tag3
tag_dao.tag_file("some-other.jpg", tag1.id).unwrap();
let mut files = HashMap::new();
files.insert(
String::from(""),
vec![
String::from("file1.txt"),
String::from("test.jpg"),
String::from("some-other.jpg"),
],
);
let request: Query<FilesRequest> =
Query::from_query("path=&tag_ids=1,3&tag_filter_mode=All").unwrap();
let response: HttpResponse = list_photos(
claims,
request,
Data::new(AppState::new(
Arc::new(StreamActor {}.start()),
String::from(""),
String::from("/tmp/thumbs"),
)),
Data::new(FakeFileSystem::new(files)),
Data::new(Mutex::new(tag_dao)),
)
.await;
assert_eq!(200, response.status());
let body: PhotosResponse = serde_json::from_str(&response.read_to_str()).unwrap();
assert_eq!(1, body.photos.len());
assert!(body.photos.contains(&String::from("test.jpg")));
}
}
#[test]
fn directory_traversal_test() {
let base = env::temp_dir();
assert_eq!(None, is_valid_full_path(&base, "../"));
assert_eq!(None, is_valid_full_path(&base, ".."));
assert_eq!(None, is_valid_full_path(&base, "fake/../../../"));
assert_eq!(None, is_valid_full_path(&base, "../../../etc/passwd"));
assert_eq!(None, is_valid_full_path(&base, "..//etc/passwd"));
assert_eq!(None, is_valid_full_path(&base, "../../etc/passwd"));
assert_eq!(None, is_valid_full_path(&base, &PathBuf::from("../")));
assert_eq!(None, is_valid_full_path(&base, &PathBuf::from("..")));
assert_eq!(
None,
is_valid_full_path(&base, &PathBuf::from("fake/../../../"))
);
assert_eq!(
None,
is_valid_full_path(&base, &PathBuf::from("../../../etc/passwd"))
);
assert_eq!(
None,
is_valid_full_path(&base, &PathBuf::from("..//etc/passwd"))
);
assert_eq!(
None,
is_valid_full_path(&base, &PathBuf::from("../../etc/passwd"))
);
}
#[test]
@@ -277,7 +463,7 @@ mod tests {
test_file.push("test.png");
File::create(test_file).unwrap();
assert!(is_valid_full_path(&base, "test.png").is_some());
assert!(is_valid_full_path(&base, &PathBuf::from("test.png")).is_some());
}
#[test]
@@ -288,7 +474,7 @@ mod tests {
let mut test_file = PathBuf::from(&base);
test_file.push(path);
assert_eq!(None, is_valid_full_path(&base, path));
assert_eq!(None, is_valid_full_path(&base, &test_file));
}
#[test]
@@ -298,11 +484,11 @@ mod tests {
test_file.push("test.png");
File::create(&test_file).unwrap();
assert!(is_valid_full_path(&base, test_file.to_str().unwrap()).is_some());
assert!(is_valid_full_path(&base, &test_file).is_some());
assert_eq!(
Some(PathBuf::from("/tmp/test.png")),
is_valid_full_path(&base, "/tmp/test.png")
is_valid_full_path(&base, &PathBuf::from("/tmp/test.png"))
);
}

View File

@@ -35,7 +35,7 @@ use log::{debug, error, info};
use crate::auth::login;
use crate::data::*;
use crate::database::*;
use crate::files::{is_image_or_video, is_valid_full_path};
use crate::files::{is_image_or_video, is_valid_full_path, RealFileSystem};
use crate::service::ServiceBuilder;
use crate::state::AppState;
use crate::tags::*;
@@ -156,9 +156,10 @@ async fn upload_image(
let path = file_path.unwrap_or_else(|| app_state.base_path.clone());
if !file_content.is_empty() {
let full_path = PathBuf::from(&path).join(file_name.unwrap());
if let Some(full_path) =
is_valid_full_path(&app_state.base_path, full_path.to_str().unwrap_or(""))
{
if let Some(full_path) = is_valid_full_path(
&app_state.base_path,
&full_path.to_str().unwrap().to_string(),
) {
if !full_path.is_file() && is_image_or_video(&full_path) {
let mut file = File::create(full_path).unwrap();
file.write_all(&file_content).unwrap();
@@ -474,7 +475,10 @@ fn main() -> std::io::Result<()> {
App::new()
.wrap(middleware::Logger::default())
.service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>)))
.service(web::resource("/photos").route(web::get().to(files::list_photos::<SqliteTagDao>)))
.service(
web::resource("/photos")
.route(web::get().to(files::list_photos::<SqliteTagDao, RealFileSystem>)),
)
.service(get_image)
.service(upload_image)
.service(generate_video)
@@ -486,6 +490,7 @@ fn main() -> std::io::Result<()> {
.service(get_file_metadata)
.add_feature(add_tag_services::<_, SqliteTagDao>)
.app_data(app_data.clone())
.app_data::<Data<RealFileSystem>>(Data::new(RealFileSystem::new(app_data.base_path.clone())))
.app_data::<Data<Mutex<SqliteUserDao>>>(Data::new(Mutex::new(user_dao)))
.app_data::<Data<Mutex<Box<dyn FavoriteDao>>>>(Data::new(Mutex::new(Box::new(
favorites_dao,