mirror of
https://github.com/tiffany352/rink-rs
synced 2024-11-10 05:34:14 +00:00
Manually invoked currency refresh (#181)
Adds `currency.fetch_on_startup` to the config, which when set to false behaves as if the `cache_duration` was infinite. This means rink will fetch the file once, and then keep reusing that file indefinitely. Adds a `--fetch-currency` CLI argument, which will make rink download the latest version of the currency data and then exit. This can be put into a cron job and used together with the config option so that rink will never block on a web request at startup, without giving up currency units. Requires manual setup though.
This commit is contained in:
parent
6e674f07f8
commit
900b07528a
7 changed files with 261 additions and 13 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,3 +8,5 @@
|
|||
**/.rpt2_cache
|
||||
**/build
|
||||
/web/data
|
||||
# created by unit tests
|
||||
/cli/currency.json
|
||||
|
|
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -101,6 +101,12 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
|
@ -479,6 +485,12 @@ dependencies = [
|
|||
"parse-zoneinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
|
@ -1085,6 +1097,12 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
|
@ -1743,6 +1761,7 @@ dependencies = [
|
|||
"eyre",
|
||||
"humantime-serde",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"rink-core",
|
||||
"rink-sandbox",
|
||||
"rustyline",
|
||||
|
@ -1751,6 +1770,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"similar-asserts",
|
||||
"tempfile",
|
||||
"tiny_http",
|
||||
"toml 0.5.11",
|
||||
"ubyte",
|
||||
]
|
||||
|
@ -2149,6 +2169,18 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"chunked_transfer",
|
||||
"httpdate",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
|
|
|
@ -42,6 +42,8 @@ path = "../sandbox"
|
|||
|
||||
[dev-dependencies]
|
||||
similar-asserts = "1.1.0"
|
||||
tiny_http = "0.12"
|
||||
once_cell = "1"
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling]
|
||||
wasm-opt = ['-g', '-O']
|
||||
|
|
|
@ -62,8 +62,10 @@ pub struct Rink {
|
|||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Currency {
|
||||
/// Set to false to disable currency fetching entirely.
|
||||
/// Set to false to disable currency loading entirely.
|
||||
pub enabled: bool,
|
||||
/// Set to false to only reuse the existing cached currency data.
|
||||
pub fetch_on_startup: bool,
|
||||
/// Which web endpoint should be used to download currency data?
|
||||
pub endpoint: String,
|
||||
/// How long to cache for.
|
||||
|
@ -176,6 +178,7 @@ impl Default for Currency {
|
|||
fn default() -> Self {
|
||||
Currency {
|
||||
enabled: true,
|
||||
fetch_on_startup: true,
|
||||
endpoint: "https://rinkcalc.app/data/currency.json".to_owned(),
|
||||
cache_duration: Duration::from_secs(60 * 60), // 1 hour
|
||||
timeout: Duration::from_secs(2),
|
||||
|
@ -257,13 +260,32 @@ fn read_from_search_path(
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn force_refresh_currency(config: &Currency) -> Result<String> {
|
||||
println!("Fetching...");
|
||||
let start = std::time::Instant::now();
|
||||
let mut path = dirs::cache_dir().ok_or_else(|| eyre!("Could not find cache directory"))?;
|
||||
path.push("rink");
|
||||
path.push("currency.json");
|
||||
let file = download_to_file(&path, &config.endpoint, config.timeout)
|
||||
.wrap_err("Fetching currency data failed")?;
|
||||
let delta = std::time::Instant::now() - start;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.wrap_err("Fetched currency file, but failed to read file metadata")?;
|
||||
let len = metadata.len();
|
||||
Ok(format!(
|
||||
"Fetched {len} byte currency file after {}ms",
|
||||
delta.as_millis()
|
||||
))
|
||||
}
|
||||
|
||||
fn load_live_currency(config: &Currency) -> Result<ast::Defs> {
|
||||
let file = cached(
|
||||
"currency.json",
|
||||
&config.endpoint,
|
||||
config.cache_duration,
|
||||
config.timeout,
|
||||
)?;
|
||||
let duration = if config.fetch_on_startup {
|
||||
Some(config.cache_duration)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let file = cached("currency.json", &config.endpoint, duration, config.timeout)?;
|
||||
let contents = file_to_string(file)?;
|
||||
serde_json::from_str(&contents).wrap_err("Invalid JSON")
|
||||
}
|
||||
|
@ -341,18 +363,19 @@ pub fn load(config: &Config) -> Result<Context> {
|
|||
Ok(ctx)
|
||||
}
|
||||
|
||||
fn read_if_current(file: File, expiration: Duration) -> Result<File> {
|
||||
fn read_if_current(file: File, expiration: Option<Duration>) -> Result<File> {
|
||||
use std::time::SystemTime;
|
||||
|
||||
let stats = file.metadata()?;
|
||||
let mtime = stats.modified()?;
|
||||
let now = SystemTime::now();
|
||||
let elapsed = now.duration_since(mtime)?;
|
||||
if elapsed > expiration {
|
||||
Err(eyre!("File is out of date"))
|
||||
} else {
|
||||
Ok(file)
|
||||
if let Some(expiration) = expiration {
|
||||
if elapsed > expiration {
|
||||
return Err(eyre!("File is out of date"));
|
||||
}
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn download_to_file(path: &Path, url: &str, timeout: Duration) -> Result<File> {
|
||||
|
@ -409,7 +432,12 @@ fn download_to_file(path: &Path, url: &str, timeout: Duration) -> Result<File> {
|
|||
.wrap_err("Failed to write to cache dir")
|
||||
}
|
||||
|
||||
fn cached(filename: &str, url: &str, expiration: Duration, timeout: Duration) -> Result<File> {
|
||||
fn cached(
|
||||
filename: &str,
|
||||
url: &str,
|
||||
expiration: Option<Duration>,
|
||||
timeout: Duration,
|
||||
) -> Result<File> {
|
||||
let mut path = dirs::cache_dir().ok_or_else(|| eyre!("Could not find cache directory"))?;
|
||||
path.push("rink");
|
||||
path.push(filename);
|
||||
|
@ -443,3 +471,154 @@ fn cached(filename: &str, url: &str, expiration: Duration, timeout: Duration) ->
|
|||
Err(err).wrap_err_with(|| format!("Failed to fetch {}", url))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tiny_http::{Response, Server, StatusCode};
|
||||
|
||||
static SERVER: Lazy<Mutex<Arc<Server>>> = Lazy::new(|| {
|
||||
Mutex::new(Arc::new(
|
||||
Server::http("127.0.0.1:3090").expect("port 3090 is needed to do http tests"),
|
||||
))
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn test_download_timeout() {
|
||||
let server = SERVER.lock().unwrap();
|
||||
let server2 = server.clone();
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
let request = server2.recv().expect("the request should not fail");
|
||||
assert_eq!(request.url(), "/data/currency.json");
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
});
|
||||
let result = super::download_to_file(
|
||||
&PathBuf::from("currency.json"),
|
||||
"http://127.0.0.1:3090/data/currency.json",
|
||||
Duration::from_millis(5),
|
||||
);
|
||||
let result = result.expect_err("this should always fail");
|
||||
assert_eq!(result.to_string(), "[28] Timeout was reached (Operation timed out after 5 milliseconds with 0 bytes received)");
|
||||
thread_handle.join().unwrap();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_404() {
|
||||
let server = SERVER.lock().unwrap();
|
||||
let server2 = server.clone();
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
let request = server2.recv().expect("the request should not fail");
|
||||
assert_eq!(request.url(), "/data/currency.json");
|
||||
let mut data = b"404 not found".to_owned();
|
||||
let cursor = std::io::Cursor::new(&mut data);
|
||||
request
|
||||
.respond(Response::new(StatusCode(404), vec![], cursor, None, None))
|
||||
.expect("the response should go through");
|
||||
});
|
||||
let result = super::download_to_file(
|
||||
&PathBuf::from("currency.json"),
|
||||
"http://127.0.0.1:3090/data/currency.json",
|
||||
Duration::from_millis(2000),
|
||||
);
|
||||
let result = result.expect_err("this should always fail");
|
||||
assert_eq!(
|
||||
result.to_string(),
|
||||
"Received status 404 while downloading http://127.0.0.1:3090/data/currency.json"
|
||||
);
|
||||
thread_handle.join().unwrap();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_success() {
|
||||
let server = SERVER.lock().unwrap();
|
||||
let server2 = server.clone();
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
let request = server2.recv().expect("the request should not fail");
|
||||
assert_eq!(request.url(), "/data/currency.json");
|
||||
let mut data = b"{}".to_owned();
|
||||
let cursor = std::io::Cursor::new(&mut data);
|
||||
request
|
||||
.respond(Response::new(StatusCode(200), vec![], cursor, None, None))
|
||||
.expect("the response should go through");
|
||||
});
|
||||
let result = super::download_to_file(
|
||||
&PathBuf::from("currency.json"),
|
||||
"http://127.0.0.1:3090/data/currency.json",
|
||||
Duration::from_millis(2000),
|
||||
);
|
||||
let mut result = result.expect("this should succeed");
|
||||
let mut string = String::new();
|
||||
result
|
||||
.read_to_string(&mut string)
|
||||
.expect("the file should exist");
|
||||
assert_eq!(string, "{}");
|
||||
thread_handle.join().unwrap();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_force_refresh_success() {
|
||||
let config = super::Currency {
|
||||
enabled: true,
|
||||
fetch_on_startup: false,
|
||||
endpoint: "http://127.0.0.1:3090/data/currency.json".to_owned(),
|
||||
cache_duration: Duration::ZERO,
|
||||
timeout: Duration::from_millis(2000),
|
||||
};
|
||||
|
||||
let server = SERVER.lock().unwrap();
|
||||
let server2 = server.clone();
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
let request = server2.recv().expect("the request should not fail");
|
||||
assert_eq!(request.url(), "/data/currency.json");
|
||||
let mut data = b"{}".to_owned();
|
||||
let cursor = std::io::Cursor::new(&mut data);
|
||||
request
|
||||
.respond(Response::new(StatusCode(200), vec![], cursor, None, None))
|
||||
.expect("the response should go through");
|
||||
});
|
||||
let result = super::force_refresh_currency(&config);
|
||||
let result = result.expect("this should succeed");
|
||||
assert!(result.starts_with("Fetched 2 byte currency file after "));
|
||||
thread_handle.join().unwrap();
|
||||
drop(server);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_force_refresh_timeout() {
|
||||
let config = super::Currency {
|
||||
enabled: true,
|
||||
fetch_on_startup: false,
|
||||
endpoint: "http://127.0.0.1:3090/data/currency.json".to_owned(),
|
||||
cache_duration: Duration::ZERO,
|
||||
timeout: Duration::from_millis(5),
|
||||
};
|
||||
|
||||
let server = SERVER.lock().unwrap();
|
||||
let server2 = server.clone();
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
let request = server2.recv().expect("the request should not fail");
|
||||
assert_eq!(request.url(), "/data/currency.json");
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
});
|
||||
let result = super::force_refresh_currency(&config);
|
||||
let result = result.expect_err("this should timeout");
|
||||
assert_eq!(result.to_string(), "Fetching currency data failed");
|
||||
thread_handle.join().unwrap();
|
||||
drop(server);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,12 @@ async fn main() -> Result<()> {
|
|||
.help("Prints a path to the config file, then exits")
|
||||
.action(ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("fetch-currency")
|
||||
.long("fetch-currency")
|
||||
.help("Fetches latest version of currency data, then exits")
|
||||
.action(ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("dump")
|
||||
.long("dump")
|
||||
|
@ -78,6 +84,17 @@ async fn main() -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if matches.get_flag("fetch-currency") {
|
||||
let result = config::force_refresh_currency(&config.currency);
|
||||
match result {
|
||||
Ok(msg) => {
|
||||
println!("{msg}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
if matches.get_flag("config-path") {
|
||||
println!("{}", config::config_path("config.toml").unwrap().display());
|
||||
Ok(())
|
||||
|
|
|
@ -25,6 +25,11 @@ Options
|
|||
**--config-path**::
|
||||
Prints a path to the config file, then exits.
|
||||
|
||||
**--fetch-currency**:
|
||||
Fetches the latest version of the currency data, then exits. Can be
|
||||
used as part of a cron job, possibly together with setting
|
||||
`currency.fetch_on_startup` to false.
|
||||
|
||||
**-f**::
|
||||
**--file** <__file__>::
|
||||
Reads expressions from a file, one per line, printing them to stdout
|
||||
|
@ -58,6 +63,9 @@ Rink looks for a configuration file in
|
|||
Rink relies on some data files, which are found using a search path.
|
||||
See rink-defs(5) and rink-dates(5).
|
||||
|
||||
Downloaded currency data is saved in
|
||||
`$XDG_CACHE_DIR/rink/currency.json`.
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
|
|
|
@ -63,6 +63,14 @@ The `[currency]` section.
|
|||
Currency fetching can be disabled for those that don't want it.
|
||||
Default: `true`
|
||||
|
||||
*fetch_on_startup* = <__bool__>::
|
||||
Setting this to `false` causes rink to behave as if
|
||||
`cache_duration` was infinite. If the currency file doesn't
|
||||
exist, it will fetch it, otherwise it will use it indefinitely.
|
||||
Rink can be invoked with `--fetch-currency` to download the
|
||||
latest version.
|
||||
Default: `true`
|
||||
|
||||
*endpoint* = <__url__>::
|
||||
Allows pointing to alternate Rink-Web instances, or to any other API
|
||||
that offers a compatible format.
|
||||
|
|
Loading…
Reference in a new issue