mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-06 13:13:20 +00:00
Initial 2
This commit is contained in:
commit
98d0065f55
53 changed files with 5857 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
**/.idea
|
||||
|
1
server/.env
Normal file
1
server/.env
Normal file
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=postgresql://hs1b:5432/web?user=jackz&password=104812jbjb&connectTimeout=30¤tSchema=storage;ROCKET_PORT=8080
|
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"
|
||||
]
|
||||
}
|
2
web/.gitignore
vendored
Normal file
2
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
.svelte-kit
|
38
web/README.md
Normal file
38
web/README.md
Normal 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
4
web/messages/en.json
Normal 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
35
web/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
web/project.inlang/cache/plugins/2sy648wh9sugi
vendored
Normal file
1
web/project.inlang/cache/plugins/2sy648wh9sugi
vendored
Normal file
File diff suppressed because one or more lines are too long
16
web/project.inlang/cache/plugins/ygx0uiahq6uw
vendored
Normal file
16
web/project.inlang/cache/plugins/ygx0uiahq6uw
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/project.inlang/project_id
Normal file
1
web/project.inlang/project_id
Normal file
|
@ -0,0 +1 @@
|
|||
Rvrk3EOCw3V6oHZLmA
|
14
web/project.inlang/settings.json
Normal file
14
web/project.inlang/settings.json
Normal 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
2
web/src/app.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/typography';
|
13
web/src/app.d.ts
vendored
Normal file
13
web/src/app.d.ts
vendored
Normal 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
12
web/src/app.html
Normal 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
7
web/src/demo.spec.ts
Normal 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
12
web/src/hooks.server.ts
Normal 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
3
web/src/hooks.ts
Normal 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
1
web/src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
7
web/src/routes/+layout.svelte
Normal file
7
web/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
2
web/src/routes/+page.svelte
Normal file
2
web/src/routes/+page.svelte
Normal 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>
|
11
web/src/routes/page.svelte.test.ts
Normal file
11
web/src/routes/page.svelte.test.ts
Normal 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
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
18
web/svelte.config.js
Normal 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
19
web/tsconfig.json
Normal 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
41
web/vite.config.ts
Normal 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}']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
18
web/vitest-setup-client.ts
Normal file
18
web/vitest-setup-client.ts
Normal 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
1824
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue