Skip to content

Commit 030986c

Browse files
committed
Add releases handler for returning scripts
Uses the GitHub API to fetch the latest versions of the scripts at all times (Previously handled in a custom, unpublished PHP handler)
1 parent 474e8a7 commit 030986c

7 files changed

Lines changed: 176 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ set-prod-env\.ps1
203203
# Added by cargo
204204

205205
/target
206+
/database/target
206207

207208
# .env
208209
/.env

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ publish = false
1111
[dependencies]
1212
actix-web = "4.0.0-beta.9"
1313
actix-http = "3.0.0-beta.9"
14+
arc-swap = "1.5"
1415
futures = "0.3"
1516
anyhow = "1.0"
1617
log = "0.4"
@@ -22,7 +23,7 @@ uuid = { version = "0.8", features = ["serde"] }
2223
chrono = { version = "0.4", features = ["serde"] }
2324
bitflags = "1.3"
2425
tokio = "1.12"
25-
reqwest = "0.11"
26+
reqwest = { version = "0.11", features = ["json"] }
2627
scraper = "0.12"
2728
ego-tree = "0.6"
2829
regex = "1.5"
@@ -36,6 +37,7 @@ crc32c = "0.6"
3637
actix-files = "0.6.0-beta.7"
3738
semval = "0.1"
3839
parse-display = "0.5"
40+
rand = "0.8"
3941
shared = { path = "./shared" }
4042
database = { path = "./database" }
4143

Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ RUN npm run build
5454

5555
FROM debian:bullseye-slim AS binary
5656

57-
# RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
58-
# libpq-dev \
59-
# ca-certificates
57+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
58+
ca-certificates
6059

6160
COPY --from=build /usr/src/qcext-server/target/release/qcext-server /usr/local/bin/
6261
COPY --from=build /usr/src/qcext-server/build /build

src/controllers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod api;
2+
pub mod releases;

src/controllers/releases.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::sync::Arc;
2+
3+
use actix_web::{error, web, HttpResponse, Result};
4+
use arc_swap::ArcSwap;
5+
use chrono::{DateTime, Duration, Utc};
6+
use once_cell::sync::Lazy;
7+
use rand::Rng;
8+
use reqwest::Client;
9+
use serde::Deserialize;
10+
11+
pub fn configure(cfg: &mut web::ServiceConfig) {
12+
cfg.service(web::resource("/{file}").route(web::get().to(get_latest_script_file)));
13+
}
14+
15+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16+
enum ScriptFile {
17+
User,
18+
Meta,
19+
}
20+
21+
impl ScriptFile {
22+
fn from_request(request: &str) -> Option<Self> {
23+
Some(match request {
24+
"qc-ext.latest.user.js" => Self::User,
25+
"qc-ext.latest.meta.js" => Self::Meta,
26+
_ => return None,
27+
})
28+
}
29+
30+
fn github_filename(self) -> &'static str {
31+
match self {
32+
Self::User => "qc-ext.user.js",
33+
Self::Meta => "qc-ext.meta.js",
34+
}
35+
}
36+
}
37+
38+
pub(crate) async fn get_latest_script_file(file: web::Path<String>) -> Result<HttpResponse> {
39+
let script_file = ScriptFile::from_request(&file).ok_or_else(|| {
40+
error::ErrorBadRequest(format!("{file} is not a valid release file name"))
41+
})?;
42+
43+
let jitter = rand::thread_rng().gen_range(-20..20);
44+
let cache = SCRIPT_CACHE.load();
45+
let cache_expired = cache.expiration - Utc::now() <= Duration::seconds(jitter);
46+
if !cache_expired {
47+
if let Some(cached_file) = match script_file {
48+
ScriptFile::User => cache.user.as_deref(),
49+
ScriptFile::Meta => cache.meta.as_deref(),
50+
} {
51+
return Ok(HttpResponse::Ok()
52+
.content_type("text/javascript; charset=utf-8")
53+
.body(cached_file.to_owned()));
54+
}
55+
}
56+
57+
let releases_response = Client::new()
58+
.get("https://api.github.com/repos/Questionable-Content-Extensions/client/releases/latest")
59+
.header(
60+
"User-Agent",
61+
"https://github.com/Questionable-Content-Extensions/server",
62+
)
63+
.send()
64+
.await
65+
.map_err(error::ErrorInternalServerError)?;
66+
67+
let releases: Releases = releases_response
68+
.json()
69+
.await
70+
.map_err(error::ErrorInternalServerError)?;
71+
72+
let requested_asset_url = releases
73+
.assets
74+
.into_iter()
75+
.find(|a| a.name == script_file.github_filename())
76+
.ok_or_else(|| {
77+
error::ErrorNotFound(format!(
78+
"According to GitHub, there is no latest release of {}! \
79+
Hopefully this is a transient error, try again in a few minutes.",
80+
file
81+
))
82+
})?
83+
.browser_download_url;
84+
85+
let asset_response = Client::new()
86+
.get(requested_asset_url)
87+
.header(
88+
"User-Agent",
89+
"https://github.com/Questionable-Content-Extensions/server",
90+
)
91+
.send()
92+
.await
93+
.map_err(error::ErrorInternalServerError)?;
94+
95+
let asset = asset_response
96+
.text()
97+
.await
98+
.map_err(error::ErrorInternalServerError)?;
99+
100+
let new_cache = if cache_expired {
101+
// Start a new cache
102+
ScriptCache {
103+
expiration: Utc::now() + Duration::hours(1),
104+
user: if script_file == ScriptFile::User {
105+
Some(asset.clone())
106+
} else {
107+
None
108+
},
109+
meta: if script_file == ScriptFile::Meta {
110+
Some(asset.clone())
111+
} else {
112+
None
113+
},
114+
}
115+
} else {
116+
// Cache wasn't expired, but we still had to fetch the asset
117+
// which means it was a cache miss, so add the missing asset
118+
// to the existing cache
119+
let mut new_cache = (&**cache).clone();
120+
if script_file == ScriptFile::User {
121+
new_cache.user = Some(asset.clone());
122+
} else {
123+
new_cache.meta = Some(asset.clone());
124+
}
125+
new_cache
126+
};
127+
128+
SCRIPT_CACHE.compare_and_swap(cache, Arc::new(new_cache));
129+
130+
Ok(HttpResponse::Ok()
131+
.content_type("text/javascript; charset=utf-8")
132+
.body(asset))
133+
}
134+
135+
#[derive(Debug, Deserialize)]
136+
struct Releases {
137+
assets: Vec<Asset>,
138+
}
139+
140+
#[derive(Debug, Deserialize)]
141+
struct Asset {
142+
name: String,
143+
browser_download_url: String,
144+
}
145+
146+
#[derive(Clone, Debug)]
147+
struct ScriptCache {
148+
expiration: DateTime<Utc>,
149+
user: Option<String>,
150+
meta: Option<String>,
151+
}
152+
153+
static SCRIPT_CACHE: Lazy<ArcSwap<ScriptCache>> = Lazy::new(|| {
154+
ArcSwap::from_pointee(ScriptCache {
155+
expiration: Utc::now() - Duration::days(1),
156+
user: None,
157+
meta: None,
158+
})
159+
});

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ use util::Environment;
8181
mod util;
8282

8383
mod controllers;
84-
//mod database;
8584
mod models;
8685

8786
#[actix_web::main]
@@ -114,6 +113,7 @@ async fn main() -> Result<()> {
114113
.wrap(actix_web::middleware::Compress::default())
115114
.wrap(actix_web::middleware::Logger::default())
116115
.service(web::scope("/api").configure(controllers::api::configure))
116+
.service(web::scope("/releases").configure(controllers::releases::configure))
117117
.service(Files::new("/", "./build/").index_file("index.html"))
118118
})
119119
.disable_signals()

0 commit comments

Comments
 (0)