Implement MIME type inference

When serving content from `public/`, use MIME type lookup to provide an
appropriate content-type header.

MIME types borrowed from:

  https://mimetype.io/all-types
  https://github.com/patrickmccallum/mimetype-io/blob/master/src/mimeData.json
This commit is contained in:
Bob Farrell 2024-02-26 22:38:43 +00:00
parent be85c13369
commit d855b9f703
11 changed files with 14008 additions and 10 deletions

View File

@ -32,10 +32,11 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :white_check_mark: Request/response headers.
* :white_check_mark: Stack trace output on error.
* :white_check_mark: Static content generation.
* :x: Param/JSON payload parsing/abstracting.
* :x: Static content paramater definitions.
* :white_check_mark: Param/JSON payload parsing/abstracting.
* :white_check_mark: Static content parameter definitions.
* :white_check_mark: Middleware interface.
* :white_check_mark: MIME type inference.
* :x: Environment configurations (develompent/production/etc.)
* :x: Middleware extensions (for e.g. authentication).
* :x: Email delivery.
* :x: Custom/non-conventional routes.
* :x: General-purpose cache.

View File

@ -1,6 +1,7 @@
const std = @import("std");
pub const GenerateRoutes = @import("src/GenerateRoutes.zig");
pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
pub const TemplateFn = @import("src/jetzig.zig").TemplateFn;
pub const StaticRequest = @import("src/jetzig.zig").StaticRequest;
pub const http = @import("src/jetzig/http.zig");
@ -20,9 +21,12 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
const mime_module = try GenerateMimeTypes.generateMimeModule(b);
b.installArtifact(lib);
const jetzig_module = b.addModule("jetzig", .{ .root_source_file = .{ .path = "src/jetzig.zig" } });
jetzig_module.addImport("mime_types", mime_module);
lib.root_module.addImport("jetzig", jetzig_module);
const zmpl_dep = b.dependency(

View File

@ -3,8 +3,8 @@
.version = "0.0.0",
.dependencies = .{
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/41ac97a026e124f93d316eb838fa015a5a52bb15.tar.gz",
.hash = "122053b4c76247572201afbbec8fbde8c014c25984a3ca780ed4f6d514cc939038df",
.url = "https://github.com/jetzig-framework/zmpl/archive/84a712349e0cf679fc5c9900b805335d51d9ce86.tar.gz",
.hash = "1220e9f2133f6cd24c370850cbe3816e3d8b97c33dd822bf7eaf8f6f0ea2cfd2f8db",
},
},

0
demo/public/styles.css Normal file
View File

47
src/GenerateMimeTypes.zig Normal file
View File

@ -0,0 +1,47 @@
const std = @import("std");
const JsonMimeType = struct {
name: []const u8,
fileTypes: [][]const u8,
};
/// Invoked at build time to parse mimeData.json into an array of `MimeType` which can then be
/// written out as a Zig struct and imported at runtime.
pub fn generateMimeModule(build: *std.Build) !*std.Build.Module {
const file = try std.fs.openFileAbsolute(build.pathFromRoot("src/jetzig/http/mime/mimeData.json"), .{});
const stat = try file.stat();
const json = try file.readToEndAlloc(build.allocator, stat.size);
defer build.allocator.free(json);
const parsed_mime_types = try std.json.parseFromSlice(
[]JsonMimeType,
build.allocator,
json,
.{ .ignore_unknown_fields = true },
);
var buf = std.ArrayList(u8).init(build.allocator);
defer buf.deinit();
const writer = buf.writer();
try writer.writeAll("pub const MimeType = struct { name: []const u8, file_type: []const u8 };");
try writer.writeAll("pub const mime_types = [_]MimeType{\n");
for (parsed_mime_types.value) |mime_type| {
for (mime_type.fileTypes) |file_type| {
const entry = try std.fmt.allocPrint(
build.allocator,
\\.{{ .name = "{s}", .file_type = "{s}" }},
\\
,
.{ mime_type.name, file_type },
);
try writer.writeAll(entry);
}
}
try writer.writeAll("};\n");
const write_files = build.addWriteFiles();
const generated_file = write_files.add("mime_types.zig", buf.items);
return build.createModule(.{ .root_source_file = generated_file });
}

View File

