diff --git a/Cargo.lock b/Cargo.lock index bd474cc..9d85ef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,19 +462,6 @@ 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" @@ -746,12 +733,6 @@ 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" @@ -819,12 +800,6 @@ 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" @@ -932,18 +907,6 @@ 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" @@ -1125,7 +1088,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] @@ -1713,7 +1676,6 @@ version = "0.1.0" dependencies = [ "brotli", "compressible", - "env_logger", "flate2", "hex", "hex-literal", diff --git a/Cargo.toml b/Cargo.toml index fa355d9..6dba2b5 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 = "0.7.2" +toml = { version = "0.7.2", default-features = false, features = ["parse"] } 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 b4cdb3c..1db2ecc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,7 +13,14 @@ use poem_openapi::{ OpenApi, SecurityScheme, }; -use crate::{config::KeyCfg, db, model::*, oai::DynParams, util, Talon}; +use crate::{ + config::{Config, KeyCfg}, + db::{self, Db}, + model::*, + oai::DynParams, + storage::Storage, + util, +}; pub struct TalonApi; @@ -27,8 +34,8 @@ pub struct TalonApi; struct ApiKeyAuthorization(KeyCfg); async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option { - let talon = req.data::()?; - talon.cfg.keys.get(&api_key.key).cloned() + let cfg = req.data::()?; + cfg.keys.get(&api_key.key).cloned() } impl ApiKeyAuthorization { @@ -64,14 +71,8 @@ impl ResponseError for ApiError { impl TalonApi { /// Get a website #[oai(path = "/website/:subdomain", method = "get")] - async fn website_get( - &self, - talon: Data<&Talon>, - subdomain: Path, - ) -> Result> { - talon - .db - .get_website(&subdomain) + async fn website_get(&self, db: Data<&Db>, subdomain: Path) -> Result> { + db.get_website(&subdomain) .map(|w| Json(Website::from((subdomain.0, w)))) .map_err(Error::from) } @@ -81,18 +82,19 @@ impl TalonApi { async fn website_post( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, + cfg: Data<&Config>, subdomain: Path, website: Json, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - if subdomain.as_str() == talon.cfg.server.internal_subdomain + if subdomain.as_str() == cfg.server.internal_subdomain || !util::validate_subdomain(&subdomain) { return Err(ApiError::InvalidSubdomain.into()); } - talon.db.insert_website(&subdomain, &website.0.into())?; + db.insert_website(&subdomain, &website.0.into())?; Ok(()) } @@ -101,13 +103,13 @@ impl TalonApi { async fn website_update( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, subdomain: Path, website: Json, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - talon.db.update_website(&subdomain, website.0.into())?; + db.update_website(&subdomain, website.0.into())?; Ok(()) } @@ -116,21 +118,19 @@ impl TalonApi { async fn website_delete( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, subdomain: Path, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - talon.db.delete_website(&subdomain, true)?; + db.delete_website(&subdomain, true)?; Ok(()) } /// Get all websites #[oai(path = "/websites", method = "get")] - async fn websites_get(&self, talon: Data<&Talon>) -> Result>> { - talon - .db - .get_websites() + async fn websites_get(&self, db: Data<&Db>) -> Result>> { + db.get_websites() .map(|r| r.map(Website::from)) .collect::, _>>() .map(Json) @@ -141,13 +141,11 @@ impl TalonApi { #[oai(path = "/website/:subdomain/versions", method = "get")] async fn website_versions( &self, - talon: Data<&Talon>, + db: Data<&Db>, subdomain: Path, ) -> Result>> { - talon.db.website_exists(&subdomain)?; - talon - .db - .get_website_versions(&subdomain) + db.website_exists(&subdomain)?; + db.get_website_versions(&subdomain) .map(|r| r.map(Version::from)) .collect::, _>>() .map(Json) @@ -158,13 +156,11 @@ impl TalonApi { #[oai(path = "/website/:subdomain/version/:id", method = "get")] async fn version_get( &self, - talon: Data<&Talon>, + db: Data<&Db>, subdomain: Path, id: Path, ) -> Result> { - talon - .db - .get_version(&subdomain, *id) + db.get_version(&subdomain, *id) .map(|v| Json(Version::from((*id, v)))) .map_err(Error::from) } @@ -174,13 +170,13 @@ impl TalonApi { async fn version_delete( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, subdomain: Path, id: Path, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - talon.db.delete_version(&subdomain, *id, true)?; + db.delete_version(&subdomain, *id, true)?; Ok(()) } @@ -189,7 +185,8 @@ impl TalonApi { async fn version_upload_zip( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, + storage: Data<&Storage>, subdomain: Path, /// Associated version data /// @@ -200,12 +197,10 @@ impl TalonApi { data: Binary>, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - let vid = talon.db.new_version_id()?; - talon - .storage - .insert_zip_archive(Cursor::new(data.as_slice()), vid)?; + let vid = db.new_version_id()?; + storage.insert_zip_archive(Cursor::new(data.as_slice()), vid)?; - talon.db.insert_version( + db.insert_version( &subdomain, vid, &db::model::Version { @@ -213,7 +208,7 @@ impl TalonApi { ..Default::default() }, )?; - talon.db.update_website( + db.update_website( &subdomain, db::model::WebsiteUpdate { latest_version: Some(Some(vid)), @@ -228,7 +223,8 @@ impl TalonApi { async fn version_upload_tgz( &self, auth: ApiKeyAuthorization, - talon: Data<&Talon>, + db: Data<&Db>, + storage: Data<&Storage>, subdomain: Path, /// Associated version data /// @@ -239,10 +235,10 @@ impl TalonApi { data: Binary>, ) -> Result<()> { auth.check_subdomain(&subdomain)?; - let vid = talon.db.new_version_id()?; - talon.storage.insert_tgz_archive(data.as_slice(), vid)?; + let vid = db.new_version_id()?; + storage.insert_tgz_archive(data.as_slice(), vid)?; - talon.db.insert_version( + db.insert_version( &subdomain, vid, &db::model::Version { @@ -250,7 +246,7 @@ impl TalonApi { ..Default::default() }, )?; - talon.db.update_website( + db.update_website( &subdomain, db::model::WebsiteUpdate { latest_version: Some(Some(vid)), diff --git a/src/config.rs b/src/config.rs index 7b39ab6..42c4bd9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,21 +57,9 @@ 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 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) - } + let cfg_str = std::fs::read_to_string(path)?; + Ok(toml::from_str::(&cfg_str)?) } } @@ -79,6 +67,7 @@ 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, @@ -87,7 +76,8 @@ pub struct ServerCfg { impl Default for ServerCfg { fn default() -> Self { Self { - address: "0.0.0.0:3000".to_owned(), + address: "0.0.0.0".to_owned(), + port: 8080, 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 10575cb..f9749f5 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -198,6 +198,7 @@ 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); @@ -463,6 +464,12 @@ 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 b6e2760..a56e21b 100644 --- a/src/db/model.rs +++ b/src/db/model.rs @@ -14,6 +14,8 @@ 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 @@ -30,6 +32,7 @@ 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(), @@ -47,6 +50,8 @@ 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 665b1a8..677a226 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,6 @@ pub mod api; pub mod config; pub mod db; pub mod model; -pub mod server; -pub mod storage; - mod oai; -mod page; +pub mod storage; mod util; - -pub use server::{Result, Talon, TalonError}; diff --git a/src/main.rs b/src/main.rs index 65b55df..6a8f4fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,95 @@ -use talon::{Result, Talon}; +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(), + }) +} #[tokio::main] -async fn main() -> Result<()> { - let talon = Talon::new("tmp")?; - talon.launch().await +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(()) } diff --git a/src/model.rs b/src/model.rs index 54b9cdd..91a1edb 100644 --- a/src/model.rs +++ b/src/model.rs @@ -18,6 +18,8 @@ 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 @@ -129,6 +131,7 @@ 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 deleted file mode 100644 index 82c0f2c..0000000 --- a/src/page.rs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 516cf24..0000000 --- a/src/server.rs +++ /dev/null @@ -1,92 +0,0 @@ -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 13ec448..397f54f 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -6,6 +6,7 @@ use std::{ ops::Bound, path::{Path, PathBuf}, str::FromStr, + sync::Arc, }; use flate2::{read::GzDecoder, write::GzEncoder}; @@ -28,7 +29,12 @@ use crate::{ util, }; +#[derive(Clone)] pub struct Storage { + i: Arc, +} + +struct StorageInner { path: PathBuf, db: Db, cfg: Config, @@ -101,9 +107,12 @@ 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 { - path: path.into(), - db, - cfg, + i: StorageInner { + path: path.into(), + db, + cfg, + } + .into(), } } @@ -126,26 +135,26 @@ impl Storage { fs::copy(file_path, &stored_file)?; - if self.cfg.compression.enabled() + if self.i.cfg.compression.enabled() && mime_guess::from_path(file_path) .first() .map(|t| compressible::is_compressible(t.essence_str())) .unwrap_or_default() { - if self.cfg.compression.gzip_en { + if self.i.cfg.compression.gzip_en { let mut encoder = GzEncoder::new( fs::File::create(stored_file.with_extension("gz"))?, - flate2::Compression::new(self.cfg.compression.gzip_level.into()), + flate2::Compression::new(self.i.cfg.compression.gzip_level.into()), ); let mut input = BufReader::new(fs::File::open(&stored_file)?); std::io::copy(&mut input, &mut encoder)?; } - if self.cfg.compression.brotli_en { + if self.i.cfg.compression.brotli_en { let mut encoder = brotli::CompressorWriter::new( fs::File::create(stored_file.with_extension("br"))?, 4096, - self.cfg.compression.brotli_level.into(), + self.i.cfg.compression.brotli_level.into(), 20, ); let mut input = BufReader::new(fs::File::open(&stored_file)?); @@ -153,7 +162,7 @@ impl Storage { } } - self.db.insert_file(version, site_path, &hash)?; + self.i.db.insert_file(version, site_path, &hash)?; Ok(()) } @@ -239,7 +248,7 @@ impl Storage { fn file_path_mkdir(&self, hash: &[u8]) -> Result { let hash_str = hash.encode_hex::(); - let subdir = self.path.join(&hash_str[..2]); + let subdir = self.i.path.join(&hash_str[..2]); if !subdir.is_dir() { fs::create_dir(&subdir)?; } @@ -248,7 +257,7 @@ impl Storage { fn file_path(&self, hash: &[u8]) -> PathBuf { let hash_str = hash.encode_hex::(); - let subdir = self.path.join(&hash_str[..2]); + let subdir = self.i.path.join(&hash_str[..2]); subdir.join(&hash_str) } @@ -256,13 +265,13 @@ impl Storage { let path = self.file_path(hash); let mut res = BTreeMap::new(); - if self.cfg.compression.gzip_en { + if self.i.cfg.compression.gzip_en { let path_gz = path.with_extension("gz"); if path_gz.is_file() { res.insert(CompressionAlg::Gzip, path_gz); } } - if self.cfg.compression.brotli_en { + if self.i.cfg.compression.brotli_en { let path_br = path.with_extension("br"); if path_br.is_file() { res.insert(CompressionAlg::Brotli, path_br); @@ -290,7 +299,7 @@ impl Storage { // Attempt to access the following pages // 1. Site path directly // 2. Site path + `/index.html` - match self.db.get_file_opt(version, sp)? { + match self.i.db.get_file_opt(version, sp)? { Some(h) => { hash = Some(h); } @@ -308,6 +317,7 @@ 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 0db1fbb..8a2e2fc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -119,14 +119,6 @@ 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 a947410..ae64a21 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -57,6 +57,9 @@ 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() @@ -69,6 +72,9 @@ 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()), @@ -83,6 +89,7 @@ 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 ddadd88..de41c6b 100644 --- a/tests/snapshots/tests__config__default.snap +++ b/tests/snapshots/tests__config__default.snap @@ -4,7 +4,8 @@ expression: "&cfg" --- ConfigInner( server: ServerCfg( - address: "127.0.0.1:3000", + address: "127.0.0.1", + port: 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 a68b605..4a2d3af 100644 --- a/tests/snapshots/tests__config__sparse.snap +++ b/tests/snapshots/tests__config__sparse.snap @@ -4,7 +4,8 @@ expression: "&cfg" --- ConfigInner( server: ServerCfg( - address: "0.0.0.0:3000", + address: "0.0.0.0", + port: 8080, 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 c29d78f..480f541 100644 --- a/tests/snapshots/tests__database__delete_website.snap +++ b/tests/snapshots/tests__database__delete_website.snap @@ -1,9 +1,9 @@ --- -source: tests/tests.rs +source: src/db/mod.rs expression: data --- -{"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":"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":"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 469053a..5970c14 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,"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":"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":"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 dcef7cc..5f84d73 100644 --- a/tests/snapshots/tests__database__get_website.snap +++ b/tests/snapshots/tests__database__get_website.snap @@ -1,5 +1,5 @@ --- -source: tests/tests.rs +source: src/db/mod.rs expression: "vec![ws1, ws2, ws3]" --- [ @@ -7,6 +7,40 @@ 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, @@ -16,6 +50,40 @@ 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"), @@ -25,6 +93,7 @@ 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 a5c5430..a30161b 100644 --- a/tests/snapshots/tests__database__get_websites.snap +++ b/tests/snapshots/tests__database__get_websites.snap @@ -7,6 +7,40 @@ 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, @@ -16,6 +50,7 @@ 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"), @@ -25,6 +60,40 @@ 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 1d30522..6c8bebb 100644 --- a/tests/snapshots/tests__database__update_website.snap +++ b/tests/snapshots/tests__database__update_website.snap @@ -1,11 +1,12 @@ --- -source: tests/tests.rs +source: src/db/mod.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 d715bb1..bd681cc 100644 --- a/tests/testfiles/config/config.toml +++ b/tests/testfiles/config/config.toml @@ -1,5 +1,6 @@ [server] -address = "127.0.0.1:3000" +address = "127.0.0.1" +port = 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 0ff7890..98c68fc 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -73,6 +73,7 @@ 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())), @@ -176,7 +177,7 @@ mod database { #[rstest] fn get_file_hashes(db: DbTest) { let hashes = db.get_file_hashes().unwrap(); - assert_eq!(hashes.len(), 10) + assert_eq!(hashes.len(), 12) } }