@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.
@@ -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
+ }