mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-09 06:53:21 +00:00
Initial 2
This commit is contained in:
commit
361459d931
52 changed files with 5856 additions and 0 deletions
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
2928
server/Cargo.lock
generated
Normal file
2928
server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
server/Cargo.toml
Normal file
16
server/Cargo.toml
Normal 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
28
server/src/library.rs
Normal 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
66
server/src/main.rs
Normal 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
2
server/src/managers.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod repos;
|
||||
pub mod libraries;
|
38
server/src/managers/libraries.rs
Normal file
38
server/src/managers/libraries.rs
Normal 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))
|
||||
}
|
||||
}
|
58
server/src/managers/repos.rs
Normal file
58
server/src/managers/repos.rs
Normal 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
3
server/src/models.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod repo;
|
||||
pub mod user;
|
||||
pub mod library;
|
47
server/src/models/library.rs
Normal file
47
server/src/models/library.rs
Normal 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
22
server/src/models/repo.rs
Normal 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
18
server/src/models/user.rs
Normal 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
2
server/src/objs.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod repo;
|
||||
pub(crate) mod library;
|
46
server/src/objs/library.rs
Normal file
46
server/src/objs/library.rs
Normal 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
38
server/src/objs/repo.rs
Normal 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
87
server/src/routes.rs
Normal 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
44
server/src/storage.rs
Normal 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>;
|
||||
}
|
72
server/src/storage/local.rs
Normal file
72
server/src/storage/local.rs
Normal 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
26
server/src/storage/s3.rs
Normal 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
23
server/src/user.rs
Normal 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
87
server/src/util.rs
Normal 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()
|
||||
}
|
||||
}
|
19
server/storage/Library/Get File.bru
Normal file
19
server/storage/Library/Get File.bru
Normal 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
|
||||
}
|
15
server/storage/Library/Info.bru
Normal file
15
server/storage/Library/Info.bru
Normal 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
|
||||
}
|
19
server/storage/Library/List Files.bru
Normal file
19
server/storage/Library/List Files.bru
Normal 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
|
||||
}
|
20
server/storage/Library/Move File.bru
Normal file
20
server/storage/Library/Move File.bru
Normal 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
|
||||
}
|
19
server/storage/Library/Upload File.bru
Normal file
19
server/storage/Library/Upload File.bru
Normal 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
|
||||
}
|
9
server/storage/bruno.json
Normal file
9
server/storage/bruno.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"version": "1",
|
||||
"name": "storage",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue