Compare commits
No commits in common. "7bf4728416fdc53fc32abf2da0d282bbd9156fd1" and "334955db438b626b8d1ca02b1bbf2250f9afc8cd" have entirely different histories.
7bf4728416
...
334955db43
16 changed files with 121 additions and 379 deletions
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -4,9 +4,9 @@ version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.22.0"
|
version = "0.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
|
@ -184,9 +184,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.11"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
|
checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
|
@ -319,9 +319,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.72"
|
version = "0.3.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
|
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cc",
|
"cc",
|
||||||
|
@ -828,9 +828,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.29.0"
|
version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
|
@ -1028,9 +1028,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.5"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
|
checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -1241,10 +1241,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.12"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
@ -1312,9 +1313,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.35.0"
|
version = "0.32.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
|
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -1505,15 +1506,6 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-crate"
|
|
||||||
version = "3.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
|
|
||||||
dependencies = [
|
|
||||||
"toml_edit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.84"
|
version = "1.0.84"
|
||||||
|
@ -1723,9 +1715,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rstest"
|
name = "rstest"
|
||||||
version = "0.20.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27059f51958c5f8496a6f79511e7c0ac396dd815dc8894e9b6e2efb5779cf6f0"
|
checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rstest_macros",
|
"rstest_macros",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
|
@ -1733,13 +1725,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rstest_macros"
|
name = "rstest_macros"
|
||||||
version = "0.20.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6132d64df104c0b3ea7a6ad7766a43f587bd773a4a9cf4cd59296d426afaf3a"
|
checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"glob",
|
"glob",
|
||||||
"proc-macro-crate",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -2219,23 +2210,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "0.6.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_edit"
|
|
||||||
version = "0.21.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"toml_datetime",
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
@ -2733,15 +2707,6 @@ version = "0.52.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winnow"
|
|
||||||
version = "0.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
|
@ -33,7 +33,7 @@ serde_json = "1.0.117"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
|
||||||
tokio-util = { version = "0.7.11", features = ["io"] }
|
tokio-util = { version = "0.7.11", features = ["io"] }
|
||||||
tower-http = { version = "0.5.2", features = ["trace", "set-header"] }
|
tower-http = { version = "0.5.2", features = ["trace"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
|
@ -44,7 +44,7 @@ yarte_helpers = "0.15.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.4.0"
|
proptest = "1.4.0"
|
||||||
rstest = { version = "0.20.0", default-features = false }
|
rstest = { version = "0.19.0", default-features = false }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "crates/*"]
|
members = [".", "crates/*"]
|
||||||
|
|
|
@ -62,7 +62,11 @@ use serde::de::{
|
||||||
value::{MapDeserializer, SeqDeserializer},
|
value::{MapDeserializer, SeqDeserializer},
|
||||||
IntoDeserializer,
|
IntoDeserializer,
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, env, iter::IntoIterator, str::MatchIndices};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
env,
|
||||||
|
iter::{empty, IntoIterator},
|
||||||
|
};
|
||||||
|
|
||||||
// Ours
|
// Ours
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -133,19 +137,19 @@ impl<'de> de::Deserializer<'de> for Val {
|
||||||
where
|
where
|
||||||
V: de::Visitor<'de>,
|
V: de::Visitor<'de>,
|
||||||
{
|
{
|
||||||
let values = SplitEscaped::new(&self.1, ";", true);
|
// std::str::split doesn't work as expected for our use case: when we
|
||||||
SeqDeserializer::new(values.map(|v| Val(self.0.clone(), v))).deserialize_seq(visitor)
|
// get an empty string we want to produce an empty Vec, but split would
|
||||||
|
// still yield an iterator with an empty string in it. So we need to
|
||||||
|
// special case empty strings.
|
||||||
|
if self.1.is_empty() {
|
||||||
|
SeqDeserializer::new(empty::<Val>()).deserialize_seq(visitor)
|
||||||
|
} else {
|
||||||
|
let values = self
|
||||||
|
.1
|
||||||
|
.split(',')
|
||||||
|
.map(|v| Val(self.0.clone(), v.trim().to_owned()));
|
||||||
|
SeqDeserializer::new(values).deserialize_seq(visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
MapDeserializer::new(SplitEscaped::new(&self.1, ";", false).filter_map(|pair| {
|
|
||||||
let mut parts = SplitEscaped::new(&pair, "=>", true);
|
|
||||||
parts.next().zip(parts.next())
|
|
||||||
}))
|
|
||||||
.deserialize_map(visitor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
||||||
|
@ -212,7 +216,7 @@ impl<'de> de::Deserializer<'de> for Val {
|
||||||
|
|
||||||
serde::forward_to_deserialize_any! {
|
serde::forward_to_deserialize_any! {
|
||||||
char str string unit
|
char str string unit
|
||||||
bytes byte_buf unit_struct tuple_struct
|
bytes byte_buf map unit_struct tuple_struct
|
||||||
identifier tuple ignored_any
|
identifier tuple ignored_any
|
||||||
struct
|
struct
|
||||||
}
|
}
|
||||||
|
@ -368,63 +372,6 @@ where
|
||||||
Prefixed(prefix.into())
|
Prefixed(prefix.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SplitEscaped<'a> {
|
|
||||||
input: &'a str,
|
|
||||||
pat: &'a str,
|
|
||||||
epat: String,
|
|
||||||
fin: bool,
|
|
||||||
separators: MatchIndices<'a, &'a str>,
|
|
||||||
start: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SplitEscaped<'a> {
|
|
||||||
pub fn new(input: &'a str, pat: &'a str, fin: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
separators: input.match_indices(pat),
|
|
||||||
input,
|
|
||||||
pat,
|
|
||||||
fin,
|
|
||||||
epat: format!("\\{pat}"),
|
|
||||||
start: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for SplitEscaped<'a> {
|
|
||||||
type Item = String;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let res = match self.separators.find(|(i, _)| {
|
|
||||||
let b1 = self.input.get(i - 1..*i);
|
|
||||||
let b2 = if *i > 1 {
|
|
||||||
self.input.get(i - 2..i - 1)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
b1 != Some(r"\") || b2 == Some(r"\")
|
|
||||||
}) {
|
|
||||||
Some((pos, _)) => Some(&self.input[self.start..pos]),
|
|
||||||
None => {
|
|
||||||
if self.start >= self.input.len() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&self.input[self.start..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(res) = res {
|
|
||||||
self.start += res.len() + self.pat.len();
|
|
||||||
let mut out = res.trim().replace(&self.epat, self.pat);
|
|
||||||
if self.fin {
|
|
||||||
out = out.replace(r"\\", r"\");
|
|
||||||
}
|
|
||||||
Some(out)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -469,7 +416,7 @@ mod tests {
|
||||||
let data = vec![
|
let data = vec![
|
||||||
(String::from("BAR"), String::from("test")),
|
(String::from("BAR"), String::from("test")),
|
||||||
(String::from("BAZ"), String::from("true")),
|
(String::from("BAZ"), String::from("true")),
|
||||||
(String::from("DOOM"), String::from("1; 2; 3 ")),
|
(String::from("DOOM"), String::from("1, 2, 3 ")),
|
||||||
// Empty string should result in empty vector.
|
// Empty string should result in empty vector.
|
||||||
(String::from("BOOM"), String::from("")),
|
(String::from("BOOM"), String::from("")),
|
||||||
(String::from("SIZE"), String::from("small")),
|
(String::from("SIZE"), String::from("small")),
|
||||||
|
@ -532,7 +479,7 @@ mod tests {
|
||||||
Ok(_) => panic!("expected failure"),
|
Ok(_) => panic!("expected failure"),
|
||||||
Err(e) => assert_eq!(
|
Err(e) => assert_eq!(
|
||||||
e,
|
e,
|
||||||
Error::Custom(String::from("error parsing boolean value: 'notabool'"))
|
Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ"))
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -543,7 +490,7 @@ mod tests {
|
||||||
(String::from("APP_BAR"), String::from("test")),
|
(String::from("APP_BAR"), String::from("test")),
|
||||||
(String::from("APP_BAZ"), String::from("true")),
|
(String::from("APP_BAZ"), String::from("true")),
|
||||||
(String::from("APP_DOOM"), String::from("")),
|
(String::from("APP_DOOM"), String::from("")),
|
||||||
(String::from("APP_BOOM"), String::from("4;5")),
|
(String::from("APP_BOOM"), String::from("4,5")),
|
||||||
(String::from("APP_SIZE"), String::from("small")),
|
(String::from("APP_SIZE"), String::from("small")),
|
||||||
(String::from("APP_PROVIDED"), String::from("test")),
|
(String::from("APP_PROVIDED"), String::from("test")),
|
||||||
(String::from("APP_NEWTYPE"), String::from("42")),
|
(String::from("APP_NEWTYPE"), String::from("42")),
|
||||||
|
@ -610,34 +557,4 @@ mod tests {
|
||||||
let res = from_iter::<_, X>(data).unwrap();
|
let res = from_iter::<_, X>(data).unwrap();
|
||||||
assert_eq!(res.val, None)
|
assert_eq!(res.val, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_map() {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct X {
|
|
||||||
val: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = vec![(
|
|
||||||
String::from("VAL"),
|
|
||||||
String::from("He\\\\llo\\=>W=>orld;this is => me"),
|
|
||||||
)];
|
|
||||||
let res = from_iter::<_, X>(data).unwrap();
|
|
||||||
assert_eq!(res.val.len(), 2);
|
|
||||||
assert_eq!(&res.val["He\\llo=>W"], "orld");
|
|
||||||
assert_eq!(&res.val["this is"], "me");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_escaped() {
|
|
||||||
let input = r"He\\llo=> Wor\=>ld ";
|
|
||||||
let res = SplitEscaped::new(input, "=>", true).collect::<Vec<_>>();
|
|
||||||
assert_eq!(res, [r"He\llo", "Wor=>ld"]);
|
|
||||||
|
|
||||||
let input = r"4;5";
|
|
||||||
let res = SplitEscaped::new(input, ";", true).collect::<Vec<_>>();
|
|
||||||
assert_eq!(res, ["4", "5"]);
|
|
||||||
|
|
||||||
assert!(SplitEscaped::new("", ";", true).next().is_none());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 268 B |
Binary file not shown.
95
src/app.rs
95
src/app.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::{net::SocketAddr, ops::Bound, path::PathBuf, str::FromStr, sync::Arc};
|
use std::{net::SocketAddr, ops::Bound, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use async_zip::tokio::read::ZipEntryReader;
|
use async_zip::tokio::read::ZipEntryReader;
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -7,7 +7,7 @@ use axum::{
|
||||||
http::{Response, Uri},
|
http::{Response, Uri},
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{any, get, post},
|
routing::{any, get, post},
|
||||||
Form, Router,
|
Form, Json, Router,
|
||||||
};
|
};
|
||||||
use headers::HeaderMapExt;
|
use headers::HeaderMapExt;
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::{HeaderMap, StatusCode};
|
||||||
|
@ -20,20 +20,17 @@ use tokio_util::{
|
||||||
compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt},
|
compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt},
|
||||||
io::ReaderStream,
|
io::ReaderStream,
|
||||||
};
|
};
|
||||||
use tower_http::{
|
use tower_http::trace::{DefaultOnResponse, TraceLayer};
|
||||||
set_header::SetResponseHeaderLayer,
|
|
||||||
trace::{DefaultOnResponse, TraceLayer},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
artifact_api::ArtifactApi,
|
artifact_api::{Artifact, ArtifactApi},
|
||||||
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile},
|
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile, IndexEntry},
|
||||||
config::Config,
|
config::Config,
|
||||||
error::Error,
|
error::{Error, Result},
|
||||||
gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN},
|
gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN},
|
||||||
query::Query,
|
query::Query,
|
||||||
templates::{self, ArtifactItem, LinkItem},
|
templates::{self, ArtifactItem, LinkItem},
|
||||||
util::{self, ErrorJson, ResponseBuilderExt},
|
util::{self, InsertTypedHeader},
|
||||||
App,
|
App,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,9 +56,6 @@ struct UrlForm {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAVICON_PATH: &str = "/favicon.ico";
|
|
||||||
const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico");
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self
|
Self
|
||||||
|
@ -71,7 +65,7 @@ impl App {
|
||||||
AppState::new()
|
AppState::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&self) -> Result<(), Error> {
|
pub async fn run(&self) -> Result<()> {
|
||||||
let address = "0.0.0.0:3000";
|
let address = "0.0.0.0:3000";
|
||||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||||
tracing::info!("Listening on http://{address}");
|
tracing::info!("Listening on http://{address}");
|
||||||
|
@ -104,8 +98,7 @@ impl App {
|
||||||
tracing::error_span!("request", url = util::full_url_from_request(request), ip)
|
tracing::error_span!("request", url = util::full_url_from_request(request), ip)
|
||||||
})
|
})
|
||||||
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
||||||
)
|
);
|
||||||
.layer(SetResponseHeaderLayer::appending(http::header::X_CONTENT_TYPE_OPTIONS, http::HeaderValue::from_static("nosniff")));
|
|
||||||
axum::serve(
|
axum::serve(
|
||||||
listener,
|
listener,
|
||||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
@ -119,23 +112,19 @@ impl App {
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<Body>> {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
|
|
||||||
if subdomain.is_empty() {
|
if subdomain.is_empty() {
|
||||||
// Main page
|
// Main page
|
||||||
if uri.path() == FAVICON_PATH {
|
|
||||||
return Self::favicon();
|
|
||||||
}
|
|
||||||
if uri.path() != "/" {
|
if uri.path() != "/" {
|
||||||
return Err(Error::NotFound("path".into()));
|
return Err(Error::NotFound("path".into()));
|
||||||
}
|
}
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.typed_header(headers::ContentType::html())
|
.typed_header(headers::ContentType::html())
|
||||||
.cache()
|
|
||||||
.body(templates::Index::default().to_string().into())?)
|
.body(templates::Index::default().to_string().into())?)
|
||||||
} else {
|
} else {
|
||||||
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
let query = Query::from_subdomain(subdomain)?;
|
||||||
state.i.cfg.check_filterlist(&query)?;
|
state.i.cfg.check_filterlist(&query)?;
|
||||||
let path = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy();
|
let path = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy();
|
||||||
let hdrs = request.headers();
|
let hdrs = request.headers();
|
||||||
|
@ -149,12 +138,12 @@ impl App {
|
||||||
state.garbage_collect();
|
state.garbage_collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
match entry.get_file(&path, uri.query().unwrap_or_default()) {
|
match entry.get_file(&path, uri.query().unwrap_or_default())? {
|
||||||
Ok(GetFileResult::File(res)) => {
|
GetFileResult::File(res) => {
|
||||||
Self::serve_artifact_file(state, entry, entry_res.zip_path, res, hdrs)
|
Self::serve_artifact_file(state, entry, entry_res.zip_path, res, hdrs)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Ok(GetFileResult::Listing(listing)) => {
|
GetFileResult::Listing(listing) => {
|
||||||
if !path.ends_with('/') {
|
if !path.ends_with('/') {
|
||||||
return Ok(Redirect::to(&format!("{path}/")).into_response());
|
return Ok(Redirect::to(&format!("{path}/")).into_response());
|
||||||
}
|
}
|
||||||
|
@ -165,7 +154,7 @@ impl App {
|
||||||
url: state
|
url: state
|
||||||
.i
|
.i
|
||||||
.cfg
|
.cfg
|
||||||
.url_with_subdomain(&query.subdomain_with_artifact(None)?),
|
.url_with_subdomain(&query.subdomain_with_artifact(None)),
|
||||||
},
|
},
|
||||||
LinkItem {
|
LinkItem {
|
||||||
name: entry.name.to_owned(),
|
name: entry.name.to_owned(),
|
||||||
|
@ -196,25 +185,13 @@ impl App {
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.typed_header(headers::ContentType::html())
|
.typed_header(headers::ContentType::html())
|
||||||
.cache_immutable()
|
|
||||||
.body(tmpl.to_string().into())?)
|
.body(tmpl.to_string().into())?)
|
||||||
}
|
}
|
||||||
Err(Error::NotFound(e)) => {
|
|
||||||
if path == FAVICON_PATH {
|
|
||||||
Self::favicon()
|
|
||||||
} else {
|
|
||||||
Err(Error::NotFound(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Query::Run(query) => {
|
Query::Run(query) => {
|
||||||
let artifacts = state.i.api.list(&query).await?;
|
let artifacts = state.i.api.list(&query).await?;
|
||||||
|
|
||||||
if uri.path() == FAVICON_PATH {
|
|
||||||
return Self::favicon();
|
|
||||||
}
|
|
||||||
if uri.path() != "/" {
|
if uri.path() != "/" {
|
||||||
return Err(Error::NotFound("path".into()));
|
return Err(Error::NotFound("path".into()));
|
||||||
}
|
}
|
||||||
|
@ -233,11 +210,10 @@ impl App {
|
||||||
artifacts: artifacts
|
artifacts: artifacts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| ArtifactItem::from_artifact(a, &query, &state.i.cfg))
|
.map(|a| ArtifactItem::from_artifact(a, &query, &state.i.cfg))
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
.collect(),
|
||||||
};
|
};
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.typed_header(headers::ContentType::html())
|
.typed_header(headers::ContentType::html())
|
||||||
.cache()
|
|
||||||
.body(tmpl.to_string().into())?)
|
.body(tmpl.to_string().into())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,12 +224,12 @@ impl App {
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
Form(url): Form<UrlForm>,
|
Form(url): Form<UrlForm>,
|
||||||
) -> Result<Redirect, Error> {
|
) -> Result<Redirect> {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
|
|
||||||
if subdomain.is_empty() {
|
if subdomain.is_empty() {
|
||||||
let query = Query::from_forge_url(&url.url, &state.i.cfg.load().site_aliases)?;
|
let query = Query::from_forge_url(&url.url)?;
|
||||||
let subdomain = query.subdomain()?;
|
let subdomain = query.subdomain();
|
||||||
let target = format!(
|
let target = format!(
|
||||||
"{}{}.{}",
|
"{}{}.{}",
|
||||||
state.i.cfg.url_proto(),
|
state.i.cfg.url_proto(),
|
||||||
|
@ -272,7 +248,7 @@ impl App {
|
||||||
zip_path: PathBuf,
|
zip_path: PathBuf,
|
||||||
res: GetFileResultFile,
|
res: GetFileResultFile,
|
||||||
hdrs: &HeaderMap,
|
hdrs: &HeaderMap,
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<Body>> {
|
||||||
let file = res.file;
|
let file = res.file;
|
||||||
|
|
||||||
// Dont serve files above the configured size limit
|
// Dont serve files above the configured size limit
|
||||||
|
@ -291,8 +267,7 @@ impl App {
|
||||||
let mut resp = Response::builder()
|
let mut resp = Response::builder()
|
||||||
.status(res.status)
|
.status(res.status)
|
||||||
.typed_header(headers::AcceptRanges::bytes())
|
.typed_header(headers::AcceptRanges::bytes())
|
||||||
.typed_header(headers::LastModified::from(entry.last_modified))
|
.typed_header(headers::LastModified::from(entry.last_modified));
|
||||||
.cache_immutable();
|
|
||||||
if let Some(mime) = res.mime {
|
if let Some(mime) = res.mime {
|
||||||
resp = resp.typed_header(headers::ContentType::from(mime));
|
resp = resp.typed_header(headers::ContentType::from(mime));
|
||||||
}
|
}
|
||||||
|
@ -391,24 +366,24 @@ impl App {
|
||||||
async fn get_artifacts(
|
async fn get_artifacts(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
) -> Result<Response<Body>, ErrorJson> {
|
) -> Result<Json<Vec<Artifact>>> {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
let query = Query::from_subdomain(subdomain)?;
|
||||||
state.i.cfg.check_filterlist(&query)?;
|
state.i.cfg.check_filterlist(&query)?;
|
||||||
let artifacts = state.i.api.list(&query.into_runquery()).await?;
|
let artifacts = state.i.api.list(&query.into_runquery()).await?;
|
||||||
Ok(Response::builder().cache().json(&artifacts)?)
|
Ok(Json(artifacts))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API endpoint to get the metadata of the current artifact
|
/// API endpoint to get the metadata of the current artifact
|
||||||
async fn get_artifact(
|
async fn get_artifact(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
) -> Result<Response<Body>, ErrorJson> {
|
) -> Result<Json<Artifact>> {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
let query = Query::from_subdomain(subdomain)?;
|
||||||
state.i.cfg.check_filterlist(&query)?;
|
state.i.cfg.check_filterlist(&query)?;
|
||||||
let artifact = state.i.api.fetch(&query.try_into_artifactquery()?).await?;
|
let artifact = state.i.api.fetch(&query.try_into_artifactquery()?).await?;
|
||||||
Ok(Response::builder().cache().json(&artifact)?)
|
Ok(Json(artifact))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API endpoint to get a file listing
|
/// API endpoint to get a file listing
|
||||||
|
@ -416,10 +391,10 @@ impl App {
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Result<Response<Body>, ErrorJson> {
|
) -> Result<Json<Vec<IndexEntry>>> {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
let ip = util::get_ip_address(&request, state.i.cfg.load().real_ip_header.as_deref())?;
|
let ip = util::get_ip_address(&request, state.i.cfg.load().real_ip_header.as_deref())?;
|
||||||
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
let query = Query::from_subdomain(subdomain)?;
|
||||||
state.i.cfg.check_filterlist(&query)?;
|
state.i.cfg.check_filterlist(&query)?;
|
||||||
let entry_res = state
|
let entry_res = state
|
||||||
.i
|
.i
|
||||||
|
@ -430,17 +405,7 @@ impl App {
|
||||||
state.garbage_collect();
|
state.garbage_collect();
|
||||||
}
|
}
|
||||||
let files = entry_res.entry.get_files();
|
let files = entry_res.entry.get_files();
|
||||||
Ok(Response::builder()
|
Ok(Json(files))
|
||||||
.typed_header(headers::LastModified::from(entry_res.entry.last_modified))
|
|
||||||
.cache_immutable()
|
|
||||||
.json(&files)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn favicon() -> Result<Response<Body>, Error> {
|
|
||||||
Ok(Response::builder()
|
|
||||||
.typed_header(headers::ContentType::from_str("image/x-icon").unwrap())
|
|
||||||
.cache_immutable()
|
|
||||||
.body(FAVICON_BYTES.as_slice().into())?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ impl ArtifactApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list<T>(&self, query: &QueryData<T>) -> Result<Vec<Artifact>> {
|
pub async fn list<T>(&self, query: &QueryData<T>) -> Result<Vec<Artifact>> {
|
||||||
let subdomain = query.subdomain_with_artifact(None)?;
|
let subdomain = query.subdomain_with_artifact(None);
|
||||||
self.qc
|
self.qc
|
||||||
.get_or_insert_async(&subdomain, async {
|
.get_or_insert_async(&subdomain, async {
|
||||||
if query.is_github() {
|
if query.is_github() {
|
||||||
|
@ -277,7 +277,6 @@ mod tests {
|
||||||
async fn fetch_forgejo() {
|
async fn fetch_forgejo() {
|
||||||
let query = ArtifactQuery {
|
let query = ArtifactQuery {
|
||||||
host: "code.thetadev.de".to_owned(),
|
host: "code.thetadev.de".to_owned(),
|
||||||
host_alias: None,
|
|
||||||
user: "HSA".to_owned(),
|
user: "HSA".to_owned(),
|
||||||
repo: "Visitenbuch".to_owned(),
|
repo: "Visitenbuch".to_owned(),
|
||||||
run: 32,
|
run: 32,
|
||||||
|
@ -294,7 +293,6 @@ mod tests {
|
||||||
async fn fetch_github() {
|
async fn fetch_github() {
|
||||||
let query = ArtifactQuery {
|
let query = ArtifactQuery {
|
||||||
host: "github.com".to_owned(),
|
host: "github.com".to_owned(),
|
||||||
host_alias: None,
|
|
||||||
user: "actions".to_owned(),
|
user: "actions".to_owned(),
|
||||||
repo: "upload-artifact".to_owned(),
|
repo: "upload-artifact".to_owned(),
|
||||||
run: 8805345396,
|
run: 8805345396,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
fs::FileTimes,
|
fs::FileTimes,
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
num::NonZeroUsize,
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
|
@ -103,7 +103,7 @@ impl Cache {
|
||||||
.load()
|
.load()
|
||||||
.limit_artifacts_per_min
|
.limit_artifacts_per_min
|
||||||
.map(|lim| RateLimiter::keyed(Quota::per_minute(lim))),
|
.map(|lim| RateLimiter::keyed(Quota::per_minute(lim))),
|
||||||
lim_gc: RateLimiter::direct(Quota::with_period(Duration::from_secs(1800)).unwrap()),
|
lim_gc: RateLimiter::direct(Quota::per_hour(NonZeroU32::MIN)),
|
||||||
cfg,
|
cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ impl Cache {
|
||||||
query: &ArtifactQuery,
|
query: &ArtifactQuery,
|
||||||
ip: &IpAddr,
|
ip: &IpAddr,
|
||||||
) -> Result<GetEntryResult> {
|
) -> Result<GetEntryResult> {
|
||||||
let subdomain = query.subdomain_noalias();
|
let subdomain = query.subdomain();
|
||||||
let zip_path = path!(self.cfg.load().cache_dir / format!("{subdomain}.zip"));
|
let zip_path = path!(self.cfg.load().cache_dir / format!("{subdomain}.zip"));
|
||||||
let downloaded = !zip_path.is_file();
|
let downloaded = !zip_path.is_file();
|
||||||
if downloaded {
|
if downloaded {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
num::{NonZeroU32, NonZeroUsize},
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -59,12 +58,8 @@ pub struct ConfigData {
|
||||||
pub real_ip_header: Option<String>,
|
pub real_ip_header: Option<String>,
|
||||||
/// Limit the amount of downloaded artifacts per IP address and minute
|
/// Limit the amount of downloaded artifacts per IP address and minute
|
||||||
pub limit_artifacts_per_min: Option<NonZeroU32>,
|
pub limit_artifacts_per_min: Option<NonZeroU32>,
|
||||||
/// List of sites/users/repos that can NOT be accessed
|
|
||||||
pub repo_blacklist: QueryFilterList,
|
pub repo_blacklist: QueryFilterList,
|
||||||
/// List of sites/users/repos that can ONLY be accessed
|
|
||||||
pub repo_whitelist: QueryFilterList,
|
pub repo_whitelist: QueryFilterList,
|
||||||
/// Aliases for sites (Example: `gh => github.com`)
|
|
||||||
pub site_aliases: HashMap<String, String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConfigData {
|
impl Default for ConfigData {
|
||||||
|
@ -84,7 +79,6 @@ impl Default for ConfigData {
|
||||||
limit_artifacts_per_min: Some(NonZeroU32::new(5).unwrap()),
|
limit_artifacts_per_min: Some(NonZeroU32::new(5).unwrap()),
|
||||||
repo_blacklist: QueryFilterList::default(),
|
repo_blacklist: QueryFilterList::default(),
|
||||||
repo_whitelist: QueryFilterList::default(),
|
repo_whitelist: QueryFilterList::default(),
|
||||||
site_aliases: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
|
|
||||||
use crate::{templates, util::ResponseBuilderExt};
|
use crate::{templates, util::InsertTypedHeader};
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod artifact_api;
|
mod artifact_api;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
|
67
src/query.rs
67
src/query.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, fmt::Write, str::FromStr};
|
use std::{fmt::Write, hash::Hash, str::FromStr};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
|
@ -18,12 +18,10 @@ pub enum Query {
|
||||||
pub type RunQuery = QueryData<()>;
|
pub type RunQuery = QueryData<()>;
|
||||||
pub type ArtifactQuery = QueryData<u64>;
|
pub type ArtifactQuery = QueryData<u64>;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct QueryData<T> {
|
pub struct QueryData<T> {
|
||||||
/// Forge host
|
/// Forge host
|
||||||
pub host: String,
|
pub host: String,
|
||||||
/// Host alias if the query was constructed using one
|
|
||||||
pub host_alias: Option<String>,
|
|
||||||
/// User/org name (case-insensitive)
|
/// User/org name (case-insensitive)
|
||||||
pub user: String,
|
pub user: String,
|
||||||
/// Repository name (case-insensitive)
|
/// Repository name (case-insensitive)
|
||||||
|
@ -37,7 +35,7 @@ pub struct QueryData<T> {
|
||||||
static RE_REPO_NAME: Lazy<Regex> = Lazy::new(|| Regex::new("^[a-z0-9\\-_\\.]+$").unwrap());
|
static RE_REPO_NAME: Lazy<Regex> = Lazy::new(|| Regex::new("^[a-z0-9\\-_\\.]+$").unwrap());
|
||||||
|
|
||||||
impl Query {
|
impl Query {
|
||||||
pub fn from_subdomain(subdomain: &str, aliases: &HashMap<String, String>) -> Result<Self> {
|
pub fn from_subdomain(subdomain: &str) -> Result<Self> {
|
||||||
let segments = subdomain.split("--").collect::<Vec<_>>();
|
let segments = subdomain.split("--").collect::<Vec<_>>();
|
||||||
if segments.len() != 4 {
|
if segments.len() != 4 {
|
||||||
return Err(Error::InvalidUrl);
|
return Err(Error::InvalidUrl);
|
||||||
|
@ -48,22 +46,14 @@ impl Query {
|
||||||
return Err(Error::InvalidUrl);
|
return Err(Error::InvalidUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut host = decode_domain(segments[0], '.');
|
let host = decode_domain(segments[0], '.');
|
||||||
let mut host_alias = None;
|
|
||||||
let user = decode_domain(segments[1], '-');
|
let user = decode_domain(segments[1], '-');
|
||||||
let repo = decode_domain(segments[2], '-');
|
let repo = decode_domain(segments[2], '-');
|
||||||
let run = run_and_artifact[0].parse().ok().ok_or(Error::InvalidUrl)?;
|
let run = run_and_artifact[0].parse().ok().ok_or(Error::InvalidUrl)?;
|
||||||
|
|
||||||
#[allow(clippy::assigning_clones)]
|
|
||||||
if let Some(alias) = aliases.get(&host) {
|
|
||||||
host_alias = Some(host);
|
|
||||||
host = alias.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(match run_and_artifact.get(1) {
|
Ok(match run_and_artifact.get(1) {
|
||||||
Some(x) => Self::Artifact(QueryData {
|
Some(x) => Self::Artifact(QueryData {
|
||||||
host,
|
host,
|
||||||
host_alias,
|
|
||||||
user,
|
user,
|
||||||
repo,
|
repo,
|
||||||
run,
|
run,
|
||||||
|
@ -71,7 +61,6 @@ impl Query {
|
||||||
}),
|
}),
|
||||||
None => Self::Run(QueryData {
|
None => Self::Run(QueryData {
|
||||||
host,
|
host,
|
||||||
host_alias,
|
|
||||||
user,
|
user,
|
||||||
repo,
|
repo,
|
||||||
run,
|
run,
|
||||||
|
@ -80,7 +69,7 @@ impl Query {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_forge_url(url: &str, aliases: &HashMap<String, String>) -> Result<Self> {
|
pub fn from_forge_url(url: &str) -> Result<Self> {
|
||||||
let (host, mut path_segs) = util::parse_url(url)?;
|
let (host, mut path_segs) = util::parse_url(url)?;
|
||||||
|
|
||||||
let user = path_segs
|
let user = path_segs
|
||||||
|
@ -104,20 +93,13 @@ impl Query {
|
||||||
return Err(Error::BadRequest("invalid repository name".into()));
|
return Err(Error::BadRequest("invalid repository name".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let host = aliases
|
|
||||||
.iter()
|
|
||||||
.find(|(_, v)| *v == host)
|
|
||||||
.map(|(k, _)| k.to_owned())
|
|
||||||
.unwrap_or_else(|| host.to_owned());
|
|
||||||
|
|
||||||
let run = path_segs
|
let run = path_segs
|
||||||
.next()
|
.next()
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
.ok_or(Error::BadRequest("no run ID".into()))?;
|
.ok_or(Error::BadRequest("no run ID".into()))?;
|
||||||
|
|
||||||
Ok(Self::Run(RunQuery {
|
Ok(Self::Run(RunQuery {
|
||||||
host,
|
host: host.to_owned(),
|
||||||
host_alias: None,
|
|
||||||
user,
|
user,
|
||||||
repo,
|
repo,
|
||||||
run,
|
run,
|
||||||
|
@ -125,7 +107,7 @@ impl Query {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subdomain(&self) -> Result<String> {
|
pub fn subdomain(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Query::Artifact(q) => q.subdomain(),
|
Query::Artifact(q) => q.subdomain(),
|
||||||
Query::Run(q) => q.subdomain(),
|
Query::Run(q) => q.subdomain(),
|
||||||
|
@ -148,33 +130,22 @@ impl Query {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtifactQuery {
|
impl ArtifactQuery {
|
||||||
pub fn subdomain(&self) -> Result<String> {
|
pub fn subdomain(&self) -> String {
|
||||||
self.subdomain_with_artifact(Some(self.artifact))
|
self.subdomain_with_artifact(Some(self.artifact))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-shortened subdomain (used for cache storage)
|
|
||||||
pub fn subdomain_noalias(&self) -> String {
|
|
||||||
self._subdomain(Some(self.artifact), false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunQuery {
|
impl RunQuery {
|
||||||
pub fn subdomain(&self) -> Result<String> {
|
pub fn subdomain(&self) -> String {
|
||||||
self.subdomain_with_artifact(None)
|
self.subdomain_with_artifact(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> QueryData<T> {
|
impl<T> QueryData<T> {
|
||||||
pub fn _subdomain(&self, artifact: Option<u64>, use_alias: bool) -> String {
|
pub fn subdomain_with_artifact(&self, artifact: Option<u64>) -> String {
|
||||||
let host = if use_alias {
|
|
||||||
self.host_alias.as_deref().unwrap_or(&self.host)
|
|
||||||
} else {
|
|
||||||
&self.host
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut res = format!(
|
let mut res = format!(
|
||||||
"{}--{}--{}--{}",
|
"{}--{}--{}--{}",
|
||||||
encode_domain(host, '.'),
|
encode_domain(&self.host, '.'),
|
||||||
encode_domain(&self.user, '-'),
|
encode_domain(&self.user, '-'),
|
||||||
encode_domain(&self.repo, '-'),
|
encode_domain(&self.repo, '-'),
|
||||||
self.run,
|
self.run,
|
||||||
|
@ -185,14 +156,6 @@ impl<T> QueryData<T> {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subdomain_with_artifact(&self, artifact: Option<u64>) -> Result<String> {
|
|
||||||
let res = self._subdomain(artifact, true);
|
|
||||||
if res.len() > 63 {
|
|
||||||
return Err(Error::BadRequest("subdomain too long".into()));
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shortid(&self) -> String {
|
pub fn shortid(&self) -> String {
|
||||||
format!("{}/{}#{}", self.user, self.repo, self.run)
|
format!("{}/{}#{}", self.user, self.repo, self.run)
|
||||||
}
|
}
|
||||||
|
@ -211,7 +174,6 @@ impl<T> QueryData<T> {
|
||||||
pub fn into_runquery(self) -> RunQuery {
|
pub fn into_runquery(self) -> RunQuery {
|
||||||
RunQuery {
|
RunQuery {
|
||||||
host: self.host,
|
host: self.host,
|
||||||
host_alias: self.host_alias,
|
|
||||||
user: self.user,
|
user: self.user,
|
||||||
repo: self.repo,
|
repo: self.repo,
|
||||||
run: self.run,
|
run: self.run,
|
||||||
|
@ -386,7 +348,7 @@ impl<'de> Deserialize<'de> for QueryFilterList {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::query::{QueryFilter, QueryFilterList};
|
use crate::query::{QueryFilter, QueryFilterList};
|
||||||
|
|
||||||
|
@ -426,19 +388,18 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn query_from_subdomain() {
|
fn query_from_subdomain() {
|
||||||
let d1 = "github-com--thetadev--newpipe-extractor--14-123";
|
let d1 = "github-com--thetadev--newpipe-extractor--14-123";
|
||||||
let query = Query::from_subdomain(d1, &HashMap::new()).unwrap();
|
let query = Query::from_subdomain(d1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query,
|
query,
|
||||||
Query::Artifact(ArtifactQuery {
|
Query::Artifact(ArtifactQuery {
|
||||||
host: "github.com".to_owned(),
|
host: "github.com".to_owned(),
|
||||||
host_alias: None,
|
|
||||||
user: "thetadev".to_owned(),
|
user: "thetadev".to_owned(),
|
||||||
repo: "newpipe-extractor".to_owned(),
|
repo: "newpipe-extractor".to_owned(),
|
||||||
run: 14,
|
run: 14,
|
||||||
artifact: 123
|
artifact: 123
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(query.subdomain().unwrap(), d1);
|
assert_eq!(query.subdomain(), d1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
|
@ -2,7 +2,6 @@ use crate::{
|
||||||
artifact_api::Artifact,
|
artifact_api::Artifact,
|
||||||
cache::{ListingEntry, Size},
|
cache::{ListingEntry, Size},
|
||||||
config::Config,
|
config::Config,
|
||||||
error::Result,
|
|
||||||
query::QueryData,
|
query::QueryData,
|
||||||
};
|
};
|
||||||
use yarte::{Render, Template};
|
use yarte::{Render, Template};
|
||||||
|
@ -62,18 +61,14 @@ pub struct ArtifactItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtifactItem {
|
impl ArtifactItem {
|
||||||
pub fn from_artifact<T>(
|
pub fn from_artifact<T>(artifact: Artifact, query: &QueryData<T>, cfg: &Config) -> Self {
|
||||||
artifact: Artifact,
|
Self {
|
||||||
query: &QueryData<T>,
|
|
||||||
cfg: &Config,
|
|
||||||
) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
name: artifact.name,
|
name: artifact.name,
|
||||||
url: cfg.url_with_subdomain(&query.subdomain_with_artifact(Some(artifact.id))?),
|
url: cfg.url_with_subdomain(&query.subdomain_with_artifact(Some(artifact.id))),
|
||||||
size: Size(artifact.size as u32),
|
size: Size(artifact.size as u32),
|
||||||
expired: artifact.expired,
|
expired: artifact.expired,
|
||||||
download_url: artifact.user_download_url.unwrap_or(artifact.download_url),
|
download_url: artifact.user_download_url.unwrap_or(artifact.download_url),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
src/util.rs
71
src/util.rs
|
@ -8,64 +8,26 @@ use async_zip::error::ZipError;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{ConnectInfo, Request},
|
extract::{ConnectInfo, Request},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
};
|
||||||
use headers::{Header, HeaderMapExt};
|
use headers::{Header, HeaderMapExt};
|
||||||
use http::{header, StatusCode};
|
use http::header;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
|
||||||
use tokio_util::bytes::{BufMut, BytesMut};
|
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
/// HTTP response builder extensions
|
pub trait InsertTypedHeader {
|
||||||
pub trait ResponseBuilderExt {
|
|
||||||
/// Inserts a typed header to this response.
|
/// Inserts a typed header to this response.
|
||||||
fn typed_header<T: Header>(self, header: T) -> Self;
|
fn typed_header<T: Header>(self, header: T) -> Self;
|
||||||
fn cache(self) -> Self;
|
|
||||||
fn cache_immutable(self) -> Self;
|
|
||||||
/// Consumes this builder, using the provided json-serializable `val` to return a constructed [`Response`]
|
|
||||||
fn json<T: Serialize>(self, val: &T) -> core::result::Result<Response, http::Error>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseBuilderExt for axum::http::response::Builder {
|
impl InsertTypedHeader for axum::http::response::Builder {
|
||||||
fn typed_header<T: Header>(mut self, header: T) -> Self {
|
fn typed_header<T: Header>(mut self, header: T) -> Self {
|
||||||
if let Some(headers) = self.headers_mut() {
|
if let Some(headers) = self.headers_mut() {
|
||||||
headers.typed_insert(header);
|
headers.typed_insert(header);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cache(self) -> Self {
|
|
||||||
self.header(
|
|
||||||
http::header::CACHE_CONTROL,
|
|
||||||
http::HeaderValue::from_static("max-age=1800,public"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cache_immutable(self) -> Self {
|
|
||||||
self.header(
|
|
||||||
http::header::CACHE_CONTROL,
|
|
||||||
http::HeaderValue::from_static("max-age=31536000,public,immutable"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json<T: Serialize>(self, val: &T) -> core::result::Result<Response, http::Error> {
|
|
||||||
// copied from axum::json::into_response
|
|
||||||
// Use a small initial capacity of 128 bytes like serde_json::to_vec
|
|
||||||
// https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
|
|
||||||
let mut buf = BytesMut::with_capacity(128).writer();
|
|
||||||
match serde_json::to_writer(&mut buf, val) {
|
|
||||||
Ok(()) => self
|
|
||||||
.typed_header(headers::ContentType::json())
|
|
||||||
.body(buf.into_inner().freeze().into()),
|
|
||||||
Err(err) => self
|
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
.typed_header(headers::ContentType::text())
|
|
||||||
.body(err.to_string().into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accepts_gzip(headers: &HeaderMap) -> bool {
|
pub fn accepts_gzip(headers: &HeaderMap) -> bool {
|
||||||
|
@ -241,33 +203,6 @@ pub fn parse_url(input: &str) -> Result<(&str, std::str::Split<char>)> {
|
||||||
Ok((host, parts))
|
Ok((host, parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct ErrorJson {
|
|
||||||
status: u16,
|
|
||||||
msg: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Error> for ErrorJson {
|
|
||||||
fn from(value: Error) -> Self {
|
|
||||||
Self {
|
|
||||||
status: value.status().as_u16(),
|
|
||||||
msg: value.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<http::Error> for ErrorJson {
|
|
||||||
fn from(value: http::Error) -> Self {
|
|
||||||
Self::from(Error::from(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for ErrorJson {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
Response::builder().json(&self).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use http::{header, HeaderMap};
|
use http::{header, HeaderMap};
|
||||||
|
|
|
@ -158,10 +158,15 @@
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
// @license
|
||||||
|
magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT var
|
||||||
var filterEl=document.getElementById("filter");function initFilter(){if(!filterEl.value){var filterParam=new URL(window.location.href).searchParams.get("filter");if(filterParam){filterEl.value=filterParam}}filter()}function filter(){var q=filterEl.value.trim().toLowerCase();var elems=document.querySelectorAll("tr.file");elems.forEach(function(el){if(!q){el.style.display="";return}var nameEl=el.querySelector("td");var nameVal=nameEl.textContent.trim().toLowerCase();if(nameVal.indexOf(q)!==-1){el.style.display=""}else{el.style.display="none"}})}
|
filterEl=document.getElementById("filter");function
|
||||||
|
initFilter(){if(!filterEl.value){var filterParam=new
|
||||||
|
URL(window.location.href).searchParams.get("filter");if(filterParam){filterEl.value=filterParam}}filter()}function
|
||||||
|
filter(){var q=filterEl.value.trim().toLowerCase();var
|
||||||
|
elems=document.querySelectorAll("tr.file");elems.forEach(function(el){if(!q){el.style.display="";return}var
|
||||||
|
nameEl=el.querySelector("td");var
|
||||||
|
nameVal=nameEl.textContent.trim().toLowerCase();if(nameVal.indexOf(q)!==-1){el.style.display=""}else{el.style.display="none"}})}
|
||||||
// @license-end
|
// @license-end
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -170,10 +170,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
// @license
|
||||||
|
magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT var
|
||||||
var filterEl=document.getElementById("filter");function initFilter(){if(!filterEl.value){var filterParam=new URL(window.location.href).searchParams.get("filter");if(filterParam){filterEl.value=filterParam}}filter()}function filter(){var q=filterEl.value.trim().toLowerCase();var elems=document.querySelectorAll("tr.file");elems.forEach(function(el){if(!q){el.style.display="";return}var nameEl=el.querySelector("td");var nameVal=nameEl.textContent.trim().toLowerCase();if(nameVal.indexOf(q)!==-1){el.style.display=""}else{el.style.display="none"}})}
|
filterEl=document.getElementById("filter");function
|
||||||
|
initFilter(){if(!filterEl.value){var filterParam=new
|
||||||
|
URL(window.location.href).searchParams.get("filter");if(filterParam){filterEl.value=filterParam}}filter()}function
|
||||||
|
filter(){var q=filterEl.value.trim().toLowerCase();var
|
||||||
|
elems=document.querySelectorAll("tr.file");elems.forEach(function(el){if(!q){el.style.display="";return}var
|
||||||
|
nameEl=el.querySelector("td");var
|
||||||
|
nameVal=nameEl.textContent.trim().toLowerCase();if(nameVal.indexOf(q)!==-1){el.style.display=""}else{el.style.display="none"}})}
|
||||||
// @license-end
|
// @license-end
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue