So I’m downloading a file via std.http.Client and I want to print out the progress as bytes are downloaded, and additionally write those bytes to a file. Essentially what I want is a version of stream that works for Writer → Writer.
For now, I have a wrapper writer that copies the buffer state of the file writer. This way, the Writer interface sees a non-empty buffer, and I don’t have to go through the interface twice (as compared to calling writeAll on the file writer from inside drain).
As far as I can tell, this works, but it feels unnecessarily convoluted.
Slightly simplified version of my implementation
ProgressWriter.zig:
const std = @import("std");
const Writer = std.Io.Writer;
sink: *Writer,
count: usize = 0,
/// Use this to perform writer operations.
interface: Writer,
pub fn init(sink: *Writer) @This() {
return .{
.sink = sink,
.interface = .{
.buffer = sink.buffer,
.end = sink.end,
.vtable = &.{
.drain = &drain,
.flush = &flush,
.rebase = &rebase,
},
},
};
}
/// Synchronise the sink to our state, so that the sink may drain properly.
pub fn sync(self: *@This()) void {
self.sink.buffer = self.interface.buffer;
self.sink.end = self.interface.end;
}
/// Synchronise our state to the sink's. Must be called after calling a function in the sink's vtable.
pub fn syncBack(self: *@This()) void {
self.interface.buffer = self.sink.buffer;
self.interface.end = self.sink.end;
}
pub fn writeProgress(self: *const @This()) void {
std.debug.print("{}B\n", .{self.count});
}
fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
const pw: *@This() = @fieldParentPtr("interface", w);
const buffered = pw.interface.end;
const written = blk: {
pw.sync();
defer pw.syncBack();
break :blk try pw.sink.vtable.drain(pw.sink, data, splat);
};
const new_buffered = pw.interface.end;
pw.count += buffered + written - new_buffered;
pw.writeProgress();
return written;
}
fn flush(w: *Writer) Writer.Error!void {
const pw: *@This() = @fieldParentPtr("interface", w);
pw.sync();
defer pw.syncBack();
pw.count += pw.interface.end;
pw.writeProgress();
return pw.sink.vtable.flush(pw.sink);
}
fn rebase(w: *Writer, preserve: usize, capacity: usize) Writer.Error!void {
const pw: *@This() = @fieldParentPtr("interface", w);
pw.sync();
defer pw.syncBack();
return pw.sink.vtable.rebase(pw.sink, preserve, capacity);
}
And the use site looks like this:
fn downloadTheFile(client: *std.http.Client, fw: *std.Io.File.Writer) !void {
var pw: ProgressWriter = .init(&fw.interface);
_ = try client.fetch(.{
// ...HTTP stuff...
.response_writer = &pw.interface,
});
pw.sync();
try fw.end();
}