@photostructure/fs-metadata 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/binding.gyp +1 -0
- package/dist/index.cjs +168 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.mts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.mjs +167 -120
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/src/binding.cpp +11 -0
- package/src/darwin/get_mount_point.cpp +96 -0
- package/src/darwin/get_mount_point.h +13 -0
- package/src/index.ts +27 -0
- package/src/mount_point_for_path.ts +54 -0
- package/src/types/native_bindings.ts +7 -0
- package/src/volume_metadata.ts +27 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/fs-metadata",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Cross-platform native filesystem metadata retrieval for Node.js",
|
|
5
5
|
"homepage": "https://photostructure.github.io/fs-metadata/",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"cross-platform"
|
|
92
92
|
],
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"node-addon-api": "^8.
|
|
94
|
+
"node-addon-api": "^8.7.0",
|
|
95
95
|
"node-gyp-build": "^4.8.4"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"jest-environment-node": "^30.3.0",
|
|
109
109
|
"jest-extended": "^7.0.0",
|
|
110
110
|
"node-gyp": "^12.2.0",
|
|
111
|
-
"npm-check-updates": "^19.6.
|
|
111
|
+
"npm-check-updates": "^19.6.6",
|
|
112
112
|
"npm-run-all2": "8.0.4",
|
|
113
113
|
"prebuildify": "^6.0.1",
|
|
114
114
|
"prettier": "^3.8.1",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
"ts-jest": "^29.4.6",
|
|
118
118
|
"tsup": "^8.5.1",
|
|
119
119
|
"tsx": "^4.21.0",
|
|
120
|
-
"typedoc": "^0.28.
|
|
120
|
+
"typedoc": "^0.28.18",
|
|
121
121
|
"typescript": "^5.9.3",
|
|
122
|
-
"typescript-eslint": "^8.57.
|
|
122
|
+
"typescript-eslint": "^8.57.2"
|
|
123
123
|
}
|
|
124
124
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/binding.cpp
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#include "windows/hidden.h"
|
|
9
9
|
#elif defined(__APPLE__)
|
|
10
10
|
#include "darwin/fs_meta.h"
|
|
11
|
+
#include "darwin/get_mount_point.h"
|
|
11
12
|
#include "darwin/hidden.h"
|
|
12
13
|
#elif defined(__linux__)
|
|
13
14
|
#include "common/volume_metadata.h"
|
|
@@ -60,6 +61,12 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
60
61
|
return FSMeta::GetVolumeMetadata(info);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
#if defined(__APPLE__)
|
|
65
|
+
Napi::Value GetMountPointForPath(const Napi::CallbackInfo &info) {
|
|
66
|
+
return FSMeta::GetMountPoint(info);
|
|
67
|
+
}
|
|
68
|
+
#endif
|
|
69
|
+
|
|
63
70
|
#if defined(_WIN32) || defined(__APPLE__)
|
|
64
71
|
Napi::Value GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
65
72
|
return FSMeta::GetHiddenAttribute(info);
|
|
@@ -81,6 +88,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
81
88
|
|
|
82
89
|
exports.Set("getVolumeMetadata", Napi::Function::New(env, GetVolumeMetadata));
|
|
83
90
|
|
|
91
|
+
#if defined(__APPLE__)
|
|
92
|
+
exports.Set("getMountPoint", Napi::Function::New(env, GetMountPointForPath));
|
|
93
|
+
#endif
|
|
94
|
+
|
|
84
95
|
#ifdef ENABLE_GIO
|
|
85
96
|
exports.Set("getGioMountPoints", Napi::Function::New(env, GetGioMountPoints));
|
|
86
97
|
#endif
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/darwin/get_mount_point.cpp
|
|
2
|
+
// Lightweight mount point lookup using fstatfs() only.
|
|
3
|
+
// Returns f_mntonname without DiskArbitration, IOKit, or space calculations.
|
|
4
|
+
|
|
5
|
+
#include "./get_mount_point.h"
|
|
6
|
+
#include "../common/debug_log.h"
|
|
7
|
+
#include "../common/error_utils.h"
|
|
8
|
+
#include "../common/fd_guard.h"
|
|
9
|
+
#include "../common/path_security.h"
|
|
10
|
+
|
|
11
|
+
#include <fcntl.h>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <sys/mount.h>
|
|
14
|
+
#include <sys/param.h>
|
|
15
|
+
#include <unistd.h>
|
|
16
|
+
|
|
17
|
+
namespace FSMeta {
|
|
18
|
+
|
|
19
|
+
class GetMountPointWorker : public Napi::AsyncWorker {
|
|
20
|
+
public:
|
|
21
|
+
GetMountPointWorker(const std::string &path,
|
|
22
|
+
const Napi::Promise::Deferred &deferred)
|
|
23
|
+
: Napi::AsyncWorker(deferred.Env()), path_(path), deferred_(deferred) {}
|
|
24
|
+
|
|
25
|
+
void Execute() override {
|
|
26
|
+
DEBUG_LOG("[GetMountPointWorker] Executing for path: %s", path_.c_str());
|
|
27
|
+
try {
|
|
28
|
+
std::string error;
|
|
29
|
+
std::string validated = ValidatePathForRead(path_, error);
|
|
30
|
+
if (validated.empty()) {
|
|
31
|
+
SetError(error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DEBUG_LOG("[GetMountPointWorker] Using validated path: %s",
|
|
36
|
+
validated.c_str());
|
|
37
|
+
|
|
38
|
+
int fd = open(validated.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
39
|
+
if (fd < 0) {
|
|
40
|
+
int err = errno;
|
|
41
|
+
DEBUG_LOG("[GetMountPointWorker] open failed: %s (%d)", strerror(err),
|
|
42
|
+
err);
|
|
43
|
+
SetError(CreatePathErrorMessage("open", path_, err));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
FdGuard guard(fd);
|
|
48
|
+
|
|
49
|
+
struct statfs fs;
|
|
50
|
+
if (fstatfs(fd, &fs) != 0) {
|
|
51
|
+
int err = errno;
|
|
52
|
+
DEBUG_LOG("[GetMountPointWorker] fstatfs failed: %s (%d)",
|
|
53
|
+
strerror(err), err);
|
|
54
|
+
SetError(CreatePathErrorMessage("fstatfs", path_, err));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
result_ = fs.f_mntonname;
|
|
59
|
+
DEBUG_LOG("[GetMountPointWorker] mount point: %s", result_.c_str());
|
|
60
|
+
} catch (const std::exception &e) {
|
|
61
|
+
DEBUG_LOG("[GetMountPointWorker] Exception: %s", e.what());
|
|
62
|
+
SetError(e.what());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void OnOK() override {
|
|
67
|
+
Napi::HandleScope scope(Env());
|
|
68
|
+
deferred_.Resolve(Napi::String::New(Env(), result_));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void OnError(const Napi::Error &error) override {
|
|
72
|
+
deferred_.Reject(error.Value());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private:
|
|
76
|
+
std::string path_;
|
|
77
|
+
std::string result_;
|
|
78
|
+
Napi::Promise::Deferred deferred_;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
Napi::Value GetMountPoint(const Napi::CallbackInfo &info) {
|
|
82
|
+
auto env = info.Env();
|
|
83
|
+
DEBUG_LOG("[GetMountPoint] called");
|
|
84
|
+
|
|
85
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
86
|
+
throw Napi::TypeError::New(env, "String argument expected");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
std::string path = info[0].As<Napi::String>().Utf8Value();
|
|
90
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
91
|
+
auto *worker = new GetMountPointWorker(path, deferred);
|
|
92
|
+
worker->Queue();
|
|
93
|
+
return deferred.Promise();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} // namespace FSMeta
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/darwin/get_mount_point.h
|
|
2
|
+
// Lightweight mount point lookup using fstatfs() only.
|
|
3
|
+
// Returns f_mntonname without DiskArbitration, IOKit, or space calculations.
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <napi.h>
|
|
8
|
+
|
|
9
|
+
namespace FSMeta {
|
|
10
|
+
|
|
11
|
+
Napi::Value GetMountPoint(const Napi::CallbackInfo &info);
|
|
12
|
+
|
|
13
|
+
} // namespace FSMeta
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isHiddenRecursiveImpl,
|
|
13
13
|
setHiddenImpl,
|
|
14
14
|
} from "./hidden";
|
|
15
|
+
import { getMountPointForPathImpl } from "./mount_point_for_path";
|
|
15
16
|
import {
|
|
16
17
|
getTimeoutMsDefault,
|
|
17
18
|
IncludeSystemVolumesDefault,
|
|
@@ -130,6 +131,32 @@ export function getVolumeMetadataForPath(
|
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Get the mount point path for an arbitrary file or directory path.
|
|
136
|
+
*
|
|
137
|
+
* This is a lightweight alternative to {@link getVolumeMetadataForPath} when
|
|
138
|
+
* you only need the mount point string. On macOS it uses a single fstatfs()
|
|
139
|
+
* call (no DiskArbitration, IOKit, or space calculations). On Linux/Windows
|
|
140
|
+
* it uses device ID matching against the mount table.
|
|
141
|
+
*
|
|
142
|
+
* Symlinks are resolved, and macOS APFS firmlinks (e.g. `/Users` →
|
|
143
|
+
* `/System/Volumes/Data`) are handled correctly.
|
|
144
|
+
*
|
|
145
|
+
* @param pathname Path to any file or directory
|
|
146
|
+
* @param opts Optional settings (timeoutMs, linuxMountTablePaths)
|
|
147
|
+
* @returns The mount point path (e.g., "/", "/System/Volumes/Data", "C:\\")
|
|
148
|
+
*/
|
|
149
|
+
export function getMountPointForPath(
|
|
150
|
+
pathname: string,
|
|
151
|
+
opts?: Partial<Pick<Options, "timeoutMs" | "linuxMountTablePaths">>,
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
return getMountPointForPathImpl(
|
|
154
|
+
pathname,
|
|
155
|
+
optionsWithDefaults(opts),
|
|
156
|
+
nativeFn,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
/**
|
|
134
161
|
* Retrieves metadata for all mounted volumes with optional filtering and
|
|
135
162
|
* concurrency control.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/mount_point_for_path.ts
|
|
2
|
+
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { withTimeout } from "./async";
|
|
6
|
+
import { debug } from "./debuglog";
|
|
7
|
+
import { statAsync } from "./fs";
|
|
8
|
+
import { isMacOS } from "./platform";
|
|
9
|
+
import { isBlank, isNotBlank } from "./string";
|
|
10
|
+
import type { NativeBindingsFn } from "./types/native_bindings";
|
|
11
|
+
import type { Options } from "./types/options";
|
|
12
|
+
import { findMountPointByDeviceId } from "./volume_metadata";
|
|
13
|
+
|
|
14
|
+
export async function getMountPointForPathImpl(
|
|
15
|
+
pathname: string,
|
|
16
|
+
opts: Options,
|
|
17
|
+
nativeFn: NativeBindingsFn,
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
if (isBlank(pathname)) {
|
|
20
|
+
throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// realpath() resolves POSIX symlinks. APFS firmlinks are NOT resolved by
|
|
24
|
+
// realpath(), but fstatfs() follows them — handled below on macOS.
|
|
25
|
+
const resolved = await realpath(pathname);
|
|
26
|
+
|
|
27
|
+
const resolvedStat = await statAsync(resolved);
|
|
28
|
+
const dir = resolvedStat.isDirectory() ? resolved : dirname(resolved);
|
|
29
|
+
|
|
30
|
+
if (isMacOS) {
|
|
31
|
+
// Use the lightweight native getMountPoint which only does fstatfs —
|
|
32
|
+
// no DiskArbitration, IOKit, or space calculations.
|
|
33
|
+
const native = await nativeFn();
|
|
34
|
+
if (native.getMountPoint) {
|
|
35
|
+
debug("[getMountPointForPath] using native getMountPoint for %s", dir);
|
|
36
|
+
const p = native.getMountPoint(dir);
|
|
37
|
+
const mountPoint = await withTimeout({
|
|
38
|
+
desc: "getMountPoint()",
|
|
39
|
+
timeoutMs: opts.timeoutMs,
|
|
40
|
+
promise: p,
|
|
41
|
+
});
|
|
42
|
+
if (isNotBlank(mountPoint)) {
|
|
43
|
+
debug("[getMountPointForPath] resolved to %s", mountPoint);
|
|
44
|
+
return mountPoint;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fallback: should not happen on macOS, but defensive
|
|
48
|
+
throw new Error("getMountPoint native function unavailable");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Linux/Windows: device ID matching + path prefix tiebreaker
|
|
52
|
+
debug("[getMountPointForPath] using device matching for %s", resolved);
|
|
53
|
+
return findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn);
|
|
54
|
+
}
|
|
@@ -54,6 +54,13 @@ export interface NativeBindings {
|
|
|
54
54
|
* subsequent parsing and extraction logic.
|
|
55
55
|
*/
|
|
56
56
|
getVolumeMetadata(options: GetVolumeMetadataOptions): Promise<VolumeMetadata>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* macOS only: lightweight mount point lookup using fstatfs().
|
|
60
|
+
* Returns the f_mntonname for the given directory path without fetching
|
|
61
|
+
* full volume metadata (no DiskArbitration, no IOKit, no space calculation).
|
|
62
|
+
*/
|
|
63
|
+
getMountPoint?(path: string): Promise<string>;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export type GetVolumeMetadataOptions = {
|
package/src/volume_metadata.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/volume_metadata.ts
|
|
2
2
|
|
|
3
|
+
import type { Stats } from "node:fs";
|
|
3
4
|
import { realpath } from "node:fs/promises";
|
|
4
5
|
import { dirname } from "node:path";
|
|
5
6
|
import { mapConcurrent, withTimeout } from "./async";
|
|
@@ -209,6 +210,30 @@ export async function getVolumeMetadataForPathImpl(
|
|
|
209
210
|
// Linux/Windows: stat().dev is reliable (no firmlinks). Find the mount point
|
|
210
211
|
// by comparing device IDs, using path prefix as a tiebreaker for bind mounts
|
|
211
212
|
// or GIO mounts that share the same device id.
|
|
213
|
+
const mountPoint = await findMountPointByDeviceId(
|
|
214
|
+
resolved,
|
|
215
|
+
resolvedStat,
|
|
216
|
+
opts,
|
|
217
|
+
nativeFn,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return getVolumeMetadataImpl({ ...opts, mountPoint }, nativeFn);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Find the mount point for a resolved path using device ID matching.
|
|
225
|
+
* Used on Linux and Windows where stat().dev is reliable (no firmlinks).
|
|
226
|
+
*
|
|
227
|
+
* Compares device IDs of mount points against the target path's device ID,
|
|
228
|
+
* using path prefix as a tiebreaker for bind mounts or GIO mounts that share
|
|
229
|
+
* the same device id. The longest prefix match wins.
|
|
230
|
+
*/
|
|
231
|
+
export async function findMountPointByDeviceId(
|
|
232
|
+
resolved: string,
|
|
233
|
+
resolvedStat: Stats,
|
|
234
|
+
opts: Options,
|
|
235
|
+
nativeFn: NativeBindingsFn,
|
|
236
|
+
): Promise<string> {
|
|
212
237
|
const targetDev = resolvedStat.dev;
|
|
213
238
|
const mountPoints = await getVolumeMountPointsImpl(
|
|
214
239
|
{ ...opts, includeSystemVolumes: true },
|
|
@@ -234,18 +259,13 @@ export async function getVolumeMetadataForPathImpl(
|
|
|
234
259
|
}),
|
|
235
260
|
);
|
|
236
261
|
|
|
237
|
-
// Longest prefix match wins; fall back to device-only match.
|
|
238
262
|
const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
|
|
239
263
|
if (candidates.length === 0) {
|
|
240
264
|
throw new Error(
|
|
241
|
-
"No mount point found for path: " + JSON.stringify(
|
|
265
|
+
"No mount point found for path: " + JSON.stringify(resolved),
|
|
242
266
|
);
|
|
243
267
|
}
|
|
244
|
-
|
|
245
|
-
a.length >= b.length ? a : b,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
return getVolumeMetadataImpl({ ...opts, mountPoint }, nativeFn);
|
|
268
|
+
return candidates.reduce((a, b) => (a.length >= b.length ? a : b));
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
export async function getAllVolumeMetadataImpl(
|