Skip to content

Commit

Permalink
Add last modify field "mtime" for FileBlob (#1431) (#2491)
Browse files Browse the repository at this point in the history
* Add lastModified field for FileBlob (#1431)

lastModified value is epoch timestamp in millisecond unit.

* update according to review comment.
  • Loading branch information
zhongweiy authored Apr 6, 2023
1 parent f788519 commit 1d13805
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 33 deletions.
16 changes: 16 additions & 0 deletions src/bun.js/bindings/ZigGeneratedClasses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ JSC_DECLARE_HOST_FUNCTION(BlobPrototype__formDataCallback);
extern "C" EncodedJSValue BlobPrototype__getJSON(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(BlobPrototype__jsonCallback);

extern "C" JSC::EncodedJSValue BlobPrototype__getLastModified(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
JSC_DECLARE_CUSTOM_GETTER(BlobPrototype__lastModifiedGetterWrap);

extern "C" JSC::EncodedJSValue BlobPrototype__getSize(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
JSC_DECLARE_CUSTOM_GETTER(BlobPrototype__sizeGetterWrap);

Expand All @@ -133,6 +136,7 @@ static const HashTableValue JSBlobPrototypeTableValues[] = {
{ "arrayBuffer"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BlobPrototype__arrayBufferCallback, 0 } },
{ "formData"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BlobPrototype__formDataCallback, 0 } },
{ "json"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BlobPrototype__jsonCallback, 0 } },
{ "lastModified"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, BlobPrototype__lastModifiedGetterWrap, 0 } },
{ "size"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, BlobPrototype__sizeGetterWrap, 0 } },
{ "slice"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BlobPrototype__sliceCallback, 2 } },
{ "stream"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BlobPrototype__streamCallback, 1 } },
Expand Down Expand Up @@ -203,6 +207,18 @@ JSC_DEFINE_HOST_FUNCTION(BlobPrototype__jsonCallback, (JSGlobalObject * lexicalG
return BlobPrototype__getJSON(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}

JSC_DEFINE_CUSTOM_GETTER(BlobPrototype__lastModifiedGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = lexicalGlobalObject->vm();
Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject);
auto throwScope = DECLARE_THROW_SCOPE(vm);
JSBlob* thisObject = jsCast<JSBlob*>(JSValue::decode(thisValue));
JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
JSC::EncodedJSValue result = BlobPrototype__getLastModified(thisObject->wrapped(), globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
RELEASE_AND_RETURN(throwScope, result);
}

JSC_DEFINE_CUSTOM_GETTER(BlobPrototype__sizeGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName))
{
auto& vm = lexicalGlobalObject->vm();
Expand Down
4 changes: 4 additions & 0 deletions src/bun.js/bindings/generated_classes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ pub const JSBlob = struct {
@compileLog("Expected Blob.getFormData to be a callback but received " ++ @typeName(@TypeOf(Blob.getFormData)));
if (@TypeOf(Blob.getJSON) != CallbackType)
@compileLog("Expected Blob.getJSON to be a callback but received " ++ @typeName(@TypeOf(Blob.getJSON)));
if (@TypeOf(Blob.getLastModified) != GetterType)
@compileLog("Expected Blob.getLastModified to be a getter");

if (@TypeOf(Blob.getSize) != GetterType)
@compileLog("Expected Blob.getSize to be a getter");

Expand All @@ -110,6 +113,7 @@ pub const JSBlob = struct {
@export(Blob.getArrayBuffer, .{ .name = "BlobPrototype__getArrayBuffer" });
@export(Blob.getFormData, .{ .name = "BlobPrototype__getFormData" });
@export(Blob.getJSON, .{ .name = "BlobPrototype__getJSON" });
@export(Blob.getLastModified, .{ .name = "BlobPrototype__getLastModified" });
@export(Blob.getSize, .{ .name = "BlobPrototype__getSize" });
@export(Blob.getSlice, .{ .name = "BlobPrototype__getSlice" });
@export(Blob.getStream, .{ .name = "BlobPrototype__getStream" });
Expand Down
118 changes: 85 additions & 33 deletions src/bun.js/webcore/blob.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ pub const Blob = struct {
pub const SizeType = u52;
pub const max_size = std.math.maxInt(SizeType);

/// According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date,
/// maximum Date in JavaScript is less than Number.MAX_SAFE_INTEGER (u52).
pub const JSTimeType = u52;
pub const init_timestamp = std.math.maxInt(JSTimeType);

pub fn getFormDataEncoding(this: *Blob) ?*bun.FormData.AsyncFormData {
var content_type_slice: ZigString.Slice = this.getContentType() orelse return null;
defer content_type_slice.deinit();
Expand Down Expand Up @@ -572,9 +577,18 @@ pub const Blob = struct {
return null;
}

if (path_or_blob == .blob and path_or_blob.blob.store == null) {
exception.* = JSC.toInvalidArguments("Blob is detached", .{}, ctx).asObjectRef();
return null;
if (path_or_blob == .blob) {
if (path_or_blob.blob.store == null) {
exception.* = JSC.toInvalidArguments("Blob is detached", .{}, ctx).asObjectRef();
return null;
} else {
// TODO only reset last_modified on success pathes instead of
// resetting last_modified at the beginning for better performance.
if (path_or_blob.blob.store.?.data == .file) {
// reset last_modified to force getLastModified() to reload after writing.
path_or_blob.blob.store.?.data.file.last_modified = init_timestamp;
}
}
}

var needs_async = false;
Expand Down Expand Up @@ -1439,7 +1453,7 @@ pub const Blob = struct {
}
}

fn resolveSize(this: *ReadFile, fd: bun.FileDescriptor) void {
fn resolveSizeAndLastModified(this: *ReadFile, fd: bun.FileDescriptor) void {
const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) {
.result => |result| result,
.err => |err| {
Expand All @@ -1448,6 +1462,13 @@ pub const Blob = struct {
return;
},
};

if (this.store) |store| {
if (store.data == .file) {
store.data.file.last_modified = toJSTime(stat.mtime().tv_sec, stat.mtime().tv_nsec);
}
}

if (std.os.S.ISDIR(stat.mode)) {
this.errno = error.EISDIR;
this.system_error = JSC.SystemError{
Expand Down Expand Up @@ -1483,7 +1504,7 @@ pub const Blob = struct {
return;
}

this.resolveSize(fd);
this.resolveSizeAndLastModified(fd);
if (this.errno != null)
return this.onFinish();

Expand Down Expand Up @@ -2170,6 +2191,8 @@ pub const Blob = struct {
mode: JSC.Node.Mode = 0,
seekable: ?bool = null,
max_size: SizeType = Blob.max_size,
// milliseconds since ECMAScript epoch
last_modified: JSTimeType = init_timestamp,

pub fn isSeekable(this: *const FileStore) ?bool {
if (this.seekable) |seekable| {
Expand Down Expand Up @@ -2513,6 +2536,23 @@ pub const Blob = struct {
return ZigString.Empty.toValue(globalThis);
}

pub fn getLastModified(
this: *Blob,
_: *JSC.JSGlobalObject,
) callconv(.C) JSValue {
if (this.store) |store| {
if (store.data == .file) {
// last_modified can be already set during read.
if (store.data.file.last_modified == init_timestamp) {
resolveFileStat(store);
}
return JSValue.jsNumber(store.data.file.last_modified);
}
}

return JSValue.jsNumber(init_timestamp);
}

pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) callconv(.C) JSValue {
if (this.size == Blob.max_size) {
this.resolveSize();
Expand Down Expand Up @@ -2544,34 +2584,7 @@ pub const Blob = struct {
return;
} else if (store.data == .file) {
if (store.data.file.seekable == null) {
if (store.data.file.pathlike == .path) {
var buffer: [bun.MAX_PATH_BYTES]u8 = undefined;
switch (JSC.Node.Syscall.stat(store.data.file.pathlike.path.sliceZ(&buffer))) {
.result => |stat| {
store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
@truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
else
Blob.max_size;
store.data.file.mode = stat.mode;
store.data.file.seekable = std.os.S.ISREG(stat.mode);
},
// the file may not exist yet. Thats's okay.
else => {},
}
} else if (store.data.file.pathlike == .fd) {
switch (JSC.Node.Syscall.fstat(store.data.file.pathlike.fd)) {
.result => |stat| {
store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
@truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
else
Blob.max_size;
store.data.file.mode = stat.mode;
store.data.file.seekable = std.os.S.ISREG(stat.mode);
},
// the file may not exist yet. Thats's okay.
else => {},
}
}
resolveFileStat(store);
}

if (store.data.file.seekable != null and store.data.file.max_size != Blob.max_size) {
Expand All @@ -2590,6 +2603,45 @@ pub const Blob = struct {
}
}

fn toJSTime(sec: isize, nsec: isize) JSTimeType {
const millisec = @intCast(u64, @divTrunc(nsec, std.time.ns_per_ms));
return @truncate(JSTimeType, @intCast(u64, sec * std.time.ms_per_s) + millisec);
}

/// resolve file stat like size, last_modified
fn resolveFileStat(store: *Store) void {
if (store.data.file.pathlike == .path) {
var buffer: [bun.MAX_PATH_BYTES]u8 = undefined;
switch (JSC.Node.Syscall.stat(store.data.file.pathlike.path.sliceZ(&buffer))) {
.result => |stat| {
store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
@truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
else
Blob.max_size;
store.data.file.mode = stat.mode;
store.data.file.seekable = std.os.S.ISREG(stat.mode);
store.data.file.last_modified = toJSTime(stat.mtime().tv_sec, stat.mtime().tv_nsec);
},
// the file may not exist yet. Thats's okay.
else => {},
}
} else if (store.data.file.pathlike == .fd) {
switch (JSC.Node.Syscall.fstat(store.data.file.pathlike.fd)) {
.result => |stat| {
store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
@truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
else
Blob.max_size;
store.data.file.mode = stat.mode;
store.data.file.seekable = std.os.S.ISREG(stat.mode);
store.data.file.last_modified = toJSTime(stat.mtime().tv_sec, stat.mtime().tv_nsec);
},
// the file may not exist yet. Thats's okay.
else => {},
}
}
}

pub fn constructor(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
Expand Down
4 changes: 4 additions & 0 deletions src/bun.js/webcore/response.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export default [
getter: "getSize",
},

lastModified: {
getter: "getLastModified",
},

writer: {
fn: "getWriter",
length: 1,
Expand Down
18 changes: 18 additions & 0 deletions test/js/bun/io/bun-write.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from "fs";
import { it, expect, describe } from "bun:test";
import path from "path";
import { gcTick, withoutAggressiveGC } from "harness";
import { tmpdir } from "os";

it("Bun.write blob", async () => {
await Bun.write(Bun.file("/tmp/response-file.test.txt"), Bun.file(path.join(import.meta.dir, "fetch.js.txt")));
Expand Down Expand Up @@ -147,6 +148,23 @@ it("Bun.file empty file", async () => {
await gcTick();
});

it("Bun.file lastModified update", async () => {
const file = Bun.file(tmpdir() + "/bun.test.lastModified.txt");
await gcTick();
// setup
await Bun.write(file, "test text.");
const lastModified0 = file.lastModified;

// sleep some time and write the file again.
await Bun.sleep(10);
await Bun.write(file, "test text2.");
const lastModified1 = file.lastModified;

// ensure the last modified timestamp is updated.
expect(lastModified1).toBeGreaterThan(lastModified0);
await gcTick();
});

it("Bun.file as a Blob", async () => {
const filePath = path.join(import.meta.path, "../fetch.js.txt");
const fixture = fs.readFileSync(filePath, "utf8");
Expand Down
1 change: 1 addition & 0 deletions test/js/web/fetch/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ describe("Bun.file", () => {
writeFileSync(path, buffer);
const file = Bun.file(path);
expect(blob.size).toBe(file.size);
expect(file.lastModified).toBeGreaterThan(0);
return file;
});

Expand Down

0 comments on commit 1d13805

Please sign in to comment.