///|
/// SQLite-backed HTTP cache for VRT/test persistence.
pub struct SqliteCacheBackend {
db : @sqlite.Database
}
///|
pub impl @http.HttpCacheBackend for SqliteCacheBackend with lookup(
self : SqliteCacheBackend,
url : String,
) -> @http.CacheEntry? {
let stmt = match
self.db.prepare(
"SELECT url, status, headers, body, etag, last_modified, cache_control, stored_at FROM http_cache WHERE url = ?",
) {
Some(s) => s
None => return None
}
stmt.bind(1, @sqlite.Text(@sqlite.string_to_bytes(url))) |> ignore
if stmt.step() {
let entry_url = @sqlite.bytes_to_string(stmt.column_text(0))
let status = stmt.column_int(1)
let headers_json = @sqlite.bytes_to_string(stmt.column_text(2))
let body = @sqlite.bytes_to_string(stmt.column_text(3))
let etag = sqlite_optional_text(stmt, 4)
let last_modified = sqlite_optional_text(stmt, 5)
let cache_control = @sqlite.bytes_to_string(stmt.column_text(6))
let stored_at = match stmt.column(7) {
@sqlite.Double(v) => v
@sqlite.Int(v) => v.to_double()
_ => 0.0
}
stmt.finalize()
let headers = sqlite_parse_headers(headers_json)
let directives = @http.parse_cache_control(cache_control)
Some({
url: entry_url,
status,
headers,
body,
etag,
last_modified,
directives,
stored_at,
})
} else {
stmt.finalize()
None
}
}
///|
pub impl @http.HttpCacheBackend for SqliteCacheBackend with store(
self : SqliteCacheBackend,
entry : @http.CacheEntry,
) -> Unit {
let stmt = match
self.db.prepare(
"INSERT OR REPLACE INTO http_cache (url, status, headers, body, etag, last_modified, cache_control, stored_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
) {
Some(s) => s
None => return
}
let cc = render_cache_control(entry.directives)
stmt.bind(1, @sqlite.Text(@sqlite.string_to_bytes(entry.url))) |> ignore
stmt.bind(2, @sqlite.Int(entry.status)) |> ignore
stmt.bind(
3,
@sqlite.Text(@sqlite.string_to_bytes(@http.headers_to_json(entry.headers))),
)
|> ignore
stmt.bind(4, @sqlite.Text(@sqlite.string_to_bytes(entry.body))) |> ignore
stmt.bind(
5,
match entry.etag {
Some(e) => @sqlite.Text(@sqlite.string_to_bytes(e))
None => @sqlite.Null
},
)
|> ignore
stmt.bind(
6,
match entry.last_modified {
Some(lm) => @sqlite.Text(@sqlite.string_to_bytes(lm))
None => @sqlite.Null
},
)
|> ignore
stmt.bind(7, @sqlite.Text(@sqlite.string_to_bytes(cc))) |> ignore
stmt.bind(8, @sqlite.Double(entry.stored_at)) |> ignore
stmt.execute() |> ignore
stmt.finalize()
}
///|
pub impl @http.HttpCacheBackend for SqliteCacheBackend with remove(
self : SqliteCacheBackend,
url : String,
) -> Unit {
let stmt = match self.db.prepare("DELETE FROM http_cache WHERE url = ?") {
Some(s) => s
None => return
}
stmt.bind(1, @sqlite.Text(@sqlite.string_to_bytes(url))) |> ignore
stmt.execute() |> ignore
stmt.finalize()
}
///|
pub impl @http.HttpCacheBackend for SqliteCacheBackend with clear(
self : SqliteCacheBackend,
) -> Unit {
self.db.exec("DELETE FROM http_cache") |> ignore
}
///|
pub fn SqliteCacheBackend::new(db_path : String) -> SqliteCacheBackend {
let db = match @sqlite.Database::open(db_path) {
Some(db) => db
None => panic()
}
db.exec(
"CREATE TABLE IF NOT EXISTS http_cache (url TEXT PRIMARY KEY, status INTEGER NOT NULL, headers TEXT NOT NULL, body TEXT, etag TEXT, last_modified TEXT, cache_control TEXT, stored_at REAL NOT NULL)",
)
|> ignore
{ db, }
}
///|
pub fn SqliteCacheBackend::lookup(
self : SqliteCacheBackend,
url : String,
) -> @http.CacheEntry? {
@http.HttpCacheBackend::lookup(self, url)
}
///|
pub fn SqliteCacheBackend::store(
self : SqliteCacheBackend,
entry : @http.CacheEntry,
) -> Unit {
@http.HttpCacheBackend::store(self, entry)
}
///|
pub fn SqliteCacheBackend::remove(
self : SqliteCacheBackend,
url : String,
) -> Unit {
@http.HttpCacheBackend::remove(self, url)
}
///|
pub fn SqliteCacheBackend::clear(self : SqliteCacheBackend) -> Unit {
@http.HttpCacheBackend::clear(self)
}
///|
fn sqlite_optional_text(stmt : @sqlite.Statement, col : Int) -> String? {
match stmt.column(col) {
@sqlite.Text(bytes) => {
let s = @sqlite.bytes_to_string(bytes)
if s.is_empty() {
None
} else {
Some(s)
}
}
@sqlite.Null => None
_ => None
}
}
///|
fn sqlite_parse_headers(json_str : String) -> Map[String, String] {
let headers : Map[String, String] = {}
if json_str.is_empty() || json_str == "{}" {
return headers
}
let json = @json.parse(json_str) catch { _ => return headers }
match json {
Object(map) =>
map.each(fn(k, v) {
match v {
String(s) => headers[k] = s
_ => ()
}
})
_ => ()
}
headers
}
///|
fn render_cache_control(directives : @http.CacheDirectives) -> String {
let parts : Array[String] = []
if directives.public_ {
parts.push("public")
}
if directives.private_ {
parts.push("private")
}
match directives.max_age {
Some(max_age) => parts.push("max-age=" + max_age.to_string())
None => ()
}
if directives.no_cache {
parts.push("no-cache")
}
if directives.no_store {
parts.push("no-store")
}
if directives.must_revalidate {
parts.push("must-revalidate")
}
if directives.immutable {
parts.push("immutable")
}
parts.join(", ")
}