@photostructure/fs-metadata 1.3.0 → 1.4.1
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/AGENTS.md +213 -0
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +3 -3
- package/binding.gyp +0 -22
- package/dist/index.cjs +10 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +10 -39
- package/dist/index.mjs.map +1 -1
- package/doc/C++_REVIEW_TODO.md +3 -36
- package/doc/LINUX_API_REFERENCE.md +4 -147
- package/doc/SECURITY_AUDIT_2026.md +4 -0
- package/doc/gotchas.md +27 -0
- package/package.json +11 -12
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.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/scripts/clang-tidy.ts +1 -3
- package/scripts/prebuild-linux-glibc.sh +1 -5
- package/scripts/sanitizers-test.sh +0 -1
- package/src/binding.cpp +5 -15
- package/src/common/metadata_worker.h +7 -6
- package/src/common/shutdown.h +162 -0
- package/src/darwin/get_mount_point.cpp +6 -4
- package/src/darwin/hidden.cpp +9 -8
- package/src/darwin/hidden.h +4 -3
- package/src/darwin/volume_metadata.cpp +15 -1
- package/src/darwin/volume_mount_points.cpp +26 -4
- package/src/linux/mount_points.ts +8 -42
- package/src/linux/volume_metadata.cpp +5 -17
- package/src/mount_point_for_path.ts +1 -1
- package/src/types/mount_point.ts +1 -1
- package/src/types/native_bindings.ts +0 -5
- package/src/volume_metadata.ts +16 -7
- package/src/volume_mount_points.ts +1 -1
- package/src/windows/hidden.cpp +10 -9
- package/src/windows/volume_metadata.cpp +5 -1
- package/src/windows/volume_mount_points.cpp +22 -4
- package/scripts/setup-native.mjs +0 -39
- package/src/linux/gio_mount_points.cpp +0 -80
- package/src/linux/gio_mount_points.h +0 -37
- package/src/linux/gio_utils.cpp +0 -115
- package/src/linux/gio_utils.h +0 -69
- package/src/linux/gio_volume_metadata.cpp +0 -81
- package/src/linux/gio_volume_metadata.h +0 -20
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "../common/error_utils.h"
|
|
8
8
|
#include "../common/fd_guard.h"
|
|
9
9
|
#include "../common/path_security.h"
|
|
10
|
+
#include "../common/shutdown.h"
|
|
10
11
|
|
|
11
12
|
#include <fcntl.h>
|
|
12
13
|
#include <string>
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
|
|
17
18
|
namespace FSMeta {
|
|
18
19
|
|
|
19
|
-
class GetMountPointWorker : public
|
|
20
|
+
class GetMountPointWorker : public SafeAsyncWorker {
|
|
20
21
|
public:
|
|
21
22
|
GetMountPointWorker(const std::string &path,
|
|
22
23
|
const Napi::Promise::Deferred &deferred)
|
|
23
|
-
:
|
|
24
|
+
: SafeAsyncWorker(deferred.Env()), path_(path), deferred_(deferred) {}
|
|
24
25
|
|
|
25
26
|
void Execute() override {
|
|
26
27
|
DEBUG_LOG("[GetMountPointWorker] Executing for path: %s", path_.c_str());
|
|
@@ -65,11 +66,12 @@ public:
|
|
|
65
66
|
|
|
66
67
|
void OnOK() override {
|
|
67
68
|
Napi::HandleScope scope(Env());
|
|
68
|
-
deferred_
|
|
69
|
+
SafeResolve(deferred_, Napi::String::New(Env(), result_));
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
void OnError(const Napi::Error &error) override {
|
|
72
|
-
|
|
73
|
+
Napi::HandleScope scope(Env());
|
|
74
|
+
SafeReject(deferred_, error.Value());
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
private:
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
5
|
#include "../common/fd_guard.h"
|
|
6
6
|
#include "../common/path_security.h"
|
|
7
|
+
#include "../common/shutdown.h"
|
|
7
8
|
#include <fcntl.h> // for open(), O_RDONLY, O_CLOEXEC
|
|
8
9
|
#include <string.h> // for strcmp
|
|
9
10
|
#include <sys/mount.h>
|
|
@@ -14,7 +15,7 @@ namespace FSMeta {
|
|
|
14
15
|
|
|
15
16
|
GetHiddenWorker::GetHiddenWorker(std::string path,
|
|
16
17
|
Napi::Promise::Deferred deferred)
|
|
17
|
-
:
|
|
18
|
+
: SafeAsyncWorker(deferred.Env()), path_(std::move(path)),
|
|
18
19
|
deferred_(deferred), is_hidden_(false) {
|
|
19
20
|
DEBUG_LOG("[GetHiddenWorker] created for path: %s", path_.c_str());
|
|
20
21
|
}
|
|
@@ -81,12 +82,12 @@ void GetHiddenWorker::Execute() {
|
|
|
81
82
|
void GetHiddenWorker::OnOK() {
|
|
82
83
|
Napi::HandleScope scope(Env());
|
|
83
84
|
auto env = Env();
|
|
84
|
-
deferred_
|
|
85
|
+
SafeResolve(deferred_, Napi::Boolean::New(env, is_hidden_));
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
void GetHiddenWorker::OnError(const Napi::Error &error) {
|
|
88
89
|
Napi::HandleScope scope(Env());
|
|
89
|
-
deferred_
|
|
90
|
+
SafeReject(deferred_, error.Value());
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
@@ -111,8 +112,8 @@ Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
111
112
|
|
|
112
113
|
SetHiddenWorker::SetHiddenWorker(std::string path, bool hidden,
|
|
113
114
|
Napi::Promise::Deferred deferred)
|
|
114
|
-
:
|
|
115
|
-
|
|
115
|
+
: SafeAsyncWorker(deferred.Env()), path_(std::move(path)), hidden_(hidden),
|
|
116
|
+
deferred_(deferred) {
|
|
116
117
|
DEBUG_LOG("[SetHiddenWorker] created for path: %s, hidden: %d", path_.c_str(),
|
|
117
118
|
hidden_);
|
|
118
119
|
}
|
|
@@ -206,12 +207,12 @@ void SetHiddenWorker::Execute() {
|
|
|
206
207
|
void SetHiddenWorker::OnOK() {
|
|
207
208
|
Napi::HandleScope scope(Env());
|
|
208
209
|
auto env = Env();
|
|
209
|
-
deferred_
|
|
210
|
+
SafeResolve(deferred_, env.Undefined());
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
void SetHiddenWorker::OnError(const Napi::Error &error) {
|
|
213
214
|
Napi::HandleScope scope(Env());
|
|
214
|
-
deferred_
|
|
215
|
+
SafeReject(deferred_, error.Value());
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
@@ -238,4 +239,4 @@ Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
238
239
|
return deferred.Promise();
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
} // namespace FSMeta
|
|
242
|
+
} // namespace FSMeta
|
package/src/darwin/hidden.h
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/darwin/hidden.h
|
|
2
2
|
#pragma once
|
|
3
3
|
#include "../common/hidden.h"
|
|
4
|
+
#include "../common/shutdown.h"
|
|
4
5
|
#include <napi.h>
|
|
5
6
|
|
|
6
7
|
namespace FSMeta {
|
|
7
8
|
|
|
8
|
-
class GetHiddenWorker : public
|
|
9
|
+
class GetHiddenWorker : public SafeAsyncWorker {
|
|
9
10
|
public:
|
|
10
11
|
GetHiddenWorker(std::string path, Napi::Promise::Deferred deferred);
|
|
11
12
|
void Execute() override;
|
|
@@ -18,7 +19,7 @@ private:
|
|
|
18
19
|
bool is_hidden_;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
class SetHiddenWorker : public
|
|
22
|
+
class SetHiddenWorker : public SafeAsyncWorker {
|
|
22
23
|
public:
|
|
23
24
|
SetHiddenWorker(std::string path, bool hidden,
|
|
24
25
|
Napi::Promise::Deferred deferred);
|
|
@@ -32,4 +33,4 @@ private:
|
|
|
32
33
|
Napi::Promise::Deferred deferred_;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
} // namespace FSMeta
|
|
36
|
+
} // namespace FSMeta
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "../common/debug_log.h"
|
|
5
5
|
#include "../common/fd_guard.h"
|
|
6
6
|
#include "../common/path_security.h"
|
|
7
|
+
#include "../common/shutdown.h"
|
|
7
8
|
#include "../common/volume_utils.h"
|
|
8
9
|
#include "./da_mutex.h"
|
|
9
10
|
#include "./fs_meta.h"
|
|
@@ -76,6 +77,11 @@ public:
|
|
|
76
77
|
void Execute() override {
|
|
77
78
|
DEBUG_LOG("[GetVolumeMetadataWorker] Executing for mount point: %s",
|
|
78
79
|
mountPoint.c_str());
|
|
80
|
+
if (IsShuttingDown()) {
|
|
81
|
+
// Avoid kicking off blocking IOKit/DA calls during env teardown.
|
|
82
|
+
SetError("fs-metadata: shutdown in progress");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
79
85
|
try {
|
|
80
86
|
// Validate and canonicalize mount point using realpath()
|
|
81
87
|
// This follows Apple's Secure Coding Guide recommendations
|
|
@@ -207,6 +213,14 @@ private:
|
|
|
207
213
|
DEBUG_LOG("[GetVolumeMetadataWorker] Getting Disk Arbitration info for: %s",
|
|
208
214
|
mountPoint.c_str());
|
|
209
215
|
|
|
216
|
+
if (IsShuttingDown()) {
|
|
217
|
+
// IOServiceGetMatchingService is uncancellable; if we're already in
|
|
218
|
+
// teardown, surface a partial result rather than block FreeEnvironment.
|
|
219
|
+
metadata.status = "partial";
|
|
220
|
+
metadata.error = "shutdown in progress";
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
// Check if this is a network filesystem
|
|
211
225
|
if (metadata.fstype == "smbfs" || metadata.fstype == "nfs" ||
|
|
212
226
|
metadata.fstype == "afpfs" || metadata.fstype == "webdav") {
|
|
@@ -370,4 +384,4 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
370
384
|
return deferred.Promise();
|
|
371
385
|
}
|
|
372
386
|
|
|
373
|
-
} // namespace FSMeta
|
|
387
|
+
} // namespace FSMeta
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "../common/volume_mount_points.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
+
#include "../common/shutdown.h"
|
|
5
6
|
#include "./da_mutex.h"
|
|
6
7
|
#include "./fs_meta.h"
|
|
7
8
|
#include "./raii_utils.h"
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
|
|
14
15
|
namespace FSMeta {
|
|
15
16
|
|
|
16
|
-
class GetVolumeMountPointsWorker : public
|
|
17
|
+
class GetVolumeMountPointsWorker : public SafeAsyncWorker {
|
|
17
18
|
private:
|
|
18
19
|
Napi::Promise::Deferred deferred_;
|
|
19
20
|
std::vector<MountPoint> mountPoints_;
|
|
@@ -22,11 +23,15 @@ private:
|
|
|
22
23
|
public:
|
|
23
24
|
GetVolumeMountPointsWorker(const Napi::Promise::Deferred &deferred,
|
|
24
25
|
uint32_t timeoutMs = 5000)
|
|
25
|
-
:
|
|
26
|
+
: SafeAsyncWorker(deferred.Env()), deferred_(deferred),
|
|
26
27
|
timeoutMs_(timeoutMs) {}
|
|
27
28
|
|
|
28
29
|
void Execute() override {
|
|
29
30
|
DEBUG_LOG("[GetVolumeMountPointsWorker] Executing");
|
|
31
|
+
if (IsShuttingDown()) {
|
|
32
|
+
SetError("fs-metadata: shutdown in progress");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
try {
|
|
31
36
|
MountBufferRAII mntbuf;
|
|
32
37
|
// Use MNT_NOWAIT for better performance - we'll verify accessibility
|
|
@@ -57,6 +62,10 @@ public:
|
|
|
57
62
|
{
|
|
58
63
|
std::lock_guard<std::mutex> lock(g_diskArbitrationMutex);
|
|
59
64
|
|
|
65
|
+
if (IsShuttingDown()) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
DASessionRAII session(DASessionCreate(kCFAllocatorDefault));
|
|
61
70
|
if (session.isValid()) {
|
|
62
71
|
static dispatch_queue_t da_queue = dispatch_queue_create(
|
|
@@ -66,6 +75,10 @@ public:
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
for (int j = 0; j < count; j++) {
|
|
78
|
+
if (IsShuttingDown()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
MountPoint mp;
|
|
70
83
|
mp.mountPoint = mntbuf.get()[j].f_mntonname;
|
|
71
84
|
mp.fstype = mntbuf.get()[j].f_fstypename;
|
|
@@ -88,6 +101,10 @@ public:
|
|
|
88
101
|
const size_t maxConcurrentChecks = 4; // Limit concurrent access checks
|
|
89
102
|
|
|
90
103
|
for (size_t i = 0; i < allMountPoints.size(); i += maxConcurrentChecks) {
|
|
104
|
+
if (IsShuttingDown()) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
std::vector<std::future<std::pair<std::string, bool>>> futures;
|
|
92
109
|
std::vector<MountPoint *> batchPtrs;
|
|
93
110
|
|
|
@@ -182,7 +199,12 @@ public:
|
|
|
182
199
|
result[i] = mountPoints_[i].ToObject(env);
|
|
183
200
|
}
|
|
184
201
|
|
|
185
|
-
deferred_
|
|
202
|
+
SafeResolve(deferred_, result);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void OnError(const Napi::Error &error) override {
|
|
206
|
+
Napi::HandleScope scope(Env());
|
|
207
|
+
SafeReject(deferred_, error.Value());
|
|
186
208
|
}
|
|
187
209
|
};
|
|
188
210
|
|
|
@@ -202,4 +224,4 @@ Napi::Promise GetVolumeMountPoints(const Napi::CallbackInfo &info) {
|
|
|
202
224
|
return deferred.Promise();
|
|
203
225
|
}
|
|
204
226
|
|
|
205
|
-
} // namespace FSMeta
|
|
227
|
+
} // namespace FSMeta
|
|
@@ -2,69 +2,35 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { debug } from "../debuglog";
|
|
4
4
|
import { toError, WrappedError } from "../error";
|
|
5
|
-
import { isMountPoint } from "../mount_point";
|
|
6
|
-
import { compactValues } from "../object";
|
|
7
5
|
import { optionsWithDefaults } from "../options";
|
|
8
6
|
import { type MountPoint } from "../types/mount_point";
|
|
9
|
-
import type { NativeBindingsFn } from "../types/native_bindings";
|
|
10
7
|
import type { Options } from "../types/options";
|
|
11
8
|
import { MountEntry, mountEntryToMountPoint, parseMtab } from "./mtab";
|
|
12
9
|
|
|
13
10
|
export async function getLinuxMountPoints(
|
|
14
|
-
native: NativeBindingsFn,
|
|
15
11
|
opts?: Pick<Options, "linuxMountTablePaths">,
|
|
16
12
|
): Promise<MountPoint[]> {
|
|
17
13
|
const o = optionsWithDefaults(opts);
|
|
18
|
-
const raw: MountPoint[] = [];
|
|
19
|
-
try {
|
|
20
|
-
// Get GIO mounts if available from native module
|
|
21
|
-
const arr = await (await native()).getGioMountPoints?.();
|
|
22
|
-
debug("[getLinuxMountPoints] GIO mount points: %o", arr);
|
|
23
|
-
if (arr != null) raw.push(...arr);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
debug("Failed to get GIO mount points: %s", error);
|
|
26
|
-
// GIO support not compiled in or failed, continue with just mtab mounts
|
|
27
|
-
}
|
|
28
|
-
|
|
29
14
|
let cause: Error | undefined;
|
|
30
15
|
for (const input of o.linuxMountTablePaths) {
|
|
31
16
|
try {
|
|
32
17
|
const mtabContent = await readFile(input, "utf8");
|
|
33
|
-
const
|
|
18
|
+
const results = parseMtab(mtabContent)
|
|
34
19
|
.map((ea) => mountEntryToMountPoint(ea))
|
|
35
20
|
.filter((ea) => ea != null);
|
|
36
|
-
debug("[getLinuxMountPoints] %s mount points: %o", input,
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
break;
|
|
21
|
+
debug("[getLinuxMountPoints] %s mount points: %o", input, results);
|
|
22
|
+
if (results.length > 0) {
|
|
23
|
+
return results;
|
|
40
24
|
}
|
|
41
25
|
} catch (error) {
|
|
42
26
|
cause ??= toError(error);
|
|
43
27
|
}
|
|
44
28
|
}
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (isMountPoint(merged)) {
|
|
51
|
-
byMountPoint.set(merged.mountPoint, merged);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (byMountPoint.size === 0) {
|
|
56
|
-
throw new WrappedError(
|
|
57
|
-
`Failed to find any mount points (tried: ${JSON.stringify(o.linuxMountTablePaths)})`,
|
|
58
|
-
{ cause },
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const results = [...byMountPoint.values()];
|
|
63
|
-
debug("[getLinuxMountPoints] %o", {
|
|
64
|
-
results: results.map((ea) => ea.mountPoint),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return results;
|
|
30
|
+
throw new WrappedError(
|
|
31
|
+
`Failed to find any mount points (tried: ${JSON.stringify(o.linuxMountTablePaths)})`,
|
|
32
|
+
{ cause },
|
|
33
|
+
);
|
|
68
34
|
}
|
|
69
35
|
|
|
70
36
|
export async function getLinuxMtabMetadata(
|
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
#include <sys/statvfs.h>
|
|
13
13
|
#include <unistd.h>
|
|
14
14
|
|
|
15
|
-
#ifdef ENABLE_GIO
|
|
16
|
-
#include "gio_volume_metadata.h"
|
|
17
|
-
#endif
|
|
18
|
-
|
|
19
15
|
namespace FSMeta {
|
|
20
16
|
|
|
21
17
|
class LinuxMetadataWorker : public MetadataWorkerBase {
|
|
@@ -31,6 +27,10 @@ public:
|
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
void Execute() override {
|
|
30
|
+
if (IsShuttingDown()) {
|
|
31
|
+
SetError("fs-metadata: shutdown in progress");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
34
|
try {
|
|
35
35
|
DEBUG_LOG("[LinuxMetadataWorker] starting statvfs for %s",
|
|
36
36
|
mountPoint.c_str());
|
|
@@ -113,18 +113,6 @@ public:
|
|
|
113
113
|
mountPoint.c_str(), metadata.size / 1e9,
|
|
114
114
|
metadata.available / 1e9);
|
|
115
115
|
|
|
116
|
-
#ifdef ENABLE_GIO
|
|
117
|
-
try {
|
|
118
|
-
DEBUG_LOG("[LinuxMetadataWorker] collecting GIO metadata for %s",
|
|
119
|
-
validated_mount_point.c_str());
|
|
120
|
-
gio::addMountMetadata(validated_mount_point, metadata);
|
|
121
|
-
} catch (const std::exception &e) {
|
|
122
|
-
DEBUG_LOG("[LinuxMetadataWorker] GIO error for %s: %s",
|
|
123
|
-
validated_mount_point.c_str(), e.what());
|
|
124
|
-
metadata.status = std::string("GIO warning: ") + e.what();
|
|
125
|
-
}
|
|
126
|
-
#endif
|
|
127
|
-
|
|
128
116
|
if (!options_.device.empty()) {
|
|
129
117
|
DEBUG_LOG("[LinuxMetadataWorker] getting blkid info for device %s",
|
|
130
118
|
options_.device.c_str());
|
|
@@ -197,4 +185,4 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
197
185
|
return deferred.Promise();
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
} // namespace FSMeta
|
|
188
|
+
} // namespace FSMeta
|
|
@@ -48,7 +48,7 @@ export async function getMountPointForPathImpl(
|
|
|
48
48
|
throw new Error("getMountPoint native function unavailable");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Linux/Windows: device ID
|
|
51
|
+
// Linux/Windows: device ID filtering + longest ancestor path matching
|
|
52
52
|
debug("[getMountPointForPath] using device matching for %s", resolved);
|
|
53
53
|
return findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn);
|
|
54
54
|
}
|
package/src/types/mount_point.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface MountPoint {
|
|
|
29
29
|
* verifyVolume`.
|
|
30
30
|
*
|
|
31
31
|
* If there are non-critical errors while extracting metadata, those error
|
|
32
|
-
* messages may be added to this field (say, from blkid
|
|
32
|
+
* messages may be added to this field (say, from blkid).
|
|
33
33
|
*
|
|
34
34
|
* @see {@link VolumeHealthStatuses} for values returned by Windows.
|
|
35
35
|
*/
|
|
@@ -43,11 +43,6 @@ export interface NativeBindings {
|
|
|
43
43
|
options?: Pick<Options, "timeoutMs">,
|
|
44
44
|
): Promise<MountPoint[]>;
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* This is only available on Linux, and only if libglib-2.0 is installed.
|
|
48
|
-
*/
|
|
49
|
-
getGioMountPoints?(): Promise<MountPoint[]>;
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* This is only a partial implementation for most platforms, to minimize
|
|
53
48
|
* native code when possible. The javascript side handles a bunch of
|
package/src/volume_metadata.ts
CHANGED
|
@@ -95,7 +95,8 @@ async function _getVolumeMetadata(
|
|
|
95
95
|
}
|
|
96
96
|
} catch (err) {
|
|
97
97
|
debug("[getVolumeMetadata] failed to get mtab info: " + err);
|
|
98
|
-
//
|
|
98
|
+
// Mtab lookup can fail for transient mounts or race conditions.
|
|
99
|
+
// Ignore and continue with whatever the native call returns.
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -140,7 +141,7 @@ async function _getVolumeMetadata(
|
|
|
140
141
|
remote,
|
|
141
142
|
}) as VolumeMetadata;
|
|
142
143
|
|
|
143
|
-
// Backfill if blkid
|
|
144
|
+
// Backfill if blkid failed us:
|
|
144
145
|
if (isLinux && isNotBlank(device)) {
|
|
145
146
|
// Sometimes blkid doesn't have the UUID in cache. Try to get it from
|
|
146
147
|
// /dev/disk/by-uuid:
|
|
@@ -209,7 +210,7 @@ export async function getVolumeMetadataForPathImpl(
|
|
|
209
210
|
|
|
210
211
|
// Linux/Windows: stat().dev is reliable (no firmlinks). Find the mount point
|
|
211
212
|
// by comparing device IDs, using path prefix as a tiebreaker for bind mounts
|
|
212
|
-
// or
|
|
213
|
+
// or GVfs/FUSE mounts that share the same device id.
|
|
213
214
|
const mountPoint = await findMountPointByDeviceId(
|
|
214
215
|
resolved,
|
|
215
216
|
resolvedStat,
|
|
@@ -221,12 +222,18 @@ export async function getVolumeMetadataForPathImpl(
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
/**
|
|
224
|
-
* Find the mount point for a resolved path using device ID
|
|
225
|
+
* Find the mount point for a resolved path using device ID + path ancestry.
|
|
225
226
|
* Used on Linux and Windows where stat().dev is reliable (no firmlinks).
|
|
226
227
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
228
|
+
* Device ID filters out unrelated filesystems. Among same-device mount points,
|
|
229
|
+
* ancestor-path matches (mount point is a parent of `resolved`) are strongly
|
|
230
|
+
* preferred over device-only matches — GVfs/FUSE mounts on Linux can share
|
|
231
|
+
* the same device ID across unrelated volumes (e.g. multiple SMB shares
|
|
232
|
+
* under /run/user/.../gvfs/), so device ID alone is ambiguous. The longest
|
|
233
|
+
* ancestor wins.
|
|
234
|
+
*
|
|
235
|
+
* The device-only fallback (`deviceMatches`) exists for bind mounts where the
|
|
236
|
+
* canonical mount point may not be a path ancestor of the target.
|
|
230
237
|
*/
|
|
231
238
|
export async function findMountPointByDeviceId(
|
|
232
239
|
resolved: string,
|
|
@@ -261,6 +268,8 @@ export async function findMountPointByDeviceId(
|
|
|
261
268
|
}),
|
|
262
269
|
);
|
|
263
270
|
|
|
271
|
+
// Prefer ancestor matches — they're unambiguous. Fall back to device-only
|
|
272
|
+
// matches only when the mount point isn't an ancestor (e.g. bind mounts).
|
|
264
273
|
const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
|
|
265
274
|
if (candidates.length === 0) {
|
|
266
275
|
throw new Error(
|
package/src/windows/hidden.cpp
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/windows/hidden.cpp
|
|
2
2
|
#include "hidden.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
|
+
#include "../common/shutdown.h"
|
|
4
5
|
#include "error_utils.h"
|
|
5
6
|
#include "security_utils.h"
|
|
6
7
|
|
|
@@ -36,14 +37,14 @@ public:
|
|
|
36
37
|
};
|
|
37
38
|
} // anonymous namespace
|
|
38
39
|
|
|
39
|
-
class GetHiddenWorker : public
|
|
40
|
+
class GetHiddenWorker : public SafeAsyncWorker {
|
|
40
41
|
const std::string path;
|
|
41
42
|
bool result = false;
|
|
42
43
|
Napi::Promise::Deferred deferred;
|
|
43
44
|
|
|
44
45
|
public:
|
|
45
46
|
GetHiddenWorker(Napi::Env env, std::string p, Napi::Promise::Deferred def)
|
|
46
|
-
:
|
|
47
|
+
: SafeAsyncWorker(env), path(std::move(p)), deferred(def) {}
|
|
47
48
|
|
|
48
49
|
void Execute() override {
|
|
49
50
|
try {
|
|
@@ -104,17 +105,17 @@ public:
|
|
|
104
105
|
DEBUG_LOG("[GetHiddenWorker] OnOK called, result=%s",
|
|
105
106
|
result ? "true" : "false");
|
|
106
107
|
Napi::HandleScope scope(Env());
|
|
107
|
-
deferred
|
|
108
|
+
SafeResolve(deferred, Napi::Boolean::New(Env(), result));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
void OnError(const Napi::Error &e) override {
|
|
111
112
|
DEBUG_LOG("[GetHiddenWorker] OnError called with: %s", e.Message().c_str());
|
|
112
113
|
Napi::HandleScope scope(Env());
|
|
113
|
-
deferred
|
|
114
|
+
SafeReject(deferred, e.Value());
|
|
114
115
|
}
|
|
115
116
|
};
|
|
116
117
|
|
|
117
|
-
class SetHiddenWorker : public
|
|
118
|
+
class SetHiddenWorker : public SafeAsyncWorker {
|
|
118
119
|
const std::string path;
|
|
119
120
|
const bool value;
|
|
120
121
|
Napi::Promise::Deferred deferred;
|
|
@@ -122,7 +123,7 @@ class SetHiddenWorker : public Napi::AsyncWorker {
|
|
|
122
123
|
public:
|
|
123
124
|
SetHiddenWorker(Napi::Env env, std::string p, bool v,
|
|
124
125
|
Napi::Promise::Deferred def)
|
|
125
|
-
:
|
|
126
|
+
: SafeAsyncWorker(env), path(std::move(p)), value(v), deferred(def) {}
|
|
126
127
|
|
|
127
128
|
void Execute() override {
|
|
128
129
|
try {
|
|
@@ -144,12 +145,12 @@ public:
|
|
|
144
145
|
|
|
145
146
|
void OnOK() override {
|
|
146
147
|
Napi::HandleScope scope(Env());
|
|
147
|
-
deferred
|
|
148
|
+
SafeResolve(deferred, Napi::Boolean::New(Env(), true));
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
void OnError(const Napi::Error &e) override {
|
|
151
152
|
Napi::HandleScope scope(Env());
|
|
152
|
-
deferred
|
|
153
|
+
SafeReject(deferred, e.Value());
|
|
153
154
|
}
|
|
154
155
|
};
|
|
155
156
|
|
|
@@ -195,4 +196,4 @@ Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
} // namespace FSMeta
|
|
199
|
+
} // namespace FSMeta
|
|
@@ -154,6 +154,10 @@ private:
|
|
|
154
154
|
VolumeMetadataOptions options_;
|
|
155
155
|
|
|
156
156
|
void Execute() override {
|
|
157
|
+
if (IsShuttingDown()) {
|
|
158
|
+
SetError("fs-metadata: shutdown in progress");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
157
161
|
try {
|
|
158
162
|
// Get drive status first
|
|
159
163
|
DriveStatus status = CheckDriveStatus(mountPoint);
|
|
@@ -250,4 +254,4 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
250
254
|
return deferred.Promise();
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
} // namespace FSMeta
|
|
257
|
+
} // namespace FSMeta
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "../common/volume_mount_points.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
+
#include "../common/shutdown.h"
|
|
5
6
|
#include "drive_status.h"
|
|
6
7
|
#include "fs_meta.h"
|
|
7
8
|
#include "security_utils.h"
|
|
@@ -21,7 +22,7 @@ struct DriveStringsBuffer {
|
|
|
21
22
|
: buffer(std::make_unique<WCHAR[]>(size)) {}
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
class GetVolumeMountPointsWorker : public
|
|
25
|
+
class GetVolumeMountPointsWorker : public SafeAsyncWorker {
|
|
25
26
|
|
|
26
27
|
private:
|
|
27
28
|
Napi::Promise::Deferred deferred_;
|
|
@@ -31,10 +32,14 @@ private:
|
|
|
31
32
|
public:
|
|
32
33
|
GetVolumeMountPointsWorker(const Napi::Promise::Deferred &deferred,
|
|
33
34
|
uint32_t timeoutMs = 5000)
|
|
34
|
-
:
|
|
35
|
+
: SafeAsyncWorker(deferred.Env()), deferred_(deferred),
|
|
35
36
|
timeoutMs_(timeoutMs) {}
|
|
36
37
|
|
|
37
38
|
void Execute() override {
|
|
39
|
+
if (IsShuttingDown()) {
|
|
40
|
+
SetError("fs-metadata: shutdown in progress");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
38
43
|
try {
|
|
39
44
|
DEBUG_LOG("[GetVolumeMountPoints] getting logical drive strings size");
|
|
40
45
|
DWORD size = GetLogicalDriveStringsW(0, nullptr);
|
|
@@ -74,12 +79,19 @@ public:
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
// Check all drive statuses in parallel
|
|
82
|
+
if (IsShuttingDown()) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
77
85
|
auto statuses = CheckDriveStatus(paths, timeoutMs_);
|
|
78
86
|
|
|
79
87
|
// Build mount points from results
|
|
80
88
|
mountPoints_.reserve(paths.size());
|
|
81
89
|
|
|
82
90
|
for (size_t i = 0; i < paths.size(); i++) {
|
|
91
|
+
if (IsShuttingDown()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
MountPoint mp;
|
|
84
96
|
mp.mountPoint = paths[i];
|
|
85
97
|
mp.status = DriveStatusToString(statuses[i]);
|
|
@@ -115,6 +127,7 @@ public:
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
void OnOK() override {
|
|
130
|
+
Napi::HandleScope scope(Env());
|
|
118
131
|
auto env = Env();
|
|
119
132
|
Napi::Array result = Napi::Array::New(env, mountPoints_.size());
|
|
120
133
|
|
|
@@ -122,7 +135,12 @@ public:
|
|
|
122
135
|
result[i] = mountPoints_[i].ToObject(env);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
deferred_
|
|
138
|
+
SafeResolve(deferred_, result);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void OnError(const Napi::Error &error) override {
|
|
142
|
+
Napi::HandleScope scope(Env());
|
|
143
|
+
SafeReject(deferred_, error.Value());
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
}; // class GetVolumeMountPointsWorker
|
|
@@ -140,4 +158,4 @@ Napi::Promise GetVolumeMountPoints(const Napi::CallbackInfo &info) {
|
|
|
140
158
|
worker->Queue();
|
|
141
159
|
return deferred.Promise();
|
|
142
160
|
}
|
|
143
|
-
} // namespace FSMeta
|
|
161
|
+
} // namespace FSMeta
|