@livedesk/hub 0.1.41 → 0.1.43
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/package.json +3 -3
- package/src/agents/agent-device-scope.js +29 -29
- package/src/agents/agent-manager.js +103 -103
- package/src/agents/agent-permission-store.js +78 -78
- package/src/agents/agent-runtime-error.js +9 -9
- package/src/agents/agent-settings.js +71 -71
- package/src/agents/codex-agent-runtime.js +594 -594
- package/src/agents/codex-mcp-server.js +100 -100
- package/src/console-relay.js +415 -0
- package/src/filesystem/directory-reader.js +187 -187
- package/src/filesystem/hub-filesystem.js +78 -78
- package/src/filesystem/path-registry.js +78 -78
- package/src/filesystem/roots.js +115 -115
- package/src/frame-packet-contract.mjs +427 -427
- package/src/http/hub-ui-session.js +119 -119
- package/src/live-capture-transition-retry.mjs +297 -297
- package/src/live-desk-update.js +167 -108
- package/src/live-stream-monitor-contract.js +38 -38
- package/src/lzo1x.js +109 -109
- package/src/mode4-atlas-pool.js +137 -137
- package/src/mode4-atlas-sizing.js +32 -32
- package/src/mode4-atlas-worker.js +599 -599
- package/src/mode4-atlas.js +306 -306
- package/src/remote-audio-subscription-contract.mjs +109 -109
- package/src/remote-audio-subscription-contract.test.mjs +72 -72
- package/src/remote-hub.js +144 -144
- package/src/server.js +244 -222
- package/src/settings/effective-device-policy.js +16 -16
- package/src/transport/udp-protocol.js +453 -453
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
4
|
-
import { isPathWithinRoot } from './path-registry.js';
|
|
5
|
-
|
|
6
|
-
function filesystemError(message, code = 'FILESYSTEM_ERROR', status = 400) {
|
|
7
|
-
const error = new Error(message);
|
|
8
|
-
error.code = code;
|
|
9
|
-
error.status = status;
|
|
10
|
-
return error;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function safeEntryName(name) {
|
|
14
|
-
return String(name || '').replace(/[\0\r\n\t]/g, ' ').slice(0, 512);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function toPublicEntry(entry) {
|
|
18
|
-
return {
|
|
19
|
-
id: entry.id,
|
|
20
|
-
parentId: entry.parentId,
|
|
21
|
-
name: entry.name,
|
|
22
|
-
type: entry.type,
|
|
23
|
-
size: entry.size || 0,
|
|
24
|
-
modifiedAt: entry.modifiedAt || null,
|
|
25
|
-
extension: entry.type === 'file' ? path.extname(entry.name).toLowerCase() : '',
|
|
26
|
-
hasChildren: entry.type !== 'file' && entry.hasChildren === true,
|
|
27
|
-
displayPath: entry.displayPath,
|
|
28
|
-
locked: entry.locked === true
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function readEntryStat(absolutePath) {
|
|
33
|
-
try {
|
|
34
|
-
return await fs.lstat(absolutePath);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
37
|
-
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function readDirectoryEntries(registry, folderId) {
|
|
43
|
-
const folder = registry.resolve(folderId);
|
|
44
|
-
if (folder.type === 'file') throw filesystemError('filesystem-entry-is-not-folder', 'NOT_A_FOLDER', 400);
|
|
45
|
-
if (folder.locked) throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
46
|
-
let entries;
|
|
47
|
-
try {
|
|
48
|
-
entries = await fs.readdir(folder.absolutePath, { withFileTypes: true });
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
51
|
-
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const output = [];
|
|
56
|
-
for (const dirent of entries) {
|
|
57
|
-
const name = safeEntryName(dirent.name);
|
|
58
|
-
if (!name || name === '.' || name === '..') continue;
|
|
59
|
-
const absolutePath = path.resolve(folder.absolutePath, name);
|
|
60
|
-
if (!isPathWithinRoot(folder.rootPath, absolutePath)) continue;
|
|
61
|
-
const stat = await readEntryStat(absolutePath).catch(error => {
|
|
62
|
-
if (error?.code === 'ACCESS_DENIED' || error?.code === 'NOT_FOUND') return null;
|
|
63
|
-
throw error;
|
|
64
|
-
});
|
|
65
|
-
if (!stat) continue;
|
|
66
|
-
const isSymlink = dirent.isSymbolicLink() || stat.isSymbolicLink();
|
|
67
|
-
const type = !isSymlink && (dirent.isDirectory() || stat.isDirectory()) ? 'folder' : 'file';
|
|
68
|
-
const child = registry.register({
|
|
69
|
-
absolutePath,
|
|
70
|
-
rootPath: folder.rootPath,
|
|
71
|
-
rootId: folder.rootId,
|
|
72
|
-
parentId: folder.id,
|
|
73
|
-
name,
|
|
74
|
-
type,
|
|
75
|
-
displayPath: path.join(folder.displayPath, name),
|
|
76
|
-
locked: isSymlink
|
|
77
|
-
});
|
|
78
|
-
output.push(toPublicEntry({
|
|
79
|
-
...child,
|
|
80
|
-
size: type === 'file' ? stat.size : 0,
|
|
81
|
-
modifiedAt: stat.mtime?.toISOString(),
|
|
82
|
-
hasChildren: type === 'folder' && !isSymlink
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
output.sort((left, right) => {
|
|
86
|
-
if (left.type !== right.type) return left.type === 'folder' ? -1 : 1;
|
|
87
|
-
return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' });
|
|
88
|
-
});
|
|
89
|
-
return {
|
|
90
|
-
folder: toPublicEntry({ ...folder, hasChildren: true }),
|
|
91
|
-
entries: output
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function walkDirectory(registry, entry, relativePrefix, output, visited, counters) {
|
|
96
|
-
const realPath = await fs.realpath(entry.absolutePath).catch(() => entry.absolutePath);
|
|
97
|
-
if (visited.has(realPath)) return;
|
|
98
|
-
visited.add(realPath);
|
|
99
|
-
let dirents;
|
|
100
|
-
try {
|
|
101
|
-
dirents = await fs.readdir(entry.absolutePath, { withFileTypes: true });
|
|
102
|
-
} catch (error) {
|
|
103
|
-
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
for (const dirent of dirents) {
|
|
107
|
-
const name = safeEntryName(dirent.name);
|
|
108
|
-
if (!name || name === '.' || name === '..' || dirent.isSymbolicLink()) continue;
|
|
109
|
-
const absolutePath = path.resolve(entry.absolutePath, name);
|
|
110
|
-
if (!isPathWithinRoot(entry.rootPath, absolutePath)) continue;
|
|
111
|
-
const stat = await readEntryStat(absolutePath);
|
|
112
|
-
const relativePath = path.posix.join(relativePrefix, name);
|
|
113
|
-
if (stat.isDirectory()) {
|
|
114
|
-
counters.folders += 1;
|
|
115
|
-
const child = registry.register({
|
|
116
|
-
absolutePath,
|
|
117
|
-
rootPath: entry.rootPath,
|
|
118
|
-
rootId: entry.rootId,
|
|
119
|
-
parentId: entry.id,
|
|
120
|
-
name,
|
|
121
|
-
type: 'folder',
|
|
122
|
-
displayPath: path.join(entry.displayPath, name)
|
|
123
|
-
});
|
|
124
|
-
await walkDirectory(registry, child, relativePath, output, visited, counters);
|
|
125
|
-
} else if (stat.isFile()) {
|
|
126
|
-
const child = registry.register({
|
|
127
|
-
absolutePath,
|
|
128
|
-
rootPath: entry.rootPath,
|
|
129
|
-
rootId: entry.rootId,
|
|
130
|
-
parentId: entry.id,
|
|
131
|
-
name,
|
|
132
|
-
type: 'file',
|
|
133
|
-
displayPath: path.join(entry.displayPath, name)
|
|
134
|
-
});
|
|
135
|
-
output.push({
|
|
136
|
-
id: child.id,
|
|
137
|
-
absolutePath,
|
|
138
|
-
relativePath,
|
|
139
|
-
name,
|
|
140
|
-
size: stat.size,
|
|
141
|
-
modifiedMs: stat.mtimeMs,
|
|
142
|
-
modifiedAt: stat.mtime?.toISOString() || null
|
|
143
|
-
});
|
|
144
|
-
counters.files += 1;
|
|
145
|
-
counters.totalBytes += stat.size;
|
|
146
|
-
}
|
|
147
|
-
if ((counters.files + counters.folders) % 100 === 0) await yieldToEventLoop();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function scanSelection(registry, itemIds) {
|
|
152
|
-
const ids = [...new Set((Array.isArray(itemIds) ? itemIds : []).map(value => String(value || '').trim()).filter(Boolean))].slice(0, 5000);
|
|
153
|
-
if (ids.length === 0) throw filesystemError('no-filesystem-items-selected', 'NO_SELECTION', 400);
|
|
154
|
-
const selected = ids.map(id => registry.resolve(id));
|
|
155
|
-
const selectedPaths = selected.filter(entry => entry.type === 'folder').map(entry => path.resolve(entry.absolutePath));
|
|
156
|
-
const topLevel = selected.filter(entry => !selectedPaths.some(parent => parent !== entry.absolutePath && isPathWithinRoot(parent, entry.absolutePath)));
|
|
157
|
-
const files = [];
|
|
158
|
-
const counters = { files: 0, folders: 0, totalBytes: 0 };
|
|
159
|
-
const visited = new Set();
|
|
160
|
-
for (const entry of topLevel) {
|
|
161
|
-
const stat = await readEntryStat(entry.absolutePath);
|
|
162
|
-
if (entry.type === 'file' || stat.isFile()) {
|
|
163
|
-
files.push({
|
|
164
|
-
id: entry.id,
|
|
165
|
-
absolutePath: entry.absolutePath,
|
|
166
|
-
relativePath: entry.name,
|
|
167
|
-
name: entry.name,
|
|
168
|
-
size: stat.size,
|
|
169
|
-
modifiedMs: stat.mtimeMs,
|
|
170
|
-
modifiedAt: stat.mtime?.toISOString() || null
|
|
171
|
-
});
|
|
172
|
-
counters.files += 1;
|
|
173
|
-
counters.totalBytes += stat.size;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
counters.folders += 1;
|
|
177
|
-
await walkDirectory(registry, entry, entry.name, files, visited, counters);
|
|
178
|
-
}
|
|
179
|
-
files.sort((left, right) => left.relativePath.localeCompare(right.relativePath, undefined, { numeric: true }));
|
|
180
|
-
return { ...counters, files };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function toFilesystemHttpError(error) {
|
|
184
|
-
return error?.status
|
|
185
|
-
? error
|
|
186
|
-
: filesystemError('filesystem-operation-failed', 'FILESYSTEM_ERROR', 500);
|
|
187
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
4
|
+
import { isPathWithinRoot } from './path-registry.js';
|
|
5
|
+
|
|
6
|
+
function filesystemError(message, code = 'FILESYSTEM_ERROR', status = 400) {
|
|
7
|
+
const error = new Error(message);
|
|
8
|
+
error.code = code;
|
|
9
|
+
error.status = status;
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeEntryName(name) {
|
|
14
|
+
return String(name || '').replace(/[\0\r\n\t]/g, ' ').slice(0, 512);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toPublicEntry(entry) {
|
|
18
|
+
return {
|
|
19
|
+
id: entry.id,
|
|
20
|
+
parentId: entry.parentId,
|
|
21
|
+
name: entry.name,
|
|
22
|
+
type: entry.type,
|
|
23
|
+
size: entry.size || 0,
|
|
24
|
+
modifiedAt: entry.modifiedAt || null,
|
|
25
|
+
extension: entry.type === 'file' ? path.extname(entry.name).toLowerCase() : '',
|
|
26
|
+
hasChildren: entry.type !== 'file' && entry.hasChildren === true,
|
|
27
|
+
displayPath: entry.displayPath,
|
|
28
|
+
locked: entry.locked === true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readEntryStat(absolutePath) {
|
|
33
|
+
try {
|
|
34
|
+
return await fs.lstat(absolutePath);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
37
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readDirectoryEntries(registry, folderId) {
|
|
43
|
+
const folder = registry.resolve(folderId);
|
|
44
|
+
if (folder.type === 'file') throw filesystemError('filesystem-entry-is-not-folder', 'NOT_A_FOLDER', 400);
|
|
45
|
+
if (folder.locked) throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await fs.readdir(folder.absolutePath, { withFileTypes: true });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
51
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') throw filesystemError('filesystem-entry-unavailable', 'NOT_FOUND', 404);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const output = [];
|
|
56
|
+
for (const dirent of entries) {
|
|
57
|
+
const name = safeEntryName(dirent.name);
|
|
58
|
+
if (!name || name === '.' || name === '..') continue;
|
|
59
|
+
const absolutePath = path.resolve(folder.absolutePath, name);
|
|
60
|
+
if (!isPathWithinRoot(folder.rootPath, absolutePath)) continue;
|
|
61
|
+
const stat = await readEntryStat(absolutePath).catch(error => {
|
|
62
|
+
if (error?.code === 'ACCESS_DENIED' || error?.code === 'NOT_FOUND') return null;
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
if (!stat) continue;
|
|
66
|
+
const isSymlink = dirent.isSymbolicLink() || stat.isSymbolicLink();
|
|
67
|
+
const type = !isSymlink && (dirent.isDirectory() || stat.isDirectory()) ? 'folder' : 'file';
|
|
68
|
+
const child = registry.register({
|
|
69
|
+
absolutePath,
|
|
70
|
+
rootPath: folder.rootPath,
|
|
71
|
+
rootId: folder.rootId,
|
|
72
|
+
parentId: folder.id,
|
|
73
|
+
name,
|
|
74
|
+
type,
|
|
75
|
+
displayPath: path.join(folder.displayPath, name),
|
|
76
|
+
locked: isSymlink
|
|
77
|
+
});
|
|
78
|
+
output.push(toPublicEntry({
|
|
79
|
+
...child,
|
|
80
|
+
size: type === 'file' ? stat.size : 0,
|
|
81
|
+
modifiedAt: stat.mtime?.toISOString(),
|
|
82
|
+
hasChildren: type === 'folder' && !isSymlink
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
output.sort((left, right) => {
|
|
86
|
+
if (left.type !== right.type) return left.type === 'folder' ? -1 : 1;
|
|
87
|
+
return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' });
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
folder: toPublicEntry({ ...folder, hasChildren: true }),
|
|
91
|
+
entries: output
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function walkDirectory(registry, entry, relativePrefix, output, visited, counters) {
|
|
96
|
+
const realPath = await fs.realpath(entry.absolutePath).catch(() => entry.absolutePath);
|
|
97
|
+
if (visited.has(realPath)) return;
|
|
98
|
+
visited.add(realPath);
|
|
99
|
+
let dirents;
|
|
100
|
+
try {
|
|
101
|
+
dirents = await fs.readdir(entry.absolutePath, { withFileTypes: true });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') throw filesystemError('filesystem-access-denied', 'ACCESS_DENIED', 403);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
for (const dirent of dirents) {
|
|
107
|
+
const name = safeEntryName(dirent.name);
|
|
108
|
+
if (!name || name === '.' || name === '..' || dirent.isSymbolicLink()) continue;
|
|
109
|
+
const absolutePath = path.resolve(entry.absolutePath, name);
|
|
110
|
+
if (!isPathWithinRoot(entry.rootPath, absolutePath)) continue;
|
|
111
|
+
const stat = await readEntryStat(absolutePath);
|
|
112
|
+
const relativePath = path.posix.join(relativePrefix, name);
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
counters.folders += 1;
|
|
115
|
+
const child = registry.register({
|
|
116
|
+
absolutePath,
|
|
117
|
+
rootPath: entry.rootPath,
|
|
118
|
+
rootId: entry.rootId,
|
|
119
|
+
parentId: entry.id,
|
|
120
|
+
name,
|
|
121
|
+
type: 'folder',
|
|
122
|
+
displayPath: path.join(entry.displayPath, name)
|
|
123
|
+
});
|
|
124
|
+
await walkDirectory(registry, child, relativePath, output, visited, counters);
|
|
125
|
+
} else if (stat.isFile()) {
|
|
126
|
+
const child = registry.register({
|
|
127
|
+
absolutePath,
|
|
128
|
+
rootPath: entry.rootPath,
|
|
129
|
+
rootId: entry.rootId,
|
|
130
|
+
parentId: entry.id,
|
|
131
|
+
name,
|
|
132
|
+
type: 'file',
|
|
133
|
+
displayPath: path.join(entry.displayPath, name)
|
|
134
|
+
});
|
|
135
|
+
output.push({
|
|
136
|
+
id: child.id,
|
|
137
|
+
absolutePath,
|
|
138
|
+
relativePath,
|
|
139
|
+
name,
|
|
140
|
+
size: stat.size,
|
|
141
|
+
modifiedMs: stat.mtimeMs,
|
|
142
|
+
modifiedAt: stat.mtime?.toISOString() || null
|
|
143
|
+
});
|
|
144
|
+
counters.files += 1;
|
|
145
|
+
counters.totalBytes += stat.size;
|
|
146
|
+
}
|
|
147
|
+
if ((counters.files + counters.folders) % 100 === 0) await yieldToEventLoop();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function scanSelection(registry, itemIds) {
|
|
152
|
+
const ids = [...new Set((Array.isArray(itemIds) ? itemIds : []).map(value => String(value || '').trim()).filter(Boolean))].slice(0, 5000);
|
|
153
|
+
if (ids.length === 0) throw filesystemError('no-filesystem-items-selected', 'NO_SELECTION', 400);
|
|
154
|
+
const selected = ids.map(id => registry.resolve(id));
|
|
155
|
+
const selectedPaths = selected.filter(entry => entry.type === 'folder').map(entry => path.resolve(entry.absolutePath));
|
|
156
|
+
const topLevel = selected.filter(entry => !selectedPaths.some(parent => parent !== entry.absolutePath && isPathWithinRoot(parent, entry.absolutePath)));
|
|
157
|
+
const files = [];
|
|
158
|
+
const counters = { files: 0, folders: 0, totalBytes: 0 };
|
|
159
|
+
const visited = new Set();
|
|
160
|
+
for (const entry of topLevel) {
|
|
161
|
+
const stat = await readEntryStat(entry.absolutePath);
|
|
162
|
+
if (entry.type === 'file' || stat.isFile()) {
|
|
163
|
+
files.push({
|
|
164
|
+
id: entry.id,
|
|
165
|
+
absolutePath: entry.absolutePath,
|
|
166
|
+
relativePath: entry.name,
|
|
167
|
+
name: entry.name,
|
|
168
|
+
size: stat.size,
|
|
169
|
+
modifiedMs: stat.mtimeMs,
|
|
170
|
+
modifiedAt: stat.mtime?.toISOString() || null
|
|
171
|
+
});
|
|
172
|
+
counters.files += 1;
|
|
173
|
+
counters.totalBytes += stat.size;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
counters.folders += 1;
|
|
177
|
+
await walkDirectory(registry, entry, entry.name, files, visited, counters);
|
|
178
|
+
}
|
|
179
|
+
files.sort((left, right) => left.relativePath.localeCompare(right.relativePath, undefined, { numeric: true }));
|
|
180
|
+
return { ...counters, files };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function toFilesystemHttpError(error) {
|
|
184
|
+
return error?.status
|
|
185
|
+
? error
|
|
186
|
+
: filesystemError('filesystem-operation-failed', 'FILESYSTEM_ERROR', 500);
|
|
187
|
+
}
|
|
@@ -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
|
+
}
|