implement user creation, getting and deletion, add locking to file uploads, add some more .bru
This commit is contained in:
parent
163d2a61ca
commit
50d0f6dfa5
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
/target
|
||||
.idea
|
||||
casket.sqlite
|
||||
casket.sqlite*
|
||||
data
|
||||
|
||||
@ -12,6 +12,7 @@ pub trait Repository {
|
||||
&self,
|
||||
id: &str,
|
||||
) -> impl std::future::Future<Output = Result<UserModel, ProblemDetails>> + Send;
|
||||
fn delete_user(&self, id: &str) -> impl std::future::Future<Output = ()> + Send;
|
||||
fn create_file(
|
||||
&self,
|
||||
file: File,
|
||||
@ -45,6 +46,6 @@ pub mod models {
|
||||
pub mod insert {
|
||||
pub struct File {
|
||||
pub user_id: String,
|
||||
pub path: String
|
||||
pub path: String,
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,15 @@ impl Repository for Sqlite {
|
||||
self.get_user(id).await.map(|option| option.unwrap())
|
||||
}
|
||||
|
||||
async fn delete_user(&self, id: &str) {
|
||||
self.get_connection()
|
||||
.execute("DELETE FROM files WHERE user_id = ?;", (id,))
|
||||
.unwrap();
|
||||
self.get_connection()
|
||||
.execute("DELETE FROM users WHERE uuid = ?;", (id,))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn create_file(&self, file: File) -> Result<FileModel, ProblemDetails> {
|
||||
self.get_connection()
|
||||
.execute(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::errors::to_internal_error;
|
||||
use crate::extractor_helper::WithProblemDetails;
|
||||
use crate::files::build_system_path;
|
||||
use crate::files::PathInfo;
|
||||
use crate::AppState;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Query, State};
|
||||
@ -20,7 +20,8 @@ pub async fn handler(
|
||||
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||
WithProblemDetails(Query(query)): WithProblemDetails<Query<SingleFileQuery>>,
|
||||
) -> Result<impl IntoResponse, ProblemDetails> {
|
||||
let filesystem_path = build_system_path(&state.config.files.directory, &user_id, &query.path)?;
|
||||
let filesystem_path =
|
||||
PathInfo::build(&state.config.files.directory, &user_id, &query.path)?.file_system_location;
|
||||
|
||||
let file_name = get_file_name(&filesystem_path)?;
|
||||
let file = open_file(&filesystem_path).await?;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::errors::to_internal_error;
|
||||
use crate::extractor_helper::WithProblemDetails;
|
||||
use crate::files::build_system_path;
|
||||
use crate::files::PathInfo;
|
||||
use crate::{errors, AppState};
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::StatusCode;
|
||||
@ -19,7 +19,8 @@ pub async fn query_file_tree(
|
||||
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||
WithProblemDetails(Query(query)): WithProblemDetails<Query<FileQuery>>,
|
||||
) -> Result<Json<PathElements>, ProblemDetails> {
|
||||
let path = build_system_path(&state.config.files.directory, &user_id, &query.path)?;
|
||||
let path =
|
||||
PathInfo::build(&state.config.files.directory, &user_id, &query.path)?.file_system_location;
|
||||
debug!("Loading path: {:?}", path);
|
||||
PathElements::load(path, query.nesting)
|
||||
.await
|
||||
|
||||
@ -7,20 +7,33 @@ pub mod download;
|
||||
pub mod list;
|
||||
pub mod upload;
|
||||
|
||||
pub fn build_system_path(
|
||||
base_directory: &Path,
|
||||
user_id: &str,
|
||||
user_path: &PathBuf,
|
||||
) -> Result<PathBuf, ProblemDetails> {
|
||||
if user_path.is_absolute() {
|
||||
Err(
|
||||
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
|
||||
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
|
||||
+ format!(" Provided Path: {user_path:?}",).as_str(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
sanitize_path(&base_directory.join(user_id).join(user_path))
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PathInfo {
|
||||
file_system_location: PathBuf,
|
||||
relative_user_location: PathBuf,
|
||||
}
|
||||
|
||||
impl PathInfo {
|
||||
pub fn build(
|
||||
base_directory: &Path,
|
||||
user_id: &str,
|
||||
user_path: &PathBuf,
|
||||
) -> Result<Self, ProblemDetails> {
|
||||
if user_path.is_absolute() {
|
||||
Err(
|
||||
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
|
||||
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
|
||||
+ format!(" Provided Path: {user_path:?}",).as_str(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let user_path = sanitize_path(user_path)?;
|
||||
let system_path = sanitize_path(&base_directory.join(&user_id))?.join(&user_path);
|
||||
Ok(Self {
|
||||
file_system_location: system_path,
|
||||
relative_user_location: user_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,21 +109,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_system_path_success() {
|
||||
fn test_build_path_info() {
|
||||
let input_path = PathBuf::from("tmp/blub");
|
||||
let user_id = "bla";
|
||||
let path_info = PathInfo::build(&PathBuf::from("/tmp/bla"), user_id, &input_path).unwrap();
|
||||
assert_eq!(
|
||||
build_system_path(&PathBuf::from("/tmp/bla"), user_id, &input_path).unwrap(),
|
||||
path_info.file_system_location,
|
||||
PathBuf::from("/tmp/bla/bla/tmp/blub")
|
||||
);
|
||||
assert_eq!(path_info.relative_user_location, PathBuf::from(input_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_system_path_error_with_absolute_user_path_returns_error() {
|
||||
let input_path = PathBuf::from("tmp/blub");
|
||||
fn test_build_path_info_error_with_absolute_user_path_returns_error() {
|
||||
let input_path = PathBuf::from("/tmp/blub");
|
||||
let user_id = "bla";
|
||||
assert_eq!(
|
||||
build_system_path(&PathBuf::from("/tmp/bla"), user_id, &input_path),
|
||||
PathInfo::build(&PathBuf::from("/tmp/bla"), user_id, &input_path),
|
||||
Err(
|
||||
ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(
|
||||
errors::ERROR_DETAILS_ABSOLUTE_PATH_NOT_ALLOWED.to_owned()
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::db::repository::{insert, Repository};
|
||||
use crate::errors::to_internal_error;
|
||||
use crate::files::build_system_path;
|
||||
use crate::files::PathInfo;
|
||||
use crate::locking::Lock;
|
||||
use crate::{errors, AppState};
|
||||
use axum::body::Bytes;
|
||||
use axum::debug_handler;
|
||||
@ -46,15 +47,38 @@ async fn handle_single_file_upload<'field_lifetime>(
|
||||
.with_detail(errors::ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD)),
|
||||
Some(field_name) => {
|
||||
let path = PathBuf::from(field_name);
|
||||
let filesystem_path = build_system_path(&state.config.files.directory, user_id, &path)?;
|
||||
save_file(&filesystem_path, map_error_to_io_error(field))
|
||||
.await
|
||||
.map_err(to_internal_error)?;
|
||||
update_file_version(state, user_id, &filesystem_path).await
|
||||
let path_info = PathInfo::build(&state.config.files.directory, user_id, &path)?;
|
||||
let lock_id = &build_lock_id(user_id, &path_info.relative_user_location);
|
||||
if state
|
||||
.get_lock()
|
||||
.lock(lock_id).await
|
||||
{
|
||||
let result = update_file(state, user_id, field, &path_info).await;
|
||||
state.get_lock().unlock(lock_id).await;
|
||||
result
|
||||
} else {
|
||||
Err(ProblemDetails::from_status_code(StatusCode::CONFLICT)
|
||||
.with_detail(format!("Cannot upload file because another application is currently modifying that: {:?}", path_info.relative_user_location)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_file<'field_lifetime>(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
field: Field<'field_lifetime>,
|
||||
path_info: &PathInfo,
|
||||
) -> Result<(), ProblemDetails> {
|
||||
save_file(
|
||||
&path_info.file_system_location,
|
||||
map_error_to_io_error(field),
|
||||
)
|
||||
.await
|
||||
.map_err(to_internal_error)?;
|
||||
update_file_version(state, user_id, &path_info.relative_user_location).await
|
||||
}
|
||||
|
||||
async fn update_file_version(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
@ -90,3 +114,8 @@ async fn save_file(
|
||||
fn map_error_to_io_error(field: Field) -> MapErr<Field, fn(MultipartError) -> Error> {
|
||||
field.map_err(|err| Error::new(io::ErrorKind::Other, err))
|
||||
}
|
||||
|
||||
fn build_lock_id(user_id: &str, path: &Path) -> String {
|
||||
let path_string = path.to_string_lossy();
|
||||
format!("UPLOAD_{user_id}_{path_string}")
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
use crate::locking::LockingResult::{LOCKED, SUCCESS};
|
||||
use axum::http::StatusCode;
|
||||
use problem_details::ProblemDetails;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
@ -37,54 +34,15 @@ impl Lock for SimpleLock {
|
||||
}
|
||||
|
||||
pub trait Lock {
|
||||
async fn lock(&self, id: &str) -> bool;
|
||||
async fn unlock(&self, id: &str);
|
||||
|
||||
async fn run_if_not_locked<T, RESULT>(
|
||||
&self,
|
||||
lock_id: &str,
|
||||
func: fn() -> RESULT,
|
||||
) -> LockingResult<T>
|
||||
where
|
||||
RESULT: Future<Output = T>,
|
||||
{
|
||||
if self.lock(lock_id).await {
|
||||
let result = func().await;
|
||||
self.unlock(lock_id).await;
|
||||
SUCCESS(result)
|
||||
} else {
|
||||
LOCKED
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_if_locked<T, RESULT>(
|
||||
&self,
|
||||
lock_id: &str,
|
||||
func: fn() -> RESULT,
|
||||
) -> Result<T, ProblemDetails>
|
||||
where
|
||||
RESULT: Future<Output = T>,
|
||||
{
|
||||
match self.run_if_not_locked(lock_id, func).await {
|
||||
LOCKED => Err(ProblemDetails::from_status_code(StatusCode::CONFLICT)
|
||||
.with_detail(format!("Locked {:?}", lock_id))),
|
||||
SUCCESS(result) => Ok(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum LockingResult<T> {
|
||||
LOCKED,
|
||||
SUCCESS(T),
|
||||
fn lock(&self, id: &str) -> impl Future<Output = bool> + Send;
|
||||
fn unlock(&self, id: &str) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::locking::{Lock, LockingResult, SimpleLock};
|
||||
use crate::locking::{Lock, SimpleLock};
|
||||
|
||||
const LOCK_ID: &str = "1";
|
||||
const RESULT: &str = "result";
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_locking() {
|
||||
@ -94,22 +52,4 @@ mod tests {
|
||||
lock.unlock(LOCK_ID).await;
|
||||
assert!(lock.lock(LOCK_ID).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_if_not_locked() {
|
||||
let lock = SimpleLock::new();
|
||||
assert_eq!(
|
||||
LockingResult::SUCCESS(RESULT),
|
||||
lock.run_if_not_locked(LOCK_ID, test_fn).await
|
||||
);
|
||||
assert!(lock.lock(LOCK_ID).await);
|
||||
assert_eq!(
|
||||
LockingResult::LOCKED,
|
||||
lock.run_if_not_locked(LOCK_ID, test_fn).await
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_fn() -> &'static str {
|
||||
RESULT
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
use crate::db::repository::Repository;
|
||||
use crate::files;
|
||||
use crate::files::download;
|
||||
use crate::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::post;
|
||||
use axum::{response::Html, routing::get, Router};
|
||||
use files::list::query_file_tree;
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
@ -13,8 +17,48 @@ pub fn routes() -> Router<AppState> {
|
||||
post(files::upload::handle_file_uploads).get(query_file_tree),
|
||||
)
|
||||
.route("/api/v1/user/{:user_id}/download", get(download::handler))
|
||||
.route(
|
||||
"/api/v1/user/{:user_id}",
|
||||
post(create_user).get(get_user).delete(delete_user),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handler() -> Html<&'static str> {
|
||||
Html("<h1>Hello, World!</h1>")
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||
) -> Result<(), ProblemDetails> {
|
||||
if state.sqlite.get_user(&user_id).await?.is_some() {
|
||||
Err(ProblemDetails::from_status_code(StatusCode::CONFLICT)
|
||||
.with_detail("User already exists".to_string()))
|
||||
} else {
|
||||
state.sqlite.create_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||
) -> Result<(), ProblemDetails> {
|
||||
if state.sqlite.get_user(&user_id).await?.is_none() {
|
||||
Err(ProblemDetails::from_status_code(StatusCode::NOT_FOUND))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_user(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||
) -> Result<(), ProblemDetails> {
|
||||
if state.sqlite.get_user(&user_id).await?.is_none() {
|
||||
Ok(())
|
||||
} else {
|
||||
state.get_repository().delete_user(&user_id).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Auth
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
15
casket-bruno-collection/Auth/No Auth Header.bru
Normal file
15
casket-bruno-collection/Auth/No Auth Header.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: No Auth Header
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user/test123/files
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 401
|
||||
}
|
||||
19
casket-bruno-collection/Auth/Not Matching User Id.bru
Normal file
19
casket-bruno-collection/Auth/Not Matching User Id.bru
Normal file
@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Not Matching User Id
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user/test123/files
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 403
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Download File
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
46
casket-bruno-collection/Files/Get Files.bru
Normal file
46
casket-bruno-collection/Files/Get Files.bru
Normal file
@ -0,0 +1,46 @@
|
||||
meta {
|
||||
name: Get Files
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}/files?path=&nesting=5
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
params:query {
|
||||
path:
|
||||
nesting: 5
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
tests {
|
||||
test("body should be correct", function() {
|
||||
const data = res.getBody();
|
||||
expect(data).to.eql({
|
||||
"files": [
|
||||
{
|
||||
"name": "test",
|
||||
"is_dir": true,
|
||||
"children": [
|
||||
{
|
||||
"name": "blub.txt",
|
||||
"is_dir": false,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Upload File
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Get Files
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}/files?path=&nesting=5
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
params:query {
|
||||
path:
|
||||
nesting: 5
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
19
casket-bruno-collection/User Management/Create User.bru
Normal file
19
casket-bruno-collection/User Management/Create User.bru
Normal file
@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Create User
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
11
casket-bruno-collection/User Management/Delete User.bru
Normal file
11
casket-bruno-collection/User Management/Delete User.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Delete User
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
meta {
|
||||
name: Invalid
|
||||
name: Get User
|
||||
type: http
|
||||
seq: 6
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user//files
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
7
casket-bruno-collection/collection.bru
Normal file
7
casket-bruno-collection/collection.bru
Normal file
@ -0,0 +1,7 @@
|
||||
auth {
|
||||
mode: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
7
casket-bruno-collection/environments/Local.bru
Normal file
7
casket-bruno-collection/environments/Local.bru
Normal file
@ -0,0 +1,7 @@
|
||||
vars {
|
||||
casket_host: http://localhost:3000
|
||||
}
|
||||
vars:secret [
|
||||
keycloak_host,
|
||||
keycloak_realm
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user