[bun dev] Implement hash: namespace for file loader to improve browser cache invalidation

This appends a hash to URLs and import paths

In `bun dev`, this means:
  `/foo.woff2` => `/hash:/foo.woff2`

`bun dev` simply ignores this.
This commit is contained in:
Jarred Sumner
2022-02-10 01:37:23 -08:00
parent bcdd2cf220
commit 2e2521c638
3 changed files with 70 additions and 22 deletions

View File

@@ -662,14 +662,26 @@ pub const FileSystem = struct {
mtime: i128 = 0,
mode: std.fs.File.Mode = 0,
threadlocal var hash_bytes: [32]u8 = undefined;
threadlocal var hash_name_buf: [1024]u8 = undefined;
pub fn hashName(
this: *const ModKey,
basename: string,
) !string {
return try std.fmt.bufPrint(
&hash_name_buf,
"{s}-{x}",
.{
basename,
this.hash(),
},
);
}
pub fn hash(
this: *const ModKey,
) u64 {
var hash_bytes: [32]u8 = undefined;
// We shouldn't just read the contents of the ModKey into memory
// The hash should be deterministic across computers and operating systems.
// inode is non-deterministic across volumes within the same compuiter
@@ -679,15 +691,10 @@ pub const FileSystem = struct {
std.mem.writeIntNative(@TypeOf(this.size), hash_bytes_remain[0..@sizeOf(@TypeOf(this.size))], this.size);
hash_bytes_remain = hash_bytes_remain[@sizeOf(@TypeOf(this.size))..];
std.mem.writeIntNative(@TypeOf(this.mtime), hash_bytes_remain[0..@sizeOf(@TypeOf(this.mtime))], this.mtime);
return try std.fmt.bufPrint(
&hash_name_buf,
"{s}-{x}",
.{
basename,
@truncate(u32, std.hash.Wyhash.hash(1, &hash_bytes)),
},
);
hash_bytes_remain = hash_bytes_remain[@sizeOf(@TypeOf(this.mtime))..];
std.debug.assert(hash_bytes_remain.len == 8);
hash_bytes_remain[0..8].* = @bitCast([8]u8, @as(u64, 0));
return std.hash.Wyhash.hash(0, &hash_bytes);
}
pub fn generate(_: *RealFS, _: string, file: std.fs.File) anyerror!ModKey {

View File

@@ -2636,9 +2636,26 @@ pub const RequestContext = struct {
return true;
}
if (ctx.url.path.len > "blob:".len and strings.eqlComptimeIgnoreLen(ctx.url.path[0.."blob:".len], "blob:")) {
try ctx.handleBlobURL(server);
return true;
if (ctx.url.path.len > "blob:".len) {
if (strings.eqlComptimeIgnoreLen(ctx.url.path[0.."blob:".len], "blob:")) {
try ctx.handleBlobURL(server);
return true;
}
// From HTTP, we serve files with a hash modkey
// The format is
// hash:${hash}/${ORIGINAL_PATH}
// hash:abcdefg123/app/foo/my-file.jpeg
// The hash exists for browser cache invalidation
if (strings.eqlComptimeIgnoreLen(ctx.url.path[0.."hash:".len], "hash:")) {
var current = ctx.url.path;
current = current["hash:".len..];
if (strings.indexOfChar(current, '/')) |i| {
current = current[i + 1 ..];
ctx.url.path = current;
return false;
}
}
}
const isMaybePrefix = ctx.url.path.len > "bun:".len;

View File

@@ -92,6 +92,20 @@ pub const Linker = struct {
};
}
pub fn getModKey(
this: *ThisLinker,
file_path: Fs.Path,
fd: ?FileDescriptorType,
) !Fs.FileSystem.RealFS.ModKey {
var file: std.fs.File = if (fd) |_fd| std.fs.File{ .handle = _fd } else try std.fs.openFileAbsolute(file_path.text, .{ .read = true });
Fs.FileSystem.setMaxFd(file.handle);
const modkey = try Fs.FileSystem.RealFS.ModKey.generate(&this.fs.fs, file_path.text, file);
if (fd == null)
file.close();
return modkey;
}
pub fn getHashedFilename(
this: *ThisLinker,
file_path: Fs.Path,
@@ -104,20 +118,15 @@ pub const Linker = struct {
return hashed_result.value_ptr.*;
}
}
var file: std.fs.File = if (fd) |_fd| std.fs.File{ .handle = _fd } else try std.fs.openFileAbsolute(file_path.text, .{ .read = true });
Fs.FileSystem.setMaxFd(file.handle);
var modkey = try Fs.FileSystem.RealFS.ModKey.generate(&this.fs.fs, file_path.text, file);
const hash_name = try modkey.hashName(file_path.name.base);
const modkey = try this.getModKey(file_path, fd);
const hash_name = modkey.hashName(file_path.text);
if (Bundler.isCacheEnabled) {
var hashed = std.hash.Wyhash.hash(0, file_path.text);
try this.hashed_filenames.put(hashed, try this.allocator.dupe(u8, hash_name));
}
if (this.fs.fs.needToCloseFiles() and fd == null) {
file.close();
}
return hash_name;
}
@@ -651,6 +660,21 @@ pub const Linker = struct {
if (use_hashed_name) {
var basepath = Fs.Path.init(source_path);
if (linker.options.serve) {
var hash_buf: [64]u8 = undefined;
const modkey = try linker.getModKey(basepath, null);
return Fs.Path.init(try origin.joinAlloc(
linker.allocator,
std.fmt.bufPrint(&hash_buf, "hash:{x}/", .{modkey.hash()}) catch unreachable,
dirname,
basename,
absolute_pathname.ext,
source_path,
));
}
basename = try linker.getHashedFilename(basepath, null);
}
@@ -690,7 +714,7 @@ pub const Linker = struct {
import_record.path = try linker.generateImportPath(
source_dir,
if (path.is_symlink and import_path_format == .absolute_url and linker.options.platform.isNotBun()) path.pretty else path.text,
Bundler.isCacheEnabled and loader == .file,
loader == .file,
path.namespace,
origin,
import_path_format,