Initial 2

This commit is contained in:
Jackzie 2025-04-14 16:25:04 -05:00
commit 98d0065f55
53 changed files with 5857 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
**/.idea

1
server/.env Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=postgresql://hs1b:5432/web?user=jackz&password=104812jbjb&connectTimeout=30&currentSchema=storage;ROCKET_PORT=8080

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"
]
}

2
web/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
.svelte-kit

38
web/README.md Normal file
View file

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

4
web/messages/en.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!"
}

35
web/package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "storage-web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"jsdom": "^26.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.5",
"vitest": "^3.0.0"
},
"dependencies": {
"@inlang/paraglide-js": "^2.0.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
Rvrk3EOCw3V6oHZLmA

View file

@ -0,0 +1,14 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": [
"en"
]
}

2
web/src/app.css Normal file
View file

@ -0,0 +1,2 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

13
web/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
web/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="%paraglide.lang%">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
web/src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

12
web/src/hooks.server.ts Normal file
View file

@ -0,0 +1,12 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
});
});
export const handle: Handle = handleParaglide;

3
web/src/hooks.ts Normal file
View file

@ -0,0 +1,3 @@
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute = (request) => deLocalizeUrl(request.url).pathname;

1
web/src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,7 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View file

@ -0,0 +1,11 @@
import { describe, test, expect } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { render, screen } from '@testing-library/svelte';
import Page from './+page.svelte';
describe('/+page.svelte', () => {
test('should render h1', () => {
render(Page);
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
});

BIN
web/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
web/svelte.config.js Normal file
View file

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

19
web/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

41
web/vite.config.ts Normal file
View file

@ -0,0 +1,41 @@
import { svelteTesting } from '@testing-library/svelte/vite';
import tailwindcss from '@tailwindcss/vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide'
})
],
test: {
workspace: [
{
extends: './vite.config.ts',
plugins: [svelteTesting()],
test: {
name: 'client',
environment: 'jsdom',
clearMocks: true,
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**'],
setupFiles: ['./vitest-setup-client.ts']
}
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
}
}
]
}
});

View file

@ -0,0 +1,18 @@
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';
// required for svelte5 + jsdom as jsdom does not support matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
enumerable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// add more mocks here if you need them

1824
web/yarn.lock Normal file

File diff suppressed because it is too large Load diff