Initial 2

This commit is contained in:
Jackzie 2025-04-14 16:25:04 -05:00
commit 361459d931
52 changed files with 5856 additions and 0 deletions

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2928
server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
server/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "storage-server"
version = "0.1.0"
edition = "2024"
[dependencies]
rocket = { version = "0.5.1", features = ["json", "uuid"] }
log = "0.4.25"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio", "uuid", "chrono"] }
tokio = { version = "1.44.2", features = ["full"] }
chrono = { version = "0.4.40", features = ["serde"] }
anyhow = "1.0.98"
serde = "1.0.219"
serde_json = "1.0.140"
int-enum = "1.2.0"

28
server/src/library.rs Normal file
View file

@ -0,0 +1,28 @@
use std::collections::HashMap;
use crate::{models, DB};
use crate::objs::repo::{Repo};
use crate::storage::StorageBackend;
use crate::user::User;
pub struct Library {
pub repo: Repo,
pub name: String,
pub owner: User,
// permissions:
}
// pub async fn get_library(pool: &DB, library_id: &str) -> Result<Option<Library>, anyhow::Error> {
// let library = models::library::get_library(pool, library_id)?;
// let Some(library) = library else {
// return Ok(None)
// };
// let repo = get_repo(pool, &library.repo_id).await?
// .ok_or(anyhow::Error::msg("Repository not found."))?;
// let owner = get_owner(pool, &library.owner_id).await?
// .ok_or(anyhow::Error::msg("Owner not found."))?;
// Ok(Some(Library {
// repo: repo,
// name: library.name,
// owner: (),
// }))
// }

66
server/src/main.rs Normal file
View file

@ -0,0 +1,66 @@
use std::sync::Arc;
use log::debug;
use rocket::{catch, launch, routes, Request, State};
use rocket::data::ByteUnit;
use sqlx::{migrate, Pool, Postgres};
use sqlx::postgres::PgPoolOptions;
use sqlx::types::Json;
use tokio::sync::Mutex;
use crate::managers::libraries::LibraryManager;
use crate::managers::repos::RepoManager;
use crate::objs::library::Library;
use crate::util::{setup_logger, JsonErrorResponse, ResponseError};
mod routes;
mod util;
mod storage;
mod library;
mod user;
mod models;
mod managers;
mod objs;
pub type DB = Pool<Postgres>;
const MAX_UPLOAD_SIZE: ByteUnit = ByteUnit::Mebibyte(100_000);
#[launch]
async fn rocket() -> _ {
setup_logger();
debug!("{}", std::env::var("DATABASE_URL").unwrap().as_str());
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(std::env::var("DATABASE_URL").unwrap().as_str())
.await
.unwrap();
migrate!("./migrations")
.run(&pool)
.await.unwrap();
let repo_manager = {
let mut manager = RepoManager::new(pool.clone());
manager.fetch_repos().await.unwrap();
manager
};
let libraries_manager = {
let mut manager = LibraryManager::new(pool.clone(), repo_manager.clone());
Arc::new(Mutex::new(manager))
};
rocket::build()
.manage(pool)
.manage(repo_manager)
.manage(libraries_manager)
.mount("/api", routes![routes::index, routes::get_library, routes::list_library_files, routes::get_library_file, routes::upload_library_file, routes::move_library_file])
}
#[catch(404)]
fn not_found(req: &Request) -> ResponseError {
ResponseError::NotFound(
JsonErrorResponse {
code: "ROUTE_NOT_FOUND".to_string(),
message: "Route not found".to_string(),
}
)
}

2
server/src/managers.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod repos;
pub mod libraries;

View file

@ -0,0 +1,38 @@
use std::collections::HashMap;
use sqlx::{Pool, Postgres};
use tokio::sync::RwLock;
use crate::objs::library::Library;
use crate::managers::repos::{RepoContainer, RepoManager};
use crate::models;
use crate::util::{JsonErrorResponse, ResponseError};
pub struct LibraryManager {
pool: Pool<Postgres>,
repos: RepoManager // TODO: make this rwlock so repo manager itself can be clone?
}
impl LibraryManager {
pub fn new(pool: Pool<Postgres>, repos: RepoManager) -> Self {
Self {
pool,
repos
}
}
pub async fn get(&self, library_id: &str) -> Result<Library, ResponseError> {
let Some(library) = models::library::get_library(&self.pool, library_id).await
.map_err(|e| ResponseError::GenericError)? else {
return Err(ResponseError::NotFound(JsonErrorResponse {
code: "LIBRARY_NOT_FOUND".to_string(),
message: "Library could not be found".to_string()
}))
};
let Some(repo) = self.repos.get_repo(&library.repo_id).await else {
return Err(ResponseError::NotFound(JsonErrorResponse {
code: "LIBRARY_INVALID_REPO".to_string(),
message: "Library is incorrectly configured, repository does not exist".to_string()
}))
};
Ok(Library::new(library, repo))
}
}

View file

@ -0,0 +1,58 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use sqlx::{PgPool, Pool, Postgres};
use tokio::sync::RwLock;
use crate::{models, DB};
use crate::models::repo::RepoModel;
use crate::objs::repo::Repo;
use crate::util::{JsonErrorResponse, ResponseError};
#[derive(Clone)]
pub struct RepoManager {
pool: Pool<Postgres>,
repos: Arc<RwLock<HashMap<String, RepoContainer>>>
}
pub type RepoContainer = Arc<RwLock<Repo>>;
impl RepoManager {
pub fn new(pool: Pool<Postgres>) -> Self {
Self {
pool,
repos: Arc::new(RwLock::new(HashMap::new()))
}
}
pub async fn fetch_repos(&mut self) -> Result<(), anyhow::Error> {
let repos = sqlx::query_as!(RepoModel, "SELECT * from storage.repos")
.fetch_all(&self.pool)
.await.map_err(anyhow::Error::msg)?;
let mut hashmap = self.repos.write().await;
for repo in repos.into_iter() {
let repo = Repo::new(repo);
let id = repo.id.to_string();
let repo: RepoContainer = Arc::new(RwLock::new(repo));
hashmap.insert(id, repo);
}
Ok(())
}
pub async fn get_repo(&self, id: &str) -> Option<RepoContainer> {
self.repos.read().await.get(id).cloned()
}
pub async fn get_repo_from_library(&self, library_id: &str) -> Result<RepoContainer, ResponseError> {
let Some(library) = models::library::get_library_with_repo(&self.pool, library_id).await
.map_err(|e| ResponseError::GenericError)? else {
return Err(ResponseError::NotFound(JsonErrorResponse {
code: "LIBRARY_NOT_FOUND".to_string(),
message: "Library could not be found".to_string()
}))
};
let Some(repo) = self.get_repo(&library.library.repo_id).await else {
return Err(ResponseError::NotFound(JsonErrorResponse {
code: "LIBRARY_INVALID_REPO".to_string(),
message: "Library is incorrectly configured, repository does not exist".to_string()
}))
};
Ok(repo)
}
}

3
server/src/models.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod repo;
pub mod user;
pub mod library;

View file

@ -0,0 +1,47 @@
use anyhow::anyhow;
use chrono::NaiveDateTime;
use rocket::serde::{Serialize, Deserialize};
use rocket::time::Date;
use sqlx::{query_as};
use sqlx::types::{Uuid};
use crate::{models, DB};
use crate::library::Library;
use crate::models::repo::RepoModel;
use crate::objs::repo::{Repo};
use crate::user::User;
#[derive(Debug, Serialize, Deserialize)]
pub struct LibraryModel {
pub id: Uuid,
pub owner_id: Uuid,
pub repo_id: String,
pub created_at: NaiveDateTime,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LibraryWithRepoModel {
pub library: LibraryModel,
pub storage_type: String,
}
pub async fn get_library(pool: &DB, library_id: &str) -> Result<Option<LibraryModel>, anyhow::Error> {
let library_id = Uuid::parse_str(library_id)?;
let library = query_as!(LibraryModel, "select * from storage.libraries where id = $1", library_id)
.fetch_optional(pool)
.await.map_err(anyhow::Error::from)?;
// if library.is_none() { return Ok(None) }
Ok(library)
}
pub async fn get_library_with_repo(pool: &DB, library_id: &str) -> Result<Option<LibraryWithRepoModel>, anyhow::Error> {
let Some(library) = get_library(pool, library_id).await? else {
return Ok(None)
};
let repo = models::repo::get_repo(pool, &library.repo_id).await?
.ok_or_else(|| anyhow!("Repository does not exist"))?;
Ok(Some(LibraryWithRepoModel {
storage_type: repo.storage_type,
library: library
}))
}

22
server/src/models/repo.rs Normal file
View file

@ -0,0 +1,22 @@
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{query_as, Value};
use sqlx::types::{Json, JsonValue, Uuid};
use crate::DB;
use crate::models::library::LibraryModel;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RepoModel {
pub id: String,
pub created_at: NaiveDateTime,
pub storage_type: String,
pub storage_settings: Json<JsonValue>, // for now
pub flags: i16,
}
pub async fn get_repo(pool: &DB, repo_id: &str) -> Result<Option<RepoModel>, anyhow::Error> {
query_as!(RepoModel, "select * from storage.repos where id = $1", repo_id)
.fetch_optional(pool)
.await.map_err(anyhow::Error::from)
}

18
server/src/models/user.rs Normal file
View file

@ -0,0 +1,18 @@
use chrono::NaiveDateTime;
use rocket::serde::uuid::Uuid;
use sqlx::query_as;
use crate::DB;
use crate::models::repo::RepoModel;
pub struct UserModel {
pub id: Uuid,
pub created_at: NaiveDateTime,
pub name: String
}
pub async fn get_user(pool: &DB, user_id: &str) -> Result<Option<UserModel>, anyhow::Error> {
let user_id = Uuid::parse_str(user_id)?;
query_as!(UserModel, "select id, created_at, name from storage.users where id = $1", user_id)
.fetch_optional(pool)
.await.map_err(anyhow::Error::from)
}

2
server/src/objs.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod repo;
pub(crate) mod library;

View file

@ -0,0 +1,46 @@
use std::path::PathBuf;
use anyhow::Error;
use crate::managers::repos::RepoContainer;
use crate::{models, DB};
use crate::models::library::LibraryModel;
use crate::models::repo::RepoModel;
use crate::storage::FileEntry;
use crate::util::{JsonErrorResponse, ResponseError};
pub struct Library {
model: LibraryModel,
repo: RepoContainer,
}
impl Library {
pub fn new(library_model: LibraryModel, repo: RepoContainer) -> Library {
Library {
model: library_model,
repo
}
}
pub async fn write_file(&self, rel_path: &PathBuf, contents: &[u8]) -> Result<(), anyhow::Error> {
let mut repo = self.repo.read().await;
repo.backend.write_file(&self.model.id.to_string(), rel_path, contents)
}
pub async fn read_file(&self, rel_path: &PathBuf) -> Result<Option<Vec<u8>>, anyhow::Error> {
let repo = self.repo.read().await;
repo.backend.read_file(&self.model.id.to_string(), rel_path)
}
pub async fn list_files(&self, rel_path: &PathBuf) -> Result<Vec<FileEntry>, anyhow::Error> {
let repo = self.repo.read().await;
repo.backend.list_files(&self.model.id.to_string(), rel_path)
}
pub async fn delete_file(&self, rel_path: &PathBuf) -> Result<(), anyhow::Error> {
let repo = self.repo.read().await;
repo.backend.delete_file(&self.model.id.to_string(), rel_path)
}
pub async fn move_file(&self, rel_path: &PathBuf, new_rel_path: &PathBuf) -> Result<(), Error> {
let repo = self.repo.read().await;
repo.backend.move_file(&self.model.id.to_string(), rel_path, new_rel_path)
}
}

38
server/src/objs/repo.rs Normal file
View file

@ -0,0 +1,38 @@
use std::path::PathBuf;
use chrono::NaiveDateTime;
use sqlx::query_as;
use sqlx::types::{Json, JsonValue};
use crate::{models, DB};
use crate::managers::repos::RepoContainer;
use crate::models::repo::RepoModel;
use crate::storage::{get_backend, StorageBackend};
use crate::util::{JsonErrorResponse, ResponseError};
pub enum RepoFlags {
None = 0,
UserAddable = 1
}
pub struct Repo {
pub id: String,
pub created_at: NaiveDateTime,
pub storage_type: String,
pub storage_settings: Json<JsonValue>, // for now
pub flags: i16,
pub backend: Box<dyn StorageBackend + Send + Sync>,
}
impl Repo {
pub fn new(model: RepoModel) -> Self {
let backend = get_backend(&model.storage_type, &model.storage_settings.0).unwrap().expect("unknown backend");
Repo {
id: model.id,
created_at: model.created_at,
storage_type: model.storage_type,
storage_settings: model.storage_settings,
flags: model.flags,
backend
}
}
}

87
server/src/routes.rs Normal file
View file

@ -0,0 +1,87 @@
use std::path::PathBuf;
use std::sync::Arc;
use log::debug;
use rocket::{get, post, Data, State};
use rocket::fs::TempFile;
use rocket::http::Status;
use rocket::response::status;
use rocket::serde::json::Json;
use sqlx::{query, Postgres};
use sqlx::types::{Uuid};
use tokio::io::AsyncReadExt;
use tokio::sync::Mutex;
use crate::{library, models, DB, MAX_UPLOAD_SIZE};
use crate::managers::libraries::LibraryManager;
use crate::managers::repos::RepoManager;
use crate::models::library::{LibraryModel, LibraryWithRepoModel};
use crate::models::user;
use crate::storage::FileEntry;
use crate::util::{JsonErrorResponse, ResponseError};
#[get("/")]
pub(crate) fn index() -> &'static str {
"Hello, world!"
}
#[get("/library/<library_id>")]
pub(crate) async fn get_library(pool: &State<DB>, library_id: &str) -> Result<Option<Json<LibraryWithRepoModel>>, ResponseError> {
let library = models::library::get_library_with_repo(pool, library_id).await
.map_err(|e| ResponseError::GenericError)?;
debug!("{:?}", library);
Ok(library.map(|lib| Json(lib)))
}
#[get("/library/<library_id>/files?<path>")]
pub(crate) async fn list_library_files(pool: &State<DB>, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: &str) -> Result<Json<Vec<FileEntry>>, ResponseError> {
let libs = libraries.lock().await;
let library = libs.get(library_id).await?;
library.list_files(&PathBuf::from(path)).await
.map(|files| Json(files))
.map_err(|e| ResponseError::InternalServerError(JsonErrorResponse {
code: "STORAGE_ERROR".to_string(),
message: e.to_string(),
}))
}
#[get("/library/<library_id>/files/download?<path>")]
pub(crate) async fn get_library_file(pool: &State<DB>, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: &str) -> Result<Vec<u8>, ResponseError> {
let libs = libraries.lock().await;
let library = libs.get(library_id).await?;
match library.read_file(&PathBuf::from(path)).await
.map_err(|e| ResponseError::GenericError)?
{
None => {
Err(ResponseError::NotFound(JsonErrorResponse {
code: "FILE_NOT_FOUND".to_string(),
message: "Requested file does not exist".to_string()
}))
}
Some(contents) => {
// TODO: headers?
Ok(contents)
}
}
}
#[post("/library/<library_id>/files/move?<from>&<to>")]
pub(crate) async fn move_library_file(pool: &State<DB>, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, from: &str, to: &str) -> Result<(), ResponseError> {
let libs = libraries.lock().await;
let library = libs.get(library_id).await?;
library.move_file(&PathBuf::from(from), &PathBuf::from(to)).await
.map_err(|e| ResponseError::GenericError)
}
#[post("/library/<library_id>/files?<path>", data = "<data>")]
pub(crate) async fn upload_library_file(pool: &State<DB>, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: &str, data: Data<'_>) -> Result<status::NoContent, ResponseError> {
let libs = libraries.lock().await;
let library = libs.get(library_id).await?;
let mut stream = data.open(MAX_UPLOAD_SIZE);
// TODO: don't just copy all to memory
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await.unwrap();
library.write_file(&PathBuf::from(path), &buf).await
.map_err(|e| ResponseError::GenericError)?;
Ok(status::NoContent)
}

44
server/src/storage.rs Normal file
View file

@ -0,0 +1,44 @@
mod local;
mod s3;
use std::path::PathBuf;
use anyhow::{anyhow, Error};
use int_enum::IntEnum;
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::types::JsonValue;
use crate::storage::local::LocalStorage;
use crate::storage::s3::S3Storage;
pub enum StorageBackendMap {
Local(LocalStorage),
S3(S3Storage)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileEntry {
pub file_name: String,
// last_modified:
pub file_size: u64,
}
pub fn get_backend(storage_type: &str, settings: &JsonValue) -> Result<Option<Box<dyn StorageBackend + Send + Sync>>, anyhow::Error> {
Ok(match storage_type {
"local" => Some(Box::new(LocalStorage::new(settings)?)),
_ => None
})
}
pub trait StorageBackend {
// fn new(settings: &JsonValue) -> Result<Self, StorageBackendError>;
fn write_file(&self, library_id: &str, rel_path: &PathBuf, contents: &[u8]) -> Result<(), anyhow::Error>;
fn read_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<Option<Vec<u8>>, anyhow::Error>;
fn list_files(&self, library_id: &str, rel_path: &PathBuf) -> Result<Vec<FileEntry>, anyhow::Error>;
fn delete_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<(), anyhow::Error>;
fn move_file(&self, library_id: &str, rel_path: &PathBuf, new_rel_path: &PathBuf) -> Result<(), Error>;
}

View file

@ -0,0 +1,72 @@
use std::env::join_paths;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Error};
use log::debug;
use sqlx::types::JsonValue;
use crate::storage::{FileEntry, StorageBackend};
pub struct LocalStorage {
folder_root: PathBuf
}
impl LocalStorage {
pub(crate) fn new(settings: &JsonValue) -> Result<Self, anyhow::Error> {
let folder_root = settings["path"].as_str().ok_or_else(|| anyhow::anyhow!("No 'path' configured"))?;
Ok(LocalStorage {
folder_root: PathBuf::from(folder_root)
})
}
}
fn get_path(folder_root: &PathBuf, library_id: &str, path: &PathBuf) -> Result<PathBuf, anyhow::Error> {
let path = folder_root.join(library_id).join(path);
// Prevent path traversal
debug!("root={:?}", folder_root);
debug!("path={:?}", path);
if !path.starts_with(&folder_root) {
return Err(anyhow!("Invalid path provided"))
}
debug!("{:?}", path);
Ok(path)
}
impl StorageBackend for LocalStorage {
fn write_file(&self, library_id: &str, rel_path: &PathBuf, contents: &[u8]) -> Result<(), Error> {
let path = get_path(&self.folder_root, library_id, rel_path)?;
std::fs::write(path, contents).map_err(|e| anyhow!(e))
}
fn read_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<Option<Vec<u8>>, Error> {
let path = get_path(&self.folder_root, library_id, rel_path)?;
match std::fs::read(path) {
Ok(contents) => Ok(Some(contents)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(anyhow!(e)),
}
}
fn list_files(&self, library_id: &str, rel_path: &PathBuf) -> Result<Vec<FileEntry>, Error> {
let path = get_path(&self.folder_root, library_id, rel_path)?;
Ok(std::fs::read_dir(path)?
.map(|entry| entry.unwrap())
.map(|entry| {
let meta = entry.metadata().unwrap();
FileEntry {
file_name: entry.file_name().into_string().unwrap(),
file_size: meta.size()
}
})
.collect())
}
fn delete_file(&self, library_id: &str, rel_path: &PathBuf) -> Result<(), Error> {
let path = get_path(&self.folder_root, library_id, rel_path)?;
// TODO: check if folder?
std::fs::remove_file(path).map_err(|e| anyhow!(e))
}
fn move_file(&self, library_id: &str, rel_path: &PathBuf, new_rel_path: &PathBuf) -> Result<(), Error> {
let path = get_path(&self.folder_root, library_id, rel_path)?;
std::fs::rename(path, new_rel_path).map_err(|e| anyhow!(e))
}
}

26
server/src/storage/s3.rs Normal file
View file

@ -0,0 +1,26 @@
use serde_json::Value;
use sqlx::types::JsonValue;
use crate::storage::{StorageBackend};
pub struct S3Storage {
// pub fn uri:
/* uri, bucket id, auth */
}
impl S3Storage {
fn new(settings: &JsonValue) -> Result<Self, anyhow::Error> {
todo!()
}
}
// impl StorageBackend for S3Storage {
//
//
// fn write_file(&mut self, path: &str, contents: &[u8]) -> Result<(), StorageBackendError> {
// unimplemented!()
// }
// fn read_file(&self, path: &str) -> Result<Vec<u8>, StorageBackendError> {
// unimplemented!()
// }
// fn delete_file(&mut self, path: &str) -> Result<(), StorageBackendError> {
// unimplemented!()
// }
// }

23
server/src/user.rs Normal file
View file

@ -0,0 +1,23 @@
use std::collections::HashMap;
use rocket::serde::uuid::Uuid;
use sqlx::query_as;
use crate::DB;
use crate::library::Library;
use crate::models::user::UserModel;
pub struct User {
libraries: HashMap<String, Library>,
}
impl User {
pub fn _idk() -> Self {
Self {
libraries: HashMap::new()
}
}
pub fn get_library(&self, id: &str) -> Option<&Library> {
self.libraries.get(id)
}
}

87
server/src/util.rs Normal file
View file

@ -0,0 +1,87 @@
use std::io::Cursor;
use rocket::http::{ContentType, Status};
use rocket::{response, Request, Response};
use rocket::response::Responder;
use rocket::serde::Serialize;
use sqlx::Error;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use crate::util::ResponseError::DatabaseError;
pub(crate) fn setup_logger() {
tracing_subscriber::registry()
.with(
tracing_subscriber::filter::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=trace,storage-server=trace", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonErrorResponse {
pub(crate) code: String,
pub(crate) message: String
}
#[derive(Debug)]
pub enum ResponseError {
NotFound(JsonErrorResponse),
GenericError,
InternalServerError(JsonErrorResponse),
DatabaseError(JsonErrorResponse),
}
impl ResponseError {
fn get_http_status(&self) -> Status {
match self {
ResponseError::InternalServerError(_) => Status::InternalServerError,
ResponseError::GenericError => Status::InternalServerError,
ResponseError::NotFound(_) => Status::NotFound,
_ => Status::BadRequest,
}
}
fn into_res_err(self) -> JsonErrorResponse {
match self {
ResponseError::NotFound(e) => e,
ResponseError::GenericError => {
JsonErrorResponse {
code: "INTERNAL_SERVER_ERROR".to_string(),
message: "An unknown error occurred".to_string(),
}
},
ResponseError::InternalServerError(e) => e,
DatabaseError(e) => e,
}
}
}
impl From<sqlx::Error> for ResponseError {
fn from(value: Error) -> Self {
let err = value.into_database_error().unwrap();
DatabaseError(JsonErrorResponse {
code: err.code().map(|s| s.to_string()).unwrap_or_else(|| "UNKNOWN".to_string()),
message: err.message().to_string(),
})
}
}
impl std::fmt::Display for ResponseError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(fmt, "Error {}.", self.get_http_status())
}
}
impl<'r> Responder<'r, 'static> for ResponseError {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
// serialize struct into json string
let status = self.get_http_status();
let err_response = serde_json::to_string(&self.into_res_err()).unwrap();
Response::build()
.status(status)
.header(ContentType::JSON)
.sized_body(err_response.len(), Cursor::new(err_response))
.ok()
}
}

View file

@ -0,0 +1,19 @@
meta {
name: Get File
type: http
seq: 3
}
get {
url: http://localhost:8080/api/library/:libraryId/files/download?path=
body: none
auth: none
}
params:query {
path:
}
params:path {
libraryId: dbabbf7d-9b63-487b-9908-57c2df11b2d2
}

View file

@ -0,0 +1,15 @@
meta {
name: Info
type: http
seq: 1
}
get {
url: http://localhost:8080/api/library/:libraryId
body: none
auth: none
}
params:path {
libraryId: dbabbf7d-9b63-487b-9908-57c2df11b2d2
}

View file

@ -0,0 +1,19 @@
meta {
name: List Files
type: http
seq: 2
}
get {
url: http://localhost:8080/api/library/:libraryId/files?path=
body: none
auth: none
}
params:query {
path:
}
params:path {
libraryId: dbabbf7d-9b63-487b-9908-57c2df11b2d2
}

View file

@ -0,0 +1,20 @@
meta {
name: Move File
type: http
seq: 4
}
post {
url: http://localhost:8080/api/library/:libraryId/files/move?from=&to=
body: none
auth: none
}
params:query {
from:
to:
}
params:path {
libraryId: dbabbf7d-9b63-487b-9908-57c2df11b2d2
}

View file

@ -0,0 +1,19 @@
meta {
name: Upload File
type: http
seq: 5
}
post {
url: http://localhost:8080/api/library/:libraryId/files/move?path=
body: none
auth: none
}
params:query {
path:
}
params:path {
libraryId: dbabbf7d-9b63-487b-9908-57c2df11b2d2
}

View file

@ -0,0 +1,9 @@
{
"version": "1",
"name": "storage",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}