mirror of
https://github.com/deps-rs/deps.rs.git
synced 2024-11-21 18:06:30 +00:00
feat: migrate server to actix-http
This commit is contained in:
parent
85a077e80d
commit
dfcdf31c72
16 changed files with 331 additions and 223 deletions
193
Cargo.lock
generated
193
Cargo.lock
generated
|
@ -8,6 +8,103 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eb9843d84c775696c37d9a418bbb01b932629d01870722c0f13eb3f95e2536d"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"ahash",
|
||||
"bitflags 2.5.0",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-service"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"paste",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||
dependencies = [
|
||||
"local-waker",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
|
@ -30,6 +127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
|
@ -169,6 +267,15 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadence"
|
||||
version = "1.4.0"
|
||||
|
@ -1493,17 +1600,6 @@ dependencies = [
|
|||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.0"
|
||||
|
@ -1523,7 +1619,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
|
@ -1539,30 +1635,6 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.3.1"
|
||||
|
@ -1574,7 +1646,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"h2 0.4.5",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
|
@ -1591,7 +1663,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
|||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
|
@ -1608,7 +1680,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
|
@ -1626,8 +1698,8 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"hyper 1.3.1",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
|
@ -1707,6 +1779,12 @@ dependencies = [
|
|||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lasso"
|
||||
version = "0.7.2"
|
||||
|
@ -1734,6 +1812,12 @@ version = "0.4.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "local-waker"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
|
@ -1835,6 +1919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
@ -1989,6 +2074,12 @@ dependencies = [
|
|||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
@ -2277,9 +2368,9 @@ dependencies = [
|
|||
"futures-util",
|
||||
"h2 0.4.5",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
|
@ -2603,6 +2694,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
|||
name = "shiny-robots"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-server",
|
||||
"actix-service",
|
||||
"anyhow",
|
||||
"badge",
|
||||
"cadence",
|
||||
|
@ -2614,7 +2708,6 @@ dependencies = [
|
|||
"futures-util",
|
||||
"gix",
|
||||
"grass",
|
||||
"hyper 0.14.28",
|
||||
"indexmap",
|
||||
"lru_time_cache",
|
||||
"maud",
|
||||
|
@ -2635,6 +2728,15 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
|
@ -2868,7 +2970,9 @@ dependencies = [
|
|||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
|
@ -3020,6 +3124,7 @@ version = "0.1.40"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
|
|
|
@ -14,6 +14,9 @@ edition = "2021"
|
|||
[dependencies]
|
||||
badge = { path = "./libs/badge" }
|
||||
|
||||
actix-http = { version = "3", features = ["http2"] }
|
||||
actix-server = "2"
|
||||
actix-service = "2"
|
||||
anyhow = "1"
|
||||
cadence = "1"
|
||||
crates-index = { version = "2", default-features = false, features = ["git"] }
|
||||
|
@ -21,7 +24,6 @@ derive_more = "0.99"
|
|||
dotenvy = "0.15"
|
||||
font-awesome-as-a-crate = "0.3"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
hyper = { version = "0.14.10", features = ["full"] }
|
||||
error_reporter = "1"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
lru_time_cache = "0.11"
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix_service::Service;
|
||||
use anyhow::{anyhow, Error};
|
||||
use cadence::{MetricSink, NopMetricSink, StatsdClient};
|
||||
use futures_util::{
|
||||
|
@ -12,7 +13,6 @@ use futures_util::{
|
|||
stream::{self, BoxStream},
|
||||
StreamExt as _,
|
||||
};
|
||||
use hyper::service::Service;
|
||||
use once_cell::sync::Lazy;
|
||||
use relative_path::{RelativePath, RelativePathBuf};
|
||||
use rustsec::database::Database;
|
||||
|
@ -277,7 +277,7 @@ impl Engine {
|
|||
) -> Result<String, Error> {
|
||||
let manifest_path = path.join(RelativePath::new("Cargo.toml"));
|
||||
|
||||
let mut service = self.retrieve_file_at_path.clone();
|
||||
let service = self.retrieve_file_at_path.clone();
|
||||
service.call((repo_path.clone(), manifest_path)).await
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
use std::{
|
||||
fmt, str,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
use actix_service::Service;
|
||||
use anyhow::{anyhow, Error};
|
||||
use crates_index::{Crate, DependencyKind};
|
||||
use futures_util::FutureExt as _;
|
||||
use hyper::service::Service;
|
||||
use semver::{Version, VersionReq};
|
||||
use serde::Deserialize;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
@ -88,11 +85,9 @@ impl Service<CrateName> for QueryCrate {
|
|||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, crate_name: CrateName) -> Self::Future {
|
||||
fn call(&self, crate_name: CrateName) -> Self::Future {
|
||||
let index = self.index.clone();
|
||||
Self::query(index, crate_name).boxed()
|
||||
}
|
||||
|
@ -152,11 +147,9 @@ impl Service<()> for GetPopularCrates {
|
|||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, _req: ()) -> Self::Future {
|
||||
fn call(&self, _req: ()) -> Self::Future {
|
||||
let client = self.client.clone();
|
||||
Self::query(client).boxed()
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
use std::{
|
||||
fmt,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
use actix_service::Service;
|
||||
use anyhow::Error;
|
||||
use futures_util::FutureExt as _;
|
||||
use hyper::service::Service;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
|
@ -74,11 +71,9 @@ impl Service<()> for GetPopularRepos {
|
|||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, _req: ()) -> Self::Future {
|
||||
fn call(&self, _req: ()) -> Self::Future {
|
||||
let client = self.client.clone();
|
||||
Self::query(client).boxed()
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
use std::{
|
||||
fmt,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
use actix_service::Service;
|
||||
use anyhow::{anyhow, Error};
|
||||
use futures_util::FutureExt as _;
|
||||
use hyper::service::Service;
|
||||
use relative_path::RelativePathBuf;
|
||||
|
||||
use crate::{models::repo::RepoPath, BoxFuture};
|
||||
|
@ -45,11 +42,9 @@ impl Service<(RepoPath, RelativePathBuf)> for RetrieveFileAtPath {
|
|||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, (repo_path, path): (RepoPath, RelativePathBuf)) -> Self::Future {
|
||||
fn call(&self, (repo_path, path): (RepoPath, RelativePathBuf)) -> Self::Future {
|
||||
let client = self.client.clone();
|
||||
Self::query(client, repo_path, path).boxed()
|
||||
}
|
||||
|
@ -57,6 +52,6 @@ impl Service<(RepoPath, RelativePathBuf)> for RetrieveFileAtPath {
|
|||
|
||||
impl fmt::Debug for RetrieveFileAtPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("RetrieveFileAtPath")
|
||||
f.debug_struct("RetrieveFileAtPath").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
use std::{
|
||||
fmt,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use actix_service::Service;
|
||||
use anyhow::Error;
|
||||
use futures_util::FutureExt as _;
|
||||
use hyper::service::Service;
|
||||
use rustsec::database::Database;
|
||||
|
||||
use crate::BoxFuture;
|
||||
|
@ -32,11 +28,9 @@ impl Service<()> for FetchAdvisoryDatabase {
|
|||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, _req: ()) -> Self::Future {
|
||||
fn call(&self, _req: ()) -> Self::Future {
|
||||
let client = self.client.clone();
|
||||
Self::fetch(client).boxed()
|
||||
}
|
||||
|
|
51
src/main.rs
51
src/main.rs
|
@ -4,17 +4,14 @@
|
|||
use std::{
|
||||
env,
|
||||
future::Future,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
net::{Ipv4Addr, UdpSocket},
|
||||
pin::Pin,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_http::{HttpService, Request};
|
||||
use actix_server::Server;
|
||||
use cadence::{QueuingMetricSink, UdpMetricSink};
|
||||
use hyper::{
|
||||
server::conn::AddrStream,
|
||||
service::{make_service_fn, service_fn},
|
||||
Server,
|
||||
};
|
||||
use reqwest::redirect::Policy as RedirectPolicy;
|
||||
use tracing::Instrument as _;
|
||||
|
||||
|
@ -59,7 +56,7 @@ fn init_tracing_subscriber() {
|
|||
.init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
init_tracing_subscriber();
|
||||
|
@ -77,8 +74,6 @@ async fn main() {
|
|||
.parse()
|
||||
.expect("could not read port");
|
||||
|
||||
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
|
||||
|
||||
let index = ManagedIndex::new();
|
||||
|
||||
{
|
||||
|
@ -92,25 +87,29 @@ async fn main() {
|
|||
let mut engine = Engine::new(client.clone(), index);
|
||||
engine.set_metrics(metrics);
|
||||
|
||||
let make_svc = make_service_fn(move |_socket: &AddrStream| {
|
||||
let engine = engine.clone();
|
||||
let server = Server::build()
|
||||
.bind("deps-rs", (Ipv4Addr::UNSPECIFIED, port), move || {
|
||||
let engine = engine.clone();
|
||||
|
||||
async move {
|
||||
let server = App::new(engine.clone());
|
||||
Ok::<_, hyper::Error>(service_fn(move |req| {
|
||||
let server = server.clone();
|
||||
async move {
|
||||
let path = req.uri().path().to_owned();
|
||||
let app = App::new(engine.clone());
|
||||
|
||||
server
|
||||
.handle(req)
|
||||
.instrument(tracing::info_span!("@", %path))
|
||||
.await
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
let server = Server::bind(&addr).serve(make_svc);
|
||||
HttpService::build()
|
||||
.client_disconnect_timeout(Duration::from_secs(5))
|
||||
.client_request_timeout(Duration::from_secs(5))
|
||||
.finish(move |req: Request| {
|
||||
let app = app.clone();
|
||||
let path = req.path().to_owned();
|
||||
|
||||
async move {
|
||||
app.handle(req)
|
||||
.instrument(tracing::info_span!("@", %path))
|
||||
.await
|
||||
}
|
||||
})
|
||||
.tcp_auto_h2c()
|
||||
})
|
||||
.unwrap()
|
||||
.run();
|
||||
|
||||
tracing::info!("Server running on port {port}");
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use std::{env, sync::Arc, time::Instant};
|
||||
|
||||
use actix_http::{
|
||||
body::MessageBody,
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE, ETAG, LOCATION},
|
||||
Method, Request, Response, StatusCode,
|
||||
};
|
||||
use badge::BadgeStyle;
|
||||
use futures_util::future;
|
||||
use hyper::{
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE, ETAG, LOCATION},
|
||||
Body, Error as HyperError, Method, Request, Response, StatusCode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use route_recognizer::{Params, Router};
|
||||
use semver::VersionReq;
|
||||
|
@ -90,7 +91,10 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle(&self, req: Request<Body>) -> Result<Response<Body>, HyperError> {
|
||||
pub async fn handle(
|
||||
&self,
|
||||
req: Request,
|
||||
) -> Result<Response<impl MessageBody>, actix_http::Error> {
|
||||
let start = Instant::now();
|
||||
|
||||
// allows `/path/` to also match `/path`
|
||||
|
@ -98,33 +102,39 @@ impl App {
|
|||
|
||||
let res = if let Ok(route_match) = self.router.recognize(normalized_path) {
|
||||
match (req.method(), route_match.handler()) {
|
||||
(&Method::GET, Route::Index) => self.index(req, route_match.params().clone()).await,
|
||||
(&Method::GET, Route::Index) => self
|
||||
.index(req, route_match.params().clone())
|
||||
.await
|
||||
.map(Response::map_into_boxed_body),
|
||||
|
||||
(&Method::GET, Route::RepoStatus(format)) => {
|
||||
self.repo_status(req, route_match.params().clone(), *format)
|
||||
.await
|
||||
(&Method::GET, Route::RepoStatus(format)) => self
|
||||
.repo_status(req, route_match.params().clone(), *format)
|
||||
.await
|
||||
.map(Response::map_into_boxed_body),
|
||||
|
||||
(&Method::GET, Route::CrateStatus(format)) => self
|
||||
.crate_status(req, route_match.params().clone(), *format)
|
||||
.await
|
||||
.map(Response::map_into_boxed_body),
|
||||
|
||||
(&Method::GET, Route::LatestCrateBadge) => self
|
||||
.crate_status(req, route_match.params().clone(), StatusFormat::Svg)
|
||||
.await
|
||||
.map(Response::map_into_boxed_body),
|
||||
|
||||
(&Method::GET, Route::CrateRedirect) => self
|
||||
.crate_redirect(req, route_match.params().clone())
|
||||
.await
|
||||
.map(Response::map_into_boxed_body),
|
||||
|
||||
(&Method::GET, Route::Static(file)) => {
|
||||
Ok(App::static_file(*file).map_into_boxed_body())
|
||||
}
|
||||
|
||||
(&Method::GET, Route::CrateStatus(format)) => {
|
||||
self.crate_status(req, route_match.params().clone(), *format)
|
||||
.await
|
||||
}
|
||||
|
||||
(&Method::GET, Route::LatestCrateBadge) => {
|
||||
self.crate_status(req, route_match.params().clone(), StatusFormat::Svg)
|
||||
.await
|
||||
}
|
||||
|
||||
(&Method::GET, Route::CrateRedirect) => {
|
||||
self.crate_redirect(req, route_match.params().clone()).await
|
||||
}
|
||||
|
||||
(&Method::GET, Route::Static(file)) => Ok(App::static_file(*file)),
|
||||
|
||||
_ => Ok(not_found()),
|
||||
_ => Ok(not_found().map_into_boxed_body()),
|
||||
}
|
||||
} else {
|
||||
Ok(not_found())
|
||||
Ok(not_found().map_into_boxed_body())
|
||||
};
|
||||
|
||||
let end = Instant::now();
|
||||
|
@ -145,9 +155,9 @@ impl App {
|
|||
impl App {
|
||||
async fn index(
|
||||
&self,
|
||||
_req: Request<Body>,
|
||||
_req: Request,
|
||||
_params: Params,
|
||||
) -> Result<Response<Body>, HyperError> {
|
||||
) -> Result<Response<impl MessageBody>, actix_http::Error> {
|
||||
let engine = self.engine.clone();
|
||||
|
||||
let popular =
|
||||
|
@ -159,20 +169,21 @@ impl App {
|
|||
let mut response =
|
||||
views::html::error::render("Could not retrieve popular items", "");
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
Ok((popular_repos, popular_crates)) => {
|
||||
Ok(views::html::index::render(popular_repos, popular_crates))
|
||||
Ok(views::html::index::render(popular_repos, popular_crates).map_into_boxed_body())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn repo_status(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
req: Request,
|
||||
params: Params,
|
||||
format: StatusFormat,
|
||||
) -> Result<Response<Body>, HyperError> {
|
||||
) -> Result<Response<impl MessageBody>, actix_http::Error> {
|
||||
let server = self.clone();
|
||||
|
||||
let site = params.find("site").expect("route param 'site' not found");
|
||||
|
@ -191,7 +202,8 @@ impl App {
|
|||
"Please make sure to provide a valid repository path.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
|
||||
Ok(repo_path) => {
|
||||
|
@ -209,7 +221,8 @@ impl App {
|
|||
SubjectPath::Repo(repo_path),
|
||||
extra_knobs,
|
||||
);
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
Ok(analysis_outcome) => {
|
||||
let response = App::status_format_analysis(
|
||||
|
@ -218,7 +231,8 @@ impl App {
|
|||
SubjectPath::Repo(repo_path),
|
||||
extra_knobs,
|
||||
);
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,9 +241,9 @@ impl App {
|
|||
|
||||
async fn crate_redirect(
|
||||
&self,
|
||||
_req: Request<Body>,
|
||||
_req: Request,
|
||||
params: Params,
|
||||
) -> Result<Response<Body>, HyperError> {
|
||||
) -> Result<Response<impl MessageBody>, actix_http::Error> {
|
||||
let engine = self.engine.clone();
|
||||
|
||||
let name = params.find("name").expect("route param 'name' not found");
|
||||
|
@ -243,7 +257,8 @@ impl App {
|
|||
"Please make sure to provide a valid crate name.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
|
||||
Ok(crate_name) => {
|
||||
|
@ -259,7 +274,8 @@ impl App {
|
|||
"Please make sure to provide a valid crate name.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
Ok(None) => {
|
||||
let mut response = views::html::error::render(
|
||||
|
@ -267,7 +283,8 @@ impl App {
|
|||
"Please make sure to provide a valid crate name.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
Ok(Some(release)) => {
|
||||
let redirect_url = format!(
|
||||
|
@ -277,13 +294,11 @@ impl App {
|
|||
release.version
|
||||
);
|
||||
|
||||
let res = Response::builder()
|
||||
.status(StatusCode::TEMPORARY_REDIRECT)
|
||||
.header(LOCATION, redirect_url)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let res = Response::build(StatusCode::TEMPORARY_REDIRECT)
|
||||
.insert_header((LOCATION, redirect_url))
|
||||
.finish();
|
||||
|
||||
Ok(res)
|
||||
Ok(res.map_into_boxed_body())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -292,10 +307,10 @@ impl App {
|
|||
|
||||
async fn crate_status(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
req: Request,
|
||||
params: Params,
|
||||
format: StatusFormat,
|
||||
) -> Result<Response<Body>, HyperError> {
|
||||
) -> Result<Response<impl MessageBody>, actix_http::Error> {
|
||||
let server = self.clone();
|
||||
|
||||
let name = params.find("name").expect("route param 'name' not found");
|
||||
|
@ -311,7 +326,8 @@ impl App {
|
|||
"Please make sure to provide a valid crate name and version.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
return Ok(response);
|
||||
|
||||
return Ok(response.map_into_boxed_body());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -321,7 +337,7 @@ impl App {
|
|||
.await
|
||||
{
|
||||
Ok(Some(latest_rel)) => latest_rel.version.to_string(),
|
||||
Ok(None) => return Ok(not_found()),
|
||||
Ok(None) => return Ok(not_found().map_into_boxed_body()),
|
||||
Err(err) => {
|
||||
tracing::error!(%err);
|
||||
let mut response = views::html::error::render(
|
||||
|
@ -329,7 +345,8 @@ impl App {
|
|||
"Please make sure to provide a valid crate name.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
return Ok(response);
|
||||
|
||||
return Ok(response.map_into_boxed_body());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,8 +363,10 @@ impl App {
|
|||
"Please make sure to provide a valid crate name and version.",
|
||||
);
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
|
||||
Ok(crate_path) => {
|
||||
let analyze_result = server
|
||||
.engine
|
||||
|
@ -363,7 +382,8 @@ impl App {
|
|||
SubjectPath::Crate(crate_path),
|
||||
badge_knobs,
|
||||
);
|
||||
Ok(response)
|
||||
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
Ok(analysis_outcome) => {
|
||||
let response = App::status_format_analysis(
|
||||
|
@ -373,7 +393,7 @@ impl App {
|
|||
badge_knobs,
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
Ok(response.map_into_boxed_body())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -385,38 +405,41 @@ impl App {
|
|||
format: StatusFormat,
|
||||
subject_path: SubjectPath,
|
||||
badge_knobs: ExtraConfig,
|
||||
) -> Response<Body> {
|
||||
) -> Response<impl MessageBody> {
|
||||
match format {
|
||||
StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref(), badge_knobs),
|
||||
StatusFormat::Svg => {
|
||||
views::badge::response(analysis_outcome.as_ref(), badge_knobs).map_into_boxed_body()
|
||||
}
|
||||
|
||||
StatusFormat::Html => {
|
||||
views::html::status::render(analysis_outcome, subject_path, badge_knobs)
|
||||
.map_into_boxed_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn static_file(file: StaticFile) -> Response<Body> {
|
||||
fn static_file(file: StaticFile) -> Response<impl MessageBody> {
|
||||
match file {
|
||||
StaticFile::StyleCss => Response::builder()
|
||||
.header(CONTENT_TYPE, "text/css; charset=utf-8")
|
||||
.header(ETAG, STATIC_STYLE_CSS_ETAG)
|
||||
.header(CACHE_CONTROL, "public, max-age=365000000, immutable")
|
||||
.body(Body::from(assets::STATIC_STYLE_CSS))
|
||||
.unwrap(),
|
||||
StaticFile::FaviconPng => Response::builder()
|
||||
.header(CONTENT_TYPE, "image/svg+xml")
|
||||
.body(Body::from(assets::STATIC_FAVICON))
|
||||
.unwrap(),
|
||||
StaticFile::LinksJs => Response::builder()
|
||||
.header(CONTENT_TYPE, "text/javascript; charset=utf-8")
|
||||
.header(ETAG, STATIC_LINKS_JS_ETAG)
|
||||
.header(CACHE_CONTROL, "public, max-age=365000000, immutable")
|
||||
.body(Body::from(assets::STATIC_LINKS_JS))
|
||||
.unwrap(),
|
||||
StaticFile::StyleCss => Response::build(StatusCode::OK)
|
||||
.insert_header((CONTENT_TYPE, "text/css; charset=utf-8"))
|
||||
.insert_header((ETAG, STATIC_STYLE_CSS_ETAG))
|
||||
.insert_header((CACHE_CONTROL, "public, max-age=365000000, immutable"))
|
||||
.body(assets::STATIC_STYLE_CSS),
|
||||
|
||||
StaticFile::FaviconPng => Response::build(StatusCode::OK)
|
||||
.insert_header((CONTENT_TYPE, "image/svg+xml"))
|
||||
.body(assets::STATIC_FAVICON),
|
||||
|
||||
StaticFile::LinksJs => Response::build(StatusCode::OK)
|
||||
.insert_header((CONTENT_TYPE, "text/javascript; charset=utf-8"))
|
||||
.insert_header((ETAG, STATIC_LINKS_JS_ETAG))
|
||||
.insert_header((CACHE_CONTROL, "public, max-age=365000000, immutable"))
|
||||
.body(assets::STATIC_LINKS_JS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn not_found() -> Response<Body> {
|
||||
fn not_found() -> Response<impl MessageBody> {
|
||||
views::html::error::render_404()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use actix_http::{body::MessageBody, header::CONTENT_TYPE, Response, StatusCode};
|
||||
use badge::{Badge, BadgeOptions};
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response};
|
||||
|
||||
use crate::{engine::AnalyzeDependenciesOutcome, server::ExtraConfig};
|
||||
|
||||
|
@ -73,11 +73,10 @@ pub fn badge(
|
|||
pub fn response(
|
||||
analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
|
||||
badge_knobs: ExtraConfig,
|
||||
) -> Response<Body> {
|
||||
) -> Response<impl MessageBody> {
|
||||
let badge = badge(analysis_outcome, badge_knobs).to_svg();
|
||||
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, "image/svg+xml; charset=utf-8")
|
||||
.body(Body::from(badge))
|
||||
.unwrap()
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header((CONTENT_TYPE, "image/svg+xml; charset=utf-8"))
|
||||
.body(badge)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use hyper::{
|
||||
use actix_http::{
|
||||
body::MessageBody,
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE},
|
||||
Body, Response, StatusCode,
|
||||
Response, StatusCode,
|
||||
};
|
||||
use maud::html;
|
||||
|
||||
use crate::server::assets::STATIC_STYLE_CSS_PATH;
|
||||
|
||||
pub fn render(title: &str, descr: &str) -> Response<Body> {
|
||||
pub fn render(title: &str, descr: &str) -> Response<impl MessageBody> {
|
||||
super::render_html(
|
||||
title,
|
||||
html! {
|
||||
|
@ -26,7 +27,7 @@ pub fn render(title: &str, descr: &str) -> Response<Body> {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn render_404() -> Response<Body> {
|
||||
pub fn render_404() -> Response<impl MessageBody> {
|
||||
let rendered = html! {
|
||||
html {
|
||||
head {
|
||||
|
@ -55,10 +56,8 @@ pub fn render_404() -> Response<Body> {
|
|||
}
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||
.header(CACHE_CONTROL, "public, max-age=300, immutable")
|
||||
.body(Body::from(rendered.0))
|
||||
.unwrap()
|
||||
Response::build(StatusCode::NOT_FOUND)
|
||||
.insert_header((CONTENT_TYPE, "text/html; charset=utf-8"))
|
||||
.insert_header((CACHE_CONTROL, "public, max-age=300, immutable"))
|
||||
.body(rendered.0)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use hyper::{Body, Response};
|
||||
use actix_http::{body::MessageBody, Response};
|
||||
use maud::{html, Markup};
|
||||
|
||||
use crate::{
|
||||
|
@ -161,7 +161,10 @@ fn popular_table(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>)
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render(popular_repos: Vec<Repository>, popular_crates: Vec<CratePath>) -> Response<Body> {
|
||||
pub fn render(
|
||||
popular_repos: Vec<Repository>,
|
||||
popular_crates: Vec<CratePath>,
|
||||
) -> Response<impl MessageBody> {
|
||||
super::render_html(
|
||||
"Keep your dependencies up-to-date",
|
||||
html! {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response};
|
||||
use actix_http::{body::MessageBody, header::CONTENT_TYPE, Response, StatusCode};
|
||||
use maud::{html, Markup, Render, DOCTYPE};
|
||||
|
||||
pub mod error;
|
||||
|
@ -9,7 +9,7 @@ pub mod status;
|
|||
|
||||
use crate::server::{assets::STATIC_STYLE_CSS_PATH, SELF_BASE_URL};
|
||||
|
||||
fn render_html<B: Render>(title: &str, body: B) -> Response<Body> {
|
||||
fn render_html<B: Render>(title: &str, body: B) -> Response<impl MessageBody> {
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html {
|
||||
|
@ -26,10 +26,9 @@ fn render_html<B: Render>(title: &str, body: B) -> Response<Body> {
|
|||
}
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||
.body(Body::from(rendered.0))
|
||||
.unwrap()
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header((CONTENT_TYPE, "text/html; charset=utf-8"))
|
||||
.body(rendered.0)
|
||||
}
|
||||
|
||||
fn render_navbar() -> Markup {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use actix_http::{body::MessageBody, Response};
|
||||
use font_awesome_as_a_crate::{svg as fa, Type as FaType};
|
||||
use hyper::{Body, Response};
|
||||
use indexmap::IndexMap;
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use pulldown_cmark::{html, Parser};
|
||||
|
@ -457,7 +457,7 @@ pub fn render(
|
|||
analysis_outcome: Option<AnalyzeDependenciesOutcome>,
|
||||
subject_path: SubjectPath,
|
||||
extra_config: ExtraConfig,
|
||||
) -> Response<Body> {
|
||||
) -> Response<impl MessageBody> {
|
||||
let title = match subject_path {
|
||||
SubjectPath::Repo(ref repo_path) => {
|
||||
format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{fmt, sync::Arc, time::Duration};
|
||||
|
||||
use actix_service::Service;
|
||||
use derive_more::{Display, Error, From};
|
||||
use hyper::service::Service;
|
||||
use lru_time_cache::LruCache;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
@ -26,7 +26,7 @@ where
|
|||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt.debug_struct("Cache")
|
||||
.field("inner", &self.inner)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ where
|
|||
cache = "miss",
|
||||
);
|
||||
|
||||
let mut service = self.inner.clone();
|
||||
let service = self.inner.clone();
|
||||
let fresh = service.call(req.clone()).await?;
|
||||
|
||||
{
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use crates_index::{Crate, GitIndex};
|
||||
use parking_lot::Mutex;
|
||||
use tokio::{
|
||||
task::spawn_blocking,
|
||||
time::{self, MissedTickBehavior},
|
||||
|
@ -24,7 +26,7 @@ impl ManagedIndex {
|
|||
}
|
||||
|
||||
pub fn crate_(&self, crate_name: CrateName) -> Option<Crate> {
|
||||
self.index.lock().crate_(crate_name.as_ref())
|
||||
self.index.lock().unwrap().crate_(crate_name.as_ref())
|
||||
}
|
||||
|
||||
pub async fn refresh_at_interval(&self, update_interval: Duration) {
|
||||
|
@ -45,7 +47,7 @@ impl ManagedIndex {
|
|||
async fn refresh(&self) -> Result<(), crates_index::Error> {
|
||||
let index = Arc::clone(&self.index);
|
||||
|
||||
spawn_blocking(move || index.lock().update())
|
||||
spawn_blocking(move || index.lock().unwrap().update())
|
||||
.await
|
||||
.expect("blocking index update task should never panic")?;
|
||||
|
||||
|
|
Loading…
Reference in a new issue