diff --git a/Cargo.toml b/Cargo.toml
index 38f04c5ae7..4677e4e494 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -199,7 +199,7 @@ nix = "=0.26.2"
fwdansi = "=1.1.0"
junction = "=0.2.0"
winapi = "=0.3.9"
-windows-sys = { version = "0.48.0", features = ["Win32_Media"] }
+windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem"] }
winres = "=0.1.12"
# NB: the `bench` and `release` profiles must remain EXACTLY the same.
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index f8c9dfc883..f4541f8866 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -255,6 +255,7 @@ deno_core::extension!(deno_node,
ops::fs::op_node_fs_exists_sync
,
ops::fs::op_node_cp_sync
,
ops::fs::op_node_cp
,
+ ops::fs::op_node_statfs
,
ops::winerror::op_node_sys_to_uv_error,
ops::v8::op_v8_cached_data_version_tag,
ops::v8::op_v8_get_heap_statistics,
@@ -373,6 +374,7 @@ deno_core::extension!(deno_node,
"_fs/_fs_rm.ts",
"_fs/_fs_rmdir.ts",
"_fs/_fs_stat.ts",
+ "_fs/_fs_statfs.js",
"_fs/_fs_symlink.ts",
"_fs/_fs_truncate.ts",
"_fs/_fs_unlink.ts",
diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs
index c5ae2371e2..28d95eabe5 100644
--- a/ext/node/ops/fs.rs
+++ b/ext/node/ops/fs.rs
@@ -9,6 +9,7 @@ use deno_core::error::AnyError;
use deno_core::op2;
use deno_core::OpState;
use deno_fs::FileSystemRc;
+use serde::Serialize;
use crate::NodePermissions;
@@ -78,3 +79,139 @@ where
fs.cp_async(path, new_path).await?;
Ok(())
}
+
+#[derive(Debug, Serialize)]
+pub struct StatFs {
+ #[serde(rename = "type")]
+ pub typ: u64,
+ pub bsize: u64,
+ pub blocks: u64,
+ pub bfree: u64,
+ pub bavail: u64,
+ pub files: u64,
+ pub ffree: u64,
+}
+
+#[op2]
+#[serde]
+pub fn op_node_statfs
(
+ state: Rc>,
+ #[string] path: String,
+ bigint: bool,
+) -> Result
+where
+ P: NodePermissions + 'static,
+{
+ {
+ let mut state = state.borrow_mut();
+ state
+ .borrow_mut::()
+ .check_read_with_api_name(Path::new(&path), Some("node:fs.statfs"))?;
+ state
+ .borrow_mut::
()
+ .check_sys("statfs", "node:fs.statfs")?;
+ }
+ #[cfg(unix)]
+ {
+ use std::ffi::OsStr;
+ use std::os::unix::ffi::OsStrExt;
+
+ let path = OsStr::new(&path);
+ let mut cpath = path.as_bytes().to_vec();
+ cpath.push(0);
+ if bigint {
+ #[cfg(not(target_os = "macos"))]
+ // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory.
+ let (code, result) = unsafe {
+ let mut result: libc::statfs64 = std::mem::zeroed();
+ (libc::statfs64(cpath.as_ptr() as _, &mut result), result)
+ };
+ #[cfg(target_os = "macos")]
+ // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory.
+ let (code, result) = unsafe {
+ let mut result: libc::statfs = std::mem::zeroed();
+ (libc::statfs(cpath.as_ptr() as _, &mut result), result)
+ };
+ if code == -1 {
+ return Err(std::io::Error::last_os_error().into());
+ }
+ Ok(StatFs {
+ typ: result.f_type as _,
+ bsize: result.f_bsize as _,
+ blocks: result.f_blocks as _,
+ bfree: result.f_bfree as _,
+ bavail: result.f_bavail as _,
+ files: result.f_files as _,
+ ffree: result.f_ffree as _,
+ })
+ } else {
+ // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory.
+ let (code, result) = unsafe {
+ let mut result: libc::statfs = std::mem::zeroed();
+ (libc::statfs(cpath.as_ptr() as _, &mut result), result)
+ };
+ if code == -1 {
+ return Err(std::io::Error::last_os_error().into());
+ }
+ Ok(StatFs {
+ typ: result.f_type as _,
+ bsize: result.f_bsize as _,
+ blocks: result.f_blocks as _,
+ bfree: result.f_bfree as _,
+ bavail: result.f_bavail as _,
+ files: result.f_files as _,
+ ffree: result.f_ffree as _,
+ })
+ }
+ }
+ #[cfg(windows)]
+ {
+ use deno_core::anyhow::anyhow;
+ use std::ffi::OsStr;
+ use std::os::windows::ffi::OsStrExt;
+ use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceW;
+
+ let _ = bigint;
+ // Using a vfs here doesn't make sense, it won't align with the windows API
+ // call below.
+ #[allow(clippy::disallowed_methods)]
+ let path = Path::new(&path).canonicalize()?;
+ let root = path
+ .ancestors()
+ .last()
+ .ok_or(anyhow!("Path has no root."))?;
+ let root = OsStr::new(root).encode_wide().collect::>();
+ let mut sectors_per_cluster = 0;
+ let mut bytes_per_sector = 0;
+ let mut available_clusters = 0;
+ let mut total_clusters = 0;
+ // SAFETY: Normal GetDiskFreeSpaceW usage.
+ let code = unsafe {
+ GetDiskFreeSpaceW(
+ root.as_ptr(),
+ &mut sectors_per_cluster,
+ &mut bytes_per_sector,
+ &mut available_clusters,
+ &mut total_clusters,
+ )
+ };
+ if code == 0 {
+ return Err(std::io::Error::last_os_error().into());
+ }
+ Ok(StatFs {
+ typ: 0,
+ bsize: (bytes_per_sector * sectors_per_cluster) as _,
+ blocks: total_clusters as _,
+ bfree: available_clusters as _,
+ bavail: available_clusters as _,
+ files: 0,
+ ffree: 0,
+ })
+ }
+ #[cfg(not(any(unix, windows)))]
+ {
+ let _ = path;
+ let _ = bigint;
+ Err(anyhow!("Unsupported platform."))
+ }
+}
diff --git a/ext/node/polyfills/_fs/_fs_statfs.js b/ext/node/polyfills/_fs/_fs_statfs.js
new file mode 100644
index 0000000000..51da1ed684
--- /dev/null
+++ b/ext/node/polyfills/_fs/_fs_statfs.js
@@ -0,0 +1,56 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { BigInt } from "ext:deno_node/internal/primordials.mjs";
+import { op_node_statfs } from "ext:core/ops";
+import { promisify } from "ext:deno_node/internal/util.mjs";
+
+class StatFs {
+ type;
+ bsize;
+ blocks;
+ bfree;
+ bavail;
+ files;
+ ffree;
+ constructor(type, bsize, blocks, bfree, bavail, files, ffree) {
+ this.type = type;
+ this.bsize = bsize;
+ this.blocks = blocks;
+ this.bfree = bfree;
+ this.bavail = bavail;
+ this.files = files;
+ this.ffree = ffree;
+ }
+}
+
+export function statfs(path, options, callback) {
+ if (typeof options === "function") {
+ callback = options;
+ options = {};
+ }
+ try {
+ const res = statfsSync(path, options);
+ callback(null, res);
+ } catch (err) {
+ callback(err, null);
+ }
+}
+
+export function statfsSync(path, options) {
+ const bigint = typeof options?.bigint === "boolean" ? options.bigint : false;
+ const statFs = op_node_statfs(
+ path,
+ bigint,
+ );
+ return new StatFs(
+ bigint ? BigInt(statFs.type) : statFs.type,
+ bigint ? BigInt(statFs.bsize) : statFs.bsize,
+ bigint ? BigInt(statFs.blocks) : statFs.blocks,
+ bigint ? BigInt(statFs.bfree) : statFs.bfree,
+ bigint ? BigInt(statFs.bavail) : statFs.bavail,
+ bigint ? BigInt(statFs.files) : statFs.files,
+ bigint ? BigInt(statFs.ffree) : statFs.ffree,
+ );
+}
+
+export const statfsPromise = promisify(statfs);
diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts
index bf43dd92e9..bdf7e4aa62 100644
--- a/ext/node/polyfills/fs.ts
+++ b/ext/node/polyfills/fs.ts
@@ -75,6 +75,11 @@ import {
Stats,
statSync,
} from "ext:deno_node/_fs/_fs_stat.ts";
+import {
+ statfs,
+ statfsPromise,
+ statfsSync,
+} from "ext:deno_node/_fs/_fs_statfs.js";
import {
symlink,
symlinkPromise,
@@ -156,6 +161,7 @@ const promises = {
symlink: symlinkPromise,
lstat: lstatPromise,
stat: statPromise,
+ statfs: statfsPromise,
link: linkPromise,
unlink: unlinkPromise,
chmod: chmodPromise,
@@ -253,6 +259,8 @@ export default {
stat,
Stats,
statSync,
+ statfs,
+ statfsSync,
symlink,
symlinkSync,
truncate,
@@ -354,6 +362,8 @@ export {
rmdirSync,
rmSync,
stat,
+ statfs,
+ statfsSync,
Stats,
statSync,
symlink,
diff --git a/ext/node/polyfills/internal/primordials.mjs b/ext/node/polyfills/internal/primordials.mjs
index d3726cf45d..f1e775bc53 100644
--- a/ext/node/polyfills/internal/primordials.mjs
+++ b/ext/node/polyfills/internal/primordials.mjs
@@ -12,6 +12,7 @@ export const ArrayPrototypeSlice = (that, ...args) => that.slice(...args);
export const ArrayPrototypeSome = (that, ...args) => that.some(...args);
export const ArrayPrototypeSort = (that, ...args) => that.sort(...args);
export const ArrayPrototypeUnshift = (that, ...args) => that.unshift(...args);
+export const BigInt = globalThis.BigInt;
export const ObjectAssign = Object.assign;
export const ObjectCreate = Object.create;
export const ObjectHasOwn = Object.hasOwn;
diff --git a/tests/integration/node_unit_tests.rs b/tests/integration/node_unit_tests.rs
index 2fd7e78f66..fc636e807f 100644
--- a/tests/integration/node_unit_tests.rs
+++ b/tests/integration/node_unit_tests.rs
@@ -44,6 +44,7 @@ util::unit_test_factory!(
_fs_rm_test = _fs / _fs_rm_test,
_fs_rmdir_test = _fs / _fs_rmdir_test,
_fs_stat_test = _fs / _fs_stat_test,
+ _fs_statfs_test = _fs / _fs_statfs_test,
_fs_symlink_test = _fs / _fs_symlink_test,
_fs_truncate_test = _fs / _fs_truncate_test,
_fs_unlink_test = _fs / _fs_unlink_test,
diff --git a/tests/unit_node/_fs/_fs_statfs_test.ts b/tests/unit_node/_fs/_fs_statfs_test.ts
new file mode 100644
index 0000000000..fde1c8fed9
--- /dev/null
+++ b/tests/unit_node/_fs/_fs_statfs_test.ts
@@ -0,0 +1,77 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import * as fs from "node:fs";
+import { assertEquals, assertRejects } from "@std/assert/mod.ts";
+import * as path from "@std/path/mod.ts";
+
+function assertStatFs(
+ statFs: fs.StatsFsBase,
+ { bigint = false } = {},
+) {
+ assertEquals(statFs.constructor.name, "StatFs");
+ const expectedType = bigint ? "bigint" : "number";
+ assertEquals(typeof statFs.type, expectedType);
+ assertEquals(typeof statFs.bsize, expectedType);
+ assertEquals(typeof statFs.blocks, expectedType);
+ assertEquals(typeof statFs.bfree, expectedType);
+ assertEquals(typeof statFs.bavail, expectedType);
+ assertEquals(typeof statFs.files, expectedType);
+ assertEquals(typeof statFs.ffree, expectedType);
+ if (Deno.build.os == "windows") {
+ assertEquals(statFs.type, bigint ? 0n : 0);
+ assertEquals(statFs.files, bigint ? 0n : 0);
+ assertEquals(statFs.ffree, bigint ? 0n : 0);
+ }
+}
+
+const filePath = path.fromFileUrl(import.meta.url);
+
+Deno.test({
+ name: "fs.statfs()",
+ async fn() {
+ await new Promise>((resolve, reject) => {
+ fs.statfs(filePath, (err, statFs) => {
+ if (err) reject(err);
+ resolve(statFs);
+ });
+ }).then((statFs) => assertStatFs(statFs));
+ },
+});
+
+Deno.test({
+ name: "fs.statfs() bigint",
+ async fn() {
+ await new Promise>((resolve, reject) => {
+ fs.statfs(filePath, { bigint: true }, (err, statFs) => {
+ if (err) reject(err);
+ resolve(statFs);
+ });
+ }).then((statFs) => assertStatFs(statFs, { bigint: true }));
+ },
+});
+
+Deno.test({
+ name: "fs.statfsSync()",
+ fn() {
+ const statFs = fs.statfsSync(filePath);
+ assertStatFs(statFs);
+ },
+});
+
+Deno.test({
+ name: "fs.statfsSync() bigint",
+ fn() {
+ const statFs = fs.statfsSync(filePath, { bigint: true });
+ assertStatFs(statFs, { bigint: true });
+ },
+});
+
+Deno.test({
+ name: "fs.statfs() non-existent path",
+ async fn() {
+ const nonExistentPath = path.join(filePath, "../non-existent");
+ await assertRejects(async () => {
+ await fs.promises.statfs(nonExistentPath);
+ }, "NotFound");
+ },
+});