@ -33,6 +33,7 @@ pub const View = views.View;
pub const config = struct {
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
pub const public_content = .{ .path = "public" };
};
pub fn init(allocator: std.mem.Allocator) !App {

View File

@ -1,6 +1,7 @@
const std = @import("std");
const jetzig = @import("../jetzig.zig");
const mime_types = @import("mime_types").mime_types; // Generated at build time.
const Self = @This();
@ -18,6 +19,10 @@ pub fn deinit(self: Self) void {
/// automatically created at build time. `templates` should be
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
var mime_map = jetzig.http.mime.MimeMap.init(self.allocator);
defer mime_map.deinit();
try mime_map.build();
var server = jetzig.http.Server.init(
self.allocator,
self.host,
@ -25,6 +30,7 @@ pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.Templ
self.server_options,
routes,
templates,
&mime_map,
);
for (routes) |*route| {

View File

@ -10,3 +10,4 @@ pub const Headers = @import("http/Headers.zig");
pub const Query = @import("http/Query.zig");
pub const status_codes = @import("http/status_codes.zig");
pub const middleware = @import("http/middleware.zig");
pub const mime = @import("http/mime.zig");

View File

@ -26,6 +26,7 @@ options: ServerOptions,
start_time: i128 = undefined,
routes: []jetzig.views.Route,
templates: []jetzig.TemplateFn,
mime_map: *jetzig.http.mime.MimeMap,
const Self = @This();
@ -36,6 +37,7 @@ pub fn init(
options: ServerOptions,
routes: []jetzig.views.Route,
templates: []jetzig.TemplateFn,
mime_map: *jetzig.http.mime.MimeMap,
) Self {
const server = std.http.Server.init(.{ .reuse_address = true });
@ -49,6 +51,7 @@ pub fn init(
.options = options,
.routes = routes,
.templates = templates,
.mime_map = mime_map,
};
}
@ -329,8 +332,8 @@ fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?jetzig
const StaticResource = struct { content: []const u8, mime_type: []const u8 = "application/octet-stream" };
fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResource {
const public_content = try matchPublicContent(request);
if (public_content) |content| return .{ .content = content };
const public_resource = try self.matchPublicContent(request);
if (public_resource) |resource| return resource;
const static_content = try self.matchStaticContent(request);
if (static_content) |content| return .{
@ -344,11 +347,14 @@ fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResou
return null;
}
fn matchPublicContent(request: *jetzig.http.Request) !?[]const u8 {
fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResource {
if (request.path.len < 2) return null;
if (request.method != .GET) return null;
var iterable_dir = std.fs.cwd().openDir("public", .{ .iterate = true }) catch |err| {
var iterable_dir = std.fs.cwd().openDir(
jetzig.config.public_content.path,
.{ .iterate = true, .no_follow = true },
) catch |err| {
switch (err) {
error.FileNotFound => return null,
else => return err,
@ -363,11 +369,17 @@ fn matchPublicContent(request: *jetzig.http.Request) !?[]const u8 {
if (file.kind != .file) continue;
if (std.mem.eql(u8, file.path, request.path[1..])) {
return try iterable_dir.readFileAlloc(
const content = try iterable_dir.readFileAlloc(
request.allocator,
file.path,
jetzig.config.max_bytes_static_content,
);
const extension = std.fs.path.extension(file.path);
const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
return .{
.content = content,
.mime_type = mime_type,
};
}
}

54
src/jetzig/http/mime.zig Normal file
View File

@ -0,0 +1,54 @@
// Mime types borrowed from here:
// https://mimetype.io/all-types
// https://github.com/patrickmccallum/mimetype-io/blob/master/src/mimeData.json
const std = @import("std");
const mime_types = @import("mime_types").mime_types; // Generated at build time.
/// Provides information about a given MIME Type.
pub const MimeType = struct {
name: []const u8,
};
/// Attempts to map a given extension to a mime type.
pub fn fromExtension(extension: []const u8) ?MimeType {
for (mime_types) |mime_type| {
if (std.mem.eql(u8, extension, mime_type.file_type)) return .{ .name = mime_type.name };
}
return null;
}
pub const MimeMap = struct {
allocator: std.mem.Allocator,
map: std.StringHashMap([]const u8),
pub fn init(allocator: std.mem.Allocator) MimeMap {
return .{
.allocator = allocator,
.map = std.StringHashMap([]const u8).init(allocator),
};
}
pub fn deinit(self: *MimeMap) void {
var it = self.map.iterator();
while (it.next()) |item| {
self.allocator.free(item.key_ptr.*);
self.allocator.free(item.value_ptr.*);
}
self.map.deinit();
}
pub fn build(self: *MimeMap) !void {
for (mime_types) |mime_type| {
try self.map.put(
try self.allocator.dupe(u8, mime_type.file_type),
try self.allocator.dupe(u8, mime_type.name),
);
}
}
pub fn get(self: *MimeMap, file_type: []const u8) ?[]const u8 {
return self.map.get(file_type);
}
};

File diff suppressed because it is too large Load Diff