@electerm/electerm-react 3.0.18 → 3.1.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.
- package/client/common/constants.js +4 -2
- package/client/components/file-transfer/conflict-resolve.jsx +30 -4
- package/client/components/file-transfer/transfer-queue.jsx +10 -4
- package/client/components/file-transfer/transfer.jsx +7 -4
- package/client/components/file-transfer/transports-action-store.jsx +14 -2
- package/client/components/setting-sync/server-data-status.jsx +2 -1
- package/client/components/setting-sync/setting-sync-form.jsx +93 -15
- package/client/components/setting-sync/setting-sync.jsx +5 -1
- package/client/components/sidebar/transfer-history-modal.jsx +2 -2
- package/client/components/tabs/tabs.styl +1 -0
- package/client/components/tabs/window-control.jsx +2 -0
- package/client/components/terminal/transfer-client-base.js +38 -17
- package/client/components/terminal-info/network.jsx +0 -1
- package/client/store/sync.js +129 -3
- package/client/store/transfer-list.js +3 -3
- package/package.json +1 -1
|
@@ -190,13 +190,15 @@ export const syncTypes = buildConst([
|
|
|
190
190
|
'github',
|
|
191
191
|
'gitee',
|
|
192
192
|
'custom',
|
|
193
|
-
'cloud'
|
|
193
|
+
'cloud',
|
|
194
|
+
'webdav'
|
|
194
195
|
])
|
|
195
196
|
export const syncTokenCreateUrls = {
|
|
196
197
|
gitee: 'https://gitee.com/github-zxdong262/electerm/wikis/Create%20personal%20access%20token?sort_id=3028409',
|
|
197
198
|
github: 'https://github.com/electerm/electerm/wiki/Create-personal-access-token',
|
|
198
199
|
custom: 'https://github.com/electerm/electerm/wiki/Custom-sync-server',
|
|
199
|
-
cloud: 'https://electerm-cloud.html5beta.com'
|
|
200
|
+
cloud: 'https://electerm-cloud.html5beta.com',
|
|
201
|
+
webdav: 'https://github.com/electerm/electerm/wiki/WebDAV-sync'
|
|
200
202
|
}
|
|
201
203
|
export const settingSyncId = 'setting-sync'
|
|
202
204
|
export const settingTerminalId = 'setting-terminal'
|
|
@@ -31,25 +31,42 @@ export default class ConfirmModalStore extends Component {
|
|
|
31
31
|
transferToConfirm: null
|
|
32
32
|
}
|
|
33
33
|
this.queue = []
|
|
34
|
+
this.queuedTransferIds = new Set()
|
|
35
|
+
this.activeTransferId = null
|
|
34
36
|
this.id = 'transfer-conflict'
|
|
35
37
|
refsStatic.add(this.id, this)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
addConflict = (transfer) => {
|
|
41
|
+
const transferId = transfer?.id
|
|
42
|
+
if (!transferId) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (this.activeTransferId === transferId || this.queuedTransferIds.has(transferId)) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
39
48
|
this.queue.push(transfer)
|
|
40
|
-
|
|
49
|
+
this.queuedTransferIds.add(transferId)
|
|
50
|
+
if (!this.activeTransferId) {
|
|
41
51
|
this.showNext()
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
showNext = () => {
|
|
46
56
|
const next = this.queue.shift()
|
|
57
|
+
if (next?.id) {
|
|
58
|
+
this.queuedTransferIds.delete(next.id)
|
|
59
|
+
}
|
|
60
|
+
this.activeTransferId = next?.id || null
|
|
47
61
|
this.setState({
|
|
48
62
|
transferToConfirm: next
|
|
49
63
|
})
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
act = (action) => {
|
|
67
|
+
if (!this.state.transferToConfirm) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
53
70
|
const { id, transferBatch } = this.state.transferToConfirm
|
|
54
71
|
const toAll = action.includes('All')
|
|
55
72
|
const policy = toAll ? action.replace('All', '') : action
|
|
@@ -60,10 +77,17 @@ export default class ConfirmModalStore extends Component {
|
|
|
60
77
|
if (doFilter) {
|
|
61
78
|
// Update all existing transfers with same batch ID in DOM
|
|
62
79
|
const prefix = `tr-${transferBatch}-`
|
|
80
|
+
const pendingConflictIds = new Set([
|
|
81
|
+
id,
|
|
82
|
+
...this.queue
|
|
83
|
+
.filter(d => d.transferBatch === transferBatch)
|
|
84
|
+
.map(d => d.id)
|
|
85
|
+
])
|
|
63
86
|
for (const [key, r] of window.refsTransfers.entries()) {
|
|
64
87
|
if (key.startsWith(prefix)) {
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
r.resolvePolicy = policy
|
|
89
|
+
const transferId = r.props.transfer?.id
|
|
90
|
+
if (key !== trid && pendingConflictIds.has(transferId)) {
|
|
67
91
|
r.onDecision(policy)
|
|
68
92
|
}
|
|
69
93
|
}
|
|
@@ -72,9 +96,11 @@ export default class ConfirmModalStore extends Component {
|
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
// Resolve current conflict
|
|
75
|
-
refsTransfers.get(trid)
|
|
99
|
+
const currentTransfer = refsTransfers.get(trid)
|
|
100
|
+
currentTransfer?.onDecision(policy)
|
|
76
101
|
|
|
77
102
|
// Move to the next item
|
|
103
|
+
this.activeTransferId = null
|
|
78
104
|
this.setState({
|
|
79
105
|
transferToConfirm: null
|
|
80
106
|
}, this.showNext)
|
|
@@ -41,7 +41,12 @@ export default class Queue extends Component {
|
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const { fileTransfers } = window.store
|
|
43
43
|
const [id, updateObj] = args
|
|
44
|
+
let completed = false
|
|
44
45
|
const end = () => {
|
|
46
|
+
if (completed) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
completed = true
|
|
45
50
|
this.currentRun && this.currentRun.stop()
|
|
46
51
|
resolve()
|
|
47
52
|
}
|
|
@@ -99,10 +104,11 @@ export default class Queue extends Component {
|
|
|
99
104
|
})()
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
// Progress and delete updates are synchronous store mutations.
|
|
108
|
+
// Resolve them here so the queue cannot stall waiting for another reaction.
|
|
109
|
+
if (!isTransferInit) {
|
|
110
|
+
checkCompletion()
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function checkCompletion () {
|
|
@@ -77,11 +77,11 @@ export default class TransportAction extends Component {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
localCheckExist = (path) => {
|
|
80
|
-
return getLocalFileInfo(path)
|
|
80
|
+
return getLocalFileInfo(path)
|
|
81
|
+
.catch(() => null)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
remoteCheckExist = (path, tabId) => {
|
|
84
|
-
// return true
|
|
85
85
|
const sftp = refs.get('sftp-' + tabId)?.sftp
|
|
86
86
|
if (!sftp) {
|
|
87
87
|
console.log('remoteCheckExist error', 'sftp not exist')
|
|
@@ -373,10 +373,12 @@ export default class TransportAction extends Component {
|
|
|
373
373
|
toFile
|
|
374
374
|
})
|
|
375
375
|
if (transfer.resolvePolicy) {
|
|
376
|
-
|
|
376
|
+
this.onDecision(transfer.resolvePolicy)
|
|
377
|
+
return true
|
|
377
378
|
}
|
|
378
379
|
if (this.resolvePolicy) {
|
|
379
|
-
|
|
380
|
+
this.onDecision(this.resolvePolicy)
|
|
381
|
+
return true
|
|
380
382
|
}
|
|
381
383
|
const transferWithToFile = {
|
|
382
384
|
...copy(transfer),
|
|
@@ -386,6 +388,7 @@ export default class TransportAction extends Component {
|
|
|
386
388
|
refsStatic.get('transfer-conflict')?.addConflict(transferWithToFile)
|
|
387
389
|
return true
|
|
388
390
|
}
|
|
391
|
+
return false
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
onDecision = (policy) => {
|
|
@@ -12,6 +12,11 @@ import { refsStatic } from '../common/ref'
|
|
|
12
12
|
window.initingFtpTabIds = new Set()
|
|
13
13
|
|
|
14
14
|
export default class TransportsActionStore extends Component {
|
|
15
|
+
constructor (props) {
|
|
16
|
+
super(props)
|
|
17
|
+
this.pendingInitIds = new Set()
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
componentDidMount () {
|
|
16
21
|
this.control()
|
|
17
22
|
}
|
|
@@ -29,6 +34,12 @@ export default class TransportsActionStore extends Component {
|
|
|
29
34
|
const {
|
|
30
35
|
fileTransfers
|
|
31
36
|
} = store
|
|
37
|
+
this.pendingInitIds = new Set(
|
|
38
|
+
Array.from(this.pendingInitIds).filter(id => {
|
|
39
|
+
const transfer = fileTransfers.find(t => t.id === id)
|
|
40
|
+
return transfer && transfer.inited !== true
|
|
41
|
+
})
|
|
42
|
+
)
|
|
32
43
|
|
|
33
44
|
// First loop: Handle same type transfers
|
|
34
45
|
for (const t of fileTransfers) {
|
|
@@ -57,7 +68,7 @@ export default class TransportsActionStore extends Component {
|
|
|
57
68
|
inited,
|
|
58
69
|
pausing
|
|
59
70
|
} = t
|
|
60
|
-
return typeTo !== typeFrom && inited && pausing !== true
|
|
71
|
+
return typeTo !== typeFrom && (inited || this.pendingInitIds.has(t.id)) && pausing !== true
|
|
61
72
|
}).length
|
|
62
73
|
|
|
63
74
|
if (count >= maxTransport) {
|
|
@@ -80,7 +91,7 @@ export default class TransportsActionStore extends Component {
|
|
|
80
91
|
|
|
81
92
|
const isTransfer = typeTo !== typeFrom
|
|
82
93
|
|
|
83
|
-
if (inited || !isTransfer) {
|
|
94
|
+
if (inited || this.pendingInitIds.has(id) || !isTransfer) {
|
|
84
95
|
continue
|
|
85
96
|
}
|
|
86
97
|
|
|
@@ -95,6 +106,7 @@ export default class TransportsActionStore extends Component {
|
|
|
95
106
|
|
|
96
107
|
if (count < maxTransport) {
|
|
97
108
|
count++
|
|
109
|
+
this.pendingInitIds.add(id)
|
|
98
110
|
refsStatic.get('transfer-queue')?.addToQueue(
|
|
99
111
|
'update',
|
|
100
112
|
id,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncTypes } from '../../common/constants'
|
|
1
2
|
import { useState } from 'react'
|
|
2
3
|
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
3
4
|
import dayjs from 'dayjs'
|
|
@@ -10,7 +11,7 @@ export default function ServerDataStatus (props) {
|
|
|
10
11
|
const [loading, setLoading] = useState(false)
|
|
11
12
|
const token = store.getSyncToken(type)
|
|
12
13
|
const gistId = store.getSyncGistId(type)
|
|
13
|
-
const canSync = token && (gistId || type === 'custom' || type === 'cloud')
|
|
14
|
+
const canSync = token && (gistId || type === 'custom' || type === 'cloud' || type === syncTypes.webdav)
|
|
14
15
|
|
|
15
16
|
async function handleReload () {
|
|
16
17
|
setLoading(true)
|
|
@@ -41,6 +41,10 @@ export default function SyncForm (props) {
|
|
|
41
41
|
if (syncType === syncTypes.cloud) {
|
|
42
42
|
return !props.formData.token
|
|
43
43
|
}
|
|
44
|
+
if (syncType === syncTypes.webdav) {
|
|
45
|
+
const { serverUrl, username, password } = props.formData
|
|
46
|
+
return !serverUrl || !username || !password
|
|
47
|
+
}
|
|
44
48
|
const {
|
|
45
49
|
token,
|
|
46
50
|
gistId
|
|
@@ -70,6 +74,14 @@ export default function SyncForm (props) {
|
|
|
70
74
|
} else {
|
|
71
75
|
up[syncType + 'Proxy'] = ''
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
// Handle WebDAV specific fields
|
|
79
|
+
if (syncType === syncTypes.webdav) {
|
|
80
|
+
up[syncType + 'ServerUrl'] = res.serverUrl || ''
|
|
81
|
+
up[syncType + 'Username'] = res.username || ''
|
|
82
|
+
up[syncType + 'Password'] = res.password || ''
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
window.store.updateSyncSetting(up)
|
|
74
86
|
const test = await window.store.testSyncToken(syncType, res.gistId)
|
|
75
87
|
if (isError(test)) {
|
|
@@ -180,6 +192,9 @@ export default function SyncForm (props) {
|
|
|
180
192
|
</p>
|
|
181
193
|
)
|
|
182
194
|
}
|
|
195
|
+
if (syncType === syncTypes.webdav) {
|
|
196
|
+
return createWebdavItems()
|
|
197
|
+
}
|
|
183
198
|
if (syncType !== syncTypes.custom) {
|
|
184
199
|
return null
|
|
185
200
|
}
|
|
@@ -199,18 +214,73 @@ export default function SyncForm (props) {
|
|
|
199
214
|
</FormItem>
|
|
200
215
|
)
|
|
201
216
|
}
|
|
217
|
+
function createWebdavItems () {
|
|
218
|
+
return (
|
|
219
|
+
<div>
|
|
220
|
+
<FormItem
|
|
221
|
+
label={createLabel('URL')}
|
|
222
|
+
name='serverUrl'
|
|
223
|
+
normalize={trim}
|
|
224
|
+
rules={[{
|
|
225
|
+
max: 500, message: '500 chars max'
|
|
226
|
+
}, {
|
|
227
|
+
required: true, message: 'Server URL is required'
|
|
228
|
+
}]}
|
|
229
|
+
>
|
|
230
|
+
<Input
|
|
231
|
+
placeholder='https://your-webdav-server.com/remote.php/dav/files/username'
|
|
232
|
+
id='sync-input-webdav-server-url'
|
|
233
|
+
/>
|
|
234
|
+
</FormItem>
|
|
235
|
+
<FormItem
|
|
236
|
+
label={createLabel(e('username'))}
|
|
237
|
+
name='username'
|
|
238
|
+
normalize={trim}
|
|
239
|
+
rules={[{
|
|
240
|
+
max: 200, message: '200 chars max'
|
|
241
|
+
}, {
|
|
242
|
+
required: true, message: 'Username is required'
|
|
243
|
+
}]}
|
|
244
|
+
>
|
|
245
|
+
<Input
|
|
246
|
+
placeholder='WebDAV username'
|
|
247
|
+
id='sync-input-webdav-username'
|
|
248
|
+
/>
|
|
249
|
+
</FormItem>
|
|
250
|
+
<FormItem
|
|
251
|
+
label={createLabel(e('password'))}
|
|
252
|
+
name='password'
|
|
253
|
+
normalize={trim}
|
|
254
|
+
rules={[{
|
|
255
|
+
max: 200, message: '200 chars max'
|
|
256
|
+
}, {
|
|
257
|
+
required: true, message: 'Password is required'
|
|
258
|
+
}]}
|
|
259
|
+
>
|
|
260
|
+
<Password
|
|
261
|
+
placeholder='WebDAV password'
|
|
262
|
+
id='sync-input-webdav-password'
|
|
263
|
+
/>
|
|
264
|
+
</FormItem>
|
|
265
|
+
</div>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
202
268
|
const desc = syncType === syncTypes.custom
|
|
203
269
|
? 'jwt secret'
|
|
204
|
-
:
|
|
270
|
+
: syncType === syncTypes.webdav
|
|
271
|
+
? 'WebDAV credentials'
|
|
272
|
+
: 'personal access token'
|
|
205
273
|
const idDesc = syncType === syncTypes.custom
|
|
206
274
|
? 'user id'
|
|
207
|
-
:
|
|
275
|
+
: syncType === syncTypes.webdav
|
|
276
|
+
? 'WebDAV server'
|
|
277
|
+
: 'gist ID'
|
|
208
278
|
const tokenLabel = createLabel('token', desc)
|
|
209
279
|
const gistLabel = createLabel('gist', idDesc)
|
|
210
280
|
const syncPasswordName = e('encrypt') + ' ' + e('password')
|
|
211
281
|
const syncPasswordLabel = createLabel(syncPasswordName, '')
|
|
212
282
|
function createIdItem () {
|
|
213
|
-
if (syncType === syncTypes.cloud) {
|
|
283
|
+
if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
|
|
214
284
|
return null
|
|
215
285
|
}
|
|
216
286
|
return (
|
|
@@ -231,7 +301,7 @@ export default function SyncForm (props) {
|
|
|
231
301
|
)
|
|
232
302
|
}
|
|
233
303
|
function createPasswordItem () {
|
|
234
|
-
if (syncType === syncTypes.cloud) {
|
|
304
|
+
if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
|
|
235
305
|
return null
|
|
236
306
|
}
|
|
237
307
|
return (
|
|
@@ -271,17 +341,11 @@ export default function SyncForm (props) {
|
|
|
271
341
|
type: syncType,
|
|
272
342
|
status: props.serverStatus
|
|
273
343
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
name={'setting-sync-form' + syncType}
|
|
280
|
-
layout='vertical'
|
|
281
|
-
initialValues={props.formData}
|
|
282
|
-
>
|
|
283
|
-
{renderWarning()}
|
|
284
|
-
{createUrlItem()}
|
|
344
|
+
function createTokenItem () {
|
|
345
|
+
if (syncType === syncTypes.webdav) {
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
return (
|
|
285
349
|
<FormItem
|
|
286
350
|
label={tokenLabel}
|
|
287
351
|
hasFeedback
|
|
@@ -298,6 +362,20 @@ export default function SyncForm (props) {
|
|
|
298
362
|
id={createId('token')}
|
|
299
363
|
/>
|
|
300
364
|
</FormItem>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
return (
|
|
368
|
+
<Form
|
|
369
|
+
onFinish={save}
|
|
370
|
+
form={form}
|
|
371
|
+
className='form-wrap pd1x'
|
|
372
|
+
name={'setting-sync-form' + syncType}
|
|
373
|
+
layout='vertical'
|
|
374
|
+
initialValues={props.formData}
|
|
375
|
+
>
|
|
376
|
+
{renderWarning()}
|
|
377
|
+
{createUrlItem()}
|
|
378
|
+
{createTokenItem()}
|
|
301
379
|
{
|
|
302
380
|
createIdItem()
|
|
303
381
|
}
|
|
@@ -44,7 +44,11 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
44
44
|
apiUrl: syncSetting[type + 'ApiUrl'],
|
|
45
45
|
lastSyncTime: syncSetting[type + 'LastSyncTime'],
|
|
46
46
|
syncPassword: syncSetting[type + 'SyncPassword'],
|
|
47
|
-
proxy: syncSetting[type + 'Proxy']
|
|
47
|
+
proxy: syncSetting[type + 'Proxy'],
|
|
48
|
+
// WebDAV specific fields
|
|
49
|
+
serverUrl: syncSetting[type + 'ServerUrl'],
|
|
50
|
+
username: syncSetting[type + 'Username'],
|
|
51
|
+
password: syncSetting[type + 'Password']
|
|
48
52
|
}
|
|
49
53
|
return (
|
|
50
54
|
<SyncForm
|
|
@@ -98,8 +98,8 @@ export default memo(function TransferHistoryModal (props) {
|
|
|
98
98
|
pageSize,
|
|
99
99
|
showSizeChanger: true,
|
|
100
100
|
pageSizeOptions: [5, 10, 20, 50, 100],
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
placement: 'topEnd',
|
|
102
|
+
onChange: handlePageSizeChange
|
|
103
103
|
},
|
|
104
104
|
size: 'small',
|
|
105
105
|
rowKey: 'id'
|
|
@@ -23,9 +23,11 @@ export default auto(function WindowControl (props) {
|
|
|
23
23
|
}
|
|
24
24
|
const maximize = () => {
|
|
25
25
|
window.pre.runGlobalAsync('maximize')
|
|
26
|
+
window.store.isMaximized = true
|
|
26
27
|
}
|
|
27
28
|
const unmaximize = () => {
|
|
28
29
|
window.pre.runGlobalAsync('unmaximize')
|
|
30
|
+
window.store.isMaximized = false
|
|
29
31
|
}
|
|
30
32
|
const closeApp = () => {
|
|
31
33
|
window.store.exit()
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// import { transferTypeMap } from '../../common/constants.js'
|
|
7
|
-
import {
|
|
7
|
+
import { getItem, setItem } from '../../common/safe-local-storage.js'
|
|
8
8
|
import { getLocalFileInfo } from '../sftp/file-read.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -176,6 +176,8 @@ export class TransferClientBase {
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* Open file select dialog
|
|
179
|
+
* Supports window._apiControlSelectFile for e2e testing
|
|
180
|
+
* Set window._apiControlSelectFile = ['/path/to/file1', '/path/to/file2'] to bypass native dialog
|
|
179
181
|
* @param {Object} options - Options for file selection
|
|
180
182
|
* @returns {Promise<Array>} - Selected files
|
|
181
183
|
*/
|
|
@@ -186,20 +188,28 @@ export class TransferClientBase {
|
|
|
186
188
|
message = 'Choose some files to send'
|
|
187
189
|
} = options
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
let files
|
|
192
|
+
if (window._apiControlSelectFile) {
|
|
193
|
+
files = Array.isArray(window._apiControlSelectFile)
|
|
194
|
+
? window._apiControlSelectFile
|
|
195
|
+
: [window._apiControlSelectFile]
|
|
196
|
+
delete window._apiControlSelectFile
|
|
197
|
+
} else {
|
|
198
|
+
const properties = [
|
|
199
|
+
directory ? 'openDirectory' : 'openFile',
|
|
200
|
+
'multiSelections',
|
|
201
|
+
'showHiddenFiles',
|
|
202
|
+
'noResolveAliases',
|
|
203
|
+
'treatPackageAsDirectory',
|
|
204
|
+
'dontAddToRecent'
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
files = await window.api.openDialog({
|
|
208
|
+
title,
|
|
209
|
+
message,
|
|
210
|
+
properties
|
|
211
|
+
}).catch(() => false)
|
|
212
|
+
}
|
|
203
213
|
|
|
204
214
|
if (!files || !files.length) {
|
|
205
215
|
return null
|
|
@@ -215,11 +225,22 @@ export class TransferClientBase {
|
|
|
215
225
|
|
|
216
226
|
/**
|
|
217
227
|
* Open save folder select dialog
|
|
228
|
+
* Supports window._apiControlSelectFolder for e2e testing
|
|
229
|
+
* Set window._apiControlSelectFolder = '/path/to/folder' to bypass native dialog
|
|
218
230
|
* @returns {Promise<string>} - Selected folder path
|
|
219
231
|
*/
|
|
220
232
|
openSaveFolderSelect = async () => {
|
|
233
|
+
if (window._apiControlSelectFolder) {
|
|
234
|
+
const folder = window._apiControlSelectFolder
|
|
235
|
+
delete window._apiControlSelectFolder
|
|
236
|
+
if (this.storageKey) {
|
|
237
|
+
setItem(this.storageKey, folder)
|
|
238
|
+
}
|
|
239
|
+
return folder
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
// Try to use last saved path
|
|
222
|
-
const lastPath = this.storageKey ?
|
|
243
|
+
const lastPath = this.storageKey ? getItem(this.storageKey) : null
|
|
223
244
|
|
|
224
245
|
const savePaths = await window.api.openDialog({
|
|
225
246
|
title: 'Choose a folder to save file(s)',
|
|
@@ -240,7 +261,7 @@ export class TransferClientBase {
|
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
if (this.storageKey) {
|
|
243
|
-
|
|
264
|
+
setItem(this.storageKey, savePaths[0])
|
|
244
265
|
}
|
|
245
266
|
return savePaths[0]
|
|
246
267
|
}
|
package/client/store/sync.js
CHANGED
|
@@ -67,6 +67,13 @@ export default (Store) => {
|
|
|
67
67
|
}
|
|
68
68
|
).join('####')
|
|
69
69
|
}
|
|
70
|
+
if (type === syncTypes.webdav) {
|
|
71
|
+
// WebDAV token format: serverUrl####username####password
|
|
72
|
+
const serverUrl = get(window.store.config, 'syncSetting.webdavServerUrl')
|
|
73
|
+
const username = get(window.store.config, 'syncSetting.webdavUsername')
|
|
74
|
+
const password = get(window.store.config, 'syncSetting.webdavPassword')
|
|
75
|
+
return [serverUrl, username, password].join('####')
|
|
76
|
+
}
|
|
70
77
|
return get(window.store.config, 'syncSetting.' + type + 'AccessToken')
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -158,7 +165,13 @@ export default (Store) => {
|
|
|
158
165
|
const types = Object.keys(syncTypes)
|
|
159
166
|
for (const type of types) {
|
|
160
167
|
const gistId = store.getSyncGistId(type)
|
|
161
|
-
if
|
|
168
|
+
// For WebDAV, check if server URL is configured
|
|
169
|
+
if (type === syncTypes.webdav) {
|
|
170
|
+
const serverUrl = get(window.store.config, 'syncSetting.webdavServerUrl')
|
|
171
|
+
if (serverUrl) {
|
|
172
|
+
await store.uploadSetting(type)
|
|
173
|
+
}
|
|
174
|
+
} else if (gistId) {
|
|
162
175
|
await store.uploadSetting(type)
|
|
163
176
|
}
|
|
164
177
|
}
|
|
@@ -183,6 +196,24 @@ export default (Store) => {
|
|
|
183
196
|
const { store } = window
|
|
184
197
|
const token = store.getSyncToken(type)
|
|
185
198
|
const gistId = store.getSyncGistId(type)
|
|
199
|
+
|
|
200
|
+
// Handle WebDAV preview differently
|
|
201
|
+
if (type === syncTypes.webdav) {
|
|
202
|
+
const gist = await fetchData(
|
|
203
|
+
type,
|
|
204
|
+
'download',
|
|
205
|
+
[],
|
|
206
|
+
token,
|
|
207
|
+
store.getSyncProxy(type)
|
|
208
|
+
)
|
|
209
|
+
if (gist && gist.files) {
|
|
210
|
+
const statusContent = get(gist, 'files["electerm-status.json"].content')
|
|
211
|
+
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
212
|
+
store.syncServerStatus[type] = status
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
186
217
|
const gist = await fetchData(
|
|
187
218
|
type,
|
|
188
219
|
'getOne',
|
|
@@ -201,7 +232,7 @@ export default (Store) => {
|
|
|
201
232
|
// await store.createGist(type)
|
|
202
233
|
// gistId = store.getSyncGistId(type)
|
|
203
234
|
// }
|
|
204
|
-
if (!gistId && type !== syncTypes.custom && type !== syncTypes.cloud) {
|
|
235
|
+
if (!gistId && type !== syncTypes.custom && type !== syncTypes.cloud && type !== syncTypes.webdav) {
|
|
205
236
|
return
|
|
206
237
|
}
|
|
207
238
|
const pass = store.getSyncPassword(type)
|
|
@@ -244,6 +275,31 @@ export default (Store) => {
|
|
|
244
275
|
electermVersion: packVer,
|
|
245
276
|
deviceName: window.pre.osInfo().find(r => r.k === 'hostname')?.v || 'unknown'
|
|
246
277
|
}
|
|
278
|
+
|
|
279
|
+
// Handle WebDAV upload differently
|
|
280
|
+
if (type === syncTypes.webdav) {
|
|
281
|
+
const uploadData = {}
|
|
282
|
+
for (const [key, value] of Object.entries(objs)) {
|
|
283
|
+
uploadData[key] = value.content
|
|
284
|
+
}
|
|
285
|
+
uploadData['electerm-status.json'] = JSON.stringify(status)
|
|
286
|
+
|
|
287
|
+
const res = await fetchData(
|
|
288
|
+
type,
|
|
289
|
+
'upload',
|
|
290
|
+
[uploadData],
|
|
291
|
+
token,
|
|
292
|
+
store.getSyncProxy(type)
|
|
293
|
+
)
|
|
294
|
+
if (res && !res.error) {
|
|
295
|
+
store.updateSyncSetting({
|
|
296
|
+
[type + 'LastSyncTime']: now
|
|
297
|
+
})
|
|
298
|
+
store.syncServerStatus[type] = status
|
|
299
|
+
}
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
247
303
|
const gistData = {
|
|
248
304
|
description: 'sync electerm data',
|
|
249
305
|
files: {
|
|
@@ -285,10 +341,80 @@ export default (Store) => {
|
|
|
285
341
|
// await store.createGist(type)
|
|
286
342
|
// gistId = store.getSyncGistId(type)
|
|
287
343
|
// }
|
|
288
|
-
if (!gistId && type !== syncTypes.custom && type !== syncTypes.cloud) {
|
|
344
|
+
if (!gistId && type !== syncTypes.custom && type !== syncTypes.cloud && type !== syncTypes.webdav) {
|
|
289
345
|
return
|
|
290
346
|
}
|
|
291
347
|
const pass = store.getSyncPassword(type)
|
|
348
|
+
|
|
349
|
+
// Handle WebDAV download differently
|
|
350
|
+
if (type === syncTypes.webdav) {
|
|
351
|
+
const gist = await fetchData(
|
|
352
|
+
type,
|
|
353
|
+
'download',
|
|
354
|
+
[],
|
|
355
|
+
token,
|
|
356
|
+
store.getSyncProxy(type)
|
|
357
|
+
)
|
|
358
|
+
if (gist && gist.files) {
|
|
359
|
+
const statusContent = get(gist, 'files["electerm-status.json"].content')
|
|
360
|
+
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
361
|
+
store.syncServerStatus[type] = status
|
|
362
|
+
|
|
363
|
+
const { names, syncConfig } = store.getDataSyncNames()
|
|
364
|
+
for (const n of names) {
|
|
365
|
+
let str = get(gist, `files["${n}.json"].content`)
|
|
366
|
+
if (!str) {
|
|
367
|
+
if (n === settingMap.bookmarks) {
|
|
368
|
+
throw new Error(('Seems you have a empty WebDAV folder, you can try upload first'))
|
|
369
|
+
} else {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!isJSON(str)) {
|
|
374
|
+
str = await window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
375
|
+
}
|
|
376
|
+
let arr = JSON.parse(str)
|
|
377
|
+
if (n === settingMap.terminalThemes) {
|
|
378
|
+
arr = store.fixThemes(arr)
|
|
379
|
+
} else if (n === settingMap.bookmarks) {
|
|
380
|
+
arr = fixBookmarks(arr)
|
|
381
|
+
}
|
|
382
|
+
let strOrder = get(gist, `files["${n}.order.json"].content`)
|
|
383
|
+
if (isJSON(strOrder)) {
|
|
384
|
+
strOrder = JSON.parse(strOrder)
|
|
385
|
+
arr.sort((a, b) => {
|
|
386
|
+
const ai = strOrder.findIndex(r => r === a.id)
|
|
387
|
+
const bi = strOrder.findIndex(r => r === b.id)
|
|
388
|
+
return ai - bi
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
store.setItems(n, arr)
|
|
392
|
+
}
|
|
393
|
+
if (syncConfig) {
|
|
394
|
+
const userConfig = parseJsonSafe(
|
|
395
|
+
get(gist, 'files["userConfig.json"].content')
|
|
396
|
+
)
|
|
397
|
+
if (userConfig) {
|
|
398
|
+
store.setConfig(userConfig)
|
|
399
|
+
}
|
|
400
|
+
if (userConfig && userConfig.theme) {
|
|
401
|
+
store.setTheme(userConfig.theme)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const up = {
|
|
406
|
+
[type + 'LastSyncTime']: Date.now()
|
|
407
|
+
}
|
|
408
|
+
if (pass) {
|
|
409
|
+
up[type + 'SyncPassword'] = pass
|
|
410
|
+
}
|
|
411
|
+
store.updateSyncSetting(up)
|
|
412
|
+
}
|
|
413
|
+
store.isSyncingSetting = false
|
|
414
|
+
store.isSyncDownload = false
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
292
418
|
const gist = await fetchData(
|
|
293
419
|
type,
|
|
294
420
|
'getOne',
|
|
@@ -21,13 +21,13 @@ export default Store => {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
Store.prototype.addTransferList = function (items) {
|
|
24
|
-
// console.log('addTransferList', JSON.stringify(items, null, 2))
|
|
25
24
|
const { fileTransfers } = window.store
|
|
26
25
|
const transferBatch = uid()
|
|
27
|
-
|
|
26
|
+
const nextItems = items.map(t => {
|
|
28
27
|
t.transferBatch = transferBatch
|
|
29
28
|
return t
|
|
30
|
-
})
|
|
29
|
+
})
|
|
30
|
+
fileTransfers.push(...nextItems)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
Store.prototype.pauseAll = function () {
|