From 6201307fecf8398a1b53bf276bc08bfbb3524899 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Mon, 17 Nov 2025 13:09:02 +0100 Subject: implement memora.Stream --- src/root.zig | 1 + src/routes/api/image/load.zig | 7 ++----- src/routes/handler-info.zig | 29 ++++++++++++++++++++--------- src/routes/static.zig | 10 +++------- src/stream/buffer.zig | 28 ++++++++++++++++++++++++++++ src/stream/file.zig | 22 ++++++++++++++++++++++ src/stream/root.zig | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 src/stream/buffer.zig create mode 100644 src/stream/file.zig create mode 100644 src/stream/root.zig diff --git a/src/root.zig b/src/root.zig index 81dcdb2..31cd751 100644 --- a/src/root.zig +++ b/src/root.zig @@ -7,3 +7,4 @@ pub const routes = @import("routes/root.zig"); pub const Server = @import("server.zig"); pub const Storage = @import("storage/root.zig"); pub const Context = @import("context.zig"); +pub const Stream = @import("stream/root.zig").Stream; diff --git a/src/routes/api/image/load.zig b/src/routes/api/image/load.zig index 51c0e26..4f0a072 100644 --- a/src/routes/api/image/load.zig +++ b/src/routes/api/image/load.zig @@ -6,11 +6,8 @@ const Storage = memora.Storage; pub const access = .users; -pub fn get(ctx: *Context) ![]const u8 { +pub fn get(ctx: *Context) !memora.Stream { const id = ctx.request.head.target["/api/image/load/".len..]; var image = Storage.Image { .id = id }; - var file = try image.file(ctx.storage); - defer file.close(); - - return try file.readToEndAlloc(ctx.allocator, std.math.maxInt(usize)); + return .from_file(try image.file(ctx.storage)); } diff --git a/src/routes/handler-info.zig b/src/routes/handler-info.zig index e8c9dfb..97eb9bd 100644 --- a/src/routes/handler-info.zig +++ b/src/routes/handler-info.zig @@ -12,7 +12,7 @@ const log = std.log.scoped(.handler_info); const Self = @This(); -const Handler = *const fn (*Context) anyerror![]const u8; +const Handler = *const fn (*Context) anyerror!memora.Stream; get: ?Handler, head: ?Handler, @@ -95,7 +95,7 @@ pub fn handle( } } - const response = handler(&context) catch |err| { + var stream = handler(&context) catch |err| { const response, const status_code: std.http.Status = switch (err) { error.BadRequest => .{ "{ \"error\": \"Bad Request\" }", .bad_request }, error.Unauthorized => .{ "{ \"error\": \"Unauthorized\" }", .unauthorized }, @@ -109,6 +109,7 @@ pub fn handle( return request.respond(response, .{ .status = status_code }); }; + defer stream.close(); var headers: std.ArrayList(std.http.Header) = .empty; defer headers.deinit(allocator); @@ -129,9 +130,19 @@ pub fn handle( }); } - try request.respond(response, .{ - .extra_headers = headers.items + var read_buffer: [1024]u8 = undefined; + var reader = stream.reader(&read_buffer); + + var write_buffer: [1024]u8 = undefined; + var body_writer = try request.respondStreaming(&write_buffer, .{ + .respond_options = .{ + .extra_headers = headers.items, + .transfer_encoding = .chunked, + }, }); + + _ = try reader.streamRemaining(&body_writer.writer); + try body_writer.end(); } fn HandlerWrapper(T: type, name: []const u8) type { @@ -140,7 +151,7 @@ fn HandlerWrapper(T: type, name: []const u8) type { const payload_type = @typeInfo(return_type).error_union.payload; return struct { - pub fn call(ctx: *Context) anyerror![]const u8 { + pub fn call(ctx: *Context) anyerror!memora.Stream { const args = args: { const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(@field(T, name)))); @@ -161,7 +172,7 @@ fn HandlerWrapper(T: type, name: []const u8) type { Body, ctx.allocator, writer.written(), - .{} + .{}, ) catch return error.BadRequest; break :args .{ ctx, body }; } else { @@ -169,16 +180,16 @@ fn HandlerWrapper(T: type, name: []const u8) type { } }; - if (payload_type == []const u8) { + if (payload_type == memora.Stream) { return @call(.auto, @field(T, name), args); } else if (payload_type == void) { try @call(.auto, @field(T, name), args); - return ""; + return memora.Stream.from_buffer(""); } else { var writer = std.Io.Writer.Allocating.init(ctx.allocator); var stringify = std.json.Stringify { .writer = &writer.writer }; try stringify.write(try @call(.auto, @field(T, name), args)); - return writer.written(); + return memora.Stream.from_buffer(writer.written()); } } }; diff --git a/src/routes/static.zig b/src/routes/static.zig index ef7d493..f52d178 100644 --- a/src/routes/static.zig +++ b/src/routes/static.zig @@ -8,16 +8,14 @@ const log = std.log.scoped(.fallback); pub const access = .everyone; -pub fn get(ctx: *Context) anyerror![]const u8 { +pub fn get(ctx: *Context) anyerror!memora.Stream { var static = try std.fs.cwd().openDir("static", .{}); defer static.close(); if (static.openFile(ctx.request.head.target[1..], .{})) |file| { - defer file.close(); - const content = file.readToEndAlloc(ctx.allocator, std.math.maxInt(usize)); const mime_type = mime.get_type(ctx.request.head.target); ctx.response.headers.content_type = mime_type; - return content; + return .from_file(file); } else |_| { var subdir = if (ctx.request.head.target.len == 1) static else (static.openDir(ctx.request.head.target[1..], .{}) catch { @@ -26,10 +24,8 @@ pub fn get(ctx: *Context) anyerror![]const u8 { defer if (ctx.request.head.target.len > 1) subdir.close(); if (subdir.openFile("index.html", .{})) |file| { - defer file.close(); - const content = file.readToEndAlloc(ctx.allocator, std.math.maxInt(usize)); ctx.response.headers.content_type = "text/html"; - return content; + return .from_file(file); } else |_| { log.warn("File '{s}' Not Found", .{ ctx.request.head.target }); return error.NotFound; diff --git a/src/stream/buffer.zig b/src/stream/buffer.zig new file mode 100644 index 0000000..fca1deb --- /dev/null +++ b/src/stream/buffer.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +const Self = @This(); + +buffer: []const u8, +buffer_reader: ?std.Io.Reader, + +pub fn init(buffer: []const u8) Self { + return .{ + .buffer = buffer, + .buffer_reader = null, + }; +} + +pub fn reader(self: *Self, buffer: []u8) *std.Io.Reader { + _ = buffer; + + if (self.buffer_reader) |*r| { + return r; + } + + self.buffer_reader = std.Io.Reader.fixed(self.buffer); + return &self.buffer_reader.?; +} + +pub fn close(self: *Self) void { + _ = self; +} diff --git a/src/stream/file.zig b/src/stream/file.zig new file mode 100644 index 0000000..bb6407b --- /dev/null +++ b/src/stream/file.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +const Self = @This(); + +file: std.fs.File, +file_reader: ?std.fs.File.Reader, + +pub fn init(file: std.fs.File) Self { + return .{ + .file = file, + .file_reader = null, + }; +} + +pub fn reader(self: *Self, buffer: []u8) *std.Io.Reader { + self.file_reader = self.file.reader(buffer); + return &self.file_reader.?.interface; +} + +pub fn close(self: *Self) void { + self.file.close(); +} diff --git a/src/stream/root.zig b/src/stream/root.zig new file mode 100644 index 0000000..11580ac --- /dev/null +++ b/src/stream/root.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +pub const Stream = union(enum) { + const Self = @This(); + + pub const File = @import("file.zig"); + pub const Buffer = @import("buffer.zig"); + + file: File, + buffer: Buffer, + + pub fn from_file(file: std.fs.File) Self { + return .{ .file = File.init(file) }; + } + + pub fn from_buffer(buffer: []const u8) Self { + return .{ .buffer = Buffer.init(buffer) }; + } + + pub fn reader(self: *Self, buffer: []u8) *std.Io.Reader { + return switch(self.*) { + .file => |*f| f.reader(buffer), + .buffer => |*b| b.reader(buffer), + }; + } + + pub fn close(self: *Self) void { + return switch(self.*) { + .file => |*f| f.close(), + .buffer => |*b| b.close(), + }; + } +}; -- cgit v1.2.3-70-g09d2