Compare commits

..

No commits in common. "0b5f369fa07020c3612f8937d50998e6f40c032e" and "09c43870eb7675562a51d00d7a718ee687168aa4" have entirely different histories.

23 changed files with 343 additions and 284 deletions

40
Cargo.lock generated
View file

@ -462,19 +462,6 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "errno" name = "errno"
version = "0.2.8" version = "0.2.8"
@ -746,12 +733,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -819,12 +800,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.24" version = "0.14.24"
@ -932,18 +907,6 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.5" version = "1.0.5"
@ -1125,7 +1088,7 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [ dependencies = [
"hermit-abi 0.2.6", "hermit-abi",
"libc", "libc",
] ]
@ -1713,7 +1676,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"brotli", "brotli",
"compressible", "compressible",
"env_logger",
"flate2", "flate2",
"hex", "hex",
"hex-literal", "hex-literal",

View file

@ -14,7 +14,7 @@ sled = "0.34.7"
serde = "1.0.152" serde = "1.0.152"
serde_json = "1.0.93" serde_json = "1.0.93"
rmp-serde = "1.1.1" rmp-serde = "1.1.1"
toml = "0.7.2" toml = { version = "0.7.2", default-features = false, features = ["parse"] }
thiserror = "1.0.38" thiserror = "1.0.38"
time = { version = "0.3.15", features = [ time = { version = "0.3.15", features = [
"macros", "macros",
@ -38,10 +38,10 @@ compressible = "0.2.0"
regex = "1.7.1" regex = "1.7.1"
log = "0.4.17" log = "0.4.17"
httpdate = "1.0.2" httpdate = "1.0.2"
env_logger = "0.10.0"
[dev-dependencies] [dev-dependencies]
rstest = "0.16.0" rstest = "0.16.0"
temp_testdir = "0.2.3" temp_testdir = "0.2.3"
insta = { version = "1.17.1", features = ["ron"] } insta = { version = "1.17.1", features = ["ron"] }
hex-literal = "0.3.4" hex-literal = "0.3.4"
path_macro = "1.0.0"

View file

@ -13,7 +13,14 @@ use poem_openapi::{
OpenApi, SecurityScheme, 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; pub struct TalonApi;
@ -27,8 +34,8 @@ pub struct TalonApi;
struct ApiKeyAuthorization(KeyCfg); struct ApiKeyAuthorization(KeyCfg);
async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option<KeyCfg> { async fn api_key_checker(req: &Request, api_key: ApiKey) -> Option<KeyCfg> {
let talon = req.data::<Talon>()?; let cfg = req.data::<Config>()?;
talon.cfg.keys.get(&api_key.key).cloned() cfg.keys.get(&api_key.key).cloned()
} }
impl ApiKeyAuthorization { impl ApiKeyAuthorization {
@ -64,14 +71,8 @@ impl ResponseError for ApiError {
impl TalonApi { impl TalonApi {
/// Get a website /// Get a website
#[oai(path = "/website/:subdomain", method = "get")] #[oai(path = "/website/:subdomain", method = "get")]
async fn website_get( async fn website_get(&self, db: Data<&Db>, subdomain: Path<String>) -> Result<Json<Website>> {
&self, db.get_website(&subdomain)
talon: Data<&Talon>,
subdomain: Path<String>,
) -> Result<Json<Website>> {
talon
.db
.get_website(&subdomain)
.map(|w| Json(Website::from((subdomain.0, w)))) .map(|w| Json(Website::from((subdomain.0, w))))
.map_err(Error::from) .map_err(Error::from)
} }
@ -81,18 +82,19 @@ impl TalonApi {
async fn website_post( async fn website_post(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
cfg: Data<&Config>,
subdomain: Path<String>, subdomain: Path<String>,
website: Json<WebsiteNew>, website: Json<WebsiteNew>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; 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) || !util::validate_subdomain(&subdomain)
{ {
return Err(ApiError::InvalidSubdomain.into()); return Err(ApiError::InvalidSubdomain.into());
} }
talon.db.insert_website(&subdomain, &website.0.into())?; db.insert_website(&subdomain, &website.0.into())?;
Ok(()) Ok(())
} }
@ -101,13 +103,13 @@ impl TalonApi {
async fn website_update( async fn website_update(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
subdomain: Path<String>, subdomain: Path<String>,
website: Json<WebsiteUpdate>, website: Json<WebsiteUpdate>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; auth.check_subdomain(&subdomain)?;
talon.db.update_website(&subdomain, website.0.into())?; db.update_website(&subdomain, website.0.into())?;
Ok(()) Ok(())
} }
@ -116,21 +118,19 @@ impl TalonApi {
async fn website_delete( async fn website_delete(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
subdomain: Path<String>, subdomain: Path<String>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; auth.check_subdomain(&subdomain)?;
talon.db.delete_website(&subdomain, true)?; db.delete_website(&subdomain, true)?;
Ok(()) Ok(())
} }
/// Get all websites /// Get all websites
#[oai(path = "/websites", method = "get")] #[oai(path = "/websites", method = "get")]
async fn websites_get(&self, talon: Data<&Talon>) -> Result<Json<Vec<Website>>> { async fn websites_get(&self, db: Data<&Db>) -> Result<Json<Vec<Website>>> {
talon db.get_websites()
.db
.get_websites()
.map(|r| r.map(Website::from)) .map(|r| r.map(Website::from))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map(Json) .map(Json)
@ -141,13 +141,11 @@ impl TalonApi {
#[oai(path = "/website/:subdomain/versions", method = "get")] #[oai(path = "/website/:subdomain/versions", method = "get")]
async fn website_versions( async fn website_versions(
&self, &self,
talon: Data<&Talon>, db: Data<&Db>,
subdomain: Path<String>, subdomain: Path<String>,
) -> Result<Json<Vec<Version>>> { ) -> Result<Json<Vec<Version>>> {
talon.db.website_exists(&subdomain)?; db.website_exists(&subdomain)?;
talon db.get_website_versions(&subdomain)
.db
.get_website_versions(&subdomain)
.map(|r| r.map(Version::from)) .map(|r| r.map(Version::from))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map(Json) .map(Json)
@ -158,13 +156,11 @@ impl TalonApi {
#[oai(path = "/website/:subdomain/version/:id", method = "get")] #[oai(path = "/website/:subdomain/version/:id", method = "get")]
async fn version_get( async fn version_get(
&self, &self,
talon: Data<&Talon>, db: Data<&Db>,
subdomain: Path<String>, subdomain: Path<String>,
id: Path<u32>, id: Path<u32>,
) -> Result<Json<Version>> { ) -> Result<Json<Version>> {
talon db.get_version(&subdomain, *id)
.db
.get_version(&subdomain, *id)
.map(|v| Json(Version::from((*id, v)))) .map(|v| Json(Version::from((*id, v))))
.map_err(Error::from) .map_err(Error::from)
} }
@ -174,13 +170,13 @@ impl TalonApi {
async fn version_delete( async fn version_delete(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
subdomain: Path<String>, subdomain: Path<String>,
id: Path<u32>, id: Path<u32>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; auth.check_subdomain(&subdomain)?;
talon.db.delete_version(&subdomain, *id, true)?; db.delete_version(&subdomain, *id, true)?;
Ok(()) Ok(())
} }
@ -189,7 +185,8 @@ impl TalonApi {
async fn version_upload_zip( async fn version_upload_zip(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
storage: Data<&Storage>,
subdomain: Path<String>, subdomain: Path<String>,
/// Associated version data /// Associated version data
/// ///
@ -200,12 +197,10 @@ impl TalonApi {
data: Binary<Vec<u8>>, data: Binary<Vec<u8>>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; auth.check_subdomain(&subdomain)?;
let vid = talon.db.new_version_id()?; let vid = db.new_version_id()?;
talon storage.insert_zip_archive(Cursor::new(data.as_slice()), vid)?;
.storage
.insert_zip_archive(Cursor::new(data.as_slice()), vid)?;
talon.db.insert_version( db.insert_version(
&subdomain, &subdomain,
vid, vid,
&db::model::Version { &db::model::Version {
@ -213,7 +208,7 @@ impl TalonApi {
..Default::default() ..Default::default()
}, },
)?; )?;
talon.db.update_website( db.update_website(
&subdomain, &subdomain,
db::model::WebsiteUpdate { db::model::WebsiteUpdate {
latest_version: Some(Some(vid)), latest_version: Some(Some(vid)),
@ -228,7 +223,8 @@ impl TalonApi {
async fn version_upload_tgz( async fn version_upload_tgz(
&self, &self,
auth: ApiKeyAuthorization, auth: ApiKeyAuthorization,
talon: Data<&Talon>, db: Data<&Db>,
storage: Data<&Storage>,
subdomain: Path<String>, subdomain: Path<String>,
/// Associated version data /// Associated version data
/// ///
@ -239,10 +235,10 @@ impl TalonApi {
data: Binary<Vec<u8>>, data: Binary<Vec<u8>>,
) -> Result<()> { ) -> Result<()> {
auth.check_subdomain(&subdomain)?; auth.check_subdomain(&subdomain)?;
let vid = talon.db.new_version_id()?; let vid = db.new_version_id()?;
talon.storage.insert_tgz_archive(data.as_slice(), vid)?; storage.insert_tgz_archive(data.as_slice(), vid)?;
talon.db.insert_version( db.insert_version(
&subdomain, &subdomain,
vid, vid,
&db::model::Version { &db::model::Version {
@ -250,7 +246,7 @@ impl TalonApi {
..Default::default() ..Default::default()
}, },
)?; )?;
talon.db.update_website( db.update_website(
&subdomain, &subdomain,
db::model::WebsiteUpdate { db::model::WebsiteUpdate {
latest_version: Some(Some(vid)), latest_version: Some(Some(vid)),

View file

@ -57,21 +57,9 @@ impl Config {
Self { i: cfg.into() } Self { i: cfg.into() }
} }
/// Read the configuration from the given file
/// or create a default config at the given location
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
if path.is_file() {
let cfg_str = std::fs::read_to_string(path)?; let cfg_str = std::fs::read_to_string(path)?;
Ok(toml::from_str::<Config>(&cfg_str)?) Ok(toml::from_str::<Config>(&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)
}
} }
} }
@ -79,6 +67,7 @@ impl Config {
#[serde(default)] #[serde(default)]
pub struct ServerCfg { pub struct ServerCfg {
pub address: String, pub address: String,
pub port: u32,
pub root_domain: String, pub root_domain: String,
pub internal_subdomain: String, pub internal_subdomain: String,
pub internal_url: String, pub internal_url: String,
@ -87,7 +76,8 @@ pub struct ServerCfg {
impl Default for ServerCfg { impl Default for ServerCfg {
fn default() -> Self { fn default() -> Self {
Self { Self {
address: "0.0.0.0:3000".to_owned(), address: "0.0.0.0".to_owned(),
port: 8080,
root_domain: "localhost".to_owned(), root_domain: "localhost".to_owned(),
internal_subdomain: "talon".to_owned(), internal_subdomain: "talon".to_owned(),
internal_url: "http://talon.localhost".to_owned(), internal_url: "http://talon.localhost".to_owned(),

View file

@ -198,6 +198,7 @@ impl Db {
let website = website.clone(); let website = website.clone();
w.name = website.name.unwrap_or(w.name); w.name = website.name.unwrap_or(w.name);
w.latest_version = website.latest_version.unwrap_or(w.latest_version); 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.color = website.color.unwrap_or(w.color);
w.visibility = website.visibility.unwrap_or(w.visibility); w.visibility = website.visibility.unwrap_or(w.visibility);
w.source_url = website.source_url.unwrap_or(w.source_url); w.source_url = website.source_url.unwrap_or(w.source_url);
@ -463,6 +464,12 @@ impl Db {
let (_, file) = f?; let (_, file) = f?;
set.insert(file.to_vec()); 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) Ok(set)
} }
} }

View file

@ -14,6 +14,8 @@ pub struct Website {
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
/// Latest version ID /// Latest version ID
pub latest_version: Option<u32>, pub latest_version: Option<u32>,
/// File hash of the page icon
pub icon: Option<Vec<u8>>,
/// Color of the page icon /// Color of the page icon
pub color: Option<u32>, pub color: Option<u32>,
/// Visibility of the page in the sidebar menu /// Visibility of the page in the sidebar menu
@ -30,6 +32,7 @@ impl Default for Website {
name: Default::default(), name: Default::default(),
created_at: OffsetDateTime::now_utc(), created_at: OffsetDateTime::now_utc(),
latest_version: Default::default(), latest_version: Default::default(),
icon: Default::default(),
color: Default::default(), color: Default::default(),
visibility: Default::default(), visibility: Default::default(),
source_url: Default::default(), source_url: Default::default(),
@ -47,6 +50,8 @@ pub struct WebsiteUpdate {
pub name: Option<String>, pub name: Option<String>,
/// Latest version ID /// Latest version ID
pub latest_version: Option<Option<u32>>, pub latest_version: Option<Option<u32>>,
/// Page icon
pub icon: Option<Option<Vec<u8>>>,
/// Color of the page icon /// Color of the page icon
pub color: Option<Option<u32>>, pub color: Option<Option<u32>>,
/// Visibility of the page in the sidebar menu /// Visibility of the page in the sidebar menu

View file

@ -2,11 +2,6 @@ pub mod api;
pub mod config; pub mod config;
pub mod db; pub mod db;
pub mod model; pub mod model;
pub mod server;
pub mod storage;
mod oai; mod oai;
mod page; pub mod storage;
mod util; mod util;
pub use server::{Result, Talon, TalonError};

View file

@ -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<Response> {
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] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let talon = Talon::new("tmp")?; let db = Db::new("tmp/db")?;
talon.launch().await 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(())
} }

View file

@ -18,6 +18,8 @@ pub struct Website {
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
/// Latest version ID /// Latest version ID
pub latest_version: Option<u32>, pub latest_version: Option<u32>,
// TODO: implement page icon
// pub icon: Option<String>,
/// Color of the page icon /// Color of the page icon
pub color: Option<u32>, pub color: Option<u32>,
/// Visibility of the page in the sidebar menu /// Visibility of the page in the sidebar menu
@ -129,6 +131,7 @@ impl From<WebsiteUpdate> for db::model::WebsiteUpdate {
Self { Self {
name: value.name, name: value.name,
latest_version: None, latest_version: None,
icon: None,
color: value.color, color: value.color,
visibility: value.visibility, visibility: value.visibility,
source_url: value.source_url, source_url: value.source_url,

View file

@ -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<Response> {
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(),
})
}

View file

@ -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<TalonInner>,
}
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<T> = std::result::Result<T, TalonError>;
impl Talon {
pub fn new<P: AsRef<Path>>(workdir: P) -> Result<Self> {
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(())
}
}

View file

@ -6,6 +6,7 @@ use std::{
ops::Bound, ops::Bound,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc,
}; };
use flate2::{read::GzDecoder, write::GzEncoder}; use flate2::{read::GzDecoder, write::GzEncoder};
@ -28,7 +29,12 @@ use crate::{
util, util,
}; };
#[derive(Clone)]
pub struct Storage { pub struct Storage {
i: Arc<StorageInner>,
}
struct StorageInner {
path: PathBuf, path: PathBuf,
db: Db, db: Db,
cfg: Config, cfg: Config,
@ -101,10 +107,13 @@ impl Storage {
/// Create a new file storage using the root folder and the database /// Create a new file storage using the root folder and the database
pub fn new<P: Into<PathBuf>>(path: P, db: Db, cfg: Config) -> Self { pub fn new<P: Into<PathBuf>>(path: P, db: Db, cfg: Config) -> Self {
Self { Self {
i: StorageInner {
path: path.into(), path: path.into(),
db, db,
cfg, cfg,
} }
.into(),
}
} }
/// Insert a single file into the store /// Insert a single file into the store
@ -126,26 +135,26 @@ impl Storage {
fs::copy(file_path, &stored_file)?; fs::copy(file_path, &stored_file)?;
if self.cfg.compression.enabled() if self.i.cfg.compression.enabled()
&& mime_guess::from_path(file_path) && mime_guess::from_path(file_path)
.first() .first()
.map(|t| compressible::is_compressible(t.essence_str())) .map(|t| compressible::is_compressible(t.essence_str()))
.unwrap_or_default() .unwrap_or_default()
{ {
if self.cfg.compression.gzip_en { if self.i.cfg.compression.gzip_en {
let mut encoder = GzEncoder::new( let mut encoder = GzEncoder::new(
fs::File::create(stored_file.with_extension("gz"))?, 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)?); let mut input = BufReader::new(fs::File::open(&stored_file)?);
std::io::copy(&mut input, &mut encoder)?; 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( let mut encoder = brotli::CompressorWriter::new(
fs::File::create(stored_file.with_extension("br"))?, fs::File::create(stored_file.with_extension("br"))?,
4096, 4096,
self.cfg.compression.brotli_level.into(), self.i.cfg.compression.brotli_level.into(),
20, 20,
); );
let mut input = BufReader::new(fs::File::open(&stored_file)?); 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(()) Ok(())
} }
@ -239,7 +248,7 @@ impl Storage {
fn file_path_mkdir(&self, hash: &[u8]) -> Result<PathBuf> { fn file_path_mkdir(&self, hash: &[u8]) -> Result<PathBuf> {
let hash_str = hash.encode_hex::<String>(); let hash_str = hash.encode_hex::<String>();
let subdir = self.path.join(&hash_str[..2]); let subdir = self.i.path.join(&hash_str[..2]);
if !subdir.is_dir() { if !subdir.is_dir() {
fs::create_dir(&subdir)?; fs::create_dir(&subdir)?;
} }
@ -248,7 +257,7 @@ impl Storage {
fn file_path(&self, hash: &[u8]) -> PathBuf { fn file_path(&self, hash: &[u8]) -> PathBuf {
let hash_str = hash.encode_hex::<String>(); let hash_str = hash.encode_hex::<String>();
let subdir = self.path.join(&hash_str[..2]); let subdir = self.i.path.join(&hash_str[..2]);
subdir.join(&hash_str) subdir.join(&hash_str)
} }
@ -256,13 +265,13 @@ impl Storage {
let path = self.file_path(hash); let path = self.file_path(hash);
let mut res = BTreeMap::new(); 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"); let path_gz = path.with_extension("gz");
if path_gz.is_file() { if path_gz.is_file() {
res.insert(CompressionAlg::Gzip, path_gz); 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"); let path_br = path.with_extension("br");
if path_br.is_file() { if path_br.is_file() {
res.insert(CompressionAlg::Brotli, path_br); res.insert(CompressionAlg::Brotli, path_br);
@ -290,7 +299,7 @@ impl Storage {
// Attempt to access the following pages // Attempt to access the following pages
// 1. Site path directly // 1. Site path directly
// 2. Site path + `/index.html` // 2. Site path + `/index.html`
match self.db.get_file_opt(version, sp)? { match self.i.db.get_file_opt(version, sp)? {
Some(h) => { Some(h) => {
hash = Some(h); hash = Some(h);
} }
@ -308,6 +317,7 @@ impl Storage {
let hash = match hash { let hash = match hash {
Some(hash) => hash, Some(hash) => hash,
None => self None => self
.i
.db .db
.get_file_opt(version, &new_path)? .get_file_opt(version, &new_path)?
.ok_or_else(|| StorageError::NotFound(sp.to_owned()))?, .ok_or_else(|| StorageError::NotFound(sp.to_owned()))?,

View file

@ -119,14 +119,6 @@ pub fn validate_subdomain(subdomain: &str) -> bool {
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
} }
pub fn create_dir_ne<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
let path = path.as_ref();
if !path.is_dir() {
std::fs::create_dir(path)?;
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -57,6 +57,9 @@ fn insert_websites(db: &Db) {
name: "ThetaDev".to_owned(), name: "ThetaDev".to_owned(),
created_at: datetime!(2023-02-18 16:30 +0), created_at: datetime!(2023-02-18 16:30 +0),
latest_version: Some(VERSION_1_2), latest_version: Some(VERSION_1_2),
icon: Some(
hex!("9f7e7971b4bfdb75429e534dea461ed90340886925078cda252cada9aa0e25f7").to_vec(),
),
color: Some(2068974), color: Some(2068974),
visibility: talon::model::Visibility::Featured, visibility: talon::model::Visibility::Featured,
..Default::default() ..Default::default()
@ -69,6 +72,9 @@ fn insert_websites(db: &Db) {
name: "Spotify-Gender-Ex".to_owned(), name: "Spotify-Gender-Ex".to_owned(),
created_at: datetime!(2023-02-18 16:30 +0), created_at: datetime!(2023-02-18 16:30 +0),
latest_version: Some(VERSION_2_1), latest_version: Some(VERSION_2_1),
icon: Some(
hex!("9b35024aacebd74010ea595ef5d180f47f5ec822df100236dd6ac808497b64f6").to_vec(),
),
color: Some(1947988), color: Some(1947988),
visibility: talon::model::Visibility::Featured, visibility: talon::model::Visibility::Featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex".to_owned()), 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(), name: "RustyPipe".to_owned(),
created_at: datetime!(2023-02-20 18:30 +0), created_at: datetime!(2023-02-20 18:30 +0),
latest_version: Some(VERSION_3_1), latest_version: Some(VERSION_3_1),
icon: None,
color: Some(7943647), color: Some(7943647),
visibility: talon::model::Visibility::Featured, visibility: talon::model::Visibility::Featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe".to_owned()), source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe".to_owned()),

View file

@ -4,7 +4,8 @@ expression: "&cfg"
--- ---
ConfigInner( ConfigInner(
server: ServerCfg( server: ServerCfg(
address: "127.0.0.1:3000", address: "127.0.0.1",
port: 3000,
root_domain: "example.com", root_domain: "example.com",
internal_subdomain: "talon-i", internal_subdomain: "talon-i",
internal_url: "http://talon-i.example.com", internal_url: "http://talon-i.example.com",

View file

@ -4,7 +4,8 @@ expression: "&cfg"
--- ---
ConfigInner( ConfigInner(
server: ServerCfg( server: ServerCfg(
address: "0.0.0.0:3000", address: "0.0.0.0",
port: 8080,
root_domain: "localhost", root_domain: "localhost",
internal_subdomain: "talon", internal_subdomain: "talon",
internal_url: "http://talon.localhost", internal_url: "http://talon.localhost",

View file

@ -1,9 +1,9 @@
--- ---
source: tests/tests.rs source: src/db/mod.rs
expression: data 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":"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,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"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":"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":"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"} {"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}

View file

@ -2,9 +2,9 @@
source: tests/tests.rs source: tests/tests.rs
expression: data 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":"-","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,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} {"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,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"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":"-: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":"-: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":{}}} {"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{}}}

View file

@ -1,5 +1,5 @@
--- ---
source: tests/tests.rs source: src/db/mod.rs
expression: "vec![ws1, ws2, ws3]" expression: "vec![ws1, ws2, ws3]"
--- ---
[ [
@ -7,6 +7,40 @@ expression: "vec![ws1, ws2, ws3]"
name: "ThetaDev", name: "ThetaDev",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(2), 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), color: Some(2068974),
visibility: featured, visibility: featured,
source_url: None, source_url: None,
@ -16,6 +50,40 @@ expression: "vec![ws1, ws2, ws3]"
name: "Spotify-Gender-Ex", name: "Spotify-Gender-Ex",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(3), 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), color: Some(1947988),
visibility: featured, visibility: featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"), source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),
@ -25,6 +93,7 @@ expression: "vec![ws1, ws2, ws3]"
name: "RustyPipe", name: "RustyPipe",
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0), created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
latest_version: Some(4), latest_version: Some(4),
icon: None,
color: Some(7943647), color: Some(7943647),
visibility: featured, visibility: featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"), source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),

View file

@ -7,6 +7,40 @@ expression: websites
name: "ThetaDev", name: "ThetaDev",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(2), 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), color: Some(2068974),
visibility: featured, visibility: featured,
source_url: None, source_url: None,
@ -16,6 +50,7 @@ expression: websites
name: "RustyPipe", name: "RustyPipe",
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0), created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
latest_version: Some(4), latest_version: Some(4),
icon: None,
color: Some(7943647), color: Some(7943647),
visibility: featured, visibility: featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"), source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
@ -25,6 +60,40 @@ expression: websites
name: "Spotify-Gender-Ex", name: "Spotify-Gender-Ex",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(3), 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), color: Some(1947988),
visibility: featured, visibility: featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"), source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),

View file

@ -1,11 +1,12 @@
--- ---
source: tests/tests.rs source: src/db/mod.rs
expression: website expression: website
--- ---
Website( Website(
name: "ThetaDev2", name: "ThetaDev2",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(2), latest_version: Some(2),
icon: None,
color: Some(1000), color: Some(1000),
visibility: hidden, visibility: hidden,
source_url: Some("https://example.com"), source_url: Some("https://example.com"),

View file

@ -1,5 +1,6 @@
[server] [server]
address = "127.0.0.1:3000" address = "127.0.0.1"
port = 3000
root_domain = "example.com" root_domain = "example.com"
internal_subdomain = "talon-i" internal_subdomain = "talon-i"
internal_url = "http://talon-i.example.com" internal_url = "http://talon-i.example.com"

View file

@ -73,6 +73,7 @@ mod database {
SUBDOMAIN_1, SUBDOMAIN_1,
WebsiteUpdate { WebsiteUpdate {
name: Some("ThetaDev2".to_owned()), name: Some("ThetaDev2".to_owned()),
icon: Some(None),
color: Some(Some(1000)), color: Some(Some(1000)),
visibility: Some(talon::model::Visibility::Hidden), visibility: Some(talon::model::Visibility::Hidden),
source_url: Some(Some("https://example.com".to_owned())), source_url: Some(Some("https://example.com".to_owned())),
@ -176,7 +177,7 @@ mod database {
#[rstest] #[rstest]
fn get_file_hashes(db: DbTest) { fn get_file_hashes(db: DbTest) {
let hashes = db.get_file_hashes().unwrap(); let hashes = db.get_file_hashes().unwrap();
assert_eq!(hashes.len(), 10) assert_eq!(hashes.len(), 12)
} }
} }