Compare commits
No commits in common. "0b5f369fa07020c3612f8937d50998e6f40c032e" and "09c43870eb7675562a51d00d7a718ee687168aa4" have entirely different histories.
0b5f369fa0
...
09c43870eb
23 changed files with 343 additions and 284 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
86
src/api.rs
86
src/api.rs
|
@ -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)),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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};
|
|
||||||
|
|
96
src/main.rs
96
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<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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
47
src/page.rs
47
src/page.rs
|
@ -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(),
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()))?,
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
7
tests/fixtures/mod.rs
vendored
7
tests/fixtures/mod.rs
vendored
|
@ -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()),
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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":{}}}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue