mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
add basic FsCache impl
This commit is contained in:
239
src/network/cache/FsCache.zig
vendored
Normal file
239
src/network/cache/FsCache.zig
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Cache = @import("Cache.zig");
|
||||
const CachedMetadata = Cache.CachedMetadata;
|
||||
const CachedResponse = Cache.CachedResponse;
|
||||
|
||||
pub const FsCache = @This();
|
||||
|
||||
dir: std.fs.Dir,
|
||||
|
||||
pub fn init(path: []const u8) !FsCache {
|
||||
const cwd = std.fs.cwd();
|
||||
|
||||
cwd.makeDir(path) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const dir = try cwd.openDir(path, .{ .iterate = true });
|
||||
return .{ .dir = dir };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FsCache) void {
|
||||
self.dir.close();
|
||||
}
|
||||
|
||||
pub fn cache(self: *FsCache) Cache {
|
||||
return Cache.init(self);
|
||||
}
|
||||
|
||||
const HASHED_KEY_LEN = 16;
|
||||
const HASHED_PATH_LEN = HASHED_KEY_LEN + 5;
|
||||
const HASHED_TMP_PATH_LEN = HASHED_PATH_LEN + 4;
|
||||
|
||||
fn hashKey(key: []const u8) [HASHED_KEY_LEN]u8 {
|
||||
const h = std.hash.Wyhash.hash(0, key);
|
||||
var hex: [HASHED_KEY_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&hex, "{x:0>16}", .{h}) catch unreachable;
|
||||
return hex;
|
||||
}
|
||||
|
||||
fn serializeMeta(writer: *std.Io.Writer, meta: *const CachedMetadata) !void {
|
||||
try writer.print("{s}\n{s}\n", .{ meta.url, meta.content_type });
|
||||
try writer.print("{d}\n{d}\n{d}\n{d}\n", .{
|
||||
meta.status,
|
||||
meta.stored_at,
|
||||
meta.age_at_store,
|
||||
meta.max_age,
|
||||
});
|
||||
try writer.print("{s}\n", .{meta.etag orelse "null"});
|
||||
try writer.print("{s}\n", .{meta.last_modified orelse "null"});
|
||||
try writer.print("{s}\n", .{meta.vary orelse "null"});
|
||||
try writer.print("{}\n{}\n{}\n", .{
|
||||
meta.must_revalidate,
|
||||
meta.no_cache,
|
||||
meta.immutable,
|
||||
});
|
||||
try writer.flush();
|
||||
}
|
||||
|
||||
fn deserializeMetaOptionalString(bytes: []const u8) ?[]const u8 {
|
||||
if (std.mem.eql(u8, bytes, "null")) return null else return bytes;
|
||||
}
|
||||
|
||||
fn deserializeMetaBoolean(bytes: []const u8) !bool {
|
||||
if (std.mem.eql(u8, bytes, "true")) return true;
|
||||
if (std.mem.eql(u8, bytes, "false")) return false;
|
||||
return error.Malformed;
|
||||
}
|
||||
|
||||
fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetadata {
|
||||
var file_buf: [1024]u8 = undefined;
|
||||
var file_reader = file.reader(&file_buf);
|
||||
const reader = &file_reader.interface;
|
||||
|
||||
const url = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk try allocator.dupeZ(u8, line);
|
||||
};
|
||||
errdefer allocator.free(url);
|
||||
|
||||
const content_type = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk try allocator.dupe(u8, line);
|
||||
};
|
||||
errdefer allocator.free(content_type);
|
||||
|
||||
const status = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk std.fmt.parseInt(u16, line, 10) catch return error.Malformed;
|
||||
};
|
||||
const stored_at = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk std.fmt.parseInt(i64, line, 10) catch return error.Malformed;
|
||||
};
|
||||
const age_at_store = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk std.fmt.parseInt(u64, line, 10) catch return error.Malformed;
|
||||
};
|
||||
const max_age = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk std.fmt.parseInt(u64, line, 10) catch return error.Malformed;
|
||||
};
|
||||
|
||||
const etag = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk if (std.mem.eql(u8, line, "null")) null else try allocator.dupe(u8, line);
|
||||
};
|
||||
const last_modified = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk if (std.mem.eql(u8, line, "null")) null else try allocator.dupe(u8, line);
|
||||
};
|
||||
const vary = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk if (std.mem.eql(u8, line, "null")) null else try allocator.dupe(u8, line);
|
||||
};
|
||||
|
||||
const must_revalidate = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk try deserializeMetaBoolean(line);
|
||||
};
|
||||
const no_cache = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk try deserializeMetaBoolean(line);
|
||||
};
|
||||
const immutable = blk: {
|
||||
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
|
||||
break :blk try deserializeMetaBoolean(line);
|
||||
};
|
||||
|
||||
return .{
|
||||
.url = url,
|
||||
.content_type = content_type,
|
||||
.status = status,
|
||||
.stored_at = stored_at,
|
||||
.age_at_store = age_at_store,
|
||||
.max_age = max_age,
|
||||
.etag = etag,
|
||||
.last_modified = last_modified,
|
||||
.must_revalidate = must_revalidate,
|
||||
.no_cache = no_cache,
|
||||
.immutable = immutable,
|
||||
.vary = vary,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get(self: *FsCache, allocator: std.mem.Allocator, key: []const u8) ?Cache.CachedResponse {
|
||||
const hashed_key = hashKey(key);
|
||||
|
||||
var meta_path: [HASHED_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&meta_path, "{s}.meta", .{hashed_key}) catch @panic("FsCache.get meta path overflowed");
|
||||
|
||||
var body_path: [HASHED_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&body_path, "{s}.body", .{hashed_key}) catch @panic("FsCache.get body path overflowed");
|
||||
|
||||
const meta_file = self.dir.openFile(&meta_path, .{ .mode = .read_only }) catch return null;
|
||||
defer meta_file.close();
|
||||
|
||||
const meta = deserializeMeta(allocator, meta_file) catch {
|
||||
self.dir.deleteFile(&meta_path) catch {};
|
||||
self.dir.deleteFile(&body_path) catch {};
|
||||
return null;
|
||||
};
|
||||
|
||||
const body_file = self.dir.openFile(&body_path, .{ .mode = .read_only }) catch return null;
|
||||
|
||||
return .{
|
||||
.metadata = meta,
|
||||
.data = .{ .file = body_file },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn put(self: *FsCache, key: []const u8, meta: CachedMetadata, body: []const u8) !void {
|
||||
const hashed_key = hashKey(key);
|
||||
|
||||
// Write meta to a temp file, then atomically rename into place
|
||||
var meta_path: [HASHED_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&meta_path, "{s}.meta", .{hashed_key}) catch
|
||||
@panic("FsCache.put meta path overflowed");
|
||||
|
||||
var meta_tmp_path: [HASHED_TMP_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&meta_tmp_path, "{s}.meta.tmp", .{hashed_key}) catch
|
||||
@panic("FsCache.put meta tmp path overflowed");
|
||||
|
||||
{
|
||||
const meta_file = try self.dir.createFile(&meta_tmp_path, .{});
|
||||
errdefer {
|
||||
meta_file.close();
|
||||
self.dir.deleteFile(&meta_tmp_path) catch {};
|
||||
}
|
||||
|
||||
var buf: [512]u8 = undefined;
|
||||
var meta_file_writer = meta_file.writer(&buf);
|
||||
try serializeMeta(&meta_file_writer.interface, &meta);
|
||||
meta_file.close();
|
||||
}
|
||||
errdefer self.dir.deleteFile(&meta_tmp_path) catch {};
|
||||
try self.dir.rename(&meta_tmp_path, &meta_path);
|
||||
|
||||
// Write body to a temp file, then atomically rename into place
|
||||
var body_path: [HASHED_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&body_path, "{s}.body", .{hashed_key}) catch
|
||||
@panic("FsCache.put body path overflowed");
|
||||
|
||||
var body_tmp_path: [HASHED_TMP_PATH_LEN]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&body_tmp_path, "{s}.body.tmp", .{hashed_key}) catch
|
||||
@panic("FsCache.put body tmp path overflowed");
|
||||
|
||||
{
|
||||
const body_file = try self.dir.createFile(&body_tmp_path, .{});
|
||||
errdefer {
|
||||
body_file.close();
|
||||
self.dir.deleteFile(&body_tmp_path) catch {};
|
||||
}
|
||||
try body_file.writeAll(body);
|
||||
body_file.close();
|
||||
}
|
||||
errdefer self.dir.deleteFile(&body_tmp_path) catch {};
|
||||
|
||||
errdefer self.dir.deleteFile(&meta_path) catch {};
|
||||
try self.dir.rename(&body_tmp_path, &body_path);
|
||||
}
|
||||
Reference in New Issue
Block a user