aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-12-04 00:55:05 -0800
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-12-04 00:55:05 -0800
commit0617896d7045e129abac8d7fd22df0e6626d92c8 (patch)
tree144c597fbff7c071713acea7a1d2e7e02faffbb4 /src
parent1c3cb22d1f99d112200ae689896d46543631fad2 (diff)
downloadbun-0617896d7045e129abac8d7fd22df0e6626d92c8.tar.gz
bun-0617896d7045e129abac8d7fd22df0e6626d92c8.tar.zst
bun-0617896d7045e129abac8d7fd22df0e6626d92c8.zip
[Bun.serve] Implement `Content-Range` support with `Bun.file()`
Diffstat (limited to 'src')
-rw-r--r--src/bun.js/api/server.zig120
-rw-r--r--src/bun.js/webcore/response.zig5
-rw-r--r--src/bun.js/webcore/streams.zig1
3 files changed, 111 insertions, 15 deletions
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig
index e2892f88f..9f3256c97 100644
--- a/src/bun.js/api/server.zig
+++ b/src/bun.js/api/server.zig
@@ -646,6 +646,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
has_sendfile_ctx: bool = false,
has_called_error_handler: bool = false,
needs_content_length: bool = false,
+ needs_content_range: bool = false,
sendfile: SendfileContext = undefined,
request_js_object: JSC.C.JSObjectRef = null,
request_body_buf: std.ArrayListUnmanaged(u8) = .{},
@@ -883,6 +884,28 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return false;
}
+ // TODO: should we cork?
+ pub fn onWritableCompleteResponseBufferAndMetadata(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
+ std.debug.assert(this.resp == resp);
+
+ if (this.aborted) {
+ this.finalizeForAbort();
+ return false;
+ }
+
+ if (!this.has_written_status) {
+ this.renderMetadata();
+ }
+
+ if (this.method == .HEAD) {
+ resp.end("", this.shouldCloseConnection());
+ this.finalize();
+ return false;
+ }
+
+ return this.sendWritableBytesForCompleteResponseBuffer(this.response_buf_owned.items, write_offset, resp);
+ }
+
pub fn onWritableCompleteResponseBuffer(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
std.debug.assert(this.resp == resp);
if (this.aborted) {
@@ -1119,7 +1142,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
const errcode = linux.getErrno(val);
- this.sendfile.remain -= @intCast(Blob.SizeType, this.sendfile.offset - start);
+ this.sendfile.remain -|= @intCast(Blob.SizeType, this.sendfile.offset -| start);
if (errcode != .SUCCESS or this.aborted or this.sendfile.remain == 0 or val == 0) {
if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) {
@@ -1132,7 +1155,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
} else {
var sbytes: std.os.off_t = adjusted_count;
const signed_offset = @bitCast(i64, @as(u64, this.sendfile.offset));
-
const errcode = std.c.getErrno(std.c.sendfile(
this.sendfile.fd,
this.sendfile.socket_fd,
@@ -1143,8 +1165,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
0,
));
const wrote = @intCast(Blob.SizeType, sbytes);
- this.sendfile.offset += wrote;
- this.sendfile.remain -= wrote;
+ this.sendfile.offset +|= wrote;
+ this.sendfile.remain -|= wrote;
if (errcode != .AGAIN or this.aborted or this.sendfile.remain == 0 or sbytes == 0) {
if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) {
Output.prettyErrorln("Error: {s}", .{@tagName(errcode)});
@@ -1280,19 +1302,39 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
}
}
- this.blob.Blob.size = @intCast(Blob.SizeType, stat.size);
+ const original_size = this.blob.Blob.size;
+ const stat_size = @intCast(Blob.SizeType, stat.size);
+ this.blob.Blob.size = if (std.os.S.ISREG(stat.mode))
+ stat_size
+ else
+ @minimum(original_size, stat_size);
+
this.needs_content_length = true;
this.sendfile = .{
.fd = fd,
- .remain = this.blob.Blob.size,
+ .remain = this.blob.Blob.offset + original_size,
+ .offset = this.blob.Blob.offset,
.auto_close = auto_close,
.socket_fd = if (!this.aborted) this.resp.getNativeHandle() else -999,
};
+ // if we are sending only part of a file, include the content-range header
+ // only include content-range automatically when using a file path instead of an fd
+ // this is to better support manually controlling the behavior
+ if (std.os.S.ISREG(stat.mode) and auto_close) {
+ this.needs_content_range = (this.sendfile.remain -| this.sendfile.offset) != stat_size;
+ }
+
+ // we know the bounds when we are sending a regular file
+ if (std.os.S.ISREG(stat.mode)) {
+ this.sendfile.offset = @minimum(this.sendfile.offset, stat_size);
+ this.sendfile.remain = @minimum(@maximum(this.sendfile.remain, this.sendfile.offset), stat_size) -| this.sendfile.offset;
+ }
+
this.resp.runCorkedWithType(*RequestContext, renderMetadataAndNewline, this);
- if (this.blob.Blob.size == 0) {
+ if (this.sendfile.remain == 0 or !this.method.hasBody()) {
this.cleanupAndFinalizeAfterSendfile();
return;
}
@@ -1339,9 +1381,28 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.blob.Blob.resolveSize();
this.doRenderBlob();
} else {
- this.blob.Blob.size = @truncate(Blob.SizeType, result.result.buf.len);
+ const stat_size = @intCast(Blob.SizeType, result.result.total_size);
+ const original_size = this.blob.Blob.size;
+
+ this.blob.Blob.size = if (original_size == 0 or original_size == Blob.max_size)
+ stat_size
+ else
+ @minimum(original_size, stat_size);
+
+ if (!this.has_written_status)
+ this.needs_content_range = true;
+
+ // this is used by content-range
+ this.sendfile = .{
+ .fd = @truncate(i32, bun.invalid_fd),
+ .remain = @truncate(Blob.SizeType, result.result.buf.len),
+ .offset = this.blob.Blob.offset,
+ .auto_close = false,
+ .socket_fd = -999,
+ };
+
this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len };
- this.resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this);
+ this.resp.onWritable(*RequestContext, onWritableCompleteResponseBufferAndMetadata, this);
}
}
@@ -2078,13 +2139,18 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
pub fn renderMetadata(this: *RequestContext) void {
var response: *JSC.WebCore.Response = this.response_ptr.?;
var status = response.statusCode();
- const size = this.blob.size();
+ var needs_content_range = this.needs_content_range;
+
+ const size = if (needs_content_range)
+ this.sendfile.remain
+ else
+ this.blob.size();
+
status = if (status == 200 and size == 0 and !this.blob.isDetached())
204
else
status;
- this.writeStatus(status);
var needs_content_type = true;
const content_type: MimeType = brk: {
if (response.body.init.headers) |headers_| {
@@ -2105,12 +2171,23 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
};
var has_content_disposition = false;
-
if (response.body.init.headers) |headers_| {
- this.writeHeaders(headers_);
has_content_disposition = headers_.fastHas(.ContentDisposition);
+ needs_content_range = needs_content_range and headers_.fastHas(.ContentRange);
+ if (needs_content_range) {
+ status = 206;
+ }
+
+ this.writeStatus(status);
+ this.writeHeaders(headers_);
+
response.body.init.headers = null;
headers_.deref();
+ } else if (needs_content_range) {
+ status = 206;
+ this.writeStatus(status);
+ } else {
+ this.writeStatus(status);
}
if (needs_content_type and
@@ -2146,6 +2223,23 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.resp.writeHeaderInt("content-length", size);
this.needs_content_length = false;
}
+
+ if (needs_content_range) {
+ var content_range_buf: [1024]u8 = undefined;
+
+ this.resp.writeHeader(
+ "content-range",
+ std.fmt.bufPrint(
+ &content_range_buf,
+ // we omit the full size of the Blob because it could
+ // change between requests and this potentially leaks
+ // PII undesirably
+ "bytes {d}-{d}/*",
+ .{ this.sendfile.offset, this.sendfile.offset + this.sendfile.remain },
+ ) catch "bytes */*",
+ );
+ this.needs_content_range = false;
+ }
}
pub fn renderBytes(this: *RequestContext) void {
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
index d6332b993..b31bd3075 100644
--- a/src/bun.js/webcore/response.zig
+++ b/src/bun.js/webcore/response.zig
@@ -2010,6 +2010,7 @@ pub const Blob = struct {
pub const Read = struct {
buf: []u8,
is_temporary: bool = false,
+ total_size: SizeType = 0,
};
pub const ResultType = SystemError.Maybe(Read);
@@ -2105,7 +2106,7 @@ pub const Blob = struct {
return;
}
- cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = true } });
+ cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.size, .is_temporary = true } });
}
pub fn run(this: *ReadFile, task: *ReadFileTask) void {
this.runAsync(task);
@@ -2154,6 +2155,8 @@ pub const Blob = struct {
const file = &this.file_store;
const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;
+ this.size = @maximum(this.read_len, this.size);
+
if (needs_close) {
this.doClose();
}
diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig
index 801fb4741..e9c075074 100644
--- a/src/bun.js/webcore/streams.zig
+++ b/src/bun.js/webcore/streams.zig
@@ -4445,7 +4445,6 @@ pub const FileReader = struct {
return .{ .owned_and_done = this.drainInternalBuffer() };
}
-
return this.readable().read(buffer, view, this.globalThis());
}