@electerm/electerm-react 3.2.0 → 3.5.6

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 (46) hide show
  1. package/client/common/constants.js +1 -8
  2. package/client/common/fs.js +84 -0
  3. package/client/components/batch-op/batch-op-alert.jsx +23 -0
  4. package/client/components/batch-op/batch-op-editor.jsx +206 -0
  5. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  6. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  7. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  8. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  9. package/client/components/bookmark-form/common/fields.jsx +15 -0
  10. package/client/components/bookmark-form/config/rdp.js +5 -0
  11. package/client/components/common/auto-check-update.jsx +31 -0
  12. package/client/components/common/notification.styl +1 -1
  13. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  14. package/client/components/footer/batch-input.jsx +10 -7
  15. package/client/components/main/error-wrapper.jsx +18 -7
  16. package/client/components/main/main.jsx +6 -7
  17. package/client/components/main/upgrade.jsx +133 -104
  18. package/client/components/main/upgrade.styl +2 -2
  19. package/client/components/rdp/file-transfer.js +375 -0
  20. package/client/components/rdp/rdp-session.jsx +169 -76
  21. package/client/components/rdp/rdp.styl +27 -0
  22. package/client/components/setting-sync/auto-sync.jsx +53 -0
  23. package/client/components/setting-sync/data-import.jsx +69 -8
  24. package/client/components/sftp/address-bar.jsx +23 -3
  25. package/client/components/sidebar/bookmark-select.jsx +3 -2
  26. package/client/components/sidebar/history-item.jsx +3 -1
  27. package/client/components/sidebar/index.jsx +0 -9
  28. package/client/components/sidebar/info-modal.jsx +7 -2
  29. package/client/components/tabs/add-btn-menu.jsx +1 -1
  30. package/client/components/tabs/add-btn.jsx +9 -15
  31. package/client/components/tabs/quick-connect.jsx +6 -10
  32. package/client/components/terminal/terminal.jsx +4 -5
  33. package/client/components/tree-list/tree-list.jsx +115 -10
  34. package/client/components/tree-list/tree-list.styl +3 -0
  35. package/client/components/tree-list/tree-search.jsx +9 -1
  36. package/client/components/widgets/widget-form.jsx +6 -0
  37. package/client/store/app-upgrade.js +2 -2
  38. package/client/store/common.js +0 -28
  39. package/client/store/load-data.js +3 -3
  40. package/client/store/mcp-handler.js +2 -2
  41. package/client/store/sync.js +25 -1
  42. package/client/store/tab.js +1 -1
  43. package/client/store/watch.js +10 -18
  44. package/client/views/index.pug +1 -2
  45. package/package.json +1 -1
  46. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -0,0 +1,375 @@
1
+ /**
2
+ * RDP File Transfer Module
3
+ * Handles file upload/download between local and remote desktop via IronRDP CLIPRDR channel
4
+ */
5
+
6
+ import fs from '../../common/fs'
7
+ import { getLocalFileInfo } from '../sftp/file-read'
8
+ import { osResolve } from '../../common/resolve'
9
+ import { filesize } from 'filesize'
10
+
11
+ const LOG_PREFIX = '[RDP-FILE-TRANSFER]'
12
+
13
+ // File open flags (POSIX standard values)
14
+ const O_RDONLY = 0
15
+ const O_WRONLY = 1
16
+ const O_CREAT = 64
17
+ const O_TRUNC = 512
18
+
19
+ export function createFileLogger () {
20
+ return (msg, type = 'info') => {
21
+ const timestamp = new Date().toISOString()
22
+ console.log(`${LOG_PREFIX}[${type}][${timestamp}] ${msg}`)
23
+ }
24
+ }
25
+
26
+ export class FileTransferManager {
27
+ constructor (getSession, log, onUploadStateChange, onUploadComplete, onDownloadComplete) {
28
+ this.getSession = getSession
29
+ this.log = log
30
+ this.uploadedFiles = new Map()
31
+ this.pendingDownloads = new Map()
32
+ this.streamToFileInfo = new Map()
33
+ this.hasRemoteFiles = false
34
+ this.onStateChange = null
35
+ this.onUploadStateChange = onUploadStateChange
36
+ this.onUploadComplete = onUploadComplete
37
+ this.onDownloadComplete = onDownloadComplete
38
+ this.uploadTimeout = null
39
+ }
40
+
41
+ setStateChangeCallback (callback) {
42
+ this.onStateChange = callback
43
+ }
44
+
45
+ setUploadInProgress (inProgress) {
46
+ if (this.onUploadStateChange) {
47
+ this.onUploadStateChange(inProgress)
48
+ }
49
+ }
50
+
51
+ notifyStateChange () {
52
+ if (this.onStateChange) {
53
+ this.onStateChange({
54
+ hasRemoteFiles: this.hasRemoteFiles,
55
+ pendingDownloads: this.pendingDownloads,
56
+ uploadedFiles: this.uploadedFiles
57
+ })
58
+ }
59
+ }
60
+
61
+ createExtensions () {
62
+ const { Extension } = window.ironRdp
63
+ const extensions = []
64
+
65
+ extensions.push(this.createFilesAvailableCallback(Extension))
66
+ extensions.push(this.createFileContentsRequestCallback(Extension))
67
+ extensions.push(this.createFileContentsResponseCallback(Extension))
68
+ extensions.push(this.createLockCallback(Extension))
69
+ extensions.push(this.createUnlockCallback(Extension))
70
+ extensions.push(this.createLocksExpiredCallback(Extension))
71
+
72
+ return extensions
73
+ }
74
+
75
+ createFilesAvailableCallback (Extension) {
76
+ return new Extension('files_available_callback', (files, clipDataId) => {
77
+ this.pendingDownloads.clear()
78
+ this.streamToFileInfo.clear()
79
+
80
+ if (files && files.length > 0) {
81
+ files.forEach((f, i) => {
82
+ this.pendingDownloads.set(i, { ...f, clipDataId })
83
+ })
84
+ }
85
+
86
+ this.hasRemoteFiles = files && files.length > 0
87
+ this.notifyStateChange()
88
+ })
89
+ }
90
+
91
+ createFileContentsRequestCallback (Extension) {
92
+ return new Extension('file_contents_request_callback', async (request) => {
93
+ const file = this.uploadedFiles.get(request.index)
94
+ if (!file) {
95
+ this.getSession().invokeExtension(new Extension('submit_file_contents', {
96
+ stream_id: request.streamId,
97
+ is_error: true,
98
+ data: new Uint8Array(0)
99
+ }))
100
+ return
101
+ }
102
+
103
+ try {
104
+ if (request.flags & 0x00000001) {
105
+ const sizeBytes = new Uint8Array(8)
106
+ const view = new DataView(sizeBytes.buffer)
107
+ view.setBigUint64(0, BigInt(file.size), true)
108
+ this.getSession().invokeExtension(new Extension('submit_file_contents', {
109
+ stream_id: request.streamId,
110
+ is_error: false,
111
+ data: sizeBytes
112
+ }))
113
+ } else if (request.flags & 0x00000002) {
114
+ const start = request.position
115
+ const length = request.size
116
+
117
+ const fd = await new Promise((resolve, reject) => {
118
+ fs.open(file.filePath, O_RDONLY, (err, fd) => {
119
+ if (err) reject(err)
120
+ else resolve(fd)
121
+ })
122
+ })
123
+
124
+ const buffer = new Uint8Array(length)
125
+ const { bytesRead, buffer: readBuffer } = await new Promise((resolve, reject) => {
126
+ fs.read(fd, buffer, 0, length, start, (err, bytesRead, buffer) => {
127
+ if (err) reject(err)
128
+ else resolve({ bytesRead, buffer })
129
+ })
130
+ })
131
+
132
+ await new Promise((resolve, reject) => {
133
+ fs.close(fd, (err) => {
134
+ if (err) reject(err)
135
+ else resolve()
136
+ })
137
+ })
138
+
139
+ const data = new Uint8Array(readBuffer.buffer, readBuffer.byteOffset, bytesRead)
140
+ this.getSession().invokeExtension(new Extension('submit_file_contents', {
141
+ stream_id: request.streamId,
142
+ is_error: false,
143
+ data
144
+ }))
145
+ this.setUploadInProgress(false)
146
+ if (this.onUploadComplete) {
147
+ this.onUploadComplete()
148
+ }
149
+ }
150
+ } catch (e) {
151
+ this.log(`Failed to read file: ${e.message}`, 'error')
152
+ this.getSession().invokeExtension(new Extension('submit_file_contents', {
153
+ stream_id: request.streamId,
154
+ is_error: true,
155
+ data: new Uint8Array(0)
156
+ }))
157
+ this.setUploadInProgress(false)
158
+ if (this.onUploadComplete) {
159
+ this.onUploadComplete()
160
+ }
161
+ }
162
+ })
163
+ }
164
+
165
+ createFileContentsResponseCallback (Extension) {
166
+ return new Extension('file_contents_response_callback', (response) => {
167
+ const streamId = response.streamId
168
+
169
+ const fileInfo = this.streamToFileInfo.get(streamId)
170
+ if (!fileInfo) {
171
+ return
172
+ }
173
+
174
+ if (response.isError) {
175
+ this.streamToFileInfo.delete(streamId)
176
+ return
177
+ }
178
+
179
+ if (!fileInfo._chunks) {
180
+ fileInfo._chunks = []
181
+ fileInfo._totalSize = 0
182
+ }
183
+
184
+ if (response.data.length === 8 && !fileInfo._sizeReceived) {
185
+ const view = new DataView(response.data.buffer)
186
+ const fileSize = Number(view.getBigUint64(0, true))
187
+ fileInfo._sizeReceived = true
188
+ fileInfo._expectedSize = fileSize
189
+
190
+ const dataStreamId = streamId + 1000
191
+ this.streamToFileInfo.set(dataStreamId, fileInfo)
192
+
193
+ const requestFileContentsExt = new Extension('request_file_contents', {
194
+ stream_id: dataStreamId,
195
+ file_index: fileInfo._fileIndex,
196
+ flags: 0x00000002,
197
+ position: 0,
198
+ size: fileSize,
199
+ clip_data_id: fileInfo.clipDataId
200
+ })
201
+ this.getSession().invokeExtension(requestFileContentsExt)
202
+ } else {
203
+ fileInfo._chunks.push(new Uint8Array(response.data))
204
+ fileInfo._totalSize += response.data.length
205
+
206
+ if (fileInfo._totalSize >= fileInfo._expectedSize) {
207
+ this.handleFileDownload(fileInfo)
208
+ this.streamToFileInfo.delete(streamId)
209
+ }
210
+ }
211
+ })
212
+ }
213
+
214
+ createLockCallback (Extension) {
215
+ return new Extension('lock_callback', (dataId) => {
216
+ this.log(`Clipboard locked: dataId=${dataId}`, 'info')
217
+ })
218
+ }
219
+
220
+ createUnlockCallback (Extension) {
221
+ return new Extension('unlock_callback', (dataId) => {
222
+ this.log(`Clipboard unlocked: dataId=${dataId}`, 'info')
223
+ })
224
+ }
225
+
226
+ createLocksExpiredCallback (Extension) {
227
+ return new Extension('locks_expired_callback', (clipDataIds) => {
228
+ this.log(`Clipboard locks expired: ${clipDataIds.length} lock(s)`, 'warn')
229
+ })
230
+ }
231
+
232
+ async handleFileDownload (fileInfo) {
233
+ try {
234
+ const savePath = await window.api.openDialog({
235
+ title: `Save ${fileInfo.name}`,
236
+ message: `Choose where to save ${fileInfo.name}`,
237
+ properties: ['openDirectory', 'createDirectory']
238
+ }).catch((err) => {
239
+ this.log(`Save dialog error: ${err.message}`, 'error')
240
+ return false
241
+ })
242
+
243
+ if (!savePath || !savePath.length) {
244
+ this.log('Download cancelled by user', 'info')
245
+ return
246
+ }
247
+
248
+ const fullPath = osResolve(savePath[0], fileInfo.name)
249
+
250
+ const fd = await new Promise((resolve, reject) => {
251
+ fs.open(fullPath, O_WRONLY | O_CREAT | O_TRUNC, (err, fd) => {
252
+ if (err) reject(err)
253
+ else resolve(fd)
254
+ })
255
+ })
256
+
257
+ const blob = new Blob(fileInfo._chunks)
258
+ const arrayBuffer = await blob.arrayBuffer()
259
+ const data = new Uint8Array(arrayBuffer)
260
+
261
+ await new Promise((resolve, reject) => {
262
+ fs.write(fd, data, (err) => {
263
+ if (err) reject(err)
264
+ else resolve()
265
+ })
266
+ })
267
+
268
+ await new Promise((resolve, reject) => {
269
+ fs.close(fd, (err) => {
270
+ if (err) reject(err)
271
+ else resolve()
272
+ })
273
+ })
274
+
275
+ this.log(`Downloaded ${fileInfo.name} (${filesize(fileInfo._totalSize)}) to ${fullPath}`, 'success')
276
+ if (this.onDownloadComplete) {
277
+ this.onDownloadComplete(fullPath, fileInfo.name, fileInfo._totalSize)
278
+ }
279
+ } catch (e) {
280
+ this.log(`Failed to save file: ${e.message}`, 'error')
281
+ }
282
+ }
283
+
284
+ async uploadFiles (files) {
285
+ const sess = this.getSession()
286
+ if (!sess) {
287
+ this.log('File transfer not available - no session', 'error')
288
+ return
289
+ }
290
+
291
+ this.setUploadInProgress(true)
292
+
293
+ try {
294
+ const fileDescriptors = files.map((file, index) => {
295
+ this.uploadedFiles.set(index, file)
296
+ return {
297
+ name: file.name,
298
+ size: file.size,
299
+ lastModified: file.modifyTime || Date.now()
300
+ }
301
+ })
302
+
303
+ const initiateFileCopyExt = new window.ironRdp.Extension('initiate_file_copy', fileDescriptors)
304
+ sess.invokeExtension(initiateFileCopyExt)
305
+ } catch (err) {
306
+ this.log(`Failed to initiate file copy: ${err.message}`, 'error')
307
+ this.setUploadInProgress(false)
308
+ }
309
+ }
310
+
311
+ async uploadFromPaths (filePaths) {
312
+ const fileInfos = []
313
+
314
+ for (const filePath of filePaths) {
315
+ try {
316
+ const stat = await getLocalFileInfo(filePath)
317
+ if (stat && (stat.isFile || stat.isDirectory === false)) {
318
+ fileInfos.push({ ...stat, filePath, path: filePath })
319
+ }
320
+ } catch (err) {
321
+ this.log(`Invalid file path: ${filePath}, error: ${err.message}`, 'warn')
322
+ }
323
+ }
324
+
325
+ if (fileInfos.length > 0) {
326
+ await this.uploadFiles(fileInfos)
327
+ }
328
+ }
329
+
330
+ downloadFiles () {
331
+ const sess = this.getSession()
332
+ if (!sess) {
333
+ this.log('File transfer not available - no session', 'error')
334
+ return
335
+ }
336
+
337
+ if (this.pendingDownloads.size === 0) {
338
+ this.log('No files available for download', 'info')
339
+ return
340
+ }
341
+
342
+ this.pendingDownloads.forEach((fileInfo, index) => {
343
+ try {
344
+ const sizeStreamId = index + 1
345
+ const fileInfoWithIndex = { ...fileInfo, _fileIndex: index }
346
+ this.streamToFileInfo.set(sizeStreamId, fileInfoWithIndex)
347
+
348
+ const requestSizeExt = new window.ironRdp.Extension('request_file_contents', {
349
+ stream_id: sizeStreamId,
350
+ file_index: index,
351
+ flags: 0x00000001,
352
+ position: 0,
353
+ size: 8,
354
+ clip_data_id: fileInfo.clipDataId
355
+ })
356
+
357
+ sess.invokeExtension(requestSizeExt)
358
+ } catch (err) {
359
+ this.log(`Failed to request ${fileInfo.name || `file_${index}`}: ${err.message}`, 'error')
360
+ }
361
+ })
362
+ }
363
+
364
+ formatFileSize (bytes) {
365
+ return filesize(bytes)
366
+ }
367
+
368
+ cleanup () {
369
+ this.uploadedFiles.clear()
370
+ this.pendingDownloads.clear()
371
+ this.streamToFileInfo.clear()
372
+ this.hasRemoteFiles = false
373
+ this.notifyStateChange()
374
+ }
375
+ }