@livedesk/hub 0.1.36 → 0.1.38

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,189 +1,181 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- const MANIFEST_VERSION = 1;
7
- const AUTO_SYNC_MS = 30_000;
8
-
9
- function publicFolder(folder, sourceId, status = 'ready', message = '') {
10
- return {
11
- id: folder.id,
12
- sourceId,
13
- displayPath: folder.displayPath,
14
- name: folder.name,
15
- autoSync: folder.autoSync === true,
16
- status,
17
- fileCount: Number(folder.fileCount || 0),
18
- totalBytes: Number(folder.totalBytes || 0),
19
- lastScannedAt: folder.lastScannedAt || undefined,
20
- lastSyncedAt: folder.lastSyncedAt || undefined,
21
- message: message || folder.message || undefined
22
- };
23
- }
24
-
25
- export class HubSharedFolders {
26
- constructor({ filesystem, transferJobs, remoteHub, dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
27
- this.filesystem = filesystem;
28
- this.transferJobs = transferJobs;
29
- this.remoteHub = remoteHub;
30
- this.dataDir = dataDir;
31
- this.filePath = path.join(dataDir, 'shared-folders.json');
32
- this.folders = new Map();
33
- this.loaded = false;
34
- this.autoSyncTimer = null;
35
- this.syncing = new Set();
36
- }
37
-
38
- async ensureLoaded() {
39
- if (this.loaded) return;
40
- this.loaded = true;
41
- try {
42
- const parsed = JSON.parse(await fs.readFile(this.filePath, 'utf8'));
43
- for (const folder of Array.isArray(parsed?.folders) ? parsed.folders : []) {
44
- if (folder?.id && folder?.sourcePath) this.folders.set(folder.id, { ...folder, manifestVersion: MANIFEST_VERSION });
45
- }
46
- } catch (error) {
47
- if (error?.code !== 'ENOENT') console.warn(`[LiveDesk Hub] shared folder store unavailable: ${error?.message || error}`);
48
- }
49
- }
50
-
51
- async persist() {
52
- await fs.mkdir(this.dataDir, { recursive: true });
53
- const temp = `${this.filePath}.tmp`;
54
- await fs.writeFile(temp, JSON.stringify({ version: MANIFEST_VERSION, folders: [...this.folders.values()] }, null, 2), 'utf8');
55
- await fs.rename(temp, this.filePath);
56
- }
57
-
58
- async list() {
59
- await this.ensureLoaded();
60
- const result = [];
61
- for (const folder of this.folders.values()) {
62
- const entry = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
63
- result.push(publicFolder(folder, entry?.id || '', entry ? 'ready' : 'error', entry ? '' : 'Drive or folder unavailable'));
64
- }
65
- return result;
66
- }
67
-
68
- async add(sourceId) {
69
- await this.ensureLoaded();
70
- const entry = this.filesystem.resolve(sourceId);
71
- if (entry.type !== 'folder') throw new Error('sync-source-must-be-folder');
72
- const existing = [...this.folders.values()].find(folder => path.resolve(folder.sourcePath) === path.resolve(entry.absolutePath));
73
- if (existing) return publicFolder(existing, sourceId);
74
- const folder = {
75
- id: `sync_${crypto.randomBytes(12).toString('base64url')}`,
76
- sourcePath: entry.absolutePath,
77
- displayPath: entry.displayPath,
78
- name: entry.name,
79
- autoSync: false,
80
- fileCount: 0,
81
- totalBytes: 0,
82
- manifestVersion: MANIFEST_VERSION,
83
- manifests: {}
84
- };
85
- this.folders.set(folder.id, folder);
86
- await this.persist();
87
- return publicFolder(folder, sourceId);
88
- }
89
-
90
- async update(id, patch = {}) {
91
- await this.ensureLoaded();
92
- const folder = this.folders.get(String(id || ''));
93
- if (!folder) throw new Error('sync-folder-not-found');
94
- if (typeof patch.autoSync === 'boolean') folder.autoSync = patch.autoSync;
95
- await this.persist();
96
- const entry = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
97
- return publicFolder(folder, entry?.id || '', entry ? 'ready' : 'error', entry ? '' : 'Drive or folder unavailable');
98
- }
99
-
100
- async remove(id) {
101
- await this.ensureLoaded();
102
- const removed = this.folders.delete(String(id || ''));
103
- if (removed) await this.persist();
104
- return { ok: removed };
105
- }
106
-
107
- async clear() {
108
- await this.ensureLoaded();
109
- const removed = this.folders.size;
110
- this.folders.clear();
111
- await this.persist();
112
- return { ok: true, removed };
113
- }
114
-
115
- async sync(id, deviceIds, remoteDirectory) {
116
- await this.ensureLoaded();
117
- const folder = this.folders.get(String(id || ''));
118
- if (!folder) throw new Error('sync-folder-not-found');
119
- if (this.syncing.has(folder.id)) throw new Error('sync-already-running');
120
- const source = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
121
- if (!source) throw new Error('sync-source-unavailable');
122
- folder.remoteDirectory = String(remoteDirectory || folder.remoteDirectory || 'Desktop/LiveDeskFiles').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
123
- const scan = await this.filesystem.scan([source.id]);
124
- folder.fileCount = scan.files.length;
125
- folder.totalBytes = scan.totalBytes;
126
- folder.lastScannedAt = new Date().toISOString();
127
- folder.message = '';
128
- const jobs = [];
129
- const targets = [...new Set((Array.isArray(deviceIds) ? deviceIds : []).map(value => String(value || '').trim()).filter(Boolean))];
130
- this.syncing.add(folder.id);
131
- try {
132
- for (const deviceId of targets) {
133
- const previous = folder.manifests?.[deviceId]?.entries || {};
134
- const changedFiles = scan.files.filter(file => previous[file.relativePath]?.size !== file.size || previous[file.relativePath]?.modifiedMs !== file.modifiedMs);
135
- if (changedFiles.length === 0) continue;
136
- const fingerprints = Object.fromEntries(scan.files.map(file => [file.relativePath, { size: file.size, modifiedMs: file.modifiedMs }]));
137
- jobs.push(this.transferJobs.create({
138
- files: changedFiles,
139
- deviceIds: [deviceId],
140
- remoteDirectory: folder.remoteDirectory,
141
- onComplete: async ({ completed }) => {
142
- if (!completed) return;
143
- folder.manifests = folder.manifests || {};
144
- folder.manifests[deviceId] = { version: MANIFEST_VERSION, entries: fingerprints, updatedAt: new Date().toISOString() };
145
- folder.lastSyncedAt = new Date().toISOString();
146
- await this.persist();
147
- }
148
- }));
149
- }
150
- folder.message = jobs.length > 0 ? `Syncing ${jobs.length} Client${jobs.length === 1 ? '' : 's'}` : 'No changed files';
151
- await this.persist();
152
- return { ok: true, jobs, files: scan.files.length, totalBytes: scan.totalBytes };
153
- } finally {
154
- this.syncing.delete(folder.id);
155
- }
156
- }
157
-
158
- startAutoSync(getDeviceIds, getRemoteDirectory = () => '') {
159
- if (this.autoSyncTimer) return;
160
- this.autoSyncTimer = setInterval(() => {
161
- void this.runAutoSync(getDeviceIds, getRemoteDirectory);
162
- }, AUTO_SYNC_MS);
163
- this.autoSyncTimer.unref?.();
164
- }
165
-
166
- async runAutoSync(getDeviceIds, getRemoteDirectory) {
167
- const deviceIds = typeof getDeviceIds === 'function' ? getDeviceIds() : [];
168
- if (!deviceIds.length) return;
169
- await this.ensureLoaded();
170
- for (const folder of this.folders.values()) {
171
- if (!folder.autoSync || this.syncing.has(folder.id)) continue;
172
- try {
173
- await this.sync(folder.id, deviceIds, folder.remoteDirectory || getRemoteDirectory());
174
- } catch (error) {
175
- folder.message = error instanceof Error ? error.message : String(error);
176
- await this.persist().catch(() => undefined);
177
- }
178
- }
179
- }
180
-
181
- close() {
182
- if (this.autoSyncTimer) clearInterval(this.autoSyncTimer);
183
- this.autoSyncTimer = null;
184
- }
185
- }
186
-
187
- export function createHubSharedFolders(options) {
188
- return new HubSharedFolders(options);
189
- }
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const MANIFEST_VERSION = 1;
7
+ const AUTO_SYNC_MS = 30_000;
8
+
9
+ function publicFolder(folder, sourceId, status = 'ready', message = '') {
10
+ return {
11
+ id: folder.id,
12
+ sourceId,
13
+ displayPath: folder.displayPath,
14
+ name: folder.name,
15
+ autoSync: folder.autoSync === true,
16
+ status,
17
+ fileCount: Number(folder.fileCount || 0),
18
+ totalBytes: Number(folder.totalBytes || 0),
19
+ lastScannedAt: folder.lastScannedAt || undefined,
20
+ lastSyncedAt: folder.lastSyncedAt || undefined,
21
+ message: message || folder.message || undefined
22
+ };
23
+ }
24
+
25
+ export class HubSharedFolders {
26
+ constructor({ filesystem, transferJobs, remoteHub, dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
27
+ this.filesystem = filesystem;
28
+ this.transferJobs = transferJobs;
29
+ this.remoteHub = remoteHub;
30
+ this.dataDir = dataDir;
31
+ this.filePath = path.join(dataDir, 'shared-folders.json');
32
+ this.folders = new Map();
33
+ this.loaded = false;
34
+ this.autoSyncTimer = null;
35
+ this.syncing = new Set();
36
+ }
37
+
38
+ async ensureLoaded() {
39
+ if (this.loaded) return;
40
+ this.loaded = true;
41
+ try {
42
+ const parsed = JSON.parse(await fs.readFile(this.filePath, 'utf8'));
43
+ for (const folder of Array.isArray(parsed?.folders) ? parsed.folders : []) {
44
+ if (folder?.id && folder?.sourcePath) this.folders.set(folder.id, { ...folder, manifestVersion: MANIFEST_VERSION });
45
+ }
46
+ } catch (error) {
47
+ if (error?.code !== 'ENOENT') console.warn(`[LiveDesk Hub] shared folder store unavailable: ${error?.message || error}`);
48
+ }
49
+ }
50
+
51
+ async persist() {
52
+ await fs.mkdir(this.dataDir, { recursive: true });
53
+ const temp = `${this.filePath}.tmp`;
54
+ await fs.writeFile(temp, JSON.stringify({ version: MANIFEST_VERSION, folders: [...this.folders.values()] }, null, 2), 'utf8');
55
+ await fs.rename(temp, this.filePath);
56
+ }
57
+
58
+ async list() {
59
+ await this.ensureLoaded();
60
+ const result = [];
61
+ for (const folder of this.folders.values()) {
62
+ const entry = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
63
+ result.push(publicFolder(folder, entry?.id || '', entry ? 'ready' : 'error', entry ? '' : 'Drive or folder unavailable'));
64
+ }
65
+ return result;
66
+ }
67
+
68
+ async add(sourceId) {
69
+ await this.ensureLoaded();
70
+ const entry = this.filesystem.resolve(sourceId);
71
+ if (entry.type !== 'folder') throw new Error('sync-source-must-be-folder');
72
+ const existing = [...this.folders.values()].find(folder => path.resolve(folder.sourcePath) === path.resolve(entry.absolutePath));
73
+ if (existing) return publicFolder(existing, sourceId);
74
+ const folder = {
75
+ id: `sync_${crypto.randomBytes(12).toString('base64url')}`,
76
+ sourcePath: entry.absolutePath,
77
+ displayPath: entry.displayPath,
78
+ name: entry.name,
79
+ autoSync: false,
80
+ fileCount: 0,
81
+ totalBytes: 0,
82
+ manifestVersion: MANIFEST_VERSION,
83
+ manifests: {}
84
+ };
85
+ this.folders.set(folder.id, folder);
86
+ await this.persist();
87
+ return publicFolder(folder, sourceId);
88
+ }
89
+
90
+ async update(id, patch = {}) {
91
+ await this.ensureLoaded();
92
+ const folder = this.folders.get(String(id || ''));
93
+ if (!folder) throw new Error('sync-folder-not-found');
94
+ if (typeof patch.autoSync === 'boolean') folder.autoSync = patch.autoSync;
95
+ await this.persist();
96
+ const entry = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
97
+ return publicFolder(folder, entry?.id || '', entry ? 'ready' : 'error', entry ? '' : 'Drive or folder unavailable');
98
+ }
99
+
100
+ async remove(id) {
101
+ await this.ensureLoaded();
102
+ const removed = this.folders.delete(String(id || ''));
103
+ if (removed) await this.persist();
104
+ return { ok: removed };
105
+ }
106
+
107
+ async sync(id, deviceIds, remoteDirectory) {
108
+ await this.ensureLoaded();
109
+ const folder = this.folders.get(String(id || ''));
110
+ if (!folder) throw new Error('sync-folder-not-found');
111
+ if (this.syncing.has(folder.id)) throw new Error('sync-already-running');
112
+ const source = await this.filesystem.registerPersistedFolder(folder.sourcePath, folder.displayPath);
113
+ if (!source) throw new Error('sync-source-unavailable');
114
+ folder.remoteDirectory = String(remoteDirectory || folder.remoteDirectory || 'Desktop/LiveDeskFiles').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
115
+ const scan = await this.filesystem.scan([source.id]);
116
+ folder.fileCount = scan.files.length;
117
+ folder.totalBytes = scan.totalBytes;
118
+ folder.lastScannedAt = new Date().toISOString();
119
+ folder.message = '';
120
+ const jobs = [];
121
+ const targets = [...new Set((Array.isArray(deviceIds) ? deviceIds : []).map(value => String(value || '').trim()).filter(Boolean))];
122
+ this.syncing.add(folder.id);
123
+ try {
124
+ for (const deviceId of targets) {
125
+ const previous = folder.manifests?.[deviceId]?.entries || {};
126
+ const changedFiles = scan.files.filter(file => previous[file.relativePath]?.size !== file.size || previous[file.relativePath]?.modifiedMs !== file.modifiedMs);
127
+ if (changedFiles.length === 0) continue;
128
+ const fingerprints = Object.fromEntries(scan.files.map(file => [file.relativePath, { size: file.size, modifiedMs: file.modifiedMs }]));
129
+ jobs.push(this.transferJobs.create({
130
+ files: changedFiles,
131
+ deviceIds: [deviceId],
132
+ remoteDirectory: folder.remoteDirectory,
133
+ onComplete: async ({ completed }) => {
134
+ if (!completed) return;
135
+ folder.manifests = folder.manifests || {};
136
+ folder.manifests[deviceId] = { version: MANIFEST_VERSION, entries: fingerprints, updatedAt: new Date().toISOString() };
137
+ folder.lastSyncedAt = new Date().toISOString();
138
+ await this.persist();
139
+ }
140
+ }));
141
+ }
142
+ folder.message = jobs.length > 0 ? `Syncing ${jobs.length} Client${jobs.length === 1 ? '' : 's'}` : 'No changed files';
143
+ await this.persist();
144
+ return { ok: true, jobs, files: scan.files.length, totalBytes: scan.totalBytes };
145
+ } finally {
146
+ this.syncing.delete(folder.id);
147
+ }
148
+ }
149
+
150
+ startAutoSync(getDeviceIds, getRemoteDirectory = () => '') {
151
+ if (this.autoSyncTimer) return;
152
+ this.autoSyncTimer = setInterval(() => {
153
+ void this.runAutoSync(getDeviceIds, getRemoteDirectory);
154
+ }, AUTO_SYNC_MS);
155
+ this.autoSyncTimer.unref?.();
156
+ }
157
+
158
+ async runAutoSync(getDeviceIds, getRemoteDirectory) {
159
+ const deviceIds = typeof getDeviceIds === 'function' ? getDeviceIds() : [];
160
+ if (!deviceIds.length) return;
161
+ await this.ensureLoaded();
162
+ for (const folder of this.folders.values()) {
163
+ if (!folder.autoSync || this.syncing.has(folder.id)) continue;
164
+ try {
165
+ await this.sync(folder.id, deviceIds, folder.remoteDirectory || getRemoteDirectory());
166
+ } catch (error) {
167
+ folder.message = error instanceof Error ? error.message : String(error);
168
+ await this.persist().catch(() => undefined);
169
+ }
170
+ }
171
+ }
172
+
173
+ close() {
174
+ if (this.autoSyncTimer) clearInterval(this.autoSyncTimer);
175
+ this.autoSyncTimer = null;
176
+ }
177
+ }
178
+
179
+ export function createHubSharedFolders(options) {
180
+ return new HubSharedFolders(options);
181
+ }