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]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
@ -184,9 +184,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.11"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
|
||||
checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498"
|
||||
dependencies = [
|
||||
"bzip2",
|
||||
"deflate64",
|
||||
|
@ -319,9 +319,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.72"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
|
@ -828,9 +828,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
|
@ -1028,9 +1028,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
|
||||
checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -1241,10 +1241,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
|
@ -1312,9 +1313,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.35.0"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -1505,15 +1506,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.84"
|
||||
|
@ -1723,9 +1715,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.20.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27059f51958c5f8496a6f79511e7c0ac396dd815dc8894e9b6e2efb5779cf6f0"
|
||||
checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
|
||||
dependencies = [
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
|
@ -1733,13 +1725,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.20.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6132d64df104c0b3ea7a6ad7766a43f587bd773a4a9cf4cd59296d426afaf3a"
|
||||
checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
|
@ -2219,23 +2210,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
|
@ -2733,15 +2707,6 @@ version = "0.52.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
|
|
|
@ -33,7 +33,7 @@ serde_json = "1.0.117"
|
|||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
|
||||
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-subscriber = "0.3.18"
|
||||
url = "2.5.0"
|
||||
|
@ -44,7 +44,7 @@ yarte_helpers = "0.15.8"
|
|||
|
||||
[dev-dependencies]
|
||||
proptest = "1.4.0"
|
||||
rstest = { version = "0.20.0", default-features = false }
|
||||
rstest = { version = "0.19.0", default-features = false }
|
||||
|
||||
[workspace]
|
||||
members = [".", "crates/*"]
|
||||
|
|
|
@ -62,7 +62,11 @@ use serde::de::{
|
|||
value::{MapDeserializer, SeqDeserializer},
|
||||
IntoDeserializer,
|
||||
};
|
||||
use std::{borrow::Cow, env, iter::IntoIterator, str::MatchIndices};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env,
|
||||
iter::{empty, IntoIterator},
|
||||
};
|
||||
|
||||
// Ours
|
||||
mod error;
|
||||
|
@ -133,19 +137,19 @@ impl<'de> de::Deserializer<'de> for Val {
|
|||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
let values = SplitEscaped::new(&self.1, ";", true);
|
||||
SeqDeserializer::new(values.map(|v| Val(self.0.clone(), v))).deserialize_seq(visitor)
|
||||
// std::str::split doesn't work as expected for our use case: when we
|
||||
// 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>
|
||||
|
@ -212,7 +216,7 @@ impl<'de> de::Deserializer<'de> for Val {
|
|||
|
||||
serde::forward_to_deserialize_any! {
|
||||
char str string unit
|
||||
bytes byte_buf unit_struct tuple_struct
|
||||
bytes byte_buf map unit_struct tuple_struct
|
||||
identifier tuple ignored_any
|
||||
struct
|
||||
}
|
||||
|
@ -368,63 +372,6 @@ where
|
|||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -469,7 +416,7 @@ mod tests {
|
|||
let data = vec![
|
||||
(String::from("BAR"), String::from("test")),
|
||||
(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.
|
||||
(String::from("BOOM"), String::from("")),
|
||||
(String::from("SIZE"), String::from("small")),
|
||||
|
@ -532,7 +479,7 @@ mod tests {
|
|||
Ok(_) => panic!("expected failure"),
|
||||
Err(e) => assert_eq!(
|
||||
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_BAZ"), String::from("true")),
|
||||
(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_PROVIDED"), String::from("test")),
|
||||
(String::from("APP_NEWTYPE"), String::from("42")),
|
||||
|
@ -610,34 +557,4 @@ mod tests {
|
|||
let res = from_iter::<_, X>(data).unwrap();
|
||||
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 axum::{
|
||||
|
@ -7,7 +7,7 @@ use axum::{
|
|||
http::{Response, Uri},
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{any, get, post},
|
||||
Form, Router,
|
||||
Form, Json, Router,
|
||||
};
|
||||
use headers::HeaderMapExt;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
|
@ -20,20 +20,17 @@ use tokio_util::{
|
|||
compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt},
|
||||
io::ReaderStream,
|
||||
};
|
||||
use tower_http::{
|
||||
set_header::SetResponseHeaderLayer,
|
||||
trace::{DefaultOnResponse, TraceLayer},
|
||||
};
|
||||
use tower_http::trace::{DefaultOnResponse, TraceLayer};
|
||||
|
||||
use crate::{
|
||||
artifact_api::ArtifactApi,
|
||||
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile},
|
||||
artifact_api::{Artifact, ArtifactApi},
|
||||
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile, IndexEntry},
|
||||
config::Config,
|
||||
error::Error,
|
||||
error::{Error, Result},
|
||||
gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN},
|
||||
query::Query,
|
||||
templates::{self, ArtifactItem, LinkItem},
|
||||
util::{self, ErrorJson, ResponseBuilderExt},
|
||||
util::{self, InsertTypedHeader},
|
||||
App,
|
||||
};
|
||||
|
||||
|
@ -59,9 +56,6 @@ struct UrlForm {
|
|||
url: String,
|
||||
}
|
||||
|
||||
const FAVICON_PATH: &str = "/favicon.ico";
|
||||
const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico");
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
|
@ -71,7 +65,7 @@ impl App {
|
|||
AppState::new()
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Error> {
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let address = "0.0.0.0:3000";
|
||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||
tracing::info!("Listening on http://{address}");
|
||||
|
@ -104,8 +98,7 @@ impl App {
|
|||
tracing::error_span!("request", url = util::full_url_from_request(request), ip)
|
||||
})
|
||||
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
||||
)
|
||||
.layer(SetResponseHeaderLayer::appending(http::header::X_CONTENT_TYPE_OPTIONS, http::HeaderValue::from_static("nosniff")));
|
||||
);
|
||||
axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
|
@ -119,23 +112,19 @@ impl App {
|
|||
Host(host): Host,
|
||||
uri: Uri,
|
||||
request: Request,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
) -> Result<Response<Body>> {
|
||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||
|
||||
if subdomain.is_empty() {
|
||||
// Main page
|
||||
if uri.path() == FAVICON_PATH {
|
||||
return Self::favicon();
|
||||
}
|
||||
if uri.path() != "/" {
|
||||
return Err(Error::NotFound("path".into()));
|
||||
}
|
||||
Ok(Response::builder()
|
||||
.typed_header(headers::ContentType::html())
|
||||
.cache()
|
||||
.body(templates::Index::default().to_string().into())?)
|
||||
} 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)?;
|
||||
let path = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy();
|
||||
let hdrs = request.headers();
|
||||
|
@ -149,12 +138,12 @@ impl App {
|
|||
state.garbage_collect();
|
||||
}
|
||||
|
||||
match entry.get_file(&path, uri.query().unwrap_or_default()) {
|
||||
Ok(GetFileResult::File(res)) => {
|
||||
match entry.get_file(&path, uri.query().unwrap_or_default())? {
|
||||
GetFileResult::File(res) => {
|
||||
Self::serve_artifact_file(state, entry, entry_res.zip_path, res, hdrs)
|
||||
.await
|
||||
}
|
||||
Ok(GetFileResult::Listing(listing)) => {
|
||||
GetFileResult::Listing(listing) => {
|
||||
if !path.ends_with('/') {
|
||||
return Ok(Redirect::to(&format!("{path}/")).into_response());
|
||||
}
|
||||
|
@ -165,7 +154,7 @@ impl App {
|
|||
url: state
|
||||
.i
|
||||
.cfg
|
||||
.url_with_subdomain(&query.subdomain_with_artifact(None)?),
|
||||
.url_with_subdomain(&query.subdomain_with_artifact(None)),
|
||||
},
|
||||
LinkItem {
|
||||
name: entry.name.to_owned(),
|
||||
|
@ -196,25 +185,13 @@ impl App {
|
|||
|
||||
Ok(Response::builder()
|
||||
.typed_header(headers::ContentType::html())
|
||||
.cache_immutable()
|
||||
.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) => {
|
||||
let artifacts = state.i.api.list(&query).await?;
|
||||
|
||||
if uri.path() == FAVICON_PATH {
|
||||
return Self::favicon();
|
||||
}
|
||||
if uri.path() != "/" {
|
||||
return Err(Error::NotFound("path".into()));
|
||||
}
|
||||
|
@ -233,11 +210,10 @@ impl App {
|
|||
artifacts: artifacts
|
||||
.into_iter()
|
||||
.map(|a| ArtifactItem::from_artifact(a, &query, &state.i.cfg))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
.collect(),
|
||||
};
|
||||
Ok(Response::builder()
|
||||
.typed_header(headers::ContentType::html())
|
||||
.cache()
|
||||
.body(tmpl.to_string().into())?)
|
||||
}
|
||||
}
|
||||
|
@ -248,12 +224,12 @@ impl App {
|
|||
State(state): State<AppState>,
|
||||
Host(host): Host,
|
||||
Form(url): Form<UrlForm>,
|
||||
) -> Result<Redirect, Error> {
|
||||
) -> Result<Redirect> {
|
||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||
|
||||
if subdomain.is_empty() {
|
||||
let query = Query::from_forge_url(&url.url, &state.i.cfg.load().site_aliases)?;
|
||||
let subdomain = query.subdomain()?;
|
||||
let query = Query::from_forge_url(&url.url)?;
|
||||
let subdomain = query.subdomain();
|
||||
let target = format!(
|
||||
"{}{}.{}",
|
||||
state.i.cfg.url_proto(),
|
||||
|
@ -272,7 +248,7 @@ impl App {
|
|||
zip_path: PathBuf,
|
||||
res: GetFileResultFile,
|
||||
hdrs: &HeaderMap,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
) -> Result<Response<Body>> {
|
||||
let file = res.file;
|
||||
|
||||
// Dont serve files above the configured size limit
|
||||
|
@ -291,8 +267,7 @@ impl App {
|
|||
let mut resp = Response::builder()
|
||||
.status(res.status)
|
||||
.typed_header(headers::AcceptRanges::bytes())
|
||||
.typed_header(headers::LastModified::from(entry.last_modified))
|
||||
.cache_immutable();
|
||||
.typed_header(headers::LastModified::from(entry.last_modified));
|
||||
if let Some(mime) = res.mime {
|
||||
resp = resp.typed_header(headers::ContentType::from(mime));
|
||||
}
|
||||
|
@ -391,24 +366,24 @@ impl App {
|
|||
async fn get_artifacts(
|
||||
State(state): State<AppState>,
|
||||
Host(host): Host,
|
||||
) -> Result<Response<Body>, ErrorJson> {
|
||||
) -> Result<Json<Vec<Artifact>>> {
|
||||
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)?;
|
||||
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
|
||||
async fn get_artifact(
|
||||
State(state): State<AppState>,
|
||||
Host(host): Host,
|
||||
) -> Result<Response<Body>, ErrorJson> {
|
||||
) -> Result<Json<Artifact>> {
|
||||
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)?;
|
||||
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
|
||||
|
@ -416,10 +391,10 @@ impl App {
|
|||
State(state): State<AppState>,
|
||||
Host(host): Host,
|
||||
request: Request,
|
||||
) -> Result<Response<Body>, ErrorJson> {
|
||||
) -> Result<Json<Vec<IndexEntry>>> {
|
||||
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 query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
||||
let query = Query::from_subdomain(subdomain)?;
|
||||
state.i.cfg.check_filterlist(&query)?;
|
||||
let entry_res = state
|
||||
.i
|
||||
|
@ -430,17 +405,7 @@ impl App {
|
|||
state.garbage_collect();
|
||||
}
|
||||
let files = entry_res.entry.get_files();
|
||||
Ok(Response::builder()
|
||||
.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())?)
|
||||
Ok(Json(files))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ impl ArtifactApi {
|
|||
}
|
||||
|
||||
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
|
||||
.get_or_insert_async(&subdomain, async {
|
||||
if query.is_github() {
|
||||
|
@ -277,7 +277,6 @@ mod tests {
|
|||
async fn fetch_forgejo() {
|
||||
let query = ArtifactQuery {
|
||||
host: "code.thetadev.de".to_owned(),
|
||||
host_alias: None,
|
||||
user: "HSA".to_owned(),
|
||||
repo: "Visitenbuch".to_owned(),
|
||||
run: 32,
|
||||
|
@ -294,7 +293,6 @@ mod tests {
|
|||
async fn fetch_github() {
|
||||
let query = ArtifactQuery {
|
||||
host: "github.com".to_owned(),
|
||||
host_alias: None,
|
||||
user: "actions".to_owned(),
|
||||
repo: "upload-artifact".to_owned(),
|
||||
run: 8805345396,
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
collections::{BTreeMap, HashMap},
|
||||
fs::FileTimes,
|
||||
net::IpAddr,
|
||||
num::NonZeroUsize,
|
||||
num::{NonZeroU32, NonZeroUsize},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
|
@ -103,7 +103,7 @@ impl Cache {
|
|||
.load()
|
||||
.limit_artifacts_per_min
|
||||
.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,
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ impl Cache {
|
|||
query: &ArtifactQuery,
|
||||
ip: &IpAddr,
|
||||
) -> Result<GetEntryResult> {
|
||||
let subdomain = query.subdomain_noalias();
|
||||
let subdomain = query.subdomain();
|
||||
let zip_path = path!(self.cfg.load().cache_dir / format!("{subdomain}.zip"));
|
||||
let downloaded = !zip_path.is_file();
|
||||
if downloaded {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
num::{NonZeroU32, NonZeroUsize},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
|
@ -59,12 +58,8 @@ pub struct ConfigData {
|
|||
pub real_ip_header: Option<String>,
|
||||
/// Limit the amount of downloaded artifacts per IP address and minute
|
||||
pub limit_artifacts_per_min: Option<NonZeroU32>,
|
||||
/// List of sites/users/repos that can NOT be accessed
|
||||
pub repo_blacklist: QueryFilterList,
|
||||
/// List of sites/users/repos that can ONLY be accessed
|
||||
pub repo_whitelist: QueryFilterList,
|
||||
/// Aliases for sites (Example: `gh => github.com`)
|
||||
pub site_aliases: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for ConfigData {
|
||||
|
@ -84,7 +79,6 @@ impl Default for ConfigData {
|
|||
limit_artifacts_per_min: Some(NonZeroU32::new(5).unwrap()),
|
||||
repo_blacklist: QueryFilterList::default(),
|
||||
repo_whitelist: QueryFilterList::default(),
|
||||
site_aliases: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use axum::{
|
|||
};
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::{templates, util::ResponseBuilderExt};
|
||||
use crate::{templates, util::InsertTypedHeader};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
mod app;
|
||||
mod artifact_api;
|
||||
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 regex::{Captures, Regex};
|
||||
|
@ -18,12 +18,10 @@ pub enum Query {
|
|||
pub type RunQuery = QueryData<()>;
|
||||
pub type ArtifactQuery = QueryData<u64>;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct QueryData<T> {
|
||||
/// Forge host
|
||||
pub host: String,
|
||||
/// Host alias if the query was constructed using one
|
||||
pub host_alias: Option<String>,
|
||||
/// User/org name (case-insensitive)
|
||||
pub user: String,
|
||||
/// 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());
|
||||
|
||||
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<_>>();
|
||||
if segments.len() != 4 {
|
||||
return Err(Error::InvalidUrl);
|
||||
|
@ -48,22 +46,14 @@ impl Query {
|
|||
return Err(Error::InvalidUrl);
|
||||
}
|
||||
|
||||
let mut host = decode_domain(segments[0], '.');
|
||||
let mut host_alias = None;
|
||||
let host = decode_domain(segments[0], '.');
|
||||
let user = decode_domain(segments[1], '-');
|
||||
let repo = decode_domain(segments[2], '-');
|
||||
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) {
|
||||
Some(x) => Self::Artifact(QueryData {
|
||||
host,
|
||||
host_alias,
|
||||
user,
|
||||
repo,
|
||||
run,
|
||||
|
@ -71,7 +61,6 @@ impl Query {
|
|||
}),
|
||||
None => Self::Run(QueryData {
|
||||
host,
|
||||
host_alias,
|
||||
user,
|
||||
repo,
|
||||
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 user = path_segs
|
||||
|
@ -104,20 +93,13 @@ impl Query {
|
|||
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
|
||||
.next()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.ok_or(Error::BadRequest("no run ID".into()))?;
|
||||
|
||||
Ok(Self::Run(RunQuery {
|
||||
host,
|
||||
host_alias: None,
|
||||
host: host.to_owned(),
|
||||
user,
|
||||
repo,
|
||||
run,
|
||||
|
@ -125,7 +107,7 @@ impl Query {
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn subdomain(&self) -> Result<String> {
|
||||
pub fn subdomain(&self) -> String {
|
||||
match self {
|
||||
Query::Artifact(q) => q.subdomain(),
|
||||
Query::Run(q) => q.subdomain(),
|
||||
|
@ -148,33 +130,22 @@ impl Query {
|
|||
}
|
||||
|
||||
impl ArtifactQuery {
|
||||
pub fn subdomain(&self) -> Result<String> {
|
||||
pub fn subdomain(&self) -> String {
|
||||
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 {
|
||||
pub fn subdomain(&self) -> Result<String> {
|
||||
pub fn subdomain(&self) -> String {
|
||||
self.subdomain_with_artifact(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryData<T> {
|
||||
pub fn _subdomain(&self, artifact: Option<u64>, use_alias: bool) -> String {
|
||||
let host = if use_alias {
|
||||
self.host_alias.as_deref().unwrap_or(&self.host)
|
||||
} else {
|
||||
&self.host
|
||||
};
|
||||
|
||||
pub fn subdomain_with_artifact(&self, artifact: Option<u64>) -> String {
|
||||
let mut res = format!(
|
||||
"{}--{}--{}--{}",
|
||||
encode_domain(host, '.'),
|
||||
encode_domain(&self.host, '.'),
|
||||
encode_domain(&self.user, '-'),
|
||||
encode_domain(&self.repo, '-'),
|
||||
self.run,
|
||||
|
@ -185,14 +156,6 @@ impl<T> QueryData<T> {
|
|||
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 {
|
||||
format!("{}/{}#{}", self.user, self.repo, self.run)
|
||||
}
|
||||
|
@ -211,7 +174,6 @@ impl<T> QueryData<T> {
|
|||
pub fn into_runquery(self) -> RunQuery {
|
||||
RunQuery {
|
||||
host: self.host,
|
||||
host_alias: self.host_alias,
|
||||
user: self.user,
|
||||
repo: self.repo,
|
||||
run: self.run,
|
||||
|
@ -386,7 +348,7 @@ impl<'de> Deserialize<'de> for QueryFilterList {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::query::{QueryFilter, QueryFilterList};
|
||||
|
||||
|
@ -426,19 +388,18 @@ mod tests {
|
|||
#[test]
|
||||
fn query_from_subdomain() {
|
||||
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!(
|
||||
query,
|
||||
Query::Artifact(ArtifactQuery {
|
||||
host: "github.com".to_owned(),
|
||||
host_alias: None,
|
||||
user: "thetadev".to_owned(),
|
||||
repo: "newpipe-extractor".to_owned(),
|
||||
run: 14,
|
||||
artifact: 123
|
||||
})
|
||||
);
|
||||
assert_eq!(query.subdomain().unwrap(), d1);
|
||||
assert_eq!(query.subdomain(), d1);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
|
@ -2,7 +2,6 @@ use crate::{
|
|||
artifact_api::Artifact,
|
||||
cache::{ListingEntry, Size},
|
||||
config::Config,
|
||||
error::Result,
|
||||
query::QueryData,
|
||||
};
|
||||
use yarte::{Render, Template};
|
||||
|
@ -62,18 +61,14 @@ pub struct ArtifactItem {
|
|||
}
|
||||
|
||||
impl ArtifactItem {
|
||||
pub fn from_artifact<T>(
|
||||
artifact: Artifact,
|
||||
query: &QueryData<T>,
|
||||
cfg: &Config,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
pub fn from_artifact<T>(artifact: Artifact, query: &QueryData<T>, cfg: &Config) -> Self {
|
||||
Self {
|
||||
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),
|
||||
expired: artifact.expired,
|
||||
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::{
|
||||
extract::{ConnectInfo, Request},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use headers::{Header, HeaderMapExt};
|
||||
use http::{header, StatusCode};
|
||||
use http::header;
|
||||
use mime_guess::Mime;
|
||||
use serde::Serialize;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
|
||||
use tokio_util::bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// HTTP response builder extensions
|
||||
pub trait ResponseBuilderExt {
|
||||
pub trait InsertTypedHeader {
|
||||
/// Inserts a typed header to this response.
|
||||
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 {
|
||||
if let Some(headers) = self.headers_mut() {
|
||||
headers.typed_insert(header);
|
||||
}
|
||||
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 {
|
||||
|
@ -241,33 +203,6 @@ pub fn parse_url(input: &str) -> Result<(&str, std::str::Split<char>)> {
|
|||
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)]
|
||||
mod tests {
|
||||
use http::{header, HeaderMap};
|
||||
|
|
|
@ -158,10 +158,15 @@
|
|||
</footer>
|
||||
<script>
|
||||
|
||||
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
||||
|
||||
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"}})}
|
||||
|
||||
// @license
|
||||
magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT 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"}})}
|
||||
// @license-end
|
||||
|
||||
</script>
|
||||
|
|
|
@ -170,10 +170,15 @@
|
|||
|
||||
<script>
|
||||
|
||||
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
||||
|
||||
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"}})}
|
||||
|
||||
// @license
|
||||
magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT 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"}})}
|
||||
// @license-end
|
||||
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue