aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Zhongwei Yao <ashi08104@gmail.com> 2023-04-06 14:01:49 -0700
committerGravatar GitHub <noreply@github.com> 2023-04-06 14:01:49 -0700
commit1d138057cb861fe540cfe5ef49905225cee40ae8 (patch)
tree9289a28c1e29a67f68b5d30142026dc56aa9953d
parentf788519263b5713cd5655a6e3bcf3e3f9b1aea02 (diff)
downloadbun-1d138057cb861fe540cfe5ef49905225cee40ae8.tar.gz
bun-1d138057cb861fe540cfe5ef49905225cee40ae8.tar.zst
bun-1d138057cb861fe540cfe5ef49905225cee40ae8.zip
Add last modify field "mtime" for FileBlob (#1431) (#2491)
* Add lastModified field for FileBlob (#1431) lastModified value is epoch timestamp in millisecond unit. * update according to review comment.
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp16
-rw-r--r--src/bun.js/bindings/generated_classes.zig4
-rw-r--r--src/bun.js/webcore/blob.zig118
-rw-r--r--src/bun.js/webcore/response.classes.ts4
-rw-r--r--test/js/bun/io/bun-write.test.js18
-rw-r--r--test/js/web/fetch/fetch.test.ts1
6 files changed, 128 insertions, 33 deletions
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index a16c8d5ad..cd263dce4 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -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);
@@ -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 } },
@@ -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();
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index ed9953004..6602309ab 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -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");
@@ -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" });
diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig
index 284351347..030b77a2c 100644
--- a/src/bun.js/webcore/blob.zig
+++ b/src/bun.js/webcore/blob.zig
@@ -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();
@@ -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;
@@ -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| {
@@ -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{
@@ -1483,7 +1504,7 @@ pub const Blob = struct {
return;
}
- this.resolveSize(fd);
+ this.resolveSizeAndLastModified(fd);
if (this.errno != null)
return this.onFinish();
@@ -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| {
@@ -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();
@@ -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) {
@@ -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,
diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts
index 4fdce1c0c..5f3ba2e4a 100644
--- a/src/bun.js/webcore/response.classes.ts
+++ b/src/bun.js/webcore/response.classes.ts
@@ -141,6 +141,10 @@ export default [
getter: "getSize",
},
+ lastModified: {
+ getter: "getLastModified",
+ },
+
writer: {
fn: "getWriter",
length: 1,
diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js
index c3b9b187b..ab8063f16 100644
--- a/test/js/bun/io/bun-write.test.js
+++ b/test/js/bun/io/bun-write.test.js
@@ -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")));
@@ -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");
diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts
index 183c5dc77..cc59ba3f6 100644
--- a/test/js/web/fetch/fetch.test.ts
+++ b/test/js/web/fetch/fetch.test.ts
@@ -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;
});