diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index d8c8979..d88175f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .zig-cache zig-out +src/embed.tar diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a59477d --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1775528787, + "narHash": "sha256-Ar1Y6AoM8lT+JT76g+C5IXVgrhXpFOpg4yMdDyvHU+A=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b1fffc96c6cdff8537861af4cca96946c59536de", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "zig2nix": "zig2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1775529733, + "narHash": "sha256-8JsTiowsvVwnyPGPwHtSvhMzoDkPl5ifnbMTtI+G/5Q=", + "owner": "Cloudef", + "repo": "zig2nix", + "rev": "0a5253a42ee278aed25bdff7c7d6b2e891fff28e", + "type": "github" + }, + "original": { + "owner": "Cloudef", + "repo": "zig2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..542f651 --- /dev/null +++ b/flake.nix @@ -0,0 +1,91 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + + zig2nix = { + url = "github:Cloudef/zig2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { zig2nix, ... }: + let + flake-utils = zig2nix.inputs.flake-utils; + in + (flake-utils.lib.eachDefaultSystem ( + system: + let + # Zig flake helper + # Check the flake.nix in zig2nix project for more options: + # + env = zig2nix.outputs.zig-env.${system} { }; + in + with builtins; + with env.pkgs.lib; + rec { + # Produces clean binaries meant to be ship'd outside of nix + # nix build .#foreign + packages.foreign = env.package { + src = cleanSource ./.; + + # Packages required for compiling + nativeBuildInputs = with env.pkgs; [ ]; + + # Packages required for linking + buildInputs = with env.pkgs; [ ]; + + # Smaller binaries and avoids shipping glibc. + zigPreferMusl = true; + }; + + # nix build . + packages.default = packages.foreign.override (attrs: { + # Prefer nix friendly settings. + zigPreferMusl = false; + + # Executables required for runtime + # These packages will be added to the PATH + zigWrapperBins = with env.pkgs; [ ]; + + # Libraries required for runtime + # These packages will be added to the LD_LIBRARY_PATH + zigWrapperLibs = attrs.buildInputs or [ ]; + }); + + # For bundling with nix bundle for running outside of nix + # example: https://github.com/ralismark/nix-appimage + apps.bundle = { + type = "app"; + program = "${packages.foreign}/bin/default"; + }; + + # nix run . + apps.default = env.app [ ] "zig build run -- \"$@\""; + + # nix run .#build + apps.build = env.app [ ] "zig build \"$@\""; + + # nix run .#test + apps.test = env.app [ ] "zig build test -- \"$@\""; + + # nix run .#docs + apps.docs = env.app [ ] "zig build docs -- \"$@\""; + + # nix run .#zig2nix + apps.zig2nix = env.app [ ] "zig2nix \"$@\""; + + # nix develop + devShells.default = env.mkShell { + # Packages required for compiling, linking and running + # Libraries added here will be automatically added to the LD_LIBRARY_PATH and PKG_CONFIG_PATH + nativeBuildInputs = + [ ] + ++ packages.default.nativeBuildInputs + ++ packages.default.buildInputs + ++ packages.default.zigWrapperBins + ++ packages.default.zigWrapperLibs; + }; + } + )); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..ed8b9a5 --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +run: + zig build run + +build: + nix build -L + +push: + @just build + nix copy --to ssh://ebil.club "$(readlink -f result)" + ssh ebil.club "rm -f /home/robin/maivi && ln -sf '$(readlink -f result)/bin/maivi' /home/robin/maivi" + ssh ebil.club "systemctl --user restart maivi" diff --git a/src/globals.zig b/src/globals.zig new file mode 100644 index 0000000..5170f93 --- /dev/null +++ b/src/globals.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +const debug_allocator_config: std.heap.DebugAllocatorConfig = .{}; + +pub var allocator: std.mem.Allocator = undefined; +var gpa: std.heap.DebugAllocator(debug_allocator_config) = undefined; + +pub fn init() void { + gpa = std.heap.GeneralPurposeAllocator(debug_allocator_config){}; + allocator = gpa.allocator(); +} + +pub fn deinit() std.heap.Check { + return gpa.deinit(); +} diff --git a/src/handler.zig b/src/handler.zig index 39997d2..894fa4b 100644 --- a/src/handler.zig +++ b/src/handler.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const globals = @import("globals.zig"); const logz = @import("logz"); const httpz = @import("httpz"); @@ -11,31 +12,27 @@ const Status = helpers.Status; const Self = @This(); -pub fn init(allocator: std.mem.Allocator, cfg: Config) !Self { +pub fn init(vfs: *Vfs, cfg: Config) !Self { return .{ - .allocator = allocator, - .logger = try logz.Pool.init(allocator, .{ .level = .Info }), + .vfs = vfs, + .logger = try logz.Pool.init(globals.allocator, .{ .level = .Info }), .config = cfg, - .vfs = Vfs.init(allocator), }; } pub fn new(self: *Self) Self { return .{ - .allocator = self.allocator, .logger = self.logger, }; } pub fn deinit(self: *Self) void { self.logger.deinit(); - self.vfs.deinit(); } -allocator: std.mem.Allocator, logger: *logz.Pool, config: Config, -vfs: Vfs, +vfs: *Vfs, pub fn logthis(self: *Self, req: *helpers.Request) logz.Logger { return self.logger.logger() @@ -46,7 +43,7 @@ pub fn logreq(self: *Self, req: *helpers.Request) logz.Logger { var l = self.logthis(req).level(.Info); var it = req.request.headers.iterator(); while (it.next()) |header| { - const k = std.mem.join(self.allocator, "/", &.{ "header", header.key }) catch header.key; + const k = std.mem.join(globals.allocator, "/", &.{ "header", header.key }) catch header.key; l = l.string(k, header.value); } return l diff --git a/src/helpers.zig b/src/helpers.zig index 5c142c4..82be1bc 100644 --- a/src/helpers.zig +++ b/src/helpers.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const globals = @import("globals.zig"); const httpz = @import("httpz"); @@ -69,10 +70,10 @@ pub const Response = struct { const size = stat.size; defer file.close(); - const buf = try h.allocator.alloc(u8, size); + const buf = try globals.allocator.alloc(u8, size); _ = try file.read(buf); - entry.value_ptr.* = Node.init(buf, httpz.ContentType.forFile(path)); + entry.value_ptr.* = Node.init(buf, content_type_for_file(path)); } self.response.content_type = entry.value_ptr.content_type orelse return error.NonFile; @@ -96,3 +97,14 @@ pub fn statusFromErr(err: Error) ?Status { else => null, }; } + +pub fn content_type_for_file(path: []const u8) httpz.ContentType { + if (std.mem.endsWith(u8, path, ".txt")) { + return httpz.ContentType.TEXT; + } + const t = httpz.ContentType.forFile(path); + return switch (t) { + .UNKNOWN => .TEXT, + else => t, + }; +} diff --git a/src/main.zig b/src/main.zig index 2968359..6b8c9ec 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const globals = @import("globals.zig"); const httpz = @import("httpz"); const maivi = @import("maivi"); @@ -7,22 +8,36 @@ const Handler = @import("handler.zig"); const helpers = @import("helpers.zig"); const Status = helpers.Status; +const Vfs = @import("vfs.zig"); +const Tar = @import("tar.zig"); + +const EMBED = @embedFile("embed.tar"); + pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer if (gpa.deinit() == .leak) { + globals.init(); + defer if (globals.deinit() == .leak) { std.log.err("Memory leak", .{}); }; - const allocator = gpa.allocator(); - const buf = try allocator.alloc(u8, 1028); + const buf = try globals.allocator.alloc(u8, 0x400); + defer globals.allocator.free(buf); const cwd = try std.process.getCwd(buf); - var handler = try Handler.init(allocator, .{ + var vfs = Vfs.init(globals.allocator); + defer vfs.deinit(); + var tar = try Tar.init(&vfs); + defer tar.deinit(); + + try tar.store(EMBED, .{ + .strip_prefix = ".rewritten", + }); + + var handler = try Handler.init(&vfs, .{ .root = std.posix.getenv("ROOT") orelse cwd, }); defer handler.deinit(); - var server = try httpz.Server(*Handler).init(allocator, .{ .port = 5882 }, &handler); + var server = try httpz.Server(*Handler).init(globals.allocator, .{ .port = 5882 }, &handler); defer { server.stop(); server.deinit(); @@ -62,7 +77,7 @@ fn handlefs(h: *Handler, req: *helpers.Request, res: *helpers.Response) !void { res.set_file(h, path) catch |err| { switch (err) { error.IsDirectory => { - req.request.url.path = try std.mem.join(h.allocator, "/", &.{ path, "index.html" }); + req.request.url.path = try std.mem.join(globals.allocator, "/", &.{ path, "index.html" }); h.logthis(req).level(.Info).string("using", req.request.url.path).log(); try handlefs(h, req, res); }, diff --git a/src/tar.zig b/src/tar.zig new file mode 100644 index 0000000..ada812a --- /dev/null +++ b/src/tar.zig @@ -0,0 +1,84 @@ +//! wrapper around vfs to store tarball contents +//! +//! Example ~ +//! +//! ```zig +//! var vfs = Vfs.init(globals.allocator); +//! defer vfs.deinit(); +//! var tar = try Tar.init(&vfs); +//! defer tar.deinit(); +//! +//! try tar.store(EMBED); +//! ``` +const std = @import("std"); +const globals = @import("globals.zig"); + +const Vfs = @import("vfs.zig"); + +const logz = @import("logz"); +const httpz = @import("httpz"); +const helpers = @import("helpers.zig"); + +const Self = @This(); + +vfs: *Vfs, +logger: *logz.Pool, + +pub fn init(vfs: *Vfs) !Self { + return .{ + .vfs = vfs, + .logger = try logz.Pool.init(globals.allocator, .{ .level = .Info }), + }; +} + +pub fn deinit(self: *Self) void { + self.logger.deinit(); +} + +pub const StoreOptions = struct { + strip_prefix: ?[]const u8 = null, + root: ?[]const u8 = null, +}; + +pub fn store(self: *Self, tar: []const u8, opts: StoreOptions) !void { + const file_name_buffer = try globals.allocator.alloc(u8, 0x1000); + defer globals.allocator.free(file_name_buffer); + const link_name_buffer = try globals.allocator.alloc(u8, 0x1000); + defer globals.allocator.free(link_name_buffer); + var files_tar_reader = std.io.Reader.fixed(tar); + + var tar_iter = std.tar.Iterator.init(&files_tar_reader, .{ + .file_name_buffer = file_name_buffer, + .link_name_buffer = link_name_buffer, + }); + + while (try tar_iter.next()) |file| { + if (file.kind != .file) continue; + + var t: ?httpz.ContentType = null; + defer self.logger.info() + .string("filename", file.name) + .int("size(bytes)", file.size) + .string("type", @tagName(t.?)) + .log(); + + const file_read_buffer = try globals.allocator.alloc(u8, file.size); + var file_read_writer = std.io.Writer.fixed(file_read_buffer); + try tar_iter.streamRemaining( + file, + &file_read_writer, + ); + + var path = try globals.allocator.dupe(u8, file.name); + if (opts.strip_prefix) |prefix| if (std.mem.startsWith(u8, path, prefix)) { + path = path[prefix.len..]; + }; + + t = helpers.content_type_for_file(path); + const node = Vfs.Node.init(file_read_buffer, t); + self.vfs.register(path, node) catch |e| switch (e) { + error.Clobber => self.logger.warn().string("msg", "unexpected clobber").string("path", file.name).log(), + else => return e, + }; + } +} diff --git a/src/vfs.zig b/src/vfs.zig index ae429f7..3ca7ff6 100644 --- a/src/vfs.zig +++ b/src/vfs.zig @@ -16,8 +16,6 @@ pub const Node = struct { }; const PathContext = struct { - allocator: std.mem.Allocator, - pub fn hash(self: @This(), s: []const u8) u64 { _ = self; if (std.fs.path.isAbsolute(s)) { @@ -34,7 +32,7 @@ map: std.HashMap([]const u8, Node, PathContext, std.hash_map.default_max_load_pe pub fn init(allocator: std.mem.Allocator) Self { return .{ - .map = .initContext(allocator, .{ .allocator = allocator }), + .map = .init(allocator), }; } @@ -48,11 +46,11 @@ pub fn deinit(self: *Self) void { /// error.Clobber => logger.warn().string("msg", "unexpected clobber").string("path", path).log(), /// else => return e, /// }; -pub fn register(self: *Self, path: []const u8, node: *Node) !void { +pub fn register(self: *Self, path: []const u8, node: Node) !void { const entry = try self.map.getOrPut(path); if (entry.found_existing) { return error.Clobber; } - entry.value_ptr.* = node.*; + entry.value_ptr.* = node; }