diff --git a/Cargo.lock b/Cargo.lock index 9d85ef6..bd474cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.2.8" @@ -733,6 +746,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -800,6 +819,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.24" @@ -907,6 +932,18 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "itoa" version = "1.0.5" @@ -1088,7 +1125,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1676,6 +1713,7 @@ version = "0.1.0" dependencies = [ "brotli", "compressible", + "env_logger", "flate2", "hex", "hex-literal", diff --git a/Cargo.toml b/Cargo.toml index 6dba2b5..fa355d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ sled = "0.34.7" serde = "1.0.152" serde_json = "1.0.93" rmp-serde = "1.1.1" -toml = { version = "0.7.2", default-features = false, features = ["parse"] } +toml = "0.7.2" thiserror = "1.0.38" time = { version = "0.3.15", features = [ "macros", @@ -38,10 +38,10 @@ compressible = "0.2.0" regex = "1.7.1" log = "0.4.17" httpdate = "1.0.2" +env_logger = "0.10.0" [dev-dependencies] rstest = "0.16.0" temp_testdir = "0.2.3" insta = { version = "1.17.1", features = ["ron"] } hex-literal = "0.3.4" -path_macro = "1.0.0" diff --git a/src/api.rs b/src/api.rs index 1db2ecc..b4cdb3c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,14 +13,7 @@ use poem_openapi::{ OpenApi, SecurityScheme, }; -use crate::{ - config::{Config, KeyCfg}, - db::{self, Db}, - model::*, - oai::DynParams, - storage::Storage, - util, -}; +use crate::{config::KeyCfg, db, model::*, oai::DynParams, util, Talon}; pub struct TalonApi; @@ -34,8 +27,8 @@ pub struct TalonApi; struct ApiKeyAuthorization(KeyCfg); async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option { - let cfg = req.data::()?; - cfg.keys.get(&api_key.key).cloned() + let talon = req.data::()?; + talon.cfg.keys.get(&api_key.key).cloned() } impl ApiKeyAuthorization { @@ -71,8 +64,14 @@ impl ResponseError for ApiError { impl TalonApi { /// Get a website #[oai(path = "/website/:subdomain", method = "get")] - async fn website_get(&self, db: Data<&Db>, subdomain: Path) -> Result> { - db.get_website(&subdomain) + async fn website_get( + &self, + talon: Data<&Talon>, + subdomain: Path, + ) -> Result> { + talon + .db + .get_website(&subdomain) .map(|w| Json(Website::from((subdomain.0, w)))) .map_err(Error::from) } @@ -82,19 +81,18 @@ impl TalonApi { async fn website_post( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, - cfg: Data<&Config>, + talon: Data<&Talon>, subdomain: Path, website: Json, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - if subdomain.as_str() == cfg.server.internal_subdomain + if subdomain.as_str() == talon.cfg.server.internal_subdomain || !util::validate_subdomain(&subdomain) { return Err(ApiError::InvalidSubdomain.into()); } - db.insert_website(&subdomain, &website.0.into())?; + talon.db.insert_website(&subdomain, &website.0.into())?; Ok(()) } @@ -103,13 +101,13 @@ impl TalonApi { async fn website_update( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, + talon: Data<&Talon>, subdomain: Path, website: Json, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - db.update_website(&subdomain, website.0.into())?; + talon.db.update_website(&subdomain, website.0.into())?; Ok(()) } @@ -118,19 +116,21 @@ impl TalonApi { async fn website_delete( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, + talon: Data<&Talon>, subdomain: Path, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - db.delete_website(&subdomain, true)?; + talon.db.delete_website(&subdomain, true)?; Ok(()) } /// Get all websites #[oai(path = "/websites", method = "get")] - async fn websites_get(&self, db: Data<&Db>) -> Result>> { - db.get_websites() + async fn websites_get(&self, talon: Data<&Talon>) -> Result>> { + talon + .db + .get_websites() .map(|r| r.map(Website::from)) .collect::, _>>() .map(Json) @@ -141,11 +141,13 @@ impl TalonApi { #[oai(path = "/website/:subdomain/versions", method = "get")] async fn website_versions( &self, - db: Data<&Db>, + talon: Data<&Talon>, subdomain: Path, ) -> Result>> { - db.website_exists(&subdomain)?; - db.get_website_versions(&subdomain) + talon.db.website_exists(&subdomain)?; + talon + .db + .get_website_versions(&subdomain) .map(|r| r.map(Version::from)) .collect::, _>>() .map(Json) @@ -156,11 +158,13 @@ impl TalonApi { #[oai(path = "/website/:subdomain/version/:id", method = "get")] async fn version_get( &self, - db: Data<&Db>, + talon: Data<&Talon>, subdomain: Path, id: Path, ) -> Result> { - db.get_version(&subdomain, *id) + talon + .db + .get_version(&subdomain, *id) .map(|v| Json(Version::from((*id, v)))) .map_err(Error::from) } @@ -170,13 +174,13 @@ impl TalonApi { async fn version_delete( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, + talon: Data<&Talon>, subdomain: Path, id: Path, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - db.delete_version(&subdomain, *id, true)?; + talon.db.delete_version(&subdomain, *id, true)?; Ok(()) } @@ -185,8 +189,7 @@ impl TalonApi { async fn version_upload_zip( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, - storage: Data<&Storage>, + talon: Data<&Talon>, subdomain: Path, /// Associated version data /// @@ -197,10 +200,12 @@ impl TalonApi { data: Binary>, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - let vid = db.new_version_id()?; - storage.insert_zip_archive(Cursor::new(data.as_slice()), vid)?; + let vid = talon.db.new_version_id()?; + talon + .storage + .insert_zip_archive(Cursor::new(data.as_slice()), vid)?; - db.insert_version( + talon.db.insert_version( &subdomain, vid, &db::model::Version { @@ -208,7 +213,7 @@ impl TalonApi { ..Default::default() }, )?; - db.update_website( + talon.db.update_website( &subdomain, db::model::WebsiteUpdate { latest_version: Some(Some(vid)), @@ -223,8 +228,7 @@ impl TalonApi { async fn version_upload_tgz( &self, auth: ApiKeyAuthorization, - db: Data<&Db>, - storage: Data<&Storage>, + talon: Data<&Talon>, subdomain: Path, /// Associated version data /// @@ -235,10 +239,10 @@ impl TalonApi { data: Binary>, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - let vid = db.new_version_id()?; - storage.insert_tgz_archive(data.as_slice(), vid)?; + let vid = talon.db.new_version_id()?; + talon.storage.insert_tgz_archive(data.as_slice(), vid)?; - db.insert_version( + talon.db.insert_version( &subdomain, vid, &db::model::Version { @@ -246,7 +250,7 @@ impl TalonApi { ..Default::default() }, )?; - db.update_website( + talon.db.update_website( &subdomain, db::model::WebsiteUpdate { latest_version: Some(Some(vid)), diff --git a/src/config.rs b/src/config.rs index 42c4bd9..7b39ab6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,9 +57,21 @@ impl Config { Self { i: cfg.into() } } + /// Read the configuration from the given file + /// or create a default config at the given location pub fn from_file>(path: P) -> Result { - let cfg_str = std::fs::read_to_string(path)?; - Ok(toml::from_str::(&cfg_str)?) + let path = path.as_ref(); + if path.is_file() { + let cfg_str = std::fs::read_to_string(path)?; + Ok(toml::from_str::(&cfg_str)?) + } else { + let cfg = Self::default(); + if let Ok(cfg_str) = toml::to_string_pretty(&cfg) { + std::fs::write(path, cfg_str) + .unwrap_or_else(|e| log::error!("Could not write default config file: {e}")); + } + Ok(cfg) + } } } @@ -67,7 +79,6 @@ impl Config { #[serde(default)] pub struct ServerCfg { pub address: String, - pub port: u32, pub root_domain: String, pub internal_subdomain: String, pub internal_url: String, @@ -76,8 +87,7 @@ pub struct ServerCfg { impl Default for ServerCfg { fn default() -> Self { Self { - address: "0.0.0.0".to_owned(), - port: 8080, + address: "0.0.0.0:3000".to_owned(), root_domain: "localhost".to_owned(), internal_subdomain: "talon".to_owned(), internal_url: "http://talon.localhost".to_owned(), diff --git a/src/db/mod.rs b/src/db/mod.rs index f9749f5..10575cb 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -198,7 +198,6 @@ impl Db { let website = website.clone(); w.name = website.name.unwrap_or(w.name); w.latest_version = website.latest_version.unwrap_or(w.latest_version); - w.icon = website.icon.unwrap_or(w.icon); w.color = website.color.unwrap_or(w.color); w.visibility = website.visibility.unwrap_or(w.visibility); w.source_url = website.source_url.unwrap_or(w.source_url); @@ -464,12 +463,6 @@ impl Db { let (_, file) = f?; set.insert(file.to_vec()); } - for w in self.get_websites() { - let (_, website) = w?; - if let Some(icon) = website.icon { - set.insert(icon); - } - } Ok(set) } } diff --git a/src/db/model.rs b/src/db/model.rs index a56e21b..b6e2760 100644 --- a/src/db/model.rs +++ b/src/db/model.rs @@ -14,8 +14,6 @@ pub struct Website { pub created_at: OffsetDateTime, /// Latest version ID pub latest_version: Option, - /// File hash of the page icon - pub icon: Option>, /// Color of the page icon pub color: Option, /// Visibility of the page in the sidebar menu @@ -32,7 +30,6 @@ impl Default for Website { name: Default::default(), created_at: OffsetDateTime::now_utc(), latest_version: Default::default(), - icon: Default::default(), color: Default::default(), visibility: Default::default(), source_url: Default::default(), @@ -50,8 +47,6 @@ pub struct WebsiteUpdate { pub name: Option, /// Latest version ID pub latest_version: Option>, - /// Page icon - pub icon: Option>>, /// Color of the page icon pub color: Option>, /// Visibility of the page in the sidebar menu diff --git a/src/lib.rs b/src/lib.rs index 677a226..665b1a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,11 @@ pub mod api; pub mod config; pub mod db; pub mod model; -mod oai; +pub mod server; pub mod storage; + +mod oai; +mod page; mod util; + +pub use server::{Result, Talon, TalonError}; diff --git a/src/main.rs b/src/main.rs index 6a8f4fd..65b55df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,95 +1,7 @@ -use poem::{ - error::ResponseError, - handler, - http::{header, StatusCode}, - listener::TcpListener, - web::{Data, Redirect}, - EndpointExt, IntoResponse, Request, Response, Result, Route, RouteDomain, Server, -}; -use poem_openapi::OpenApiService; -use talon::{api::TalonApi, config::Config, db::Db, storage::Storage}; - -#[derive(thiserror::Error, Debug)] -enum PageError { - #[error("invalid subdomain")] - InvalidSubdomain, - #[error("website does not have any version")] - NoVersion, -} - -impl ResponseError for PageError { - fn status(&self) -> StatusCode { - StatusCode::NOT_FOUND - } -} - -#[handler] -fn page( - request: &Request, - db: Data<&Db>, - storage: Data<&Storage>, - cfg: Data<&Config>, -) -> Result { - let host = request - .header(header::HOST) - .ok_or(PageError::InvalidSubdomain)?; - let subdomain = if host == cfg.server.root_domain { - "-" - } else { - host.strip_suffix(&format!(".{}", cfg.server.root_domain)) - .ok_or(PageError::InvalidSubdomain)? - }; - - let ws = db.get_website(subdomain)?; - let version = ws.latest_version.ok_or(PageError::NoVersion)?; - let file = storage.get_file(version, request.original_uri().path(), request.headers())?; - - Ok(match file.rd_path { - Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(), - None => file.to_response(request.headers())?.into_response(), - }) -} +use talon::{Result, Talon}; #[tokio::main] -async fn main() -> Result<(), Box> { - let db = Db::new("tmp/db")?; - let cfg = Config::from_file("tmp/config.toml")?; - let storage = Storage::new("tmp/storage", db.clone(), cfg.clone()); - - let api_service = OpenApiService::new(TalonApi, "Talon", "0.1.0") - .server(format!("{}/api", cfg.server.internal_url)); - let swagger_ui = api_service.swagger_ui(); - let spec = api_service.spec(); - - let route_internal = Route::new() - .at( - "/", - poem::endpoint::make_sync(|_| "Hello World, I am Talon"), - ) - .nest("/api", api_service) - .nest("/api/swagger", swagger_ui) - .at( - "/api/spec", - poem::endpoint::make_sync(move |_| spec.clone()), - ); - - let internal_domain = format!( - "{}.{}", - cfg.server.internal_subdomain, cfg.server.root_domain - ); - let site_domains = format!("+.{}", cfg.server.root_domain); - println!("internal_domain: {internal_domain}"); - - let route = RouteDomain::new() - .at(&internal_domain, route_internal) - .at(&site_domains, page) - .at(&cfg.server.root_domain, page) - .data(db) - .data(cfg) - .data(storage); - - Server::new(TcpListener::bind("0.0.0.0:3000")) - .run(route) - .await?; - Ok(()) +async fn main() -> Result<()> { + let talon = Talon::new("tmp")?; + talon.launch().await } diff --git a/src/model.rs b/src/model.rs index 91a1edb..54b9cdd 100644 --- a/src/model.rs +++ b/src/model.rs @@ -18,8 +18,6 @@ pub struct Website { pub created_at: OffsetDateTime, /// Latest version ID pub latest_version: Option, - // TODO: implement page icon - // pub icon: Option, /// Color of the page icon pub color: Option, /// Visibility of the page in the sidebar menu @@ -131,7 +129,6 @@ impl From for db::model::WebsiteUpdate { Self { name: value.name, latest_version: None, - icon: None, color: value.color, visibility: value.visibility, source_url: value.source_url, diff --git a/src/page.rs b/src/page.rs new file mode 100644 index 0000000..82c0f2c --- /dev/null +++ b/src/page.rs @@ -0,0 +1,47 @@ +use poem::{ + error::ResponseError, + handler, + http::{header, StatusCode}, + web::{Data, Redirect}, + IntoResponse, Request, Response, Result, +}; + +use crate::Talon; + +#[derive(thiserror::Error, Debug)] +pub enum PageError { + #[error("invalid subdomain")] + InvalidSubdomain, + #[error("website does not have any version")] + NoVersion, +} + +impl ResponseError for PageError { + fn status(&self) -> StatusCode { + StatusCode::NOT_FOUND + } +} + +#[handler] +pub fn page(request: &Request, talon: Data<&Talon>) -> Result { + let host = request + .header(header::HOST) + .ok_or(PageError::InvalidSubdomain)?; + let subdomain = if host == talon.cfg.server.root_domain { + "-" + } else { + host.strip_suffix(&format!(".{}", talon.cfg.server.root_domain)) + .ok_or(PageError::InvalidSubdomain)? + }; + + let ws = talon.db.get_website(subdomain)?; + let version = ws.latest_version.ok_or(PageError::NoVersion)?; + let file = talon + .storage + .get_file(version, request.original_uri().path(), request.headers())?; + + Ok(match file.rd_path { + Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(), + None => file.to_response(request.headers())?.into_response(), + }) +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..516cf24 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,92 @@ +use std::{ops::Deref, path::Path, sync::Arc}; + +use crate::{api::TalonApi, config::Config, db::Db, page::page, storage::Storage, util}; +use path_macro::path; +use poem::{listener::TcpListener, EndpointExt, Route, RouteDomain, Server}; +use poem_openapi::OpenApiService; + +#[derive(Clone)] +pub struct Talon { + i: Arc, +} + +pub struct TalonInner { + pub cfg: Config, + pub db: Db, + pub storage: Storage, +} + +impl Deref for Talon { + type Target = TalonInner; + + fn deref(&self) -> &Self::Target { + &self.i + } +} + +#[derive(thiserror::Error, Debug)] +pub enum TalonError { + #[error("db error: {0}")] + Db(#[from] crate::db::DbError), + #[error("db error: {0}")] + Storage(#[from] crate::storage::StorageError), + #[error("db error: {0}")] + Config(#[from] crate::config::ConfigError), + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + +pub type Result = std::result::Result; + +impl Talon { + pub fn new>(workdir: P) -> Result { + let db_dir = path!(workdir / "db"); + let storage_dir = path!(workdir / "storage"); + util::create_dir_ne(&db_dir)?; + util::create_dir_ne(&storage_dir)?; + + let cfg = Config::from_file(path!(workdir / "config.toml"))?; + let db = Db::new(db_dir)?; + let storage = Storage::new(storage_dir, db.clone(), cfg.clone()); + + Ok(Self { + i: TalonInner { cfg, db, storage }.into(), + }) + } + + pub async fn launch(&self) -> Result<()> { + let api_service = OpenApiService::new(TalonApi, "Talon", "0.1.0") + .server(format!("{}/api", self.i.cfg.server.internal_url)); + let swagger_ui = api_service.swagger_ui(); + let spec = api_service.spec(); + + let route_internal = Route::new() + .at( + "/", + poem::endpoint::make_sync(|_| "Hello World, I am Talon"), + ) + .nest("/api", api_service) + .nest("/api/swagger", swagger_ui) + .at( + "/api/spec", + poem::endpoint::make_sync(move |_| spec.clone()), + ); + + let internal_domain = format!( + "{}.{}", + self.i.cfg.server.internal_subdomain, self.i.cfg.server.root_domain + ); + let site_domains = format!("+.{}", self.i.cfg.server.root_domain); + + let route = RouteDomain::new() + .at(&internal_domain, route_internal) + .at(&site_domains, page) + .at(&self.i.cfg.server.root_domain, page) + .data(self.clone()); + + Server::new(TcpListener::bind(&self.i.cfg.server.address)) + .run(route) + .await?; + Ok(()) + } +} diff --git a/src/storage.rs b/src/storage.rs index 397f54f..13ec448 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -6,7 +6,6 @@ use std::{ ops::Bound, path::{Path, PathBuf}, str::FromStr, - sync::Arc, }; use flate2::{read::GzDecoder, write::GzEncoder}; @@ -29,12 +28,7 @@ use crate::{ util, }; -#[derive(Clone)] pub struct Storage { - i: Arc, -} - -struct StorageInner { path: PathBuf, db: Db, cfg: Config, @@ -107,12 +101,9 @@ impl Storage { /// Create a new file storage using the root folder and the database pub fn new>(path: P, db: Db, cfg: Config) -> Self { Self { - i: StorageInner { - path: path.into(), - db, - cfg, - } - .into(), + path: path.into(), + db, + cfg, } } @@ -135,26 +126,26 @@ impl Storage { fs::copy(file_path, &stored_file)?; - if self.i.cfg.compression.enabled() + if self.cfg.compression.enabled() && mime_guess::from_path(file_path) .first() .map(|t| compressible::is_compressible(t.essence_str())) .unwrap_or_default() { - if self.i.cfg.compression.gzip_en { + if self.cfg.compression.gzip_en { let mut encoder = GzEncoder::new( fs::File::create(stored_file.with_extension("gz"))?, - flate2::Compression::new(self.i.cfg.compression.gzip_level.into()), + flate2::Compression::new(self.cfg.compression.gzip_level.into()), ); let mut input = BufReader::new(fs::File::open(&stored_file)?); std::io::copy(&mut input, &mut encoder)?; } - if self.i.cfg.compression.brotli_en { + if self.cfg.compression.brotli_en { let mut encoder = brotli::CompressorWriter::new( fs::File::create(stored_file.with_extension("br"))?, 4096, - self.i.cfg.compression.brotli_level.into(), + self.cfg.compression.brotli_level.into(), 20, ); let mut input = BufReader::new(fs::File::open(&stored_file)?); @@ -162,7 +153,7 @@ impl Storage { } } - self.i.db.insert_file(version, site_path, &hash)?; + self.db.insert_file(version, site_path, &hash)?; Ok(()) } @@ -248,7 +239,7 @@ impl Storage { fn file_path_mkdir(&self, hash: &[u8]) -> Result { let hash_str = hash.encode_hex::(); - let subdir = self.i.path.join(&hash_str[..2]); + let subdir = self.path.join(&hash_str[..2]); if !subdir.is_dir() { fs::create_dir(&subdir)?; } @@ -257,7 +248,7 @@ impl Storage { fn file_path(&self, hash: &[u8]) -> PathBuf { let hash_str = hash.encode_hex::(); - let subdir = self.i.path.join(&hash_str[..2]); + let subdir = self.path.join(&hash_str[..2]); subdir.join(&hash_str) } @@ -265,13 +256,13 @@ impl Storage { let path = self.file_path(hash); let mut res = BTreeMap::new(); - if self.i.cfg.compression.gzip_en { + if self.cfg.compression.gzip_en { let path_gz = path.with_extension("gz"); if path_gz.is_file() { res.insert(CompressionAlg::Gzip, path_gz); } } - if self.i.cfg.compression.brotli_en { + if self.cfg.compression.brotli_en { let path_br = path.with_extension("br"); if path_br.is_file() { res.insert(CompressionAlg::Brotli, path_br); @@ -299,7 +290,7 @@ impl Storage { // Attempt to access the following pages // 1. Site path directly // 2. Site path + `/index.html` - match self.i.db.get_file_opt(version, sp)? { + match self.db.get_file_opt(version, sp)? { Some(h) => { hash = Some(h); } @@ -317,7 +308,6 @@ impl Storage { let hash = match hash { Some(hash) => hash, None => self - .i .db .get_file_opt(version, &new_path)? .ok_or_else(|| StorageError::NotFound(sp.to_owned()))?, diff --git a/src/util.rs b/src/util.rs index 8a2e2fc..0db1fbb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -119,6 +119,14 @@ pub fn validate_subdomain(subdomain: &str) -> bool { .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') } +pub fn create_dir_ne>(path: P) -> Result<(), std::io::Error> { + let path = path.as_ref(); + if !path.is_dir() { + std::fs::create_dir(path)?; + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs index ae64a21..a947410 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -57,9 +57,6 @@ fn insert_websites(db: &Db) { name: "ThetaDev".to_owned(), created_at: datetime!(2023-02-18 16:30 +0), latest_version: Some(VERSION_1_2), - icon: Some( - hex!("9f7e7971b4bfdb75429e534dea461ed90340886925078cda252cada9aa0e25f7").to_vec(), - ), color: Some(2068974), visibility: talon::model::Visibility::Featured, ..Default::default() @@ -72,9 +69,6 @@ fn insert_websites(db: &Db) { name: "Spotify-Gender-Ex".to_owned(), created_at: datetime!(2023-02-18 16:30 +0), latest_version: Some(VERSION_2_1), - icon: Some( - hex!("9b35024aacebd74010ea595ef5d180f47f5ec822df100236dd6ac808497b64f6").to_vec(), - ), color: Some(1947988), visibility: talon::model::Visibility::Featured, source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex".to_owned()), @@ -89,7 +83,6 @@ fn insert_websites(db: &Db) { name: "RustyPipe".to_owned(), created_at: datetime!(2023-02-20 18:30 +0), latest_version: Some(VERSION_3_1), - icon: None, color: Some(7943647), visibility: talon::model::Visibility::Featured, source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe".to_owned()), diff --git a/tests/snapshots/tests__config__default.snap b/tests/snapshots/tests__config__default.snap index de41c6b..ddadd88 100644 --- a/tests/snapshots/tests__config__default.snap +++ b/tests/snapshots/tests__config__default.snap @@ -4,8 +4,7 @@ expression: "&cfg" --- ConfigInner( server: ServerCfg( - address: "127.0.0.1", - port: 3000, + address: "127.0.0.1:3000", root_domain: "example.com", internal_subdomain: "talon-i", internal_url: "http://talon-i.example.com", diff --git a/tests/snapshots/tests__config__sparse.snap b/tests/snapshots/tests__config__sparse.snap index 4a2d3af..a68b605 100644 --- a/tests/snapshots/tests__config__sparse.snap +++ b/tests/snapshots/tests__config__sparse.snap @@ -4,8 +4,7 @@ expression: "&cfg" --- ConfigInner( server: ServerCfg( - address: "0.0.0.0", - port: 8080, + address: "0.0.0.0:3000", root_domain: "localhost", internal_subdomain: "talon", internal_url: "http://talon.localhost", diff --git a/tests/snapshots/tests__database__delete_website.snap b/tests/snapshots/tests__database__delete_website.snap index 480f541..c29d78f 100644 --- a/tests/snapshots/tests__database__delete_website.snap +++ b/tests/snapshots/tests__database__delete_website.snap @@ -1,9 +1,9 @@ --- -source: src/db/mod.rs +source: tests/tests.rs expression: data --- -{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"icon":null,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} -{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"icon":[155,53,2,74,172,235,215,64,16,234,89,94,245,209,128,244,127,94,200,34,223,16,2,54,221,106,200,8,73,123,100,246],"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} +{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} +{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{}}} {"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{}}} {"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"} diff --git a/tests/snapshots/tests__database__export.snap b/tests/snapshots/tests__database__export.snap index 5970c14..469053a 100644 --- a/tests/snapshots/tests__database__export.snap +++ b/tests/snapshots/tests__database__export.snap @@ -2,9 +2,9 @@ source: tests/tests.rs expression: data --- -{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"icon":[159,126,121,113,180,191,219,117,66,158,83,77,234,70,30,217,3,64,136,105,37,7,140,218,37,44,173,169,170,14,37,247],"color":2068974,"visibility":"featured","source_url":null,"source_icon":null}} -{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"icon":null,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} -{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"icon":[155,53,2,74,172,235,215,64,16,234,89,94,245,209,128,244,127,94,200,34,223,16,2,54,221,106,200,8,73,123,100,246],"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} +{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null}} +{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} +{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"}}} {"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"}}} {"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{}}} diff --git a/tests/snapshots/tests__database__get_website.snap b/tests/snapshots/tests__database__get_website.snap index 5f84d73..dcef7cc 100644 --- a/tests/snapshots/tests__database__get_website.snap +++ b/tests/snapshots/tests__database__get_website.snap @@ -1,5 +1,5 @@ --- -source: src/db/mod.rs +source: tests/tests.rs expression: "vec![ws1, ws2, ws3]" --- [ @@ -7,40 +7,6 @@ expression: "vec![ws1, ws2, ws3]" name: "ThetaDev", created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), latest_version: Some(2), - icon: Some([ - 159, - 126, - 121, - 113, - 180, - 191, - 219, - 117, - 66, - 158, - 83, - 77, - 234, - 70, - 30, - 217, - 3, - 64, - 136, - 105, - 37, - 7, - 140, - 218, - 37, - 44, - 173, - 169, - 170, - 14, - 37, - 247, - ]), color: Some(2068974), visibility: featured, source_url: None, @@ -50,40 +16,6 @@ expression: "vec![ws1, ws2, ws3]" name: "Spotify-Gender-Ex", created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), latest_version: Some(3), - icon: Some([ - 155, - 53, - 2, - 74, - 172, - 235, - 215, - 64, - 16, - 234, - 89, - 94, - 245, - 209, - 128, - 244, - 127, - 94, - 200, - 34, - 223, - 16, - 2, - 54, - 221, - 106, - 200, - 8, - 73, - 123, - 100, - 246, - ]), color: Some(1947988), visibility: featured, source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"), @@ -93,7 +25,6 @@ expression: "vec![ws1, ws2, ws3]" name: "RustyPipe", created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0), latest_version: Some(4), - icon: None, color: Some(7943647), visibility: featured, source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"), diff --git a/tests/snapshots/tests__database__get_websites.snap b/tests/snapshots/tests__database__get_websites.snap index a30161b..a5c5430 100644 --- a/tests/snapshots/tests__database__get_websites.snap +++ b/tests/snapshots/tests__database__get_websites.snap @@ -7,40 +7,6 @@ expression: websites name: "ThetaDev", created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), latest_version: Some(2), - icon: Some([ - 159, - 126, - 121, - 113, - 180, - 191, - 219, - 117, - 66, - 158, - 83, - 77, - 234, - 70, - 30, - 217, - 3, - 64, - 136, - 105, - 37, - 7, - 140, - 218, - 37, - 44, - 173, - 169, - 170, - 14, - 37, - 247, - ]), color: Some(2068974), visibility: featured, source_url: None, @@ -50,7 +16,6 @@ expression: websites name: "RustyPipe", created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0), latest_version: Some(4), - icon: None, color: Some(7943647), visibility: featured, source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"), @@ -60,40 +25,6 @@ expression: websites name: "Spotify-Gender-Ex", created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), latest_version: Some(3), - icon: Some([ - 155, - 53, - 2, - 74, - 172, - 235, - 215, - 64, - 16, - 234, - 89, - 94, - 245, - 209, - 128, - 244, - 127, - 94, - 200, - 34, - 223, - 16, - 2, - 54, - 221, - 106, - 200, - 8, - 73, - 123, - 100, - 246, - ]), color: Some(1947988), visibility: featured, source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"), diff --git a/tests/snapshots/tests__database__update_website.snap b/tests/snapshots/tests__database__update_website.snap index 6c8bebb..1d30522 100644 --- a/tests/snapshots/tests__database__update_website.snap +++ b/tests/snapshots/tests__database__update_website.snap @@ -1,12 +1,11 @@ --- -source: src/db/mod.rs +source: tests/tests.rs expression: website --- Website( name: "ThetaDev2", created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), latest_version: Some(2), - icon: None, color: Some(1000), visibility: hidden, source_url: Some("https://example.com"), diff --git a/tests/testfiles/config/config.toml b/tests/testfiles/config/config.toml index bd681cc..d715bb1 100644 --- a/tests/testfiles/config/config.toml +++ b/tests/testfiles/config/config.toml @@ -1,6 +1,5 @@ [server] -address = "127.0.0.1" -port = 3000 +address = "127.0.0.1:3000" root_domain = "example.com" internal_subdomain = "talon-i" internal_url = "http://talon-i.example.com" diff --git a/tests/tests.rs b/tests/tests.rs index 98c68fc..0ff7890 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -73,7 +73,6 @@ mod database { SUBDOMAIN_1, WebsiteUpdate { name: Some("ThetaDev2".to_owned()), - icon: Some(None), color: Some(Some(1000)), visibility: Some(talon::model::Visibility::Hidden), source_url: Some(Some("https://example.com".to_owned())), @@ -177,7 +176,7 @@ mod database { #[rstest] fn get_file_hashes(db: DbTest) { let hashes = db.get_file_hashes().unwrap(); - assert_eq!(hashes.len(), 12) + assert_eq!(hashes.len(), 10) } }