@photostructure/fs-metadata 0.1.6 → 0.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CODE_OF_CONDUCT.md +128 -0
  3. package/CONTRIBUTING.md +46 -0
  4. package/README.md +4 -65
  5. package/SECURITY.md +9 -0
  6. package/dist/index.cjs +234 -225
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.mjs +232 -223
  9. package/dist/index.mjs.map +1 -0
  10. package/dist/types/array.d.ts +0 -5
  11. package/dist/types/debuglog.d.ts +0 -1
  12. package/dist/types/exports.d.ts +2 -1
  13. package/dist/types/mount_point.d.ts +1 -9
  14. package/dist/types/number.d.ts +0 -4
  15. package/dist/types/object.d.ts +0 -4
  16. package/dist/types/volume_metadata.d.ts +3 -3
  17. package/dist/types/volume_mount_points.d.ts +9 -0
  18. package/package.json +9 -9
  19. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  21. package/src/array.ts +44 -0
  22. package/src/async.ts +145 -0
  23. package/src/debuglog.ts +30 -0
  24. package/src/defer.ts +30 -0
  25. package/src/error.ts +79 -0
  26. package/src/exports.ts +156 -0
  27. package/src/fs.ts +89 -0
  28. package/src/glob.ts +127 -0
  29. package/src/hidden.ts +249 -0
  30. package/src/index.cts +15 -0
  31. package/src/index.mts +17 -0
  32. package/src/linux/dev_disk.ts +77 -0
  33. package/src/linux/mount_points.ts +91 -0
  34. package/src/linux/mtab.ts +136 -0
  35. package/src/mount_point.ts +58 -0
  36. package/src/number.ts +21 -0
  37. package/src/object.ts +55 -0
  38. package/src/options.ts +179 -0
  39. package/src/path.ts +54 -0
  40. package/src/platform.ts +9 -0
  41. package/src/random.ts +40 -0
  42. package/src/remote_info.ts +161 -0
  43. package/src/setup.ts +69 -0
  44. package/src/string.ts +99 -0
  45. package/src/string_enum.ts +41 -0
  46. package/src/system_volume.ts +67 -0
  47. package/src/test-utils/assert.ts +69 -0
  48. package/src/test-utils/hidden-tests.ts +33 -0
  49. package/src/test-utils/jest-matchers.ts +29 -0
  50. package/src/test-utils/platform.ts +39 -0
  51. package/src/types/native_bindings.ts +64 -0
  52. package/src/types/node-gyp-build.d.ts +6 -0
  53. package/src/unc.ts +63 -0
  54. package/src/units.ts +31 -0
  55. package/src/uuid.ts +24 -0
  56. package/src/volume_health_status.ts +58 -0
  57. package/src/volume_metadata.ts +294 -0
  58. package/src/volume_mount_points.ts +109 -0
  59. package/tsup.config.ts +8 -0
  60. package/dist/types/cache.d.ts +0 -4
