Skip to content

Commit

Permalink
Add last modify field "mtime" for FileBlob (#1431)
Browse files Browse the repository at this point in the history
mtime value is epoch time stamp in millisecond unit.
  • Loading branch information
zhongweiy committed Mar 27, 2023
1 parent 319efe9 commit d60477c
Show file tree
Hide file tree
Showing 6 changed files with 129 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__getMtime(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
JSC_DECLARE_CUSTOM_GETTER(BlobPrototype__mtimeGetterWrap);

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 } },
{ "mtime"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, BlobPrototype__mtimeGetterWrap, 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__mtimeGetterWrap, (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__getMtime(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.getMtime) != GetterType)
@compileLog("Expected Blob.getMtime 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.getMtime, .{ .name = "BlobPrototype__getMtime" });
@export(Blob.getSize, .{ .name = "BlobPrototype__getSize" });
@export(Blob.getSlice, .{ .name = "BlobPrototype__getSlice" });
@export(Blob.getStream, .{ .name = "BlobPrototype__getStream" });
Expand Down
120 changes: 87 additions & 33 deletions src/bun.js/webcore/blob.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ 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 MillisecPerSec = 1000;
pub const NanosecPerMillisec = 1000000;

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 +578,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 set mtime 0 on success pathes instead of
// setting mtime to 0 at the beginning for better performance.
if (path_or_blob.blob.store.?.data == .file) {
// set mtime to 0 to force getMTime() to reload after writing.
path_or_blob.blob.store.?.data.file.mtime = 0;
}
}
}

var needs_async = false;
Expand Down Expand Up @@ -1278,6 +1293,7 @@ pub const Blob = struct {
onCompleteCtx: *anyopaque = undefined,
onCompleteCallback: OnReadFileCallback = undefined,
io_task: ?*ReadFileTask = null,
mtime: JSTimeType = 0,

pub const Read = struct {
buf: []u8,
Expand Down Expand Up @@ -1439,7 +1455,7 @@ pub const Blob = struct {
}
}

fn resolveSize(this: *ReadFile, fd: bun.FileDescriptor) void {
fn resolveSizeAndMtime(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 +1464,13 @@ pub const Blob = struct {
return;
},
};

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

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

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

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

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

pub fn getMtime(
this: *Blob,
_: *JSC.JSGlobalObject,
) callconv(.C) JSValue {
if (this.store) |store| {
if (store.data == .file) {
// if mtime is not 0, it can be already set during read.
if (store.data.file.mtime == 0) {
resolveFileStat(store);
}
return JSValue.jsNumber(store.data.file.mtime);
}
}

return JSValue.jsNumber(0);
}

pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) callconv(.C) JSValue {
if (this.size == Blob.max_size) {
this.resolveSize();
Expand Down Expand Up @@ -2544,34 +2586,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 +2605,45 @@ pub const Blob = struct {
}
}

fn toJSTime(sec: isize, nsec: isize) JSTimeType {
const millisec = @intCast(u64, @divTrunc(nsec, NanosecPerMillisec));
return @truncate(JSTimeType, @intCast(u64, sec * MillisecPerSec) + millisec);
}

/// resolve file stat like size, mtime
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.mtime = toJSTime(stat.mtim.tv_sec, stat.mtim.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.mtime = toJSTime(stat.mtim.tv_sec, stat.mtim.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",
},

mtime: {
getter: "getMtime",
},

writer: {
fn: "getWriter",
length: 1,
Expand Down
17 changes: 17 additions & 0 deletions test/js/bun/io/bun-write.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ it("Bun.file empty file", async () => {
await gcTick();
});

it("Bun.file mtime update", async () => {
const file = Bun.file("/tmp/bun.test.mtime.txt");
await gcTick();
// setup
await Bun.write(file, "test text.");
const mtime0 = file.mtime;

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

// ensure the last modified timestamp is updated.
expect(mtime1).toBeGreaterThan(mtime0);
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.mtime).toBeGreaterThan(0);
return file;
});

Expand Down

0 comments on commit d60477c

Please sign in to comment.