Write metadata to file cache

Then, before a GET, read the metadata and add any etag to the headers and handle 304 in response.
This commit is contained in:
Tyler Hallada 2020-11-05 19:19:17 -05:00
parent dcc3590ac3
commit 127a68687e
8 changed files with 299 additions and 101 deletions

34
Cargo.lock generated
View File

@ -109,6 +109,7 @@ dependencies = [
"base64 0.13.0", "base64 0.13.0",
"bytes", "bytes",
"cbindgen", "cbindgen",
"chrono",
"dirs", "dirs",
"http-api-problem", "http-api-problem",
"log", "log",
@ -183,6 +184,20 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.1" version = "2.33.1"
@ -742,6 +757,25 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.0"

View File

@ -13,6 +13,7 @@ cbindgen = "0.14.4"
anyhow = "1.0" anyhow = "1.0"
base64 = "0.13" base64 = "0.13"
bytes = "0.5" bytes = "0.5"
chrono = { version = "0.4", features = ["serde"] }
http-api-problem = "0.17" http-api-problem = "0.17"
mockito = "0.26.0" mockito = "0.26.0"
reqwest = { version = "0.10", features = ["blocking", "json", "gzip"] } reqwest = { version = "0.10", features = ["blocking", "json", "gzip"] }

View File

@ -3,7 +3,9 @@ use std::{fs::create_dir_all, fs::File, io::BufReader, io::Write, path::Path, pa
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use base64::{encode_config, URL_SAFE_NO_PAD}; use base64::{encode_config, URL_SAFE_NO_PAD};
use bytes::Bytes; use bytes::Bytes;
use serde::Deserialize; use chrono::{DateTime, Utc};
use reqwest::{blocking::Response, header::HeaderMap};
use serde::{Deserialize, Serialize};
#[cfg(test)] #[cfg(test)]
use tempfile::tempfile; use tempfile::tempfile;
@ -14,6 +16,12 @@ use std::println as info;
use super::API_VERSION; use super::API_VERSION;
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
pub etag: Option<String>,
pub date: Option<DateTime<Utc>>,
}
pub fn file_cache_dir(api_url: &str) -> Result<PathBuf> { pub fn file_cache_dir(api_url: &str) -> Result<PathBuf> {
let encoded_url = encode_config(api_url, URL_SAFE_NO_PAD); let encoded_url = encode_config(api_url, URL_SAFE_NO_PAD);
let path = Path::new("Data/SKSE/Plugins/BazaarRealmCache") let path = Path::new("Data/SKSE/Plugins/BazaarRealmCache")
@ -34,6 +42,34 @@ pub fn update_file_cache(cache_path: &Path, bytes: &Bytes) -> Result<()> {
Ok(()) Ok(())
} }
pub fn update_metadata_file_cache(cache_path: &Path, headers: &HeaderMap) -> Result<()> {
#[cfg(not(test))]
let mut file = File::create(cache_path)?;
#[cfg(test)]
let mut file = tempfile()?;
let etag = headers
.get("etag")
.map(|val| val.to_str().unwrap_or("").to_string());
let date = headers
.get("date")
.map(|val| val.to_str().unwrap_or("").parse().unwrap_or(Utc::now()));
let metadata = Metadata { etag, date };
serde_json::to_writer(file, &metadata)?;
Ok(())
}
pub fn update_file_caches(
body_cache_path: &Path,
metadata_cache_path: &Path,
response: Response,
) -> Result<Bytes> {
update_metadata_file_cache(metadata_cache_path, &response.headers())?;
let bytes = response.bytes()?;
update_file_cache(body_cache_path, &bytes)?;
Ok(bytes)
}
pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Result<T> { pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Result<T> {
#[cfg(not(test))] #[cfg(not(test))]
let file = File::open(cache_path).context(format!( let file = File::open(cache_path).context(format!(
@ -47,3 +83,18 @@ pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Resul
info!("returning value from cache: {:?}", cache_path); info!("returning value from cache: {:?}", cache_path);
Ok(serde_json::from_reader(reader)?) Ok(serde_json::from_reader(reader)?)
} }
pub fn load_metadata_from_file_cache(cache_path: &Path) -> Result<Metadata> {
#[cfg(not(test))]
let file = File::open(cache_path).context(format!(
"Object not found in API or in cache: {}",
cache_path.file_name().unwrap_or_default().to_string_lossy()
))?;
#[cfg(test)]
let file = tempfile()?; // cache always reads from an empty temp file in cfg(test)
let reader = BufReader::new(file);
info!("returning value from cache: {:?}", cache_path);
let metadata: Metadata = serde_json::from_reader(reader)?;
Ok(metadata)
}

View File

@ -1,7 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice}; use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result; use anyhow::Result;
use reqwest::Url; use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(test))] #[cfg(not(test))]
@ -10,8 +10,9 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::update_file_cache, log_server_error, cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
result::FFIResult, cache::update_file_cache, cache::update_file_caches, cache::update_metadata_file_cache,
log_server_error, result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -94,6 +95,7 @@ pub struct RawInteriorRefVec {
pub cap: usize, pub cap: usize,
} }
// TODO: delete me if unused
#[no_mangle] #[no_mangle]
pub extern "C" fn create_interior_ref_list( pub extern "C" fn create_interior_ref_list(
api_url: *const c_char, api_url: *const c_char,
@ -133,13 +135,20 @@ pub extern "C" fn create_interior_ref_list(
.json(&interior_ref_list) .json(&interior_ref_list)
.send()?; .send()?;
info!("create interior_ref_list response from api: {:?}", &resp); info!("create interior_ref_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?; let json: InteriorRefList = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id { if let Some(id) = json.id {
update_file_cache( update_file_cache(
&file_cache_dir(api_url)?.join(format!("interior_ref_list_{}.json", id)), &cache_dir.join(format!("interior_ref_list_{}.json", id)),
&bytes, &bytes,
)?; )?;
update_metadata_file_cache(
&cache_dir.join(format!("interior_ref_list_{}_metadata.json", id)),
&headers,
)?;
} }
Ok(json) Ok(json)
} }
@ -209,14 +218,13 @@ pub extern "C" fn update_interior_ref_list(
.json(&interior_ref_list) .json(&interior_ref_list)
.send()?; .send()?;
info!("update interior_ref_list response from api: {:?}", &resp); info!("update interior_ref_list response from api: {:?}", &resp);
let bytes = resp.bytes()?;
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shops_{}_interior_ref_list.json", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shops_{}_interior_ref_list_metadata.json", shop_id));
let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?; let json: InteriorRefList = serde_json::from_slice(&bytes)?;
if let Some(_id) = json.id {
update_file_cache(
&file_cache_dir(api_url)?.join(format!("shops_{}_interior_ref_list.json", shop_id)),
&bytes,
)?;
}
Ok(json) Ok(json)
} }
@ -245,6 +253,7 @@ pub extern "C" fn update_interior_ref_list(
} }
} }
// TODO: delete me if unused
#[no_mangle] #[no_mangle]
pub extern "C" fn get_interior_ref_list( pub extern "C" fn get_interior_ref_list(
api_url: *const c_char, api_url: *const c_char,
@ -268,25 +277,38 @@ pub extern "C" fn get_interior_ref_list(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = file_cache_dir(api_url)? let cache_dir = file_cache_dir(api_url)?;
.join(format!("interior_ref_list_{}.json", interior_ref_list_id)); let body_cache_path =
cache_dir.join(format!("interior_ref_list_{}.json", interior_ref_list_id));
let metadata_cache_path = cache_dir.join(format!(
"interior_ref_list_{}_metadata.json",
interior_ref_list_id
));
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!("get_interior_ref_list response from api: {:?}", &resp); info!("get_interior_ref_list response from api: {:?}", &resp);
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
error!("get_interior_ref_list api request error: {}", err); error!("get_interior_ref_list api request error: {}", err);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }
@ -355,23 +377,33 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = let cache_dir = file_cache_dir(api_url)?;
file_cache_dir(api_url)?.join(format!("shops_{}_interior_ref_list.json", shop_id)); let body_cache_path = cache_dir.join(format!("shops_{}_interior_ref_list.json", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shops_{}_interior_ref_list_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!( info!(
"get_interior_ref_list_by_shop_id response from api: {:?}", "get_interior_ref_list_by_shop_id response from api: {:?}",
&resp &resp
); );
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
@ -379,7 +411,7 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
"get_interior_ref_list_by_shop_id api request error: {}", "get_interior_ref_list_by_shop_id api request error: {}",
err err
); );
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }

View File

@ -1,7 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice}; use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result; use anyhow::Result;
use reqwest::Url; use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(test))] #[cfg(not(test))]
@ -10,18 +10,19 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::update_file_cache, log_server_error, cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
result::FFIResult, cache::update_file_cache, cache::update_file_caches, cache::update_metadata_file_cache,
log_server_error, result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MerchandiseList { pub struct MerchandiseList {
pub id: Option<i32>, pub id: Option<i32>,
pub shop_id: i32, pub shop_id: i32,
pub form_list: Vec<Merchandise>, pub form_list: Vec<Merchandise>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Merchandise { pub struct Merchandise {
pub mod_name: String, pub mod_name: String,
pub local_form_id: u32, pub local_form_id: u32,
@ -77,6 +78,7 @@ pub struct RawMerchandiseVec {
pub cap: usize, pub cap: usize,
} }
// TODO: delete me if unused
#[no_mangle] #[no_mangle]
pub extern "C" fn create_merchandise_list( pub extern "C" fn create_merchandise_list(
api_url: *const c_char, api_url: *const c_char,
@ -116,13 +118,20 @@ pub extern "C" fn create_merchandise_list(
.json(&merchandise_list) .json(&merchandise_list)
.send()?; .send()?;
info!("create merchandise_list response from api: {:?}", &resp); info!("create merchandise_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?; let json: MerchandiseList = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id { if let Some(id) = json.id {
update_file_cache( update_file_cache(
&file_cache_dir(api_url)?.join(format!("merchandise_list_{}.json", id)), &cache_dir.join(format!("merchandise_list_{}.json", id)),
&bytes, &bytes,
)?; )?;
update_metadata_file_cache(
&cache_dir.join(format!("merchandise_list_{}_metadata.json", id)),
&headers,
)?;
} }
Ok(json) Ok(json)
} }
@ -194,14 +203,13 @@ pub extern "C" fn update_merchandise_list(
.json(&merchandise_list) .json(&merchandise_list)
.send()?; .send()?;
info!("update merchandise_list response from api: {:?}", &resp); info!("update merchandise_list response from api: {:?}", &resp);
let bytes = resp.bytes()?;
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shops_{}_merchandise_list.json", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shops_{}_merchandise_list_metadata.json", shop_id));
let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?; let json: MerchandiseList = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
update_file_cache(
&file_cache_dir(api_url)?.join(format!("shops_{}_merchandise_list.json", id)),
&bytes,
)?;
}
Ok(json) Ok(json)
} }
@ -232,6 +240,7 @@ pub extern "C" fn update_merchandise_list(
} }
} }
// TODO: delete me if unused
#[no_mangle] #[no_mangle]
pub extern "C" fn get_merchandise_list( pub extern "C" fn get_merchandise_list(
api_url: *const c_char, api_url: *const c_char,
@ -256,25 +265,38 @@ pub extern "C" fn get_merchandise_list(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = let cache_dir = file_cache_dir(api_url)?;
file_cache_dir(api_url)?.join(format!("merchandise_list_{}.json", merchandise_list_id)); let body_cache_path =
cache_dir.join(format!("merchandise_list_{}.json", merchandise_list_id));
let metadata_cache_path = cache_dir.join(format!(
"merchandise_list_{}_metadata.json",
merchandise_list_id
));
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!("get_merchandise_list response from api: {:?}", &resp); info!("get_merchandise_list response from api: {:?}", &resp);
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
error!("get_merchandise_list api request error: {}", err); error!("get_merchandise_list api request error: {}", err);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }
@ -336,28 +358,38 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = let cache_dir = file_cache_dir(api_url)?;
file_cache_dir(api_url)?.join(format!("shops_{}_merchandise_list.json", shop_id)); let body_cache_path = cache_dir.join(format!("shops_{}_merchandise_list.json", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shops_{}_merchandise_list_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!( info!(
"get_merchandise_list_by_shop_id response from api: {:?}", "get_merchandise_list_by_shop_id response from api: {:?}",
&resp &resp
); );
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
error!("get_merchandise_list_by_shop_id api request error: {}", err); error!("get_merchandise_list_by_shop_id api request error: {}", err);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }

View File

@ -9,7 +9,10 @@ use log::{error, info};
#[cfg(test)] #[cfg(test)]
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{cache::file_cache_dir, cache::update_file_cache, result::FFIResult}; use crate::{
cache::file_cache_dir, cache::update_file_cache, cache::update_file_caches,
cache::update_metadata_file_cache, result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Owner { pub struct Owner {
@ -69,12 +72,16 @@ pub extern "C" fn create_owner(
.json(&owner) .json(&owner)
.send()?; .send()?;
info!("create owner response from api: {:?}", &resp); info!("create owner response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Owner = serde_json::from_slice(&bytes)?; let json: Owner = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id { if let Some(id) = json.id {
update_file_cache( update_file_cache(&cache_dir.join(format!("owner_{}.json", id)), &bytes)?;
&file_cache_dir(api_url)?.join(format!("owner_{}.json", id)), update_metadata_file_cache(
&bytes, &cache_dir.join(format!("owner_{}_metadata.json", id)),
&headers,
)?; )?;
} }
Ok(json) Ok(json)
@ -144,14 +151,12 @@ pub extern "C" fn update_owner(
.json(&owner) .json(&owner)
.send()?; .send()?;
info!("update owner response from api: {:?}", &resp); info!("update owner response from api: {:?}", &resp);
let bytes = resp.bytes()?;
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("owner_{}.json", id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id));
let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
let json: Owner = serde_json::from_slice(&bytes)?; let json: Owner = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
update_file_cache(
&file_cache_dir(api_url)?.join(format!("owner_{}.json", id)),
&bytes,
)?;
}
Ok(json) Ok(json)
} else { } else {
Err(anyhow!("api-key not defined")) Err(anyhow!("api-key not defined"))

View File

@ -1,7 +1,7 @@
use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char}; use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use reqwest::Url; use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(test))] #[cfg(not(test))]
@ -10,8 +10,9 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::update_file_cache, log_server_error, cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
result::FFIResult, cache::update_file_cache, cache::update_file_caches, cache::update_metadata_file_cache,
log_server_error, result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -110,12 +111,16 @@ pub extern "C" fn create_shop(
.json(&shop) .json(&shop)
.send()?; .send()?;
info!("create shop response from api: {:?}", &resp); info!("create shop response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Shop = serde_json::from_slice(&bytes)?; let json: Shop = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id { if let Some(id) = json.id {
update_file_cache( update_file_cache(&cache_dir.join(format!("shop_{}.json", id)), &bytes)?;
&file_cache_dir(api_url)?.join(format!("shop_{}.json", id)), update_metadata_file_cache(
&bytes, &cache_dir.join(format!("shop_{}_metadata.json", id)),
&headers,
)?; )?;
} }
Ok(json) Ok(json)
@ -179,14 +184,12 @@ pub extern "C" fn update_shop(
.json(&shop) .json(&shop)
.send()?; .send()?;
info!("update shop response from api: {:?}", &resp); info!("update shop response from api: {:?}", &resp);
let bytes = resp.bytes()?;
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}.json", id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", id));
let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
let json: Shop = serde_json::from_slice(&bytes)?; let json: Shop = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
update_file_cache(
&file_cache_dir(api_url)?.join(format!("shop_{}.json", id)),
&bytes,
)?;
}
Ok(json) Ok(json)
} }
@ -237,24 +240,34 @@ pub extern "C" fn get_shop(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = file_cache_dir(api_url)?.join(format!("shop_{}.json", shop_id)); let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}.json", shop_id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!("get_shop response from api: {:?}", &resp); info!("get_shop response from api: {:?}", &resp);
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
error!("get_shop api request error: {}", err); error!("get_shop api request error: {}", err);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }
@ -301,24 +314,34 @@ pub extern "C" fn list_shops(
info!("api_url: {:?}", url); info!("api_url: {:?}", url);
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_path = file_cache_dir(api_url)?.join("shops.json"); let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join("shops.json");
let metadata_cache_path = cache_dir.join("shops_metadata.json");
let mut request = client.get(url).header("Api-Key", api_key);
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
request = request.header("If-None-Match", etag);
}
}
match client.get(url).header("Api-Key", api_key).send() { match request.send() {
Ok(resp) => { Ok(resp) => {
info!("list_shops response from api: {:?}", &resp); info!("list_shops response from api: {:?}", &resp);
if resp.status().is_success() { if resp.status().is_success() {
let bytes = resp.bytes()?; let bytes = update_file_caches(&body_cache_path, &metadata_cache_path, resp)?;
update_file_cache(&cache_path, &bytes)?;
let json = serde_json::from_slice(&bytes)?; let json = serde_json::from_slice(&bytes)?;
Ok(json) Ok(json)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else { } else {
log_server_error(resp); log_server_error(resp);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
Err(err) => { Err(err) => {
error!("list_shops api request error: {}", err); error!("list_shops api request error: {}", err);
from_file_cache(&cache_path) from_file_cache(&body_cache_path)
} }
} }
} }

View File

@ -11,8 +11,8 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::update_file_cache, log_server_error, cache::file_cache_dir, cache::from_file_cache, cache::update_file_cache,
result::FFIResult, cache::update_metadata_file_cache, log_server_error, result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -157,31 +157,43 @@ pub extern "C" fn create_transaction(
.json(&transaction) .json(&transaction)
.send()?; .send()?;
info!("create transaction response from api: {:?}", &resp); info!("create transaction response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let status = resp.status(); let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
if status.is_success() { if status.is_success() {
let json: Transaction = serde_json::from_slice(&bytes)?; let json: Transaction = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id { if let Some(id) = json.id {
update_file_cache( update_file_cache(&cache_dir.join(format!("transaction_{}.json", id)), &bytes)?;
&file_cache_dir(api_url)?.join(format!("transaction_{}.json", id)), update_metadata_file_cache(
&bytes, &cache_dir.join(format!("transaction_{}_metadata.json", id)),
&headers,
)?; )?;
} }
Ok(json) Ok(json)
} else { } else {
// TODO: abstract this away into a separate helper
match serde_json::from_slice::<HttpApiProblem>(&bytes) { match serde_json::from_slice::<HttpApiProblem>(&bytes) {
Ok(api_problem) => { Ok(api_problem) => {
let detail = api_problem.detail.unwrap_or("".to_string()); let detail = api_problem.detail.unwrap_or("".to_string());
error!("Server {} error: {}. {}", status, api_problem.title, detail); error!(
"Server {}: {}. {}",
status.as_u16(),
api_problem.title,
detail
);
Err(anyhow!(format!( Err(anyhow!(format!(
"Server {} error: {}. {}", "Server {}: {}. {}",
status, api_problem.title, detail status.as_u16(),
api_problem.title,
detail
))) )))
} }
Err(_) => { Err(_) => {
let detail = str::from_utf8(&bytes).unwrap_or("unknown"); let detail = str::from_utf8(&bytes).unwrap_or("unknown");
error!("Server {} error: {}", status, detail); error!("Server {}: {}", status.as_u16(), detail);
Err(anyhow!(format!("Server {} error: {}", status, detail))) Err(anyhow!(format!("Server {}: {}", status.as_u16(), detail)))
} }
} }
} }
@ -293,7 +305,15 @@ mod tests {
fn test_create_transaction_server_error() { fn test_create_transaction_server_error() {
let mock = mock("POST", "/v1/transactions") let mock = mock("POST", "/v1/transactions")
.with_status(500) .with_status(500)
.with_body("Internal Server Error") .with_header("content-type", "application/problem+json")
.with_body(
r#"{
"detail": "Some error detail",
"instance": "https://httpstatuses.com/500",
"status": 500,
"title": "Internal Server Error"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -323,7 +343,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1" "Server 500: Internal Server Error. Some error detail"
); );
} }
} }