@@ -0,0 +1,294 @@
1
+ // src/volume_metadata.ts
2
+
3
+ import { mapConcurrent, withTimeout } from "./async.js";
4
+ import { debug } from "./debuglog.js";
5
+ import { WrappedError } from "./error.js";
6
+ import { getLabelFromDevDisk, getUuidFromDevDisk } from "./linux/dev_disk.js";
7
+ import { getLinuxMtabMetadata } from "./linux/mount_points.js";
8
+ import {
9
+ type MtabVolumeMetadata,
10
+ mountEntryToPartialVolumeMetadata,
11
+ } from "./linux/mtab.js";
12
+ import type { MountPoint } from "./mount_point.js";
13
+ import { compactValues } from "./object.js";
14
+ import {
15
+ IncludeSystemVolumesDefault,
16
+ type Options,
17
+ optionsWithDefaults,
18
+ } from "./options.js";
19
+ import { normalizePath } from "./path.js";
20
+ import { isLinux, isWindows } from "./platform.js";
21
+ import {
22
+ type RemoteInfo,
23
+ extractRemoteInfo,
24
+ isRemoteFsType,
25
+ } from "./remote_info.js";
26
+ import { isBlank, isNotBlank } from "./string.js";
27
+ import { assignSystemVolume } from "./system_volume.js";
28
+ import type {
29
+ GetVolumeMetadataOptions,
30
+ NativeBindingsFn,
31
+ } from "./types/native_bindings.js";
32
+ import { parseUNCPath } from "./unc.js";
33
+ import { extractUUID } from "./uuid.js";
34
+ import {
35
+ VolumeHealthStatuses,
36
+ directoryStatus,
37
+ } from "./volume_health_status.js";
38
+ import { getVolumeMountPoints } from "./volume_mount_points.js";
39
+
40
+ /**
41
+ * Metadata associated to a volume.
42
+ *
43
+ * @see https://en.wikipedia.org/wiki/Volume_(computing)
44
+ */
45
+ export interface VolumeMetadata extends RemoteInfo, MountPoint {
46
+ /**
47
+ * The name of the partition
48
+ */
49
+ label?: string;
50
+ /**
51
+ * Total size in bytes
52
+ */
53
+ size?: number;
54
+ /**
55
+ * Used size in bytes
56
+ */
57
+ used?: number;
58
+ /**
59
+ * Available size in bytes
60
+ */
61
+ available?: number;
62
+
63
+ /**
64
+ * Path to the device or service that the mountpoint is from.
65
+ *
66
+ * Examples include `/dev/sda1`, `nfs-server:/export`,
67
+ * `//username@remoteHost/remoteShare`, or `//cifs-server/share`.
68
+ *
69
+ * May be undefined for remote volumes.
70
+ */
71
+ mountFrom?: string;
72
+
73
+ /**
74
+ * The name of the mount. This may match the resolved mountPoint.
75
+ */
76
+ mountName?: string;
77
+
78
+ /**
79
+ * UUID for the volume, like "c9b08f6e-b392-11ef-bf19-4b13bb7db4b4".
80
+ *
81
+ * On windows, this _may_ be the 128-bit volume UUID, but if that is not
82
+ * available, like in the case of remote volumes, we fallback to the 32-bit
83
+ * volume serial number, rendered in lowercase hexadecimal.
84
+ */
85
+ uuid?: string;
86
+ }
87
+
88
+ export async function getVolumeMetadata(
89
+ o: GetVolumeMetadataOptions & Options,
90
+ nativeFn: NativeBindingsFn,
91
+ ): Promise<VolumeMetadata> {
92
+ if (isBlank(o.mountPoint)) {
93
+ throw new TypeError(
94
+ "Invalid mountPoint: got " + JSON.stringify(o.mountPoint),
95
+ );
96
+ }
97
+
98
+ const p = _getVolumeMetadata(o, nativeFn);
99
+ // we rely on the native bindings on Windows to do proper timeouts
100
+ return isWindows
101
+ ? p
102
+ : withTimeout({
103
+ desc: "getVolumeMetadata()",
104
+ timeoutMs: o.timeoutMs,
105
+ promise: p,
106
+ });
107
+ }
108
+
109
+ async function _getVolumeMetadata(
110
+ o: GetVolumeMetadataOptions & Options,
111
+ nativeFn: NativeBindingsFn,
112
+ ): Promise<VolumeMetadata> {
113
+ o = optionsWithDefaults(o);
114
+ const norm = normalizePath(o.mountPoint);
115
+ if (norm == null) {
116
+ throw new Error("Invalid mountPoint: " + JSON.stringify(o.mountPoint));
117
+ }
118
+ o.mountPoint = norm;
119
+
120
+ debug(
121
+ "[getVolumeMetadata] starting metadata collection for %s",
122
+ o.mountPoint,
123
+ );
124
+ debug("[getVolumeMetadata] options: %o", o);
125
+
126
+ const { status, error } = await directoryStatus(o.mountPoint, o.timeoutMs);
127
+ if (status !== VolumeHealthStatuses.healthy) {
128
+ debug("[getVolumeMetadata] directoryStatus error: %s", error);
129
+ throw error ?? new Error("Volume not healthy: " + status);
130
+ }
131
+
132
+ debug("[getVolumeMetadata] readdir status: %s", status);
133
+
134
+ let remote: boolean = false;
135
+ // Get filesystem info from mtab first on Linux
136
+ let mtabInfo: undefined | MtabVolumeMetadata;
137
+ let device: undefined | string;
138
+ if (isLinux) {
139
+ debug("[getVolumeMetadata] collecting Linux mtab info");
140
+ try {
141
+ const m = await getLinuxMtabMetadata(o.mountPoint, o);
142
+ mtabInfo = mountEntryToPartialVolumeMetadata(m, o);
143
+ debug("[getVolumeMetadata] mtab info: %o", mtabInfo);
144
+ if (mtabInfo.remote) {
145
+ remote = true;
146
+ }
147
+ if (isNotBlank(m.fs_spec)) {
148
+ device = m.fs_spec;
149
+ }
150
+ } catch (err) {
151
+ debug("[getVolumeMetadata] failed to get mtab info: " + err);
152
+ // this may be a GIO mount. Ignore the error and continue.
153
+ }
154
+ }
155
+
156
+ if (isNotBlank(device)) {
157
+ o.device = device;
158
+ debug("[getVolumeMetadata] using device: %s", device);
159
+ }
160
+
161
+ debug("[getVolumeMetadata] requesting native metadata");
162
+ const metadata = (await (
163
+ await nativeFn()
164
+ ).getVolumeMetadata(o)) as VolumeMetadata;
165
+ debug("[getVolumeMetadata] native metadata: %o", metadata);
166
+
167
+ // Some OS implementations leave it up to us to extract remote info:
168
+ const remoteInfo =
169
+ mtabInfo ??
170
+ extractRemoteInfo(metadata.uri) ??
171
+ extractRemoteInfo(metadata.mountFrom) ??
172
+ (isWindows ? parseUNCPath(o.mountPoint) : undefined);
173
+
174
+ debug("[getVolumeMetadata] extracted remote info: %o", remoteInfo);
175
+
176
+ remote ||=
177
+ isRemoteFsType(metadata.fstype) ||
178
+ (remoteInfo?.remote ?? metadata.remote ?? false);
179
+
180
+ debug("[getVolumeMetadata] assembling: %o", {
181
+ status,
182
+ mtabInfo,
183
+ remoteInfo,
184
+ metadata,
185
+ mountPoint: o.mountPoint,
186
+ remote,
187
+ });
188
+ const result = compactValues({
189
+ status, // < let the implementation's status win by having this first
190
+ ...compactValues(remoteInfo),
191
+ ...compactValues(metadata),
192
+ ...compactValues(mtabInfo),
193
+ mountPoint: o.mountPoint,
194
+ remote,
195
+ }) as VolumeMetadata;
196
+
197
+ // Backfill if blkid or gio failed us:
198
+ if (isLinux && isNotBlank(device)) {
199
+ // Sometimes blkid doesn't have the UUID in cache. Try to get it from
200
+ // /dev/disk/by-uuid:
201
+ result.uuid ??= (await getUuidFromDevDisk(device)) ?? "";
202
+ result.label ??= (await getLabelFromDevDisk(device)) ?? "";
203
+ }
204
+
205
+ assignSystemVolume(result, o);
206
+
207
+ // Fix microsoft's UUID format:
208
+ result.uuid = extractUUID(result.uuid) ?? result.uuid ?? "";
209
+
210
+ debug("[getVolumeMetadata] final result for %s: %o", o.mountPoint, result);
211
+ return compactValues(result) as VolumeMetadata;
212
+ }
213
+
214
+ export async function getAllVolumeMetadata(
215
+ opts: Required<Options> & {
216
+ includeSystemVolumes?: boolean;
217
+ maxConcurrency?: number;
218
+ },
219
+ nativeFn: NativeBindingsFn,
220
+ ): Promise<VolumeMetadata[]> {
221
+ const o = optionsWithDefaults(opts);
222
+ debug("[getAllVolumeMetadata] starting with options: %o", o);
223
+
224
+ const arr = await getVolumeMountPoints(o, nativeFn);
225
+ debug("[getAllVolumeMetadata] found %d mount points", arr.length);
226
+
227
+ const unhealthyMountPoints = arr
228
+ .filter(
229
+ (ea) => ea.status != null && ea.status !== VolumeHealthStatuses.healthy,
230
+ )
231
+ .map((ea) => ({
232
+ mountPoint: ea.mountPoint,
233
+ error: new WrappedError("volume not healthy: " + ea.status, {
234
+ name: "Skipped",
235
+ }),
236
+ }));
237
+
238
+ const includeSystemVolumes =
239
+ opts?.includeSystemVolumes ?? IncludeSystemVolumesDefault;
240
+
241
+ const systemMountPoints = includeSystemVolumes
242
+ ? []
243
+ : arr
244
+ .filter((ea) => ea.isSystemVolume)
245
+ .map((ea) => ({
246
+ mountPoint: ea.mountPoint,
247
+ error: new WrappedError("system volume", { name: "Skipped" }),
248
+ }));
249
+
250
+ const healthy = arr.filter(
251
+ (ea) => ea.status == null || ea.status === VolumeHealthStatuses.healthy,
252
+ );
253
+
254
+ debug("[getAllVolumeMetadata] ", {
255
+ allMountPoints: arr.map((ea) => ea.mountPoint),
256
+ healthyMountPoints: healthy.map((ea) => ea.mountPoint),
257
+ });
258
+
259
+ debug(
260
+ "[getAllVolumeMetadata] processing %d healthy volumes with max concurrency %d",
261
+ healthy.length,
262
+ o.maxConcurrency,
263
+ );
264
+
265
+ const results = await (mapConcurrent({
266
+ maxConcurrency: o.maxConcurrency,
267
+ items:
268
+ (opts?.includeSystemVolumes ?? IncludeSystemVolumesDefault)
269
+ ? healthy
270
+ : healthy.filter((ea) => !ea.isSystemVolume),
271
+ fn: async (mp) =>
272
+ getVolumeMetadata({ ...mp, ...o }, nativeFn).catch((error) => ({
273
+ mountPoint: mp.mountPoint,
274
+ error,
275
+ })),
276
+ }) as Promise<(VolumeMetadata | { mountPoint: string; error: Error })[]>);
277
+
278
+ debug("[getAllVolumeMetadata] completed processing all volumes");
279
+ return arr.map(
280
+ (result) =>
281
+ (results.find((ea) => ea.mountPoint === result.mountPoint) ??
282
+ unhealthyMountPoints.find(
283
+ (ea) => ea.mountPoint === result.mountPoint,
284
+ ) ??
285
+ systemMountPoints.find((ea) => ea.mountPoint === result.mountPoint) ?? {
286
+ ...result,
287
+ error: new WrappedError("Mount point metadata not retrieved", {
288
+ name: "NotApplicableError",
289
+ }),
290
+ }) as VolumeMetadata,
291
+ );
292
+ }
293
+
294
+ export const _ = undefined;
@@ -0,0 +1,109 @@
1
+ // src/mount_point.ts
2
+
3
+ import { uniqBy } from "./array.js";
4
+ import { mapConcurrent, withTimeout } from "./async.js";
5
+ import { debug } from "./debuglog.js";
6
+ import { getLinuxMountPoints } from "./linux/mount_points.js";
7
+ import { MountPoint } from "./mount_point.js";
8
+ import { compactValues } from "./object.js";
9
+ import { Options } from "./options.js";
10
+ import { isMacOS, isWindows } from "./platform.js";
11
+ import {
12
+ isBlank,
13
+ isNotBlank,
14
+ sortObjectsByLocale,
15
+ toNotBlank,
16
+ } from "./string.js";
17
+ import { assignSystemVolume, SystemVolumeConfig } from "./system_volume.js";
18
+ import type { NativeBindingsFn } from "./types/native_bindings.js";
19
+ import { directoryStatus } from "./volume_health_status.js";
20
+
21
+ export type GetVolumeMountPointOptions = Partial<
22
+ Pick<
23
+ Options,
24
+ | "timeoutMs"
25
+ | "linuxMountTablePaths"
26
+ | "maxConcurrency"
27
+ | "includeSystemVolumes"
28
+ > &
29
+ SystemVolumeConfig
30
+ >;
31
+
32
+ /**
33
+ * Helper function for {@link getVolumeMountPoints}.
34
+ */
35
+ export async function getVolumeMountPoints(
36
+ opts: Required<GetVolumeMountPointOptions>,
37
+ nativeFn: NativeBindingsFn,
38
+ ): Promise<MountPoint[]> {
39
+ const p = _getVolumeMountPoints(opts, nativeFn);
40
+ // we rely on the native bindings on Windows to do proper timeouts
41
+ return isWindows
42
+ ? p
43
+ : withTimeout({ desc: "getVolumeMountPoints", ...opts, promise: p });
44
+ }
45
+
46
+ async function _getVolumeMountPoints(
47
+ o: Required<GetVolumeMountPointOptions>,
48
+ nativeFn: NativeBindingsFn,
49
+ ): Promise<MountPoint[]> {
50
+ debug("[getVolumeMountPoints] gathering mount points with options: %o", o);
51
+
52
+ const raw = await (isWindows || isMacOS
53
+ ? (async () => {
54
+ debug("[getVolumeMountPoints] using native implementation");
55
+ const points = await (await nativeFn()).getVolumeMountPoints(o);
56
+ debug(
57
+ "[getVolumeMountPoints] native returned %d mount points",
58
+ points.length,
59
+ );
60
+ return points;
61
+ })()
62
+ : getLinuxMountPoints(nativeFn, o));
63
+
64
+ debug("[getVolumeMountPoints] raw mount points: %o", raw);
65
+
66
+ const compacted = raw
67
+ .map((ea) => compactValues(ea) as MountPoint)
68
+ .filter((ea) => isNotBlank(ea.mountPoint));
69
+
70
+ for (const ea of compacted) {
71
+ assignSystemVolume(ea, o);
72
+ }
73
+
74
+ const filtered = o.includeSystemVolumes
75
+ ? compacted
76
+ : compacted.filter((ea) => !ea.isSystemVolume);
77
+
78
+ const uniq = uniqBy(filtered, (ea) => toNotBlank(ea.mountPoint));
79
+ debug("[getVolumeMountPoints] found %d unique mount points", uniq.length);
80
+
81
+ const results = sortObjectsByLocale(uniq, (ea) => ea.mountPoint);
82
+ debug(
83
+ "[getVolumeMountPoints] getting status for %d mount points",
84
+ results.length,
85
+ );
86
+
87
+ await mapConcurrent({
88
+ maxConcurrency: o.maxConcurrency,
89
+ items: results.filter(
90
+ // trust but verify
91
+ (ea) => isBlank(ea.status) || ea.status === "healthy",
92
+ ),
93
+ fn: async (mp) => {
94
+ debug("[getVolumeMountPoints] checking status of %s", mp.mountPoint);
95
+ mp.status = (await directoryStatus(mp.mountPoint, o.timeoutMs)).status;
96
+ debug(
97
+ "[getVolumeMountPoints] status for %s: %s",
98
+ mp.mountPoint,
99
+ mp.status,
100
+ );
101
+ },
102
+ });
103
+
104
+ debug(
105
+ "[getVolumeMountPoints] completed with %d mount points",
106
+ results.length,
107
+ );
108
+ return results;
109
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ outExtension: ({ format }) => ({
5
+ js: format === "cjs" ? ".cjs" : ".mjs", // Use .cjs for CommonJS and .mjs for ESM
6
+ }),
7
+ sourcemap: true,
8
+ });
@@ -1,4 +0,0 @@
1
- /**
2
- * Cache the result of a function for a given time-to-live (TTL).
3
- */
4
- export declare function ttlCache<Args extends unknown[], R>(fn: (...args: Args) => R, ttl: number): (...args: Args) => R;