@photostructure/fs-metadata 1.0.1 → 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 +44 -0
- package/CLAUDE.md +13 -0
- package/binding.gyp +1 -0
- package/claude.sh +29 -5
- package/dist/index.cjs +237 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.mts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.mjs +236 -130
- package/dist/index.mjs.map +1 -1
- package/doc/SECURITY_AUDIT_2025.md +1 -1
- package/doc/SECURITY_AUDIT_2026.md +361 -0
- package/doc/TPP-GUIDE.md +144 -0
- package/doc/system-volume-detection.md +268 -0
- package/package.json +12 -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/src/binding.cpp +11 -0
- package/src/common/volume_metadata.h +10 -3
- package/src/common/volume_mount_points.h +7 -1
- package/src/darwin/da_mutex.h +23 -0
- package/src/darwin/get_mount_point.cpp +96 -0
- package/src/darwin/get_mount_point.h +13 -0
- package/src/darwin/raii_utils.h +39 -0
- package/src/darwin/system_volume.h +156 -0
- package/src/darwin/volume_metadata.cpp +18 -2
- package/src/darwin/volume_mount_points.cpp +46 -14
- package/src/index.ts +49 -0
- package/src/linux/mtab.ts +6 -0
- package/src/mount_point_for_path.ts +54 -0
- package/src/options.ts +7 -17
- package/src/path.ts +16 -1
- package/src/system_volume.ts +5 -9
- package/src/test-utils/assert.ts +4 -0
- package/src/types/mount_point.ts +28 -1
- package/src/types/native_bindings.ts +7 -0
- package/src/volume_metadata.ts +117 -2
- package/src/windows/system_volume.h +21 -16
- package/src/windows/volume_metadata.cpp +13 -7
- package/src/windows/volume_mount_points.cpp +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,50 @@ Fixed for any bug fixes.
|
|
|
14
14
|
Security in case of vulnerabilities.
|
|
15
15
|
-->
|
|
16
16
|
|
|
17
|
+
## 1.2.0 - 2026-03-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- New `getMountPointForPath(pathname)` function: a lightweight alternative to `getVolumeMetadataForPath()` that returns only the mount point string without fetching full volume metadata (size, UUID, label, etc.). On macOS, uses a single `fstatfs()` call — no DiskArbitration, IOKit, or space calculations. On Linux/Windows, uses the same device-ID matching logic. Handles symlinks and APFS firmlinks correctly.
|
|
22
|
+
|
|
23
|
+
## 1.1.0 - 2026-03-16
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- New `getVolumeMetadataForPath(pathname)` function: given any file or directory path, returns the `VolumeMetadata` for the volume that contains it. Mirrors the behavior of `df pathname`:
|
|
28
|
+
- Resolves POSIX symlinks via `realpath()`
|
|
29
|
+
- On **macOS**: uses `fstatfs()` `f_mntonname` to correctly resolve APFS firmlinks (e.g. `/Users` → `/System/Volumes/Data`) — `stat().dev` does not follow firmlinks and would give the wrong result
|
|
30
|
+
- On **Linux**: uses `stat().dev` device ID matching with path-prefix disambiguation for bind mounts and GIO mounts that share a device ID. Also works correctly in Docker containers, where `/proc/self/mounts` reflects the container's mount namespace.
|
|
31
|
+
- On **Windows**: uses device ID and path-prefix matching against logical drives
|
|
32
|
+
|
|
33
|
+
- New `isReadOnly` field on `MountPoint` (and by extension `VolumeMetadata`) indicating whether a volume is mounted read-only. This is useful for identifying volumes with unstable UUIDs, like the macOS APFS system snapshot at `/`, whose UUID changes on every OS update. Available on all platforms:
|
|
34
|
+
- **macOS**: reads `MNT_RDONLY` from `statfs` flags
|
|
35
|
+
- **Linux**: parses `ro` from mount options in `/proc/mounts`
|
|
36
|
+
- **Windows**: checks `FILE_READ_ONLY_VOLUME` from `GetVolumeInformation`
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **macOS `isSystemVolume` detection now uses APFS volume roles via IOKit** instead of path pattern heuristics. Each APFS volume has a role (System, Data, VM, Preboot, Recovery, etc.) stored in its superblock. We read this via `DADiskCopyIOMedia()` → `IORegistryEntryCreateCFProperty("Role")`, with a `MNT_SNAPSHOT` fallback if DiskArbitration is unavailable. This is factual (Apple assigns the roles), not heuristic, and correctly distinguishes:
|
|
41
|
+
- `/` (System role) → `isSystemVolume: true` — sealed OS snapshot, unstable UUID
|
|
42
|
+
- `/System/Volumes/Data` (Data role) → `isSystemVolume: false` — primary user data volume
|
|
43
|
+
- `/System/Volumes/VM`, `Preboot`, `Update`, `Hardware`, `xarts`, etc. → `isSystemVolume: true`
|
|
44
|
+
- See [`doc/system-volume-detection.md`](./doc/system-volume-detection.md) for full details
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- macOS: RAII wrapper (`IOObjectGuard`) for IOKit objects in APFS volume role detection, preventing Mach port resource leaks if exceptions occur during `GetApfsVolumeRole()`
|
|
49
|
+
- macOS: DiskArbitration operations in `getVolumeMountPoints()` now serialize through the same `g_diskArbitrationMutex` used by `getVolumeMetadata()`, preventing potential data races when both APIs are called concurrently
|
|
50
|
+
- See [`doc/SECURITY_AUDIT_2026.md`](./doc/SECURITY_AUDIT_2026.md) for full audit details
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Zero-initialized `VolumeMetadata` size/used/available fields to prevent uninitialized values when volume info retrieval fails early
|
|
55
|
+
- Windows: eliminated redundant `GetVolumeInformationW` call per drive in `getVolumeMountPoints()`
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- Removed macOS `/System/Volumes/*` path patterns from `SystemPathPatternsDefault` — these are now handled natively via APFS volume roles. The Spotlight, FSEvents, and Trashes glob patterns (`**/.Spotlight-V100`, `**/.fseventsd`, etc.) were also removed as they matched directories within volumes, not mount points.
|
|
60
|
+
|
|
17
61
|
## 1.0.1 - 2026-03-01
|
|
18
62
|
|
|
19
63
|
### Fixed
|
package/CLAUDE.md
CHANGED
|
@@ -44,6 +44,19 @@ Use `_dirname()` from `./dirname` instead of `__dirname` - works in both CommonJ
|
|
|
44
44
|
|
|
45
45
|
Jest 30 doesn't support Node.js 23. Use Node.js 20, 22, or 24.
|
|
46
46
|
|
|
47
|
+
## System Volume Detection
|
|
48
|
+
|
|
49
|
+
**IMPORTANT: Read `doc/system-volume-detection.md` before modifying any system volume detection logic.** It documents the full detection strategy across all platforms, including flag matrices and rationale for each approach.
|
|
50
|
+
|
|
51
|
+
Summary:
|
|
52
|
+
|
|
53
|
+
- The root `/` is a sealed, read-only APFS snapshot whose **UUID changes on every OS update** — never use it for persistent identification.
|
|
54
|
+
- **Primary detection** combines mount flags with APFS volume roles: `MNT_SNAPSHOT || (MNT_DONTBROWSE && hasApfsRole && role != "Data")`. See `ClassifyMacVolume()` in `src/darwin/system_volume.h`.
|
|
55
|
+
- The APFS role string is exposed as `volumeRole` on `MountPoint` and `VolumeMetadata`.
|
|
56
|
+
- **Fallback** uses `MNT_SNAPSHOT` only from `statfs` `f_flags` if DA session creation fails.
|
|
57
|
+
- `MNT_DONTBROWSE` is safe to use **only when combined with a non-Data APFS role**. The Data volume (`/System/Volumes/Data`) has `MNT_DONTBROWSE` but role `"Data"`, so it is correctly excluded.
|
|
58
|
+
- Pseudo-filesystems like `devfs` (no IOMedia, no APFS role) are caught by TypeScript fstype/path heuristics.
|
|
59
|
+
|
|
47
60
|
## Windows-Specific Issues
|
|
48
61
|
|
|
49
62
|
### Windows CI Jest Worker Failures
|
package/binding.gyp
CHANGED
package/claude.sh
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Claude Code wrapper: appends a project-specific system prompt to every session.
|
|
4
|
+
#
|
|
5
|
+
# Appends TPP instructions and mandatory guidelines via --append-system-prompt.
|
|
6
|
+
# See https://photostructure.com/coding/claude-code-tpp/ for details.
|
|
7
|
+
#
|
|
8
|
+
# Setup: add this function to your ~/.bashrc, ~/.bash_aliases, or ~/.zshrc:
|
|
9
|
+
#
|
|
10
|
+
# cla() {
|
|
11
|
+
# if [ -f "./claude.sh" ]; then ./claude.sh "$@"; else command claude "$@"; fi
|
|
12
|
+
# }
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# cla # Starts a TPP-aware session
|
|
16
|
+
# cla --resume # Resume with TPP context
|
|
17
|
+
# claude update # Vanilla claude still works for non-TPP use
|
|
18
|
+
#
|
|
19
|
+
# The --append-system-prompt below is also a good place to add brief,
|
|
20
|
+
# high-value instructions that Claude tends to ignore in CLAUDE.md.
|
|
21
|
+
# Keep it concise! Every token here reduces your available context window.
|
|
5
22
|
|
|
6
|
-
echo "Adding
|
|
23
|
+
echo "Adding project system prompt..."
|
|
7
24
|
|
|
8
25
|
DATE=$(date +%Y-%m-%d)
|
|
9
26
|
|
|
10
|
-
claude --append-system-prompt "$(
|
|
11
|
-
cat <<
|
|
27
|
+
command claude --append-system-prompt "$(
|
|
28
|
+
cat <<EOF
|
|
12
29
|
# MANDATORY GUIDELINES
|
|
13
30
|
- **Study your CLAUDE.md** - Every conversation begins by studying CLAUDE.md
|
|
14
31
|
- **Always Start By Reading** - You must study the referenced codebase and related documentation before making any change. NEVER assume APIs or implementation details.
|
|
@@ -22,5 +39,12 @@ claude --append-system-prompt "$(
|
|
|
22
39
|
- **It's YOUR JOB to keep docs current** - If your edits change **any** behavior or type signatures, search and update both code comments and documentation and edit them to reflect those changes.
|
|
23
40
|
- **Do not delete files without asking** - If you need to delete a file, please ask for permission first, and provide a justification for why it should be deleted.
|
|
24
41
|
- The current date is $DATE -- it is not 2024.
|
|
42
|
+
|
|
43
|
+
# TECHNICAL PROJECT PLANS (TPPs)
|
|
44
|
+
This project uses Technical Project Plans (TPPs) in \`_todo/*.md\` to share research, design decisions, and next steps between sessions.
|
|
45
|
+
|
|
46
|
+
- When you exit plan mode, your first step should be to write or update a relevant TPP using the /handoff skill.
|
|
47
|
+
- When you run low on context and you are working on a TPP, run the /handoff skill.
|
|
48
|
+
- Check \`_todo/\` at the start of every session for active TPPs relevant to the current task.
|
|
25
49
|
EOF
|
|
26
50
|
)" "$@"
|
package/dist/index.cjs
CHANGED
|
@@ -40,8 +40,10 @@ __export(index_exports, {
|
|
|
40
40
|
VolumeHealthStatuses: () => VolumeHealthStatuses,
|
|
41
41
|
getAllVolumeMetadata: () => getAllVolumeMetadata,
|
|
42
42
|
getHiddenMetadata: () => getHiddenMetadata,
|
|
43
|
+
getMountPointForPath: () => getMountPointForPath,
|
|
43
44
|
getTimeoutMsDefault: () => getTimeoutMsDefault,
|
|
44
45
|
getVolumeMetadata: () => getVolumeMetadata,
|
|
46
|
+
getVolumeMetadataForPath: () => getVolumeMetadataForPath,
|
|
45
47
|
getVolumeMountPoints: () => getVolumeMountPoints,
|
|
46
48
|
isHidden: () => isHidden,
|
|
47
49
|
isHiddenRecursive: () => isHiddenRecursive,
|
|
@@ -460,6 +462,11 @@ function isRootDirectory(path) {
|
|
|
460
462
|
const n = normalizePath(path);
|
|
461
463
|
return n == null ? false : isWindows ? (0, import_node_path3.dirname)(n) === n : n === "/";
|
|
462
464
|
}
|
|
465
|
+
function isAncestorOrSelf(ancestor, descendant) {
|
|
466
|
+
if (ancestor === descendant) return true;
|
|
467
|
+
const prefix = isRootDirectory(ancestor) ? ancestor : ancestor + import_node_path3.sep;
|
|
468
|
+
return descendant.startsWith(prefix);
|
|
469
|
+
}
|
|
463
470
|
|
|
464
471
|
// src/hidden.ts
|
|
465
472
|
var HiddenSupportByPlatform = {
|
|
@@ -624,6 +631,74 @@ async function setHiddenImpl(pathname, hide, method, nativeFn2) {
|
|
|
624
631
|
return { pathname: norm, actions };
|
|
625
632
|
}
|
|
626
633
|
|
|
634
|
+
// src/mount_point_for_path.ts
|
|
635
|
+
var import_promises6 = require("fs/promises");
|
|
636
|
+
var import_node_path7 = require("path");
|
|
637
|
+
|
|
638
|
+
// src/volume_metadata.ts
|
|
639
|
+
var import_promises5 = require("fs/promises");
|
|
640
|
+
var import_node_path6 = require("path");
|
|
641
|
+
|
|
642
|
+
// src/linux/dev_disk.ts
|
|
643
|
+
var import_promises3 = require("fs/promises");
|
|
644
|
+
var import_node_path5 = require("path");
|
|
645
|
+
async function getUuidFromDevDisk(devicePath) {
|
|
646
|
+
try {
|
|
647
|
+
const result = await getBasenameLinkedTo(
|
|
648
|
+
"/dev/disk/by-uuid",
|
|
649
|
+
(0, import_node_path5.resolve)(devicePath)
|
|
650
|
+
);
|
|
651
|
+
debug("[getUuidFromDevDisk] result: %o", result);
|
|
652
|
+
return result;
|
|
653
|
+
} catch (error) {
|
|
654
|
+
debug("[getUuidFromDevDisk] failed: " + error);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function getLabelFromDevDisk(devicePath) {
|
|
659
|
+
try {
|
|
660
|
+
const result = await getBasenameLinkedTo(
|
|
661
|
+
"/dev/disk/by-label",
|
|
662
|
+
(0, import_node_path5.resolve)(devicePath)
|
|
663
|
+
);
|
|
664
|
+
debug("[getLabelFromDevDisk] result: %o", result);
|
|
665
|
+
return result;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
debug("[getLabelFromDevDisk] failed: " + error);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function getBasenameLinkedTo(linkDir, linkPath) {
|
|
672
|
+
for await (const ea of readLinks(linkDir)) {
|
|
673
|
+
if (ea.linkTarget === linkPath) {
|
|
674
|
+
return decodeEscapeSequences(ea.dirent.name);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
async function* readLinks(directory) {
|
|
680
|
+
for (const dirent of await (0, import_promises3.readdir)(directory, { withFileTypes: true })) {
|
|
681
|
+
if (dirent.isSymbolicLink()) {
|
|
682
|
+
try {
|
|
683
|
+
const linkTarget = (0, import_node_path5.resolve)(
|
|
684
|
+
directory,
|
|
685
|
+
await (0, import_promises3.readlink)((0, import_node_path5.join)(directory, dirent.name))
|
|
686
|
+
);
|
|
687
|
+
yield { dirent, linkTarget };
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/linux/mount_points.ts
|
|
695
|
+
var import_promises4 = require("fs/promises");
|
|
696
|
+
|
|
697
|
+
// src/mount_point.ts
|
|
698
|
+
function isMountPoint(obj) {
|
|
699
|
+
return isObject(obj) && "mountPoint" in obj && isNotBlank(obj.mountPoint);
|
|
700
|
+
}
|
|
701
|
+
|
|
627
702
|
// src/options.ts
|
|
628
703
|
var import_node_os2 = require("os");
|
|
629
704
|
var import_node_process3 = require("process");
|
|
@@ -686,23 +761,13 @@ var SystemPathPatternsDefault = [
|
|
|
686
761
|
"/mnt/wslg/doc",
|
|
687
762
|
"/mnt/wslg/versions.txt",
|
|
688
763
|
"/usr/lib/wsl/drivers",
|
|
689
|
-
// macOS system
|
|
690
|
-
"
|
|
691
|
-
//
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
"/
|
|
696
|
-
"/System/Volumes/Reserved",
|
|
697
|
-
"/System/Volumes/Update",
|
|
698
|
-
"/System/Volumes/VM",
|
|
699
|
-
"/System/Volumes/xarts",
|
|
700
|
-
// macOS per-volume metadata (Spotlight, FSEvents, versioning, Trash):
|
|
701
|
-
// https://eclecticlight.co/2021/01/28/spotlight-on-search-how-spotlight-works/
|
|
702
|
-
"**/.DocumentRevisions-V100",
|
|
703
|
-
"**/.fseventsd",
|
|
704
|
-
"**/.Spotlight-V100",
|
|
705
|
-
"**/.Trashes"
|
|
764
|
+
// macOS system volumes are detected natively via APFS volume roles
|
|
765
|
+
// (IOKit IOMedia "Role" property) with MNT_SNAPSHOT as a fallback.
|
|
766
|
+
// No path patterns needed. See src/darwin/system_volume.h.
|
|
767
|
+
//
|
|
768
|
+
// /private/var/vm is the macOS swap directory (not a mount point on most
|
|
769
|
+
// systems, but included for completeness if it appears as one).
|
|
770
|
+
"/private/var/vm"
|
|
706
771
|
];
|
|
707
772
|
var SystemFsTypesDefault = [
|
|
708
773
|
"autofs",
|
|
@@ -834,109 +899,6 @@ function optionsWithDefaults(overrides = {}) {
|
|
|
834
899
|
};
|
|
835
900
|
}
|
|
836
901
|
|
|
837
|
-
// src/string_enum.ts
|
|
838
|
-
function stringEnum(...o) {
|
|
839
|
-
const set = new Set(o);
|
|
840
|
-
const dict = {};
|
|
841
|
-
for (const key of o) {
|
|
842
|
-
dict[key] = key;
|
|
843
|
-
}
|
|
844
|
-
return {
|
|
845
|
-
...dict,
|
|
846
|
-
values: Object.freeze([...set]),
|
|
847
|
-
size: set.size,
|
|
848
|
-
get: (s) => s != null && set.has(s) ? s : void 0
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/volume_health_status.ts
|
|
853
|
-
var VolumeHealthStatuses = stringEnum(
|
|
854
|
-
"healthy",
|
|
855
|
-
"timeout",
|
|
856
|
-
"inaccessible",
|
|
857
|
-
"disconnected",
|
|
858
|
-
"unknown"
|
|
859
|
-
);
|
|
860
|
-
async function directoryStatus(dir, timeoutMs, canReaddirImpl = canReaddir) {
|
|
861
|
-
try {
|
|
862
|
-
if (await canReaddirImpl(dir, timeoutMs)) {
|
|
863
|
-
return { status: VolumeHealthStatuses.healthy };
|
|
864
|
-
}
|
|
865
|
-
} catch (error) {
|
|
866
|
-
debug("[directoryStatus] %s: %s", dir, error);
|
|
867
|
-
let status = VolumeHealthStatuses.unknown;
|
|
868
|
-
if (error instanceof TimeoutError) {
|
|
869
|
-
status = VolumeHealthStatuses.timeout;
|
|
870
|
-
} else if (isObject(error) && error instanceof Error && "code" in error) {
|
|
871
|
-
if (error.code === "EPERM" || error.code === "EACCES") {
|
|
872
|
-
status = VolumeHealthStatuses.inaccessible;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
return { status, error: toError(error) };
|
|
876
|
-
}
|
|
877
|
-
return { status: VolumeHealthStatuses.unknown };
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/linux/dev_disk.ts
|
|
881
|
-
var import_promises3 = require("fs/promises");
|
|
882
|
-
var import_node_path5 = require("path");
|
|
883
|
-
async function getUuidFromDevDisk(devicePath) {
|
|
884
|
-
try {
|
|
885
|
-
const result = await getBasenameLinkedTo(
|
|
886
|
-
"/dev/disk/by-uuid",
|
|
887
|
-
(0, import_node_path5.resolve)(devicePath)
|
|
888
|
-
);
|
|
889
|
-
debug("[getUuidFromDevDisk] result: %o", result);
|
|
890
|
-
return result;
|
|
891
|
-
} catch (error) {
|
|
892
|
-
debug("[getUuidFromDevDisk] failed: " + error);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
async function getLabelFromDevDisk(devicePath) {
|
|
897
|
-
try {
|
|
898
|
-
const result = await getBasenameLinkedTo(
|
|
899
|
-
"/dev/disk/by-label",
|
|
900
|
-
(0, import_node_path5.resolve)(devicePath)
|
|
901
|
-
);
|
|
902
|
-
debug("[getLabelFromDevDisk] result: %o", result);
|
|
903
|
-
return result;
|
|
904
|
-
} catch (error) {
|
|
905
|
-
debug("[getLabelFromDevDisk] failed: " + error);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
async function getBasenameLinkedTo(linkDir, linkPath) {
|
|
910
|
-
for await (const ea of readLinks(linkDir)) {
|
|
911
|
-
if (ea.linkTarget === linkPath) {
|
|
912
|
-
return decodeEscapeSequences(ea.dirent.name);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
async function* readLinks(directory) {
|
|
918
|
-
for (const dirent of await (0, import_promises3.readdir)(directory, { withFileTypes: true })) {
|
|
919
|
-
if (dirent.isSymbolicLink()) {
|
|
920
|
-
try {
|
|
921
|
-
const linkTarget = (0, import_node_path5.resolve)(
|
|
922
|
-
directory,
|
|
923
|
-
await (0, import_promises3.readlink)((0, import_node_path5.join)(directory, dirent.name))
|
|
924
|
-
);
|
|
925
|
-
yield { dirent, linkTarget };
|
|
926
|
-
} catch {
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// src/linux/mount_points.ts
|
|
933
|
-
var import_promises4 = require("fs/promises");
|
|
934
|
-
|
|
935
|
-
// src/mount_point.ts
|
|
936
|
-
function isMountPoint(obj) {
|
|
937
|
-
return isObject(obj) && "mountPoint" in obj && isNotBlank(obj.mountPoint);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
902
|
// src/remote_info.ts
|
|
941
903
|
function isRemoteInfo(obj) {
|
|
942
904
|
if (!isObject(obj)) return false;
|
|
@@ -1161,20 +1123,20 @@ function isSystemVolume(mountPoint, fstype, config = {}) {
|
|
|
1161
1123
|
}
|
|
1162
1124
|
function assignSystemVolume(mp, config) {
|
|
1163
1125
|
const result = isSystemVolume(mp.mountPoint, mp.fstype, config);
|
|
1164
|
-
|
|
1165
|
-
mp.isSystemVolume ??= result;
|
|
1166
|
-
} else {
|
|
1167
|
-
mp.isSystemVolume = result;
|
|
1168
|
-
}
|
|
1126
|
+
mp.isSystemVolume = mp.isSystemVolume || result;
|
|
1169
1127
|
}
|
|
1170
1128
|
|
|
1171
1129
|
// src/linux/mtab.ts
|
|
1130
|
+
function isReadOnlyMount(fs_mntops) {
|
|
1131
|
+
return fs_mntops?.split(",").includes("ro") ?? false;
|
|
1132
|
+
}
|
|
1172
1133
|
function mountEntryToMountPoint(entry) {
|
|
1173
1134
|
const mountPoint = normalizePosixPath(entry.fs_file);
|
|
1174
1135
|
const fstype = toNotBlank(entry.fs_vfstype) ?? toNotBlank(entry.fs_spec);
|
|
1175
1136
|
return mountPoint == null || fstype == null ? void 0 : {
|
|
1176
1137
|
mountPoint,
|
|
1177
|
-
fstype
|
|
1138
|
+
fstype,
|
|
1139
|
+
isReadOnly: isReadOnlyMount(entry.fs_mntops)
|
|
1178
1140
|
};
|
|
1179
1141
|
}
|
|
1180
1142
|
function mountEntryToPartialVolumeMetadata(entry, options = {}) {
|
|
@@ -1184,6 +1146,7 @@ function mountEntryToPartialVolumeMetadata(entry, options = {}) {
|
|
|
1184
1146
|
fstype: entry.fs_vfstype,
|
|
1185
1147
|
mountFrom: entry.fs_spec,
|
|
1186
1148
|
isSystemVolume: isSystemVolume(entry.fs_file, entry.fs_vfstype, options),
|
|
1149
|
+
isReadOnly: isReadOnlyMount(entry.fs_mntops),
|
|
1187
1150
|
remote: false,
|
|
1188
1151
|
// < default to false, but it may be overridden by extractRemoteInfo
|
|
1189
1152
|
...extractRemoteInfo(entry.fs_spec, networkFsTypes)
|
|
@@ -1317,6 +1280,49 @@ function extractUUID(uuid) {
|
|
|
1317
1280
|
return toS(uuid).match(uuidRegex)?.[0];
|
|
1318
1281
|
}
|
|
1319
1282
|
|
|
1283
|
+
// src/string_enum.ts
|
|
1284
|
+
function stringEnum(...o) {
|
|
1285
|
+
const set = new Set(o);
|
|
1286
|
+
const dict = {};
|
|
1287
|
+
for (const key of o) {
|
|
1288
|
+
dict[key] = key;
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
...dict,
|
|
1292
|
+
values: Object.freeze([...set]),
|
|
1293
|
+
size: set.size,
|
|
1294
|
+
get: (s) => s != null && set.has(s) ? s : void 0
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/volume_health_status.ts
|
|
1299
|
+
var VolumeHealthStatuses = stringEnum(
|
|
1300
|
+
"healthy",
|
|
1301
|
+
"timeout",
|
|
1302
|
+
"inaccessible",
|
|
1303
|
+
"disconnected",
|
|
1304
|
+
"unknown"
|
|
1305
|
+
);
|
|
1306
|
+
async function directoryStatus(dir, timeoutMs, canReaddirImpl = canReaddir) {
|
|
1307
|
+
try {
|
|
1308
|
+
if (await canReaddirImpl(dir, timeoutMs)) {
|
|
1309
|
+
return { status: VolumeHealthStatuses.healthy };
|
|
1310
|
+
}
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
debug("[directoryStatus] %s: %s", dir, error);
|
|
1313
|
+
let status = VolumeHealthStatuses.unknown;
|
|
1314
|
+
if (error instanceof TimeoutError) {
|
|
1315
|
+
status = VolumeHealthStatuses.timeout;
|
|
1316
|
+
} else if (isObject(error) && error instanceof Error && "code" in error) {
|
|
1317
|
+
if (error.code === "EPERM" || error.code === "EACCES") {
|
|
1318
|
+
status = VolumeHealthStatuses.inaccessible;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return { status, error: toError(error) };
|
|
1322
|
+
}
|
|
1323
|
+
return { status: VolumeHealthStatuses.unknown };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1320
1326
|
// src/array.ts
|
|
1321
1327
|
function uniqBy(arr, keyFn) {
|
|
1322
1328
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1467,6 +1473,63 @@ async function _getVolumeMetadata(o, nativeFn2) {
|
|
|
1467
1473
|
debug("[getVolumeMetadata] final result for %s: %o", o.mountPoint, result);
|
|
1468
1474
|
return compactValues(result);
|
|
1469
1475
|
}
|
|
1476
|
+
async function getVolumeMetadataForPathImpl(pathname, opts, nativeFn2) {
|
|
1477
|
+
if (isBlank(pathname)) {
|
|
1478
|
+
throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
|
|
1479
|
+
}
|
|
1480
|
+
const resolved = await (0, import_promises5.realpath)(pathname);
|
|
1481
|
+
const resolvedStat = await statAsync(resolved);
|
|
1482
|
+
const dir = resolvedStat.isDirectory() ? resolved : (0, import_node_path6.dirname)(resolved);
|
|
1483
|
+
if (isMacOS) {
|
|
1484
|
+
const probe = await getVolumeMetadataImpl(
|
|
1485
|
+
{ ...opts, mountPoint: dir },
|
|
1486
|
+
nativeFn2
|
|
1487
|
+
);
|
|
1488
|
+
const canonicalMountPoint = isNotBlank(probe.mountName) ? probe.mountName : dir;
|
|
1489
|
+
if (canonicalMountPoint === dir) return probe;
|
|
1490
|
+
return getVolumeMetadataImpl(
|
|
1491
|
+
{ ...opts, mountPoint: canonicalMountPoint },
|
|
1492
|
+
nativeFn2
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
const mountPoint = await findMountPointByDeviceId(
|
|
1496
|
+
resolved,
|
|
1497
|
+
resolvedStat,
|
|
1498
|
+
opts,
|
|
1499
|
+
nativeFn2
|
|
1500
|
+
);
|
|
1501
|
+
return getVolumeMetadataImpl({ ...opts, mountPoint }, nativeFn2);
|
|
1502
|
+
}
|
|
1503
|
+
async function findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn2) {
|
|
1504
|
+
const targetDev = resolvedStat.dev;
|
|
1505
|
+
const mountPoints = await getVolumeMountPointsImpl(
|
|
1506
|
+
{ ...opts, includeSystemVolumes: true },
|
|
1507
|
+
nativeFn2
|
|
1508
|
+
);
|
|
1509
|
+
const prefixMatches = [];
|
|
1510
|
+
const deviceMatches = [];
|
|
1511
|
+
await Promise.all(
|
|
1512
|
+
mountPoints.map(async ({ mountPoint }) => {
|
|
1513
|
+
try {
|
|
1514
|
+
const mpDev = (await statAsync(mountPoint)).dev;
|
|
1515
|
+
if (mpDev !== targetDev) return;
|
|
1516
|
+
if (isAncestorOrSelf(mountPoint, resolved)) {
|
|
1517
|
+
prefixMatches.push(mountPoint);
|
|
1518
|
+
} else {
|
|
1519
|
+
deviceMatches.push(mountPoint);
|
|
1520
|
+
}
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
})
|
|
1524
|
+
);
|
|
1525
|
+
const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
|
|
1526
|
+
if (candidates.length === 0) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
"No mount point found for path: " + JSON.stringify(resolved)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
return candidates.reduce((a, b) => a.length >= b.length ? a : b);
|
|
1532
|
+
}
|
|
1470
1533
|
async function getAllVolumeMetadataImpl(opts, nativeFn2) {
|
|
1471
1534
|
const o = optionsWithDefaults(opts);
|
|
1472
1535
|
debug("[getAllVolumeMetadata] starting with options: %o", o);
|
|
@@ -1518,15 +1581,44 @@ async function getAllVolumeMetadataImpl(opts, nativeFn2) {
|
|
|
1518
1581
|
);
|
|
1519
1582
|
}
|
|
1520
1583
|
|
|
1584
|
+
// src/mount_point_for_path.ts
|
|
1585
|
+
async function getMountPointForPathImpl(pathname, opts, nativeFn2) {
|
|
1586
|
+
if (isBlank(pathname)) {
|
|
1587
|
+
throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
|
|
1588
|
+
}
|
|
1589
|
+
const resolved = await (0, import_promises6.realpath)(pathname);
|
|
1590
|
+
const resolvedStat = await statAsync(resolved);
|
|
1591
|
+
const dir = resolvedStat.isDirectory() ? resolved : (0, import_node_path7.dirname)(resolved);
|
|
1592
|
+
if (isMacOS) {
|
|
1593
|
+
const native = await nativeFn2();
|
|
1594
|
+
if (native.getMountPoint) {
|
|
1595
|
+
debug("[getMountPointForPath] using native getMountPoint for %s", dir);
|
|
1596
|
+
const p = native.getMountPoint(dir);
|
|
1597
|
+
const mountPoint = await withTimeout({
|
|
1598
|
+
desc: "getMountPoint()",
|
|
1599
|
+
timeoutMs: opts.timeoutMs,
|
|
1600
|
+
promise: p
|
|
1601
|
+
});
|
|
1602
|
+
if (isNotBlank(mountPoint)) {
|
|
1603
|
+
debug("[getMountPointForPath] resolved to %s", mountPoint);
|
|
1604
|
+
return mountPoint;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
throw new Error("getMountPoint native function unavailable");
|
|
1608
|
+
}
|
|
1609
|
+
debug("[getMountPointForPath] using device matching for %s", resolved);
|
|
1610
|
+
return findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn2);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1521
1613
|
// src/index.ts
|
|
1522
1614
|
var nativeFn = defer2(async () => {
|
|
1523
1615
|
const start = Date.now();
|
|
1524
1616
|
try {
|
|
1525
|
-
const
|
|
1526
|
-
const dir = await findAncestorDir(
|
|
1617
|
+
const dirname6 = _dirname();
|
|
1618
|
+
const dir = await findAncestorDir(dirname6, "binding.gyp");
|
|
1527
1619
|
if (dir == null) {
|
|
1528
1620
|
throw new Error(
|
|
1529
|
-
"Could not find bindings.gyp in any ancestor directory of " +
|
|
1621
|
+
"Could not find bindings.gyp in any ancestor directory of " + dirname6
|
|
1530
1622
|
);
|
|
1531
1623
|
}
|
|
1532
1624
|
const bindings = (0, import_node_gyp_build.default)(dir);
|
|
@@ -1549,6 +1641,20 @@ function getVolumeMetadata(mountPoint, opts) {
|
|
|
1549
1641
|
nativeFn
|
|
1550
1642
|
);
|
|
1551
1643
|
}
|
|
1644
|
+
function getVolumeMetadataForPath(pathname, opts) {
|
|
1645
|
+
return getVolumeMetadataForPathImpl(
|
|
1646
|
+
pathname,
|
|
1647
|
+
optionsWithDefaults(opts),
|
|
1648
|
+
nativeFn
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
function getMountPointForPath(pathname, opts) {
|
|
1652
|
+
return getMountPointForPathImpl(
|
|
1653
|
+
pathname,
|
|
1654
|
+
optionsWithDefaults(opts),
|
|
1655
|
+
nativeFn
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1552
1658
|
function getAllVolumeMetadata(opts) {
|
|
1553
1659
|
return getAllVolumeMetadataImpl(optionsWithDefaults(opts), nativeFn);
|
|
1554
1660
|
}
|
|
@@ -1576,8 +1682,10 @@ function setHidden(pathname, hidden, method = "auto") {
|
|
|
1576
1682
|
VolumeHealthStatuses,
|
|
1577
1683
|
getAllVolumeMetadata,
|
|
1578
1684
|
getHiddenMetadata,
|
|
1685
|
+
getMountPointForPath,
|
|
1579
1686
|
getTimeoutMsDefault,
|
|
1580
1687
|
getVolumeMetadata,
|
|
1688
|
+
getVolumeMetadataForPath,
|
|
1581
1689
|
getVolumeMountPoints,
|
|
1582
1690
|
isHidden,
|
|
1583
1691
|
isHiddenRecursive,
|