Makko, the people-oriented static site generator made for blogging. forge.starlightnet.work/Team/Makko
ssg static-site-generator makko starlight-network
at main 6.7 kB view raw
1const std = @import("std"); 2const httpz = @import("httpz"); 3const helper = @import("helper.zig"); 4 5const Makko = @import("Makko.zig"); 6const QR = @import("qrcode").QR; 7const Log = @import("Log.zig"); 8 9const getLanIP = @import("getLanIP.zig").getLanIP; 10 11const Handle = struct { 12 changed: std.atomic.Value(i16), 13 log: Log, 14 directory: std.fs.Dir, 15 port: u16, 16 17 pub fn update(self: *Handle) void { 18 self.log.header("Reloading"); 19 _ = self.changed.fetchAdd(1, .monotonic); 20 } 21 22 pub fn dispatch( 23 self: *Handle, 24 action: httpz.Action(*Handle), 25 req: *httpz.Request, 26 res: *httpz.Response, 27 ) !void { 28 if (std.mem.startsWith(u8, req.url.path, "/.makko")) 29 return action(self, req, res); 30 31 const args = .{ @tagName(req.method), req.url.path }; 32 if (self.log.colors) 33 self.log.raw("[{s}] \x1b[2m{s}\x1b[0m\n", args) 34 else 35 self.log.raw("[{s}] {s}\n", args); 36 37 action(self, req, res) catch |err| { 38 const writer = res.writer(); 39 try writer.print( 40 @embedFile("gui/error.html"), 41 .{ 500, req.url.path, err }, 42 ); 43 44 const query = try req.query(); 45 const enable_banner = query.get("makko-disable-banner") == null; 46 47 if (enable_banner) 48 try writer.writeAll(@embedFile("gui/banner.html")); 49 50 res.content_type = .HTML; 51 res.status = 500; 52 }; 53 } 54}; 55 56pub const Server = httpz.Server(*Handle); 57 58pub fn init( 59 makko: *Makko, 60 public: bool, 61 port: u16, 62) !Server { 63 const address = if (public) "0.0.0.0" else "127.0.0.1"; 64 65 const printable = if (public) blk: { 66 var buffer: [32]u8 = undefined; 67 if (getLanIP(&buffer)) |lan_ip| { 68 try showQR(makko.allocator, lan_ip, port); 69 break :blk lan_ip; 70 } 71 72 makko.log.err("Could not fetch local IP! No QR, sorry.", .{}); 73 break :blk "0.0.0.0"; 74 } else "127.0.0.1"; 75 76 var handler: Handle = .{ 77 .changed = .{ .raw = 0 }, 78 .directory = makko.paths.output, 79 .log = makko.log, 80 .port = port, 81 }; 82 83 var server = try Server.init( 84 makko.allocator, 85 .{ 86 .port = port, 87 .address = address, 88 }, 89 &handler, 90 ); 91 92 var router = try server.router(.{}); 93 94 router.get("/*", static, .{}); 95 router.get("/", redirect, .{}); 96 router.get("/.makko/state", state, .{}); 97 router.get("/.makko/", makko_ui, .{}); 98 99 handler.log.info("Serving at http://{s}:{}/\n", .{ printable, port }); 100 101 return server; 102} 103 104// For rendering our QR code 105fn showQR(allocator: std.mem.Allocator, address: []const u8, port: u16) !void { 106 const whole = try std.fmt.allocPrint( 107 allocator, 108 "http://{s}:{}", 109 .{ address, port }, 110 ); 111 defer allocator.free(whole); 112 113 var qr = try QR.fromText(allocator, 2, .low, whole); 114 defer qr.deinit(); 115 116 const stdout = std.io.getStdOut(); 117 const writer = stdout.writer(); 118 119 try writer.writeByte(' '); // small left margin 120 121 // We iterate through each pixel of the QR code and 122 // we render it using ASCII codes. 123 var y: usize = 0; 124 while (y < qr.size) : (y += 2) { 125 try writer.writeAll("\n "); 126 127 for (0..qr.size) |x| { 128 const top = qr.getModule(@intCast(x), @intCast(y)); 129 const bottom = qr.getModule(@intCast(x), @intCast(y + 1)); 130 131 const char = if (top and bottom) 132 "" 133 else if (top and !bottom) 134 "" 135 else if (!top and bottom) 136 "" 137 else 138 " "; 139 140 try writer.writeAll(char); 141 } 142 } 143 144 try writer.writeAll("\n\n"); 145} 146 147// in case we want to access either /directory/ or just /, we redirect to 148// /directory/index.html and /index.html 149fn redirect(_: *Handle, req: *httpz.Request, res: *httpz.Response) !void { 150 const query = try req.query(); 151 const enable_banner = query.get("makko-disable-banner") == null; 152 153 const alloc = try std.mem.concat(res.arena, u8, &.{ 154 req.url.path, 155 "index.html", 156 if (!enable_banner) "?makko-disable-banner=1" else "", 157 }); 158 res.headers.add("Location", alloc); 159 res.status = 308; 160 return; 161} 162 163fn makko_ui(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void { 164 const addr = req.conn.address.in.sa.addr; 165 166 // checks if NOT accessing from eiher 127.0.0.1 or 0.0.0.0 167 const is_local = addr == 0x0100007f or addr == 0; 168 if (!is_local) { 169 const new_addr = 170 try std.fmt.allocPrint(res.arena, "http://localhost:{}/.makko", .{handle.port}); 171 172 res.headers.add("Location", new_addr); 173 res.status = 308; 174 return; 175 } 176 177 const writer = res.writer(); 178 try writer.writeAll(@embedFile("gui/shell.html")); 179 res.content_type = .HTML; 180 res.status = 200; 181} 182 183fn state(handle: *Handle, _: *httpz.Request, res: *httpz.Response) !void { 184 try res.writer().print("{}", .{handle.changed.raw}); 185 res.status = 200; 186 res.content_type = .TEXT; 187} 188 189fn static(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void { 190 const query = try req.query(); 191 const enable_banner = query.get("makko-disable-banner") == null; 192 193 const path = req.url.path[1..]; 194 195 // If path is a directory, attempt to redirect to $(path)/index.html 196 if (path[path.len - 1] == '/') 197 return try redirect(handle, req, res); 198 199 const output = handle.directory; 200 if (!helper.exists(output, path)) { 201 const writer = res.writer(); 202 try writer.print( 203 @embedFile("gui/error.html"), 204 .{ 404, path, error.FileNotFound }, 205 ); 206 207 if (enable_banner) 208 try writer.writeAll(@embedFile("gui/banner.html")); 209 210 res.content_type = .HTML; 211 res.status = 404; 212 return; 213 } 214 215 // open the file! 216 const file = try output.openFile(path, .{}); 217 defer file.close(); 218 219 res.content_type = httpz.ContentType.forFile(path); 220 221 const writer = res.writer(); 222 const size = try file.getEndPos(); 223 224 // we read the file up and write as we read 225 var file_buf: [4096]u8 = undefined; 226 var remaining = size; 227 while (remaining > 0) { 228 const len = try file.read(&file_buf); 229 if (len == 0) break; 230 remaining -= len; 231 try writer.writeAll(file_buf[0..len]); 232 } 233 234 // we inject the banner if we're dealing with HTML 235 if (enable_banner and res.content_type.? == .HTML) 236 try writer.writeAll(@embedFile("gui/banner.html")); 237 238 try res.write(); 239 res.status = 200; 240}