@livedesk/hub 0.1.41 → 0.1.42

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.
@@ -1,78 +1,78 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { discoverFilesystemRoots } from './roots.js';
4
- import { OpaquePathRegistry, isPathWithinRoot } from './path-registry.js';
5
- import { readDirectoryEntries, scanSelection } from './directory-reader.js';
6
-
7
- export class HubFilesystem {
8
- constructor() {
9
- this.registry = new OpaquePathRegistry();
10
- this.rootsCache = null;
11
- this.rootsLoadedAt = 0;
12
- }
13
-
14
- async getRoots({ refresh = false } = {}) {
15
- if (!refresh && this.rootsCache && Date.now() - this.rootsLoadedAt < 30_000) return this.rootsCache;
16
- const roots = await discoverFilesystemRoots();
17
- const entries = roots.map(root => this.registry.register({
18
- absolutePath: root.path,
19
- rootPath: root.path,
20
- rootId: `root:${root.path}`,
21
- parentId: 'this-pc',
22
- name: root.name,
23
- type: 'folder',
24
- displayPath: root.displayPath
25
- }));
26
- this.rootsCache = {
27
- roots: entries.map((entry, index) => ({
28
- id: entry.id,
29
- parentId: 'this-pc',
30
- name: entry.name,
31
- type: 'drive',
32
- size: 0,
33
- modifiedAt: null,
34
- hasChildren: roots[index].hasChildren,
35
- displayPath: roots[index].displayPath,
36
- driveType: roots[index].driveType,
37
- totalBytes: roots[index].totalBytes,
38
- freeBytes: roots[index].freeBytes
39
- }))
40
- };
41
- this.rootsLoadedAt = Date.now();
42
- return this.rootsCache;
43
- }
44
-
45
- async getEntries(id) {
46
- return readDirectoryEntries(this.registry, id);
47
- }
48
-
49
- async scan(itemIds) {
50
- return scanSelection(this.registry, itemIds);
51
- }
52
-
53
- resolve(id) {
54
- return this.registry.resolve(id);
55
- }
56
-
57
- async registerPersistedFolder(absolutePath, displayPath = absolutePath) {
58
- const normalized = path.resolve(String(absolutePath || ''));
59
- const roots = await discoverFilesystemRoots();
60
- const root = roots.find(candidate => isPathWithinRoot(candidate.path, normalized));
61
- if (!root) return null;
62
- const stat = await fs.lstat(normalized).catch(() => null);
63
- if (!stat?.isDirectory()) return null;
64
- return this.registry.register({
65
- absolutePath: normalized,
66
- rootPath: root.path,
67
- rootId: `root:${root.path}`,
68
- parentId: null,
69
- name: path.basename(normalized) || normalized,
70
- type: 'folder',
71
- displayPath
72
- });
73
- }
74
- }
75
-
76
- export function createHubFilesystem() {
77
- return new HubFilesystem();
78
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { discoverFilesystemRoots } from './roots.js';
4
+ import { OpaquePathRegistry, isPathWithinRoot } from './path-registry.js';
5
+ import { readDirectoryEntries, scanSelection } from './directory-reader.js';
6
+
7
+ export class HubFilesystem {
8
+ constructor() {
9
+ this.registry = new OpaquePathRegistry();
10
+ this.rootsCache = null;
11
+ this.rootsLoadedAt = 0;
12
+ }
13
+
14
+ async getRoots({ refresh = false } = {}) {
15
+ if (!refresh && this.rootsCache && Date.now() - this.rootsLoadedAt < 30_000) return this.rootsCache;
16
+ const roots = await discoverFilesystemRoots();
17
+ const entries = roots.map(root => this.registry.register({
18
+ absolutePath: root.path,
19
+ rootPath: root.path,
20
+ rootId: `root:${root.path}`,
21
+ parentId: 'this-pc',
22
+ name: root.name,
23
+ type: 'folder',
24
+ displayPath: root.displayPath
25
+ }));
26
+ this.rootsCache = {
27
+ roots: entries.map((entry, index) => ({
28
+ id: entry.id,
29
+ parentId: 'this-pc',
30
+ name: entry.name,
31
+ type: 'drive',
32
+ size: 0,
33
+ modifiedAt: null,
34
+ hasChildren: roots[index].hasChildren,
35
+ displayPath: roots[index].displayPath,
36
+ driveType: roots[index].driveType,
37
+ totalBytes: roots[index].totalBytes,
38
+ freeBytes: roots[index].freeBytes
39
+ }))
40
+ };
41
+ this.rootsLoadedAt = Date.now();
42
+ return this.rootsCache;
43
+ }
44
+
45
+ async getEntries(id) {
46
+ return readDirectoryEntries(this.registry, id);
47
+ }
48
+
49
+ async scan(itemIds) {
50
+ return scanSelection(this.registry, itemIds);
51
+ }
52
+
53
+ resolve(id) {
54
+ return this.registry.resolve(id);
55
+ }
56
+
57
+ async registerPersistedFolder(absolutePath, displayPath = absolutePath) {
58
+ const normalized = path.resolve(String(absolutePath || ''));
59
+ const roots = await discoverFilesystemRoots();
60
+ const root = roots.find(candidate => isPathWithinRoot(candidate.path, normalized));
61
+ if (!root) return null;
62
+ const stat = await fs.lstat(normalized).catch(() => null);
63
+ if (!stat?.isDirectory()) return null;
64
+ return this.registry.register({
65
+ absolutePath: normalized,
66
+ rootPath: root.path,
67
+ rootId: `root:${root.path}`,
68
+ parentId: null,
69
+ name: path.basename(normalized) || normalized,
70
+ type: 'folder',
71
+ displayPath
72
+ });
73
+ }
74
+ }
75
+
76
+ export function createHubFilesystem() {
77
+ return new HubFilesystem();
78
+ }
@@ -1,78 +1,78 @@
1
- import crypto from 'node:crypto';
2
- import path from 'node:path';
3
-
4
- function isWithinRoot(rootPath, candidatePath) {
5
- const relative = path.relative(rootPath, candidatePath);
6
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
7
- }
8
-
9
- export class OpaquePathRegistry {
10
- constructor({ ttlMs = 30 * 60 * 1000 } = {}) {
11
- this.ttlMs = ttlMs;
12
- this.entries = new Map();
13
- this.byPath = new Map();
14
- }
15
-
16
- register({ absolutePath, rootPath, rootId, parentId = null, name, type, displayPath = absolutePath, locked = false }) {
17
- const normalizedPath = path.resolve(absolutePath);
18
- const normalizedRoot = path.resolve(rootPath);
19
- if (!isWithinRoot(normalizedRoot, normalizedPath)) {
20
- const error = new Error('filesystem-path-outside-root');
21
- error.code = 'PATH_OUTSIDE_ROOT';
22
- throw error;
23
- }
24
- const key = `${rootId}:${normalizedPath}`;
25
- const existing = this.byPath.get(key);
26
- if (existing && this.entries.has(existing)) {
27
- const entry = this.entries.get(existing);
28
- entry.expiresAt = Date.now() + this.ttlMs;
29
- return entry;
30
- }
31
- const entry = {
32
- id: `fs_${crypto.randomBytes(18).toString('base64url')}`,
33
- absolutePath: normalizedPath,
34
- rootPath: normalizedRoot,
35
- rootId: String(rootId),
36
- parentId,
37
- name: String(name || path.basename(normalizedPath) || normalizedPath),
38
- type: type === 'file' ? 'file' : 'folder',
39
- displayPath: String(displayPath || normalizedPath),
40
- locked: locked === true,
41
- expiresAt: Date.now() + this.ttlMs
42
- };
43
- this.entries.set(entry.id, entry);
44
- this.byPath.set(key, entry.id);
45
- return entry;
46
- }
47
-
48
- resolve(id) {
49
- this.prune();
50
- const entry = this.entries.get(String(id || ''));
51
- if (!entry) {
52
- const error = new Error('invalid-filesystem-id');
53
- error.code = 'INVALID_ID';
54
- error.status = 400;
55
- throw error;
56
- }
57
- entry.expiresAt = Date.now() + this.ttlMs;
58
- return entry;
59
- }
60
-
61
- remove(id) {
62
- const entry = this.entries.get(String(id || ''));
63
- if (!entry) return;
64
- this.entries.delete(entry.id);
65
- this.byPath.delete(`${entry.rootId}:${entry.absolutePath}`);
66
- }
67
-
68
- prune() {
69
- const now = Date.now();
70
- for (const [id, entry] of this.entries) {
71
- if (entry.expiresAt <= now) this.remove(id);
72
- }
73
- }
74
- }
75
-
76
- export function isPathWithinRoot(rootPath, candidatePath) {
77
- return isWithinRoot(path.resolve(rootPath), path.resolve(candidatePath));
78
- }
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+
4
+ function isWithinRoot(rootPath, candidatePath) {
5
+ const relative = path.relative(rootPath, candidatePath);
6
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
7
+ }
8
+
9
+ export class OpaquePathRegistry {
10
+ constructor({ ttlMs = 30 * 60 * 1000 } = {}) {
11
+ this.ttlMs = ttlMs;
12
+ this.entries = new Map();
13
+ this.byPath = new Map();
14
+ }
15
+
16
+ register({ absolutePath, rootPath, rootId, parentId = null, name, type, displayPath = absolutePath, locked = false }) {
17
+ const normalizedPath = path.resolve(absolutePath);
18
+ const normalizedRoot = path.resolve(rootPath);
19
+ if (!isWithinRoot(normalizedRoot, normalizedPath)) {
20
+ const error = new Error('filesystem-path-outside-root');
21
+ error.code = 'PATH_OUTSIDE_ROOT';
22
+ throw error;
23
+ }
24
+ const key = `${rootId}:${normalizedPath}`;
25
+ const existing = this.byPath.get(key);
26
+ if (existing && this.entries.has(existing)) {
27
+ const entry = this.entries.get(existing);
28
+ entry.expiresAt = Date.now() + this.ttlMs;
29
+ return entry;
30
+ }
31
+ const entry = {
32
+ id: `fs_${crypto.randomBytes(18).toString('base64url')}`,
33
+ absolutePath: normalizedPath,
34
+ rootPath: normalizedRoot,
35
+ rootId: String(rootId),
36
+ parentId,
37
+ name: String(name || path.basename(normalizedPath) || normalizedPath),
38
+ type: type === 'file' ? 'file' : 'folder',
39
+ displayPath: String(displayPath || normalizedPath),
40
+ locked: locked === true,
41
+ expiresAt: Date.now() + this.ttlMs
42
+ };
43
+ this.entries.set(entry.id, entry);
44
+ this.byPath.set(key, entry.id);
45
+ return entry;
46
+ }
47
+
48
+ resolve(id) {
49
+ this.prune();
50
+ const entry = this.entries.get(String(id || ''));
51
+ if (!entry) {
52
+ const error = new Error('invalid-filesystem-id');
53
+ error.code = 'INVALID_ID';
54
+ error.status = 400;
55
+ throw error;
56
+ }
57
+ entry.expiresAt = Date.now() + this.ttlMs;
58
+ return entry;
59
+ }
60
+
61
+ remove(id) {
62
+ const entry = this.entries.get(String(id || ''));
63
+ if (!entry) return;
64
+ this.entries.delete(entry.id);
65
+ this.byPath.delete(`${entry.rootId}:${entry.absolutePath}`);
66
+ }
67
+
68
+ prune() {
69
+ const now = Date.now();
70
+ for (const [id, entry] of this.entries) {
71
+ if (entry.expiresAt <= now) this.remove(id);
72
+ }
73
+ }
74
+ }
75
+
76
+ export function isPathWithinRoot(rootPath, candidatePath) {
77
+ return isWithinRoot(path.resolve(rootPath), path.resolve(candidatePath));
78
+ }
@@ -1,115 +1,115 @@
1
- import fs from 'node:fs/promises';
2
- import { constants as fsConstants } from 'node:fs';
3
- import os from 'node:os';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
-
7
- const execFileAsync = promisify(execFile);
8
-
9
- function normalizeRootPath(value) {
10
- const text = String(value || '').trim();
11
- if (!text) return '';
12
- return process.platform === 'win32'
13
- ? text.replace(/[\\/]+$/, '\\')
14
- : text.replace(/\/+$/, '') || '/';
15
- }
16
-
17
- async function statfsCapacity(rootPath) {
18
- try {
19
- const stat = await fs.statfs(rootPath);
20
- const blockSize = Number(stat.bsize || stat.frsize || 0);
21
- return {
22
- totalBytes: blockSize > 0 ? Number(stat.blocks || 0) * blockSize : 0,
23
- freeBytes: blockSize > 0 ? Number(stat.bavail || stat.bfree || 0) * blockSize : 0
24
- };
25
- } catch {
26
- return { totalBytes: 0, freeBytes: 0 };
27
- }
28
- }
29
-
30
- async function windowsDriveTypes() {
31
- if (process.platform !== 'win32') return new Map();
32
- try {
33
- const { stdout } = await execFileAsync('powershell.exe', [
34
- '-NoProfile', '-NonInteractive', '-Command',
35
- '$ProgressPreference="SilentlyContinue"; Get-CimInstance Win32_LogicalDisk | Select-Object DeviceID,DriveType | ConvertTo-Json -Compress'
36
- ], { timeout: 2500, windowsHide: true, maxBuffer: 1024 * 1024 });
37
- const parsed = JSON.parse(String(stdout || 'null'));
38
- const rows = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
39
- const names = new Map([[2, 'removable'], [3, 'fixed'], [4, 'network'], [5, 'optical']]);
40
- return new Map(rows.map(row => [String(row.DeviceID || '').toUpperCase(), names.get(Number(row.DriveType)) || 'unknown']));
41
- } catch {
42
- return new Map();
43
- }
44
- }
45
-
46
- async function windowsRoots() {
47
- const types = await windowsDriveTypes();
48
- const roots = [];
49
- for (let code = 65; code <= 90; code += 1) {
50
- const letter = String.fromCharCode(code);
51
- const path = `${letter}:\\`;
52
- try {
53
- await fs.access(path, fsConstants.R_OK);
54
- const capacity = await statfsCapacity(path);
55
- const driveType = types.get(`${letter}:`.toUpperCase()) || 'fixed';
56
- roots.push({
57
- path,
58
- name: `${driveType === 'network' ? 'Network' : driveType === 'removable' ? 'External' : 'Drive'} (${letter}:)`,
59
- displayPath: path,
60
- type: 'drive',
61
- driveType,
62
- ...capacity,
63
- hasChildren: true
64
- });
65
- } catch {
66
- // An unavailable or inaccessible drive is not exposed to the browser.
67
- }
68
- }
69
- return roots;
70
- }
71
-
72
- async function mountedRoots() {
73
- const candidates = new Set(['/']);
74
- if (process.platform === 'darwin') candidates.add('/Volumes');
75
- if (process.platform === 'linux') {
76
- candidates.add('/mnt');
77
- candidates.add('/media');
78
- }
79
- const roots = [];
80
- for (const candidate of candidates) {
81
- try {
82
- const stat = await fs.stat(candidate);
83
- if (!stat.isDirectory()) continue;
84
- const capacity = await statfsCapacity(candidate);
85
- roots.push({
86
- path: candidate,
87
- name: candidate === '/' ? 'System root (/)' : candidate,
88
- displayPath: candidate,
89
- type: 'drive',
90
- driveType: candidate === '/' ? 'fixed' : 'mount',
91
- ...capacity,
92
- hasChildren: true
93
- });
94
- } catch {
95
- // Keep probing the remaining mount points.
96
- }
97
- }
98
- return roots;
99
- }
100
-
101
- export async function discoverFilesystemRoots() {
102
- const roots = process.platform === 'win32' ? await windowsRoots() : await mountedRoots();
103
- if (roots.length > 0) return roots;
104
- const fallback = process.platform === 'win32' ? `${process.env.SystemDrive || 'C:'}\\` : os.homedir();
105
- return [{
106
- path: fallback,
107
- name: fallback,
108
- displayPath: fallback,
109
- type: 'drive',
110
- driveType: 'unknown',
111
- totalBytes: 0,
112
- freeBytes: 0,
113
- hasChildren: true
114
- }];
115
- }
1
+ import fs from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import os from 'node:os';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ function normalizeRootPath(value) {
10
+ const text = String(value || '').trim();
11
+ if (!text) return '';
12
+ return process.platform === 'win32'
13
+ ? text.replace(/[\\/]+$/, '\\')
14
+ : text.replace(/\/+$/, '') || '/';
15
+ }
16
+
17
+ async function statfsCapacity(rootPath) {
18
+ try {
19
+ const stat = await fs.statfs(rootPath);
20
+ const blockSize = Number(stat.bsize || stat.frsize || 0);
21
+ return {
22
+ totalBytes: blockSize > 0 ? Number(stat.blocks || 0) * blockSize : 0,
23
+ freeBytes: blockSize > 0 ? Number(stat.bavail || stat.bfree || 0) * blockSize : 0
24
+ };
25
+ } catch {
26
+ return { totalBytes: 0, freeBytes: 0 };
27
+ }
28
+ }
29
+
30
+ async function windowsDriveTypes() {
31
+ if (process.platform !== 'win32') return new Map();
32
+ try {
33
+ const { stdout } = await execFileAsync('powershell.exe', [
34
+ '-NoProfile', '-NonInteractive', '-Command',
35
+ '$ProgressPreference="SilentlyContinue"; Get-CimInstance Win32_LogicalDisk | Select-Object DeviceID,DriveType | ConvertTo-Json -Compress'
36
+ ], { timeout: 2500, windowsHide: true, maxBuffer: 1024 * 1024 });
37
+ const parsed = JSON.parse(String(stdout || 'null'));
38
+ const rows = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
39
+ const names = new Map([[2, 'removable'], [3, 'fixed'], [4, 'network'], [5, 'optical']]);
40
+ return new Map(rows.map(row => [String(row.DeviceID || '').toUpperCase(), names.get(Number(row.DriveType)) || 'unknown']));
41
+ } catch {
42
+ return new Map();
43
+ }
44
+ }
45
+
46
+ async function windowsRoots() {
47
+ const types = await windowsDriveTypes();
48
+ const roots = [];
49
+ for (let code = 65; code <= 90; code += 1) {
50
+ const letter = String.fromCharCode(code);
51
+ const path = `${letter}:\\`;
52
+ try {
53
+ await fs.access(path, fsConstants.R_OK);
54
+ const capacity = await statfsCapacity(path);
55
+ const driveType = types.get(`${letter}:`.toUpperCase()) || 'fixed';
56
+ roots.push({
57
+ path,
58
+ name: `${driveType === 'network' ? 'Network' : driveType === 'removable' ? 'External' : 'Drive'} (${letter}:)`,
59
+ displayPath: path,
60
+ type: 'drive',
61
+ driveType,
62
+ ...capacity,
63
+ hasChildren: true
64
+ });
65
+ } catch {
66
+ // An unavailable or inaccessible drive is not exposed to the browser.
67
+ }
68
+ }
69
+ return roots;
70
+ }
71
+
72
+ async function mountedRoots() {
73
+ const candidates = new Set(['/']);
74
+ if (process.platform === 'darwin') candidates.add('/Volumes');
75
+ if (process.platform === 'linux') {
76
+ candidates.add('/mnt');
77
+ candidates.add('/media');
78
+ }
79
+ const roots = [];
80
+ for (const candidate of candidates) {
81
+ try {
82
+ const stat = await fs.stat(candidate);
83
+ if (!stat.isDirectory()) continue;
84
+ const capacity = await statfsCapacity(candidate);
85
+ roots.push({
86
+ path: candidate,
87
+ name: candidate === '/' ? 'System root (/)' : candidate,
88
+ displayPath: candidate,
89
+ type: 'drive',
90
+ driveType: candidate === '/' ? 'fixed' : 'mount',
91
+ ...capacity,
92
+ hasChildren: true
93
+ });
94
+ } catch {
95
+ // Keep probing the remaining mount points.
96
+ }
97
+ }
98
+ return roots;
99
+ }
100
+
101
+ export async function discoverFilesystemRoots() {
102
+ const roots = process.platform === 'win32' ? await windowsRoots() : await mountedRoots();
103
+ if (roots.length > 0) return roots;
104
+ const fallback = process.platform === 'win32' ? `${process.env.SystemDrive || 'C:'}\\` : os.homedir();
105
+ return [{
106
+ path: fallback,
107
+ name: fallback,
108
+ displayPath: fallback,
109
+ type: 'drive',
110
+ driveType: 'unknown',
111
+ totalBytes: 0,
112
+ freeBytes: 0,
113
+ hasChildren: true
114
+ }];
115
+ }