@photostructure/fs-metadata 0.3.2 → 0.4.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 +17 -3
- package/README.md +3 -3
- package/dist/index.cjs +324 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +329 -215
- package/dist/index.mjs.map +1 -1
- package/dist/types/debuglog.d.ts +2 -6
- package/dist/types/defer.d.ts +1 -2
- package/dist/types/dirname.d.ts +1 -0
- package/dist/types/hidden.d.ts +5 -42
- package/dist/types/index.d.ts +91 -2
- package/dist/types/linux/mount_points.d.ts +2 -2
- package/dist/types/linux/mtab.d.ts +2 -2
- package/dist/types/mount_point.d.ts +1 -46
- package/dist/types/object.d.ts +8 -3
- package/dist/types/options.d.ts +2 -48
- package/dist/types/platform.d.ts +1 -0
- package/dist/types/remote_info.d.ts +2 -34
- package/dist/types/stack_path.d.ts +2 -0
- package/dist/types/system_volume.d.ts +2 -2
- package/dist/types/types/hidden_metadata.d.ts +32 -0
- package/dist/types/types/mount_point.d.ts +46 -0
- package/dist/types/types/native_bindings.d.ts +3 -3
- package/dist/types/types/options.d.ts +47 -0
- package/dist/types/types/remote_info.d.ts +33 -0
- package/dist/types/types/volume_metadata.d.ts +46 -0
- package/dist/types/unc.d.ts +1 -1
- package/dist/types/units.d.ts +25 -3
- package/dist/types/volume_metadata.d.ts +4 -50
- package/dist/types/volume_mount_points.d.ts +3 -6
- package/jest.config.base.cjs +63 -0
- package/jest.config.cjs +3 -16
- package/package.json +13 -15
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/src/async.ts +9 -0
- package/src/darwin/hidden.cpp +14 -0
- package/src/darwin/raii_utils.h +85 -0
- package/src/darwin/volume_metadata.cpp +35 -60
- package/src/darwin/volume_mount_points.cpp +31 -22
- package/src/debuglog.ts +6 -2
- package/src/defer.ts +1 -1
- package/src/dirname.ts +13 -0
- package/src/global.d.ts +1 -0
- package/src/hidden.ts +6 -42
- package/src/{exports.ts → index.ts} +75 -30
- package/src/linux/mount_points.ts +4 -2
- package/src/linux/mtab.ts +3 -3
- package/src/mount_point.ts +2 -53
- package/src/object.ts +8 -3
- package/src/options.ts +5 -54
- package/src/path.ts +12 -5
- package/src/platform.ts +5 -5
- package/src/remote_info.ts +44 -49
- package/src/stack_path.ts +71 -0
- package/src/system_volume.ts +3 -6
- package/src/test-utils/assert.ts +1 -1
- package/src/test-utils/debuglog-child.ts +15 -0
- package/src/types/hidden_metadata.ts +38 -0
- package/src/types/mount_point.ts +53 -0
- package/src/types/native_bindings.ts +3 -3
- package/src/types/options.ts +54 -0
- package/src/types/remote_info.ts +35 -0
- package/src/types/volume_metadata.ts +52 -0
- package/src/unc.ts +1 -1
- package/src/units.ts +39 -7
- package/src/volume_metadata.ts +9 -66
- package/src/volume_mount_points.ts +3 -6
- package/tsup.config.ts +1 -0
- package/dist/types/exports.d.ts +0 -99
- package/dist/types/setup.d.ts +0 -2
- package/src/index.cts +0 -15
- package/src/index.mts +0 -17
- package/src/setup.ts +0 -69
|
@@ -1,54 +1,8 @@
|
|
|
1
|
-
import type { MountPoint } from "./mount_point.js";
|
|
2
|
-
import { type Options } from "./options.js";
|
|
3
|
-
import { type RemoteInfo } from "./remote_info.js";
|
|
4
1
|
import type { GetVolumeMetadataOptions, NativeBindingsFn } from "./types/native_bindings.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
export interface VolumeMetadata extends RemoteInfo, MountPoint {
|
|
11
|
-
/**
|
|
12
|
-
* The name of the partition
|
|
13
|
-
*/
|
|
14
|
-
label?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Total size in bytes
|
|
17
|
-
*/
|
|
18
|
-
size?: number;
|
|
19
|
-
/**
|
|
20
|
-
* Used size in bytes
|
|
21
|
-
*/
|
|
22
|
-
used?: number;
|
|
23
|
-
/**
|
|
24
|
-
* Available size in bytes
|
|
25
|
-
*/
|
|
26
|
-
available?: number;
|
|
27
|
-
/**
|
|
28
|
-
* Path to the device or service that the mountpoint is from.
|
|
29
|
-
*
|
|
30
|
-
* Examples include `/dev/sda1`, `nfs-server:/export`,
|
|
31
|
-
* `//username@remoteHost/remoteShare`, or `//cifs-server/share`.
|
|
32
|
-
*
|
|
33
|
-
* May be undefined for remote volumes.
|
|
34
|
-
*/
|
|
35
|
-
mountFrom?: string;
|
|
36
|
-
/**
|
|
37
|
-
* The name of the mount. This may match the resolved mountPoint.
|
|
38
|
-
*/
|
|
39
|
-
mountName?: string;
|
|
40
|
-
/**
|
|
41
|
-
* UUID for the volume, like "c9b08f6e-b392-11ef-bf19-4b13bb7db4b4".
|
|
42
|
-
*
|
|
43
|
-
* On windows, this _may_ be the 128-bit volume UUID, but if that is not
|
|
44
|
-
* available, like in the case of remote volumes, we fallback to the 32-bit
|
|
45
|
-
* volume serial number, rendered in lowercase hexadecimal.
|
|
46
|
-
*/
|
|
47
|
-
uuid?: string;
|
|
48
|
-
}
|
|
49
|
-
export declare function getVolumeMetadata(o: GetVolumeMetadataOptions & Options, nativeFn: NativeBindingsFn): Promise<VolumeMetadata>;
|
|
50
|
-
export declare function getAllVolumeMetadata(opts: Required<Options> & {
|
|
2
|
+
import type { Options } from "./types/options.js";
|
|
3
|
+
import type { VolumeMetadata } from "./types/volume_metadata.js";
|
|
4
|
+
export declare function getVolumeMetadataImpl(o: GetVolumeMetadataOptions & Options, nativeFn: NativeBindingsFn): Promise<VolumeMetadata>;
|
|
5
|
+
export declare function getAllVolumeMetadataImpl(opts: Required<Options> & {
|
|
51
6
|
includeSystemVolumes?: boolean;
|
|
52
7
|
maxConcurrency?: number;
|
|
53
8
|
}, nativeFn: NativeBindingsFn): Promise<VolumeMetadata[]>;
|
|
54
|
-
export declare const _: undefined;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { MountPoint } from "./mount_point.js";
|
|
2
|
-
import { Options } from "./options.js";
|
|
3
1
|
import { SystemVolumeConfig } from "./system_volume.js";
|
|
2
|
+
import type { MountPoint } from "./types/mount_point.js";
|
|
4
3
|
import type { NativeBindingsFn } from "./types/native_bindings.js";
|
|
4
|
+
import type { Options } from "./types/options.js";
|
|
5
5
|
export type GetVolumeMountPointOptions = Partial<Pick<Options, "timeoutMs" | "linuxMountTablePaths" | "maxConcurrency" | "includeSystemVolumes"> & SystemVolumeConfig>;
|
|
6
|
-
|
|
7
|
-
* Helper function for {@link getVolumeMountPoints}.
|
|
8
|
-
*/
|
|
9
|
-
export declare function getVolumeMountPoints(opts: Required<GetVolumeMountPointOptions>, nativeFn: NativeBindingsFn): Promise<MountPoint[]>;
|
|
6
|
+
export declare function getVolumeMountPointsImpl(opts: Required<GetVolumeMountPointOptions>, nativeFn: NativeBindingsFn): Promise<MountPoint[]>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// jest.config.base.cjs
|
|
4
|
+
|
|
5
|
+
const { argv, platform } = require("node:process");
|
|
6
|
+
|
|
7
|
+
const otherPlatforms = ["linux", "darwin", "windows"]
|
|
8
|
+
.filter((ea) => ea !== (platform === "win32" ? "windows" : platform))
|
|
9
|
+
.map((ea) => `/${ea}/`);
|
|
10
|
+
|
|
11
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
12
|
+
const baseConfig = {
|
|
13
|
+
testEnvironment: "jest-environment-node",
|
|
14
|
+
roots: ["<rootDir>/src"],
|
|
15
|
+
coverageProvider: "v8",
|
|
16
|
+
moduleNameMapper: {
|
|
17
|
+
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
18
|
+
},
|
|
19
|
+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
20
|
+
verbose: true,
|
|
21
|
+
silent: false,
|
|
22
|
+
randomize: true,
|
|
23
|
+
setupFilesAfterEnv: [
|
|
24
|
+
"jest-extended/all",
|
|
25
|
+
"<rootDir>/src/test-utils/jest-matchers.ts",
|
|
26
|
+
],
|
|
27
|
+
collectCoverage: !argv.includes("--no-coverage"),
|
|
28
|
+
coverageDirectory: "coverage",
|
|
29
|
+
coverageReporters: ["text", "lcov", "html"],
|
|
30
|
+
collectCoverageFrom: [
|
|
31
|
+
"src/**/*.ts",
|
|
32
|
+
// We have to include dist/*js because there are integration tests that
|
|
33
|
+
// import/require the root package directory:
|
|
34
|
+
"dist/*js",
|
|
35
|
+
],
|
|
36
|
+
coveragePathIgnorePatterns: [
|
|
37
|
+
"exports",
|
|
38
|
+
"setup",
|
|
39
|
+
"/test-utils/",
|
|
40
|
+
"\.d.ts$",
|
|
41
|
+
"/types/",
|
|
42
|
+
...otherPlatforms,
|
|
43
|
+
],
|
|
44
|
+
coverageThreshold: {
|
|
45
|
+
// As of 20250106 on linux:
|
|
46
|
+
// % Stmts | % Branch | % Funcs | % Lines
|
|
47
|
+
// 93.63 | 87.05 | 91.86 | 93.63
|
|
48
|
+
// As of 20250106 on darwin:
|
|
49
|
+
// % Stmts | % Branch | % Funcs | % Lines
|
|
50
|
+
// 85.91 | 84.03 | 88.69 | 85.91
|
|
51
|
+
// As of 20250106 on windows:
|
|
52
|
+
// % Stmts | % Branch | % Funcs | % Lines
|
|
53
|
+
// 85.91 | 84.03 | 88.69 | 85.91
|
|
54
|
+
global: {
|
|
55
|
+
statements: 80,
|
|
56
|
+
branches: 80,
|
|
57
|
+
functions: 80,
|
|
58
|
+
lines: 80,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = baseConfig;
|
package/jest.config.cjs
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const baseConfig = require("./jest.config.base.cjs");
|
|
4
4
|
|
|
5
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
5
6
|
const config = {
|
|
7
|
+
...baseConfig,
|
|
6
8
|
displayName: "@photostructure/fs-metadata (CJS)",
|
|
7
|
-
testEnvironment: "jest-environment-node",
|
|
8
|
-
roots: ["<rootDir>/src"],
|
|
9
|
-
coverageProvider: "v8",
|
|
10
|
-
moduleNameMapper: {
|
|
11
|
-
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
12
|
-
},
|
|
13
9
|
transform: {
|
|
14
10
|
"^.+\\.(c)?ts$": [
|
|
15
11
|
"ts-jest",
|
|
@@ -19,15 +15,6 @@ const config = {
|
|
|
19
15
|
},
|
|
20
16
|
],
|
|
21
17
|
},
|
|
22
|
-
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
23
|
-
collectCoverage: false,
|
|
24
|
-
verbose: true,
|
|
25
|
-
silent: false,
|
|
26
|
-
randomize: true,
|
|
27
|
-
setupFilesAfterEnv: [
|
|
28
|
-
"jest-extended/all",
|
|
29
|
-
"<rootDir>/src/test-utils/jest-matchers.ts",
|
|
30
|
-
],
|
|
31
18
|
};
|
|
32
19
|
|
|
33
20
|
module.exports = config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/fs-metadata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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/types/index.d.ts",
|
|
@@ -21,23 +21,20 @@
|
|
|
21
21
|
"configure": "node scripts/configure.mjs",
|
|
22
22
|
"prebuildify": "prebuildify --napi --tag-libc --strip",
|
|
23
23
|
"prebuild": "run-s configure prebuildify",
|
|
24
|
+
"// clang-tidy": "on ubuntu: `sudo apt install clang-tidy`, on mac: `brew install llvm && alias clang-tidy=$(brew --prefix llvm)/bin/clang-tidy`. Note that there will be warnings for non-platform-relevant files.",
|
|
24
25
|
"clang-tidy": "node-gyp configure -- -f compile_commands_json && clang-tidy -p build/Release src/**/*.cpp",
|
|
25
26
|
"compile": "run-p compile:*",
|
|
26
27
|
"compile:esm": "tsc -p tsconfig.esm.json --noEmit",
|
|
27
28
|
"compile:cjs": "tsc -p tsconfig.cjs.json --noEmit",
|
|
28
29
|
"compile:types": "tsc -p tsconfig.types.json",
|
|
29
|
-
"postcompile:types": "mv dist/types/index.d.mts dist/types/index.d.ts",
|
|
30
|
-
"watch": "tsc --watch",
|
|
31
30
|
"bundle": "run-p bundle:* compile:types",
|
|
32
|
-
"bundle:cjs": "tsup src/index.
|
|
33
|
-
"bundle:esm": "tsup src/index.
|
|
34
|
-
"docs": "typedoc --tsconfig tsconfig.esm.json --out docs src/index.
|
|
35
|
-
"jest:coverage": "jest --coverage",
|
|
36
|
-
"jest:watch": "npm t -- --watch",
|
|
37
|
-
"jest:clear": "jest --clearCache",
|
|
31
|
+
"bundle:cjs": "tsup src/index.ts --format cjs --tsconfig tsconfig.cjs.json",
|
|
32
|
+
"bundle:esm": "tsup src/index.ts --format esm --tsconfig tsconfig.esm.json",
|
|
33
|
+
"docs": "typedoc --tsconfig tsconfig.esm.json --out docs src/index.ts",
|
|
38
34
|
"// tests": "`compile` validates the typescript compiles with tsc. `lint` checks for style issues. `bundle` uses tsup to emit the CJS and ESM rollups that the integration tests depend on. `test:*` runs the tests.",
|
|
35
|
+
"// test": "support `npm t name_of_file` (and don't fail due to missing coverage)",
|
|
36
|
+
"test": "npm run test:cjs -- --no-coverage",
|
|
39
37
|
"tests": "run-s compile lint bundle test:*",
|
|
40
|
-
"test": "npm run test:cjs",
|
|
41
38
|
"test:cjs": "jest --config jest.config.cjs",
|
|
42
39
|
"test:esm": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js --config jest.config.mjs",
|
|
43
40
|
"// test:memory:todo": "set up valgrind or similar",
|
|
@@ -52,7 +49,7 @@
|
|
|
52
49
|
"fmt:json": "prettier --write \"**/*.json\"",
|
|
53
50
|
"fmt:pkg": "npm pkg fix",
|
|
54
51
|
"fmt:ts": "prettier --write \"**/*.(c|m)?ts\"",
|
|
55
|
-
"precommit": "run-s fmt clean prebuild tests
|
|
52
|
+
"precommit": "run-s fmt clean prebuild tests",
|
|
56
53
|
"prepare-release": "npm run bundle",
|
|
57
54
|
"release": "release-it"
|
|
58
55
|
},
|
|
@@ -89,9 +86,9 @@
|
|
|
89
86
|
"devDependencies": {
|
|
90
87
|
"@eslint/js": "^9.17.0",
|
|
91
88
|
"@types/jest": "^29.5.14",
|
|
92
|
-
"@types/node": "^22.10.
|
|
93
|
-
"@typescript-eslint/eslint-plugin": "^8.19.
|
|
94
|
-
"@typescript-eslint/parser": "^8.19.
|
|
89
|
+
"@types/node": "^22.10.5",
|
|
90
|
+
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
|
91
|
+
"@typescript-eslint/parser": "^8.19.1",
|
|
95
92
|
"cross-env": "^7.0.3",
|
|
96
93
|
"del-cli": "^6.0.0",
|
|
97
94
|
"eslint": "^9.17.0",
|
|
@@ -110,8 +107,9 @@
|
|
|
110
107
|
"terser": "^5.37.0",
|
|
111
108
|
"ts-jest": "^29.2.5",
|
|
112
109
|
"tsup": "^8.3.5",
|
|
110
|
+
"tsx": "^4.19.2",
|
|
113
111
|
"typedoc": "^0.27.6",
|
|
114
112
|
"typescript": "^5.7.2",
|
|
115
|
-
"typescript-eslint": "^8.19.
|
|
113
|
+
"typescript-eslint": "^8.19.1"
|
|
116
114
|
}
|
|
117
115
|
}
|
|
Binary file
|
|
Binary file
|
package/src/async.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { availableParallelism } from "node:os";
|
|
|
2
2
|
import { env } from "node:process";
|
|
3
3
|
import { gt0, isNumber } from "./number.js";
|
|
4
4
|
import { isBlank } from "./string.js";
|
|
5
|
+
import { DayMs } from "./units.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* An error that is thrown when a promise does not resolve within the specified
|
|
@@ -53,6 +54,14 @@ export async function withTimeout<T>(opts: {
|
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (timeoutMs > DayMs) {
|
|
58
|
+
throw new TypeError(
|
|
59
|
+
desc +
|
|
60
|
+
": Invalid timeoutMs is too large: must be less than one day, but got " +
|
|
61
|
+
timeoutMs,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
if (timeoutMs === 0) {
|
|
57
66
|
return opts.promise;
|
|
58
67
|
}
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -15,6 +15,13 @@ GetHiddenWorker::GetHiddenWorker(std::string path,
|
|
|
15
15
|
|
|
16
16
|
void GetHiddenWorker::Execute() {
|
|
17
17
|
DEBUG_LOG("[GetHiddenWorker] checking hidden status for: %s", path_.c_str());
|
|
18
|
+
|
|
19
|
+
// Add path validation to prevent directory traversal
|
|
20
|
+
if (path_.find("..") != std::string::npos) {
|
|
21
|
+
SetError("Invalid path containing '..'");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
struct stat statbuf;
|
|
19
26
|
if (stat(path_.c_str(), &statbuf) != 0) {
|
|
20
27
|
int error = errno;
|
|
@@ -73,6 +80,13 @@ SetHiddenWorker::SetHiddenWorker(std::string path, bool hidden,
|
|
|
73
80
|
void SetHiddenWorker::Execute() {
|
|
74
81
|
DEBUG_LOG("[SetHiddenWorker] setting hidden=%d for: %s", hidden_,
|
|
75
82
|
path_.c_str());
|
|
83
|
+
|
|
84
|
+
// Add path validation to prevent directory traversal
|
|
85
|
+
if (path_.find("..") != std::string::npos) {
|
|
86
|
+
SetError("Invalid path containing '..'");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
struct stat statbuf;
|
|
77
91
|
if (stat(path_.c_str(), &statbuf) != 0) {
|
|
78
92
|
int error = errno;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
4
|
+
#include <sys/mount.h>
|
|
5
|
+
|
|
6
|
+
namespace FSMeta {
|
|
7
|
+
|
|
8
|
+
// Generic RAII wrapper for resources that need free()
|
|
9
|
+
template <typename T> class ResourceRAII {
|
|
10
|
+
private:
|
|
11
|
+
T *resource_;
|
|
12
|
+
|
|
13
|
+
public:
|
|
14
|
+
ResourceRAII() : resource_(nullptr) {}
|
|
15
|
+
~ResourceRAII() {
|
|
16
|
+
if (resource_) {
|
|
17
|
+
free(resource_);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
T **ptr() { return &resource_; }
|
|
22
|
+
T *get() { return resource_; }
|
|
23
|
+
|
|
24
|
+
// Add move operations for better resource management
|
|
25
|
+
ResourceRAII(ResourceRAII &&other) noexcept : resource_(other.resource_) {
|
|
26
|
+
other.resource_ = nullptr;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ResourceRAII &operator=(ResourceRAII &&other) noexcept {
|
|
30
|
+
if (this != &other) {
|
|
31
|
+
if (resource_)
|
|
32
|
+
free(resource_);
|
|
33
|
+
resource_ = other.resource_;
|
|
34
|
+
other.resource_ = nullptr;
|
|
35
|
+
}
|
|
36
|
+
return *this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Prevent copying
|
|
40
|
+
ResourceRAII(const ResourceRAII &) = delete;
|
|
41
|
+
ResourceRAII &operator=(const ResourceRAII &) = delete;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Specialized for mount info
|
|
45
|
+
using MountBufferRAII = ResourceRAII<struct statfs>;
|
|
46
|
+
|
|
47
|
+
// CoreFoundation RAII wrapper
|
|
48
|
+
template <typename T> class CFReleaser {
|
|
49
|
+
private:
|
|
50
|
+
T ref_;
|
|
51
|
+
|
|
52
|
+
public:
|
|
53
|
+
explicit CFReleaser(T ref = nullptr) noexcept : ref_(ref) {}
|
|
54
|
+
~CFReleaser() { reset(); }
|
|
55
|
+
|
|
56
|
+
void reset(T ref = nullptr) {
|
|
57
|
+
if (ref_) {
|
|
58
|
+
CFRelease(ref_);
|
|
59
|
+
}
|
|
60
|
+
ref_ = ref;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
operator T() const noexcept { return ref_; }
|
|
64
|
+
T get() const noexcept { return ref_; }
|
|
65
|
+
bool isValid() const noexcept { return ref_ != nullptr; }
|
|
66
|
+
|
|
67
|
+
// Prevent copying
|
|
68
|
+
CFReleaser(const CFReleaser &) = delete;
|
|
69
|
+
CFReleaser &operator=(const CFReleaser &) = delete;
|
|
70
|
+
|
|
71
|
+
// Allow moving
|
|
72
|
+
CFReleaser(CFReleaser &&other) noexcept : ref_(other.ref_) {
|
|
73
|
+
other.ref_ = nullptr;
|
|
74
|
+
}
|
|
75
|
+
CFReleaser &operator=(CFReleaser &&other) noexcept {
|
|
76
|
+
if (this != &other) {
|
|
77
|
+
reset();
|
|
78
|
+
ref_ = other.ref_;
|
|
79
|
+
other.ref_ = nullptr;
|
|
80
|
+
}
|
|
81
|
+
return *this;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
} // namespace FSMeta
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "./fs_meta.h"
|
|
5
|
+
#include "./raii_utils.h"
|
|
5
6
|
|
|
6
7
|
#include <CoreFoundation/CoreFoundation.h>
|
|
7
8
|
#include <DiskArbitration/DiskArbitration.h>
|
|
@@ -36,55 +37,6 @@ static std::string CFStringToString(CFStringRef cfString) {
|
|
|
36
37
|
return result;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// Improved CFReleaser with proper Core Foundation support
|
|
40
|
-
template <typename T> class CFReleaser {
|
|
41
|
-
private:
|
|
42
|
-
T ref_;
|
|
43
|
-
|
|
44
|
-
public:
|
|
45
|
-
explicit CFReleaser(T ref = nullptr) noexcept : ref_(ref) {}
|
|
46
|
-
|
|
47
|
-
// Delete copy operations
|
|
48
|
-
CFReleaser(const CFReleaser &) = delete;
|
|
49
|
-
CFReleaser &operator=(const CFReleaser &) = delete;
|
|
50
|
-
|
|
51
|
-
// Move operations
|
|
52
|
-
CFReleaser(CFReleaser &&other) noexcept : ref_(other.ref_) {
|
|
53
|
-
other.ref_ = nullptr;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
CFReleaser &operator=(CFReleaser &&other) noexcept {
|
|
57
|
-
if (this != &other) {
|
|
58
|
-
reset();
|
|
59
|
-
ref_ = other.ref_;
|
|
60
|
-
other.ref_ = nullptr;
|
|
61
|
-
}
|
|
62
|
-
return *this;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
~CFReleaser() { reset(); }
|
|
66
|
-
|
|
67
|
-
void reset(T ref = nullptr) {
|
|
68
|
-
if (ref_) {
|
|
69
|
-
CFRelease(ref_);
|
|
70
|
-
}
|
|
71
|
-
ref_ = ref;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Implicit conversion operator for Core Foundation APIs
|
|
75
|
-
operator T() const noexcept { return ref_; }
|
|
76
|
-
|
|
77
|
-
T get() const noexcept { return ref_; }
|
|
78
|
-
bool isValid() const noexcept { return ref_ != nullptr; }
|
|
79
|
-
|
|
80
|
-
// Release ownership
|
|
81
|
-
T release() noexcept {
|
|
82
|
-
T temp = ref_;
|
|
83
|
-
ref_ = nullptr;
|
|
84
|
-
return temp;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
40
|
class GetVolumeMetadataWorker : public MetadataWorkerBase {
|
|
89
41
|
public:
|
|
90
42
|
GetVolumeMetadataWorker(const std::string &mountPoint,
|
|
@@ -178,12 +130,12 @@ private:
|
|
|
178
130
|
// Check if this is a network filesystem
|
|
179
131
|
if (metadata.fstype == "smbfs" || metadata.fstype == "nfs" ||
|
|
180
132
|
metadata.fstype == "afpfs" || metadata.fstype == "webdav") {
|
|
181
|
-
// For network filesystems, we consider them healthy even without DA info
|
|
182
133
|
metadata.remote = true;
|
|
183
134
|
metadata.status = "healthy";
|
|
184
135
|
return;
|
|
185
136
|
}
|
|
186
137
|
|
|
138
|
+
// Create session on current thread
|
|
187
139
|
CFReleaser<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
|
188
140
|
if (!session.isValid()) {
|
|
189
141
|
DEBUG_LOG("[GetVolumeMetadataWorker] Failed to create DA session");
|
|
@@ -193,21 +145,39 @@ private:
|
|
|
193
145
|
}
|
|
194
146
|
|
|
195
147
|
try {
|
|
196
|
-
//
|
|
148
|
+
// Get thread-local runloop or create new one if needed
|
|
149
|
+
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
|
150
|
+
if (!runLoop) {
|
|
151
|
+
// If no runloop exists, create a new one for this thread
|
|
152
|
+
CFRunLoopRun();
|
|
153
|
+
runLoop = CFRunLoopGetCurrent();
|
|
154
|
+
if (!runLoop) {
|
|
155
|
+
throw std::runtime_error("Failed to create thread-local runloop");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Schedule session with our runloop
|
|
160
|
+
DASessionScheduleWithRunLoop(session.get(), runLoop,
|
|
161
|
+
kCFRunLoopDefaultMode);
|
|
162
|
+
|
|
163
|
+
// Use RAII to ensure cleanup
|
|
197
164
|
struct RunLoopCleaner {
|
|
198
165
|
DASessionRef session;
|
|
199
|
-
|
|
166
|
+
CFRunLoopRef runLoop;
|
|
167
|
+
bool shouldStop;
|
|
168
|
+
RunLoopCleaner(DASessionRef s, CFRunLoopRef l, bool stop = false)
|
|
169
|
+
: session(s), runLoop(l), shouldStop(stop) {}
|
|
200
170
|
~RunLoopCleaner() {
|
|
201
|
-
DASessionUnscheduleFromRunLoop(session,
|
|
171
|
+
DASessionUnscheduleFromRunLoop(session, runLoop,
|
|
202
172
|
kCFRunLoopDefaultMode);
|
|
173
|
+
if (shouldStop) {
|
|
174
|
+
CFRunLoopStop(runLoop);
|
|
175
|
+
}
|
|
203
176
|
}
|
|
204
|
-
};
|
|
177
|
+
} scopeGuard(session.get(), runLoop, !CFRunLoopGetCurrent());
|
|
205
178
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
kCFRunLoopDefaultMode);
|
|
209
|
-
|
|
210
|
-
auto scopeGuard = std::make_unique<RunLoopCleaner>(session.get());
|
|
179
|
+
// Run the run loop briefly to ensure DA is ready
|
|
180
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);
|
|
211
181
|
|
|
212
182
|
CFReleaser<DADiskRef> disk(DADiskCreateFromBSDName(
|
|
213
183
|
kCFAllocatorDefault, session.get(), metadata.mountFrom.c_str()));
|
|
@@ -228,12 +198,17 @@ private:
|
|
|
228
198
|
return;
|
|
229
199
|
}
|
|
230
200
|
|
|
201
|
+
// Ensure we have a complete description before continuing
|
|
202
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
|
|
203
|
+
|
|
204
|
+
// Process description synchronously since we're already on the right
|
|
205
|
+
// thread
|
|
231
206
|
ProcessDiskDescription(description.get());
|
|
232
207
|
|
|
233
|
-
// Only set ready if we got this far without errors
|
|
234
208
|
if (metadata.status != "partial") {
|
|
235
209
|
metadata.status = "healthy";
|
|
236
210
|
}
|
|
211
|
+
|
|
237
212
|
} catch (const std::exception &e) {
|
|
238
213
|
DEBUG_LOG("[GetVolumeMetadataWorker] Exception: %s", e.what());
|
|
239
214
|
metadata.status = "error";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/darwin/volume_mount_points.cpp
|
|
2
2
|
#include "../common/volume_mount_points.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
|
-
#include "fs_meta.h"
|
|
4
|
+
#include "./fs_meta.h"
|
|
5
|
+
#include "./raii_utils.h"
|
|
5
6
|
#include <chrono>
|
|
6
7
|
#include <future>
|
|
7
8
|
#include <sys/mount.h>
|
|
@@ -24,9 +25,12 @@ public:
|
|
|
24
25
|
void Execute() override {
|
|
25
26
|
DEBUG_LOG("[GetVolumeMountPointsWorker] Executing");
|
|
26
27
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
MountBufferRAII mntbuf;
|
|
29
|
+
// Use MNT_NOWAIT for better performance - we'll verify accessibility
|
|
30
|
+
// separately and our error handling already covers mount state changes
|
|
31
|
+
// See https://github.com/swiftlang/swift-corelibs-foundation/issues/4649
|
|
32
|
+
|
|
33
|
+
int count = getmntinfo_r_np(mntbuf.ptr(), MNT_NOWAIT);
|
|
30
34
|
|
|
31
35
|
if (count <= 0) {
|
|
32
36
|
throw std::runtime_error("Failed to get mount information");
|
|
@@ -34,36 +38,39 @@ public:
|
|
|
34
38
|
|
|
35
39
|
for (int i = 0; i < count; i++) {
|
|
36
40
|
MountPoint mp;
|
|
37
|
-
mp.mountPoint =
|
|
38
|
-
mp.fstype =
|
|
41
|
+
mp.mountPoint = mntbuf.get()[i].f_mntonname;
|
|
42
|
+
mp.fstype = mntbuf.get()[i].f_fstypename;
|
|
39
43
|
mp.error = ""; // Initialize error field
|
|
40
44
|
|
|
41
45
|
DEBUG_LOG("[GetVolumeMountPointsWorker] Checking mount point: %s",
|
|
42
46
|
mp.mountPoint.c_str());
|
|
43
47
|
|
|
44
48
|
try {
|
|
45
|
-
// Use
|
|
46
|
-
std::
|
|
49
|
+
// Use RAII to manage future
|
|
50
|
+
auto future = std::make_shared<std::future<bool>>(
|
|
47
51
|
std::async(std::launch::async, [path = mp.mountPoint]() {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
// Use faccessat for better security
|
|
53
|
+
return faccessat(AT_FDCWD, path.c_str(), R_OK, AT_EACCESS) == 0;
|
|
54
|
+
}));
|
|
50
55
|
|
|
51
|
-
auto status = future
|
|
56
|
+
auto status = future->wait_for(std::chrono::milliseconds(timeoutMs_));
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
switch (status) {
|
|
59
|
+
case std::future_status::timeout:
|
|
54
60
|
mp.status = "disconnected";
|
|
55
61
|
mp.error = "Access check timed out";
|
|
56
|
-
DEBUG_LOG(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Access check timed out: %s",
|
|
63
|
+
mp.mountPoint.c_str());
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case std::future_status::ready:
|
|
60
67
|
try {
|
|
61
|
-
bool isAccessible = future
|
|
68
|
+
bool isAccessible = future->get();
|
|
62
69
|
mp.status = isAccessible ? "healthy" : "inaccessible";
|
|
63
70
|
if (!isAccessible) {
|
|
64
71
|
mp.error = "Path is not accessible";
|
|
65
72
|
}
|
|
66
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker] Access check %s
|
|
73
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Access check %s: %s",
|
|
67
74
|
isAccessible ? "succeeded" : "failed",
|
|
68
75
|
mp.mountPoint.c_str());
|
|
69
76
|
} catch (const std::exception &e) {
|
|
@@ -71,12 +78,14 @@ public:
|
|
|
71
78
|
mp.error = std::string("Access check failed: ") + e.what();
|
|
72
79
|
DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
default:
|
|
75
84
|
mp.status = "error";
|
|
76
85
|
mp.error = "Unexpected future status";
|
|
77
|
-
DEBUG_LOG(
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Unexpected status: %s",
|
|
87
|
+
mp.mountPoint.c_str());
|
|
88
|
+
break;
|
|
80
89
|
}
|
|
81
90
|
} catch (const std::exception &e) {
|
|
82
91
|
mp.status = "error";
|
package/src/debuglog.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { debuglog, format } from "node:util";
|
|
2
|
-
import { defer } from "./defer.js";
|
|
3
2
|
|
|
4
|
-
//
|
|
3
|
+
// inlined as a hack to get around relative imports broken in ts-node (used by
|
|
4
|
+
// the debuglog tests):
|
|
5
|
+
function defer<T>(thunk: () => T) {
|
|
6
|
+
let t: T;
|
|
7
|
+
return () => (t ??= thunk());
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
export const debugLogContext = defer(() => {
|
|
7
11
|
for (const ea of ["fs-metadata", "fs-meta"]) {
|
package/src/defer.ts
CHANGED
package/src/dirname.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCallerDirname } from "./stack_path";
|
|
2
|
+
|
|
3
|
+
// Thanks to tsup shims, __dirname should always be defined except when run by
|
|
4
|
+
// jest (which will use the stack_path shim)
|
|
5
|
+
export function _dirname() {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
8
|
+
} catch {
|
|
9
|
+
// ignore
|
|
10
|
+
}
|
|
11
|
+
// we must be in jest. Use the stack_path ~~hack~~ shim:
|
|
12
|
+
return getCallerDirname();
|
|
13
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="jest-extended" />
|