Compare commits
2 commits
09c43870eb
...
0b5f369fa0
Author | SHA1 | Date | |
---|---|---|---|
0b5f369fa0 | |||
eca140331b |
23 changed files with 284 additions and 343 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -462,6 +462,19 @@ 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"
|
||||||
|
@ -733,6 +746,12 @@ 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"
|
||||||
|
@ -800,6 +819,12 @@ 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"
|
||||||
|
@ -907,6 +932,18 @@ 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"
|
||||||
|
@ -1088,7 +1125,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",
|
"hermit-abi 0.2.6",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1676,6 +1713,7 @@ 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 = { version = "0.7.2", default-features = false, features = ["parse"] }
|
toml = "0.7.2"
|
||||||
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,14 +13,7 @@ use poem_openapi::{
|
||||||
OpenApi, SecurityScheme,
|
OpenApi, SecurityScheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::KeyCfg, db, model::*, oai::DynParams, util, Talon};
|
||||||
config::{Config, KeyCfg},
|
|
||||||
db::{self, Db},
|
|
||||||
model::*,
|
|
||||||
oai::DynParams,
|
|
||||||
storage::Storage,
|
|
||||||
util,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct TalonApi;
|
pub struct TalonApi;
|
||||||
|
|
||||||
|
@ -34,8 +27,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 cfg = req.data::<Config>()?;
|
let talon = req.data::<Talon>()?;
|
||||||
cfg.keys.get(&api_key.key).cloned()
|
talon.cfg.keys.get(&api_key.key).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiKeyAuthorization {
|
impl ApiKeyAuthorization {
|
||||||
|
@ -71,8 +64,14 @@ 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(&self, db: Data<&Db>, subdomain: Path<String>) -> Result<Json<Website>> {
|
async fn website_get(
|
||||||
db.get_website(&subdomain)
|
&self,
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
@ -82,19 +81,18 @@ impl TalonApi {
|
||||||
async fn website_post(
|
async fn website_post(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
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() == cfg.server.internal_subdomain
|
if subdomain.as_str() == talon.cfg.server.internal_subdomain
|
||||||
|| !util::validate_subdomain(&subdomain)
|
|| !util::validate_subdomain(&subdomain)
|
||||||
{
|
{
|
||||||
return Err(ApiError::InvalidSubdomain.into());
|
return Err(ApiError::InvalidSubdomain.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insert_website(&subdomain, &website.0.into())?;
|
talon.db.insert_website(&subdomain, &website.0.into())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,13 +101,13 @@ impl TalonApi {
|
||||||
async fn website_update(
|
async fn website_update(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
website: Json<WebsiteUpdate>,
|
website: Json<WebsiteUpdate>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain)?;
|
auth.check_subdomain(&subdomain)?;
|
||||||
|
|
||||||
db.update_website(&subdomain, website.0.into())?;
|
talon.db.update_website(&subdomain, website.0.into())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,19 +116,21 @@ impl TalonApi {
|
||||||
async fn website_delete(
|
async fn website_delete(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain)?;
|
auth.check_subdomain(&subdomain)?;
|
||||||
|
|
||||||
db.delete_website(&subdomain, true)?;
|
talon.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, db: Data<&Db>) -> Result<Json<Vec<Website>>> {
|
async fn websites_get(&self, talon: Data<&Talon>) -> Result<Json<Vec<Website>>> {
|
||||||
db.get_websites()
|
talon
|
||||||
|
.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,11 +141,13 @@ 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,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
) -> Result<Json<Vec<Version>>> {
|
) -> Result<Json<Vec<Version>>> {
|
||||||
db.website_exists(&subdomain)?;
|
talon.db.website_exists(&subdomain)?;
|
||||||
db.get_website_versions(&subdomain)
|
talon
|
||||||
|
.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)
|
||||||
|
@ -156,11 +158,13 @@ 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,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
id: Path<u32>,
|
id: Path<u32>,
|
||||||
) -> Result<Json<Version>> {
|
) -> Result<Json<Version>> {
|
||||||
db.get_version(&subdomain, *id)
|
talon
|
||||||
|
.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)
|
||||||
}
|
}
|
||||||
|
@ -170,13 +174,13 @@ impl TalonApi {
|
||||||
async fn version_delete(
|
async fn version_delete(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
id: Path<u32>,
|
id: Path<u32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain)?;
|
auth.check_subdomain(&subdomain)?;
|
||||||
|
|
||||||
db.delete_version(&subdomain, *id, true)?;
|
talon.db.delete_version(&subdomain, *id, true)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,8 +189,7 @@ impl TalonApi {
|
||||||
async fn version_upload_zip(
|
async fn version_upload_zip(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
storage: Data<&Storage>,
|
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
/// Associated version data
|
/// Associated version data
|
||||||
///
|
///
|
||||||
|
@ -197,10 +200,12 @@ impl TalonApi {
|
||||||
data: Binary<Vec<u8>>,
|
data: Binary<Vec<u8>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain)?;
|
auth.check_subdomain(&subdomain)?;
|
||||||
let vid = db.new_version_id()?;
|
let vid = talon.db.new_version_id()?;
|
||||||
storage.insert_zip_archive(Cursor::new(data.as_slice()), vid)?;
|
talon
|
||||||
|
.storage
|
||||||
|
.insert_zip_archive(Cursor::new(data.as_slice()), vid)?;
|
||||||
|
|
||||||
db.insert_version(
|
talon.db.insert_version(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
vid,
|
vid,
|
||||||
&db::model::Version {
|
&db::model::Version {
|
||||||
|
@ -208,7 +213,7 @@ impl TalonApi {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
db.update_website(
|
talon.db.update_website(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
db::model::WebsiteUpdate {
|
db::model::WebsiteUpdate {
|
||||||
latest_version: Some(Some(vid)),
|
latest_version: Some(Some(vid)),
|
||||||
|
@ -223,8 +228,7 @@ impl TalonApi {
|
||||||
async fn version_upload_tgz(
|
async fn version_upload_tgz(
|
||||||
&self,
|
&self,
|
||||||
auth: ApiKeyAuthorization,
|
auth: ApiKeyAuthorization,
|
||||||
db: Data<&Db>,
|
talon: Data<&Talon>,
|
||||||
storage: Data<&Storage>,
|
|
||||||
subdomain: Path<String>,
|
subdomain: Path<String>,
|
||||||
/// Associated version data
|
/// Associated version data
|
||||||
///
|
///
|
||||||
|
@ -235,10 +239,10 @@ impl TalonApi {
|
||||||
data: Binary<Vec<u8>>,
|
data: Binary<Vec<u8>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
auth.check_subdomain(&subdomain)?;
|
auth.check_subdomain(&subdomain)?;
|
||||||
let vid = db.new_version_id()?;
|
let vid = talon.db.new_version_id()?;
|
||||||
storage.insert_tgz_archive(data.as_slice(), vid)?;
|
talon.storage.insert_tgz_archive(data.as_slice(), vid)?;
|
||||||
|
|
||||||
db.insert_version(
|
talon.db.insert_version(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
vid,
|
vid,
|
||||||
&db::model::Version {
|
&db::model::Version {
|
||||||
|
@ -246,7 +250,7 @@ impl TalonApi {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
db.update_website(
|
talon.db.update_website(
|
||||||
&subdomain,
|
&subdomain,
|
||||||
db::model::WebsiteUpdate {
|
db::model::WebsiteUpdate {
|
||||||
latest_version: Some(Some(vid)),
|
latest_version: Some(Some(vid)),
|
||||||
|
|
|
@ -57,9 +57,21 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +79,6 @@ 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,
|
||||||
|
@ -76,8 +87,7 @@ pub struct ServerCfg {
|
||||||
impl Default for ServerCfg {
|
impl Default for ServerCfg {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: "0.0.0.0".to_owned(),
|
address: "0.0.0.0:3000".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,7 +198,6 @@ 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);
|
||||||
|
@ -464,12 +463,6 @@ 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,8 +14,6 @@ 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
|
||||||
|
@ -32,7 +30,6 @@ 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(),
|
||||||
|
@ -50,8 +47,6 @@ 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,6 +2,11 @@ pub mod api;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
mod oai;
|
pub mod server;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|
||||||
|
mod oai;
|
||||||
|
mod page;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
pub use server::{Result, Talon, TalonError};
|
||||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -1,95 +1,7 @@
|
||||||
use poem::{
|
use talon::{Result, Talon};
|
||||||
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<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<()> {
|
||||||
let db = Db::new("tmp/db")?;
|
let talon = Talon::new("tmp")?;
|
||||||
let cfg = Config::from_file("tmp/config.toml")?;
|
talon.launch().await
|
||||||
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,8 +18,6 @@ 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
|
||||||
|
@ -131,7 +129,6 @@ 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
Normal file
47
src/page.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use poem::{
|
||||||
|
error::ResponseError,
|
||||||
|
handler,
|
||||||
|
http::{header, StatusCode},
|
||||||
|
web::{Data, Redirect},
|
||||||
|
IntoResponse, Request, Response, Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Talon;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum PageError {
|
||||||
|
#[error("invalid subdomain")]
|
||||||
|
InvalidSubdomain,
|
||||||
|
#[error("website does not have any version")]
|
||||||
|
NoVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for PageError {
|
||||||
|
fn status(&self) -> StatusCode {
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub fn page(request: &Request, talon: Data<&Talon>) -> Result<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(),
|
||||||
|
})
|
||||||
|
}
|
92
src/server.rs
Normal file
92
src/server.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use std::{ops::Deref, path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{api::TalonApi, config::Config, db::Db, page::page, storage::Storage, util};
|
||||||
|
use path_macro::path;
|
||||||
|
use poem::{listener::TcpListener, EndpointExt, Route, RouteDomain, Server};
|
||||||
|
use poem_openapi::OpenApiService;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Talon {
|
||||||
|
i: Arc<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,7 +6,6 @@ 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};
|
||||||
|
@ -29,12 +28,7 @@ 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,
|
||||||
|
@ -107,13 +101,10 @@ 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
|
||||||
|
@ -135,26 +126,26 @@ impl Storage {
|
||||||
|
|
||||||
fs::copy(file_path, &stored_file)?;
|
fs::copy(file_path, &stored_file)?;
|
||||||
|
|
||||||
if self.i.cfg.compression.enabled()
|
if self.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.i.cfg.compression.gzip_en {
|
if self.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.i.cfg.compression.gzip_level.into()),
|
flate2::Compression::new(self.cfg.compression.gzip_level.into()),
|
||||||
);
|
);
|
||||||
let mut input = BufReader::new(fs::File::open(&stored_file)?);
|
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.i.cfg.compression.brotli_en {
|
if self.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.i.cfg.compression.brotli_level.into(),
|
self.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)?);
|
||||||
|
@ -162,7 +153,7 @@ impl Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.i.db.insert_file(version, site_path, &hash)?;
|
self.db.insert_file(version, site_path, &hash)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -248,7 +239,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.i.path.join(&hash_str[..2]);
|
let subdir = self.path.join(&hash_str[..2]);
|
||||||
if !subdir.is_dir() {
|
if !subdir.is_dir() {
|
||||||
fs::create_dir(&subdir)?;
|
fs::create_dir(&subdir)?;
|
||||||
}
|
}
|
||||||
|
@ -257,7 +248,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.i.path.join(&hash_str[..2]);
|
let subdir = self.path.join(&hash_str[..2]);
|
||||||
subdir.join(&hash_str)
|
subdir.join(&hash_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,13 +256,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.i.cfg.compression.gzip_en {
|
if self.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.i.cfg.compression.brotli_en {
|
if self.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);
|
||||||
|
@ -299,7 +290,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.i.db.get_file_opt(version, sp)? {
|
match self.db.get_file_opt(version, sp)? {
|
||||||
Some(h) => {
|
Some(h) => {
|
||||||
hash = Some(h);
|
hash = Some(h);
|
||||||
}
|
}
|
||||||
|
@ -317,7 +308,6 @@ 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,6 +119,14 @@ 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,9 +57,6 @@ 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()
|
||||||
|
@ -72,9 +69,6 @@ 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()),
|
||||||
|
@ -89,7 +83,6 @@ 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,8 +4,7 @@ expression: "&cfg"
|
||||||
---
|
---
|
||||||
ConfigInner(
|
ConfigInner(
|
||||||
server: ServerCfg(
|
server: ServerCfg(
|
||||||
address: "127.0.0.1",
|
address: "127.0.0.1:3000",
|
||||||
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,8 +4,7 @@ expression: "&cfg"
|
||||||
---
|
---
|
||||||
ConfigInner(
|
ConfigInner(
|
||||||
server: ServerCfg(
|
server: ServerCfg(
|
||||||
address: "0.0.0.0",
|
address: "0.0.0.0:3000",
|
||||||
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: src/db/mod.rs
|
source: tests/tests.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,"icon":null,"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,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
|
||||||
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"icon":[155,53,2,74,172,235,215,64,16,234,89,94,245,209,128,244,127,94,200,34,223,16,2,54,221,106,200,8,73,123,100,246],"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
|
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
|
||||||
{"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{}}}
|
{"type":"version","key":"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,"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":"-","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,"icon":null,"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,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
|
||||||
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"icon":[155,53,2,74,172,235,215,64,16,234,89,94,245,209,128,244,127,94,200,34,223,16,2,54,221,106,200,8,73,123,100,246],"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
|
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
|
||||||
{"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"}}}
|
{"type":"version","key":"-: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: src/db/mod.rs
|
source: tests/tests.rs
|
||||||
expression: "vec![ws1, ws2, ws3]"
|
expression: "vec![ws1, ws2, ws3]"
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
|
@ -7,40 +7,6 @@ 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,
|
||||||
|
@ -50,40 +16,6 @@ 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"),
|
||||||
|
@ -93,7 +25,6 @@ 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,40 +7,6 @@ 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,
|
||||||
|
@ -50,7 +16,6 @@ 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"),
|
||||||
|
@ -60,40 +25,6 @@ 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,12 +1,11 @@
|
||||||
---
|
---
|
||||||
source: src/db/mod.rs
|
source: tests/tests.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,6 +1,5 @@
|
||||||
[server]
|
[server]
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1:3000"
|
||||||
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,7 +73,6 @@ 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())),
|
||||||
|
@ -177,7 +176,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(), 12)
|
assert_eq!(hashes.len(), 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue