@pyrokine/mcp-ssh 1.0.0 → 1.1.2
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/README.md +162 -83
- package/README_zh.md +161 -83
- package/dist/file-ops.js +36 -20
- package/dist/index.js +197 -23
- package/dist/session-manager.d.ts +62 -51
- package/dist/session-manager.js +201 -168
- package/dist/ssh-config.d.ts +39 -0
- package/dist/ssh-config.js +216 -0
- package/package.json +2 -2
- package/src/file-ops.ts +602 -577
- package/src/index.ts +991 -800
- package/src/session-manager.ts +986 -945
- package/src/ssh-config.ts +264 -0
- package/src/types.ts +89 -89
- package/tsconfig.json +7 -2
package/src/file-ops.ts
CHANGED
|
@@ -2,374 +2,394 @@
|
|
|
2
2
|
* SSH File Operations - 文件操作
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import {execSync} from 'child_process'
|
|
6
|
+
import * as fs from 'fs'
|
|
7
|
+
import * as path from 'path'
|
|
8
|
+
import {Stats} from 'ssh2'
|
|
9
|
+
import {sessionManager} from './session-manager.js'
|
|
10
|
+
import {FileInfo, TransferProgress} from './types.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 上传文件
|
|
14
14
|
*/
|
|
15
15
|
export async function uploadFile(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
alias: string,
|
|
17
|
+
localPath: string,
|
|
18
|
+
remotePath: string,
|
|
19
|
+
onProgress?: (progress: TransferProgress) => void,
|
|
20
20
|
): Promise<{ success: boolean; size: number }> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
21
|
+
if (!fs.existsSync(localPath)) {
|
|
22
|
+
throw new Error(`Local file not found: ${localPath}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
26
|
+
const stats = fs.statSync(localPath)
|
|
27
|
+
const totalSize = stats.size
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const readStream = fs.createReadStream(localPath)
|
|
31
|
+
const writeStream = sftp.createWriteStream(remotePath)
|
|
32
|
+
let settled = false
|
|
33
|
+
|
|
34
|
+
const cleanup = (err?: Error) => {
|
|
35
|
+
if (settled) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
settled = true
|
|
39
|
+
sftp.end()
|
|
40
|
+
if (err) {
|
|
41
|
+
reject(err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let transferred = 0
|
|
46
|
+
|
|
47
|
+
readStream.on('data', (chunk: Buffer) => {
|
|
48
|
+
transferred += chunk.length
|
|
49
|
+
if (onProgress) {
|
|
50
|
+
onProgress({
|
|
51
|
+
transferred,
|
|
52
|
+
total: totalSize,
|
|
53
|
+
percent: totalSize > 0 ? Math.round((transferred / totalSize) * 100) : 100,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
readStream.on('error', (err: Error) => cleanup(err))
|
|
59
|
+
writeStream.on('error', (err: Error) => cleanup(err))
|
|
60
|
+
|
|
61
|
+
writeStream.on('close', () => {
|
|
62
|
+
if (!settled) {
|
|
63
|
+
settled = true
|
|
64
|
+
sftp.end()
|
|
65
|
+
resolve({success: true, size: totalSize})
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
readStream.pipe(writeStream)
|
|
70
|
+
})
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* 下载文件
|
|
71
75
|
*/
|
|
72
76
|
export async function downloadFile(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
alias: string,
|
|
78
|
+
remotePath: string,
|
|
79
|
+
localPath: string,
|
|
80
|
+
onProgress?: (progress: TransferProgress) => void,
|
|
77
81
|
): Promise<{ success: boolean; size: number }> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
82
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
83
|
+
|
|
84
|
+
// 获取远程文件大小
|
|
85
|
+
const stats = await new Promise<Stats>((resolve, reject) => {
|
|
86
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
87
|
+
if (err) {
|
|
88
|
+
reject(err)
|
|
89
|
+
} else {
|
|
90
|
+
resolve(stats)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
const totalSize = stats.size
|
|
95
|
+
|
|
96
|
+
// 确保本地目录存在
|
|
97
|
+
const localDir = path.dirname(localPath)
|
|
98
|
+
if (!fs.existsSync(localDir)) {
|
|
99
|
+
fs.mkdirSync(localDir, {recursive: true})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const readStream = sftp.createReadStream(remotePath)
|
|
104
|
+
const writeStream = fs.createWriteStream(localPath)
|
|
105
|
+
let settled = false
|
|
106
|
+
|
|
107
|
+
const cleanup = (err?: Error) => {
|
|
108
|
+
if (settled) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
settled = true
|
|
112
|
+
sftp.end()
|
|
113
|
+
if (err) {
|
|
114
|
+
reject(err)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let transferred = 0
|
|
119
|
+
|
|
120
|
+
readStream.on('data', (chunk: Buffer) => {
|
|
121
|
+
transferred += chunk.length
|
|
122
|
+
if (onProgress) {
|
|
123
|
+
onProgress({
|
|
124
|
+
transferred,
|
|
125
|
+
total: totalSize,
|
|
126
|
+
percent: totalSize > 0 ? Math.round((transferred / totalSize) * 100) : 100,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
readStream.on('error', (err: Error) => cleanup(err))
|
|
132
|
+
writeStream.on('error', (err: Error) => cleanup(err))
|
|
133
|
+
|
|
134
|
+
writeStream.on('close', () => {
|
|
135
|
+
if (!settled) {
|
|
136
|
+
settled = true
|
|
137
|
+
sftp.end()
|
|
138
|
+
resolve({success: true, size: totalSize})
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
readStream.pipe(writeStream)
|
|
143
|
+
})
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
/**
|
|
136
147
|
* 读取远程文件内容
|
|
137
148
|
*/
|
|
138
149
|
export async function readFile(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
alias: string,
|
|
151
|
+
remotePath: string,
|
|
152
|
+
maxBytes: number = 1024 * 1024, // 默认最大 1MB
|
|
142
153
|
): Promise<{ content: string; size: number; truncated: boolean }> {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
154
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
155
|
+
|
|
156
|
+
// 获取文件大小
|
|
157
|
+
const stats = await new Promise<Stats>((resolve, reject) => {
|
|
158
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
159
|
+
if (err) {
|
|
160
|
+
reject(err)
|
|
161
|
+
} else {
|
|
162
|
+
resolve(stats)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const actualSize = stats.size
|
|
168
|
+
const truncated = actualSize > maxBytes
|
|
169
|
+
|
|
170
|
+
// 处理空文件
|
|
171
|
+
if (actualSize === 0) {
|
|
172
|
+
sftp.end()
|
|
173
|
+
return {content: '', size: 0, truncated: false}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const readSize = Math.min(actualSize, maxBytes)
|
|
177
|
+
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const chunks: Buffer[] = []
|
|
180
|
+
|
|
181
|
+
const readStream = sftp.createReadStream(remotePath, {
|
|
182
|
+
start: 0,
|
|
183
|
+
end: readSize - 1,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
readStream.on('data', (chunk: Buffer) => {
|
|
187
|
+
chunks.push(chunk)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
readStream.on('end', () => {
|
|
191
|
+
sftp.end()
|
|
192
|
+
const content = Buffer.concat(chunks).toString('utf-8')
|
|
193
|
+
resolve({
|
|
194
|
+
content,
|
|
195
|
+
size: actualSize,
|
|
196
|
+
truncated,
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
readStream.on('error', (err: Error) => {
|
|
201
|
+
sftp.end()
|
|
202
|
+
reject(err)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
191
205
|
}
|
|
192
206
|
|
|
193
207
|
/**
|
|
194
208
|
* 写入远程文件
|
|
195
209
|
*/
|
|
196
210
|
export async function writeFile(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
alias: string,
|
|
212
|
+
remotePath: string,
|
|
213
|
+
content: string,
|
|
214
|
+
append: boolean = false,
|
|
201
215
|
): Promise<{ success: boolean; size: number }> {
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
217
|
+
const flags = append ? 'a' : 'w'
|
|
204
218
|
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const writeStream = sftp.createWriteStream(remotePath, {flags})
|
|
207
221
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
writeStream.on('close', () => {
|
|
223
|
+
sftp.end()
|
|
224
|
+
resolve({success: true, size: content.length})
|
|
225
|
+
})
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
writeStream.on('error', (err: Error) => {
|
|
228
|
+
sftp.end()
|
|
229
|
+
reject(err)
|
|
230
|
+
})
|
|
217
231
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
232
|
+
writeStream.write(content)
|
|
233
|
+
writeStream.end()
|
|
234
|
+
})
|
|
221
235
|
}
|
|
222
236
|
|
|
223
237
|
/**
|
|
224
238
|
* 列出目录内容
|
|
225
239
|
*/
|
|
226
240
|
export async function listDir(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
241
|
+
alias: string,
|
|
242
|
+
remotePath: string,
|
|
243
|
+
showHidden: boolean = false,
|
|
230
244
|
): Promise<FileInfo[]> {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
245
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
246
|
+
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
sftp.readdir(remotePath, (err, list) => {
|
|
249
|
+
if (err) {
|
|
250
|
+
sftp.end()
|
|
251
|
+
reject(err)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const files: FileInfo[] = list
|
|
256
|
+
.filter((item) => showHidden || !item.filename.startsWith('.'))
|
|
257
|
+
.map((item) => ({
|
|
258
|
+
name: item.filename,
|
|
259
|
+
path: path.posix.join(remotePath, item.filename),
|
|
260
|
+
size: item.attrs.size,
|
|
261
|
+
isDirectory: (item.attrs.mode & 0o40000) !== 0,
|
|
262
|
+
isFile: (item.attrs.mode & 0o100000) !== 0,
|
|
263
|
+
isSymlink: (item.attrs.mode & 0o120000) !== 0,
|
|
264
|
+
permissions: formatPermissions(item.attrs.mode),
|
|
265
|
+
owner: item.attrs.uid,
|
|
266
|
+
group: item.attrs.gid,
|
|
267
|
+
mtime: new Date(item.attrs.mtime * 1000),
|
|
268
|
+
atime: new Date(item.attrs.atime * 1000),
|
|
269
|
+
}))
|
|
270
|
+
.sort((a, b) => {
|
|
271
|
+
// 目录在前
|
|
272
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
273
|
+
return a.isDirectory ? -1 : 1
|
|
274
|
+
}
|
|
275
|
+
return a.name.localeCompare(b.name)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
sftp.end()
|
|
279
|
+
resolve(files)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
/**
|
|
271
285
|
* 获取文件信息
|
|
272
286
|
*/
|
|
273
287
|
export async function getFileInfo(
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
alias: string,
|
|
289
|
+
remotePath: string,
|
|
276
290
|
): Promise<FileInfo> {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
291
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
292
|
+
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
295
|
+
sftp.end()
|
|
296
|
+
|
|
297
|
+
if (err) {
|
|
298
|
+
reject(err)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
resolve({
|
|
303
|
+
name: path.posix.basename(remotePath),
|
|
304
|
+
path: remotePath,
|
|
305
|
+
size: stats.size,
|
|
306
|
+
isDirectory: (stats.mode & 0o40000) !== 0,
|
|
307
|
+
isFile: (stats.mode & 0o100000) !== 0,
|
|
308
|
+
isSymlink: (stats.mode & 0o120000) !== 0,
|
|
309
|
+
permissions: formatPermissions(stats.mode),
|
|
310
|
+
owner: stats.uid,
|
|
311
|
+
group: stats.gid,
|
|
312
|
+
mtime: new Date(stats.mtime * 1000),
|
|
313
|
+
atime: new Date(stats.atime * 1000),
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
})
|
|
303
317
|
}
|
|
304
318
|
|
|
305
319
|
/**
|
|
306
320
|
* 检查文件是否存在
|
|
307
321
|
*/
|
|
308
322
|
export async function fileExists(
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
alias: string,
|
|
324
|
+
remotePath: string,
|
|
311
325
|
): Promise<boolean> {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
326
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
327
|
+
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
sftp.stat(remotePath, (err) => {
|
|
330
|
+
sftp.end()
|
|
331
|
+
resolve(!err)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
320
334
|
}
|
|
321
335
|
|
|
322
336
|
/**
|
|
323
337
|
* 创建目录
|
|
324
338
|
*/
|
|
325
339
|
export async function mkdir(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
340
|
+
alias: string,
|
|
341
|
+
remotePath: string,
|
|
342
|
+
recursive: boolean = false,
|
|
329
343
|
): Promise<boolean> {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
if (recursive) {
|
|
345
|
+
// 通过 exec 实现递归创建
|
|
346
|
+
const result = await sessionManager.exec(alias, `mkdir -p "${remotePath}"`)
|
|
347
|
+
return result.exitCode === 0
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
sftp.mkdir(remotePath, (err) => {
|
|
353
|
+
sftp.end()
|
|
354
|
+
if (err) {
|
|
355
|
+
reject(err)
|
|
356
|
+
} else {
|
|
357
|
+
resolve(true)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
})
|
|
344
361
|
}
|
|
345
362
|
|
|
346
363
|
/**
|
|
347
364
|
* 删除文件
|
|
348
365
|
*/
|
|
349
366
|
export async function removeFile(
|
|
350
|
-
|
|
351
|
-
|
|
367
|
+
alias: string,
|
|
368
|
+
remotePath: string,
|
|
352
369
|
): Promise<boolean> {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
370
|
+
const sftp = await sessionManager.getSftp(alias)
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
sftp.unlink(remotePath, (err) => {
|
|
373
|
+
sftp.end()
|
|
374
|
+
if (err) {
|
|
375
|
+
reject(err)
|
|
376
|
+
} else {
|
|
377
|
+
resolve(true)
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
})
|
|
361
381
|
}
|
|
362
382
|
|
|
363
383
|
/**
|
|
364
384
|
* 检查远程是否安装 rsync
|
|
365
385
|
*/
|
|
366
386
|
export async function checkRsync(alias: string): Promise<boolean> {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
387
|
+
try {
|
|
388
|
+
const result = await sessionManager.exec(alias, 'which rsync')
|
|
389
|
+
return result.exitCode === 0 && result.stdout.trim().length > 0
|
|
390
|
+
} catch {
|
|
391
|
+
return false
|
|
392
|
+
}
|
|
373
393
|
}
|
|
374
394
|
|
|
375
395
|
/**
|
|
@@ -382,40 +402,40 @@ export async function checkRsync(alias: string): Promise<boolean> {
|
|
|
382
402
|
* @param options 同步选项
|
|
383
403
|
*/
|
|
384
404
|
export async function syncFiles(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
405
|
+
alias: string,
|
|
406
|
+
localPath: string,
|
|
407
|
+
remotePath: string,
|
|
408
|
+
direction: 'upload' | 'download',
|
|
409
|
+
options: {
|
|
410
|
+
delete?: boolean; // 删除目标端多余文件
|
|
411
|
+
dryRun?: boolean; // 仅显示将执行的操作
|
|
412
|
+
exclude?: string[]; // 排除模式
|
|
413
|
+
recursive?: boolean; // 递归同步目录
|
|
414
|
+
} = {},
|
|
395
415
|
): Promise<{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
416
|
+
success: boolean;
|
|
417
|
+
method: 'rsync' | 'sftp';
|
|
418
|
+
filesTransferred?: number;
|
|
419
|
+
bytesTransferred?: number;
|
|
420
|
+
output?: string;
|
|
401
421
|
}> {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
422
|
+
// 检查远程 rsync
|
|
423
|
+
const hasRsync = await checkRsync(alias)
|
|
424
|
+
|
|
425
|
+
if (hasRsync) {
|
|
426
|
+
// 使用 rsync(通过远程端执行)
|
|
427
|
+
return syncWithRsync(alias, localPath, remotePath, direction, options)
|
|
428
|
+
} else {
|
|
429
|
+
// 回退到 SFTP
|
|
430
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options)
|
|
431
|
+
}
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
/**
|
|
415
435
|
* 转义 shell 路径参数
|
|
416
436
|
*/
|
|
417
437
|
function escapeShellPath(p: string): string {
|
|
418
|
-
|
|
438
|
+
return `'${p.replace(/'/g, '\'\\\'\'')}'`
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
/**
|
|
@@ -423,308 +443,313 @@ function escapeShellPath(p: string): string {
|
|
|
423
443
|
* 通过本地执行 rsync 连接到远程(需要密钥认证或 ssh-agent)
|
|
424
444
|
*/
|
|
425
445
|
async function syncWithRsync(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
446
|
+
alias: string,
|
|
447
|
+
localPath: string,
|
|
448
|
+
remotePath: string,
|
|
449
|
+
direction: 'upload' | 'download',
|
|
450
|
+
options: {
|
|
451
|
+
delete?: boolean;
|
|
452
|
+
dryRun?: boolean;
|
|
453
|
+
exclude?: string[];
|
|
454
|
+
recursive?: boolean;
|
|
455
|
+
},
|
|
436
456
|
): Promise<{
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
457
|
+
success: boolean;
|
|
458
|
+
method: 'rsync' | 'sftp';
|
|
459
|
+
filesTransferred?: number;
|
|
460
|
+
bytesTransferred?: number;
|
|
461
|
+
output?: string;
|
|
442
462
|
}> {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (options.recursive === false) {
|
|
472
|
-
args.push('--dirs'); // 不递归,只传输目录本身
|
|
473
|
-
}
|
|
474
|
-
if (options.exclude) {
|
|
475
|
-
for (const pattern of options.exclude) {
|
|
476
|
-
args.push(`--exclude=${escapeShellPath(pattern)}`);
|
|
463
|
+
// 检查本地是否有 rsync
|
|
464
|
+
let hasLocalRsync = false
|
|
465
|
+
try {
|
|
466
|
+
execSync('which rsync', {stdio: 'pipe'})
|
|
467
|
+
hasLocalRsync = true
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!hasLocalRsync) {
|
|
472
|
+
// 本地没有 rsync,回退到 SFTP
|
|
473
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 获取会话信息以构建 rsync 命令
|
|
477
|
+
const sessions = sessionManager.listSessions()
|
|
478
|
+
const sessionInfo = sessions.find(s => s.alias === alias)
|
|
479
|
+
if (!sessionInfo) {
|
|
480
|
+
throw new Error(`Session '${alias}' not found`)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 构建 rsync 参数
|
|
484
|
+
const args: string[] = ['-avz', '--progress']
|
|
485
|
+
|
|
486
|
+
if (options.delete) {
|
|
487
|
+
args.push('--delete')
|
|
488
|
+
}
|
|
489
|
+
if (options.dryRun) {
|
|
490
|
+
args.push('--dry-run')
|
|
477
491
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// 构建 rsync 命令(本地执行)
|
|
481
|
-
// 注意:这需要密钥认证或 ssh-agent,密码认证不支持
|
|
482
|
-
const sshCmd = `ssh -p ${sessionInfo.port} -o StrictHostKeyChecking=no -o BatchMode=yes`;
|
|
483
|
-
const remoteSpec = `${sessionInfo.username}@${sessionInfo.host}:${escapeShellPath(remotePath)}`;
|
|
484
|
-
const rsyncCmd = direction === 'upload'
|
|
485
|
-
? `rsync ${args.join(' ')} -e "${sshCmd}" ${escapeShellPath(localPath)} ${remoteSpec}`
|
|
486
|
-
: `rsync ${args.join(' ')} -e "${sshCmd}" ${remoteSpec} ${escapeShellPath(localPath)}`;
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
const result = execSync(rsyncCmd, {
|
|
490
|
-
encoding: 'utf-8',
|
|
491
|
-
timeout: 600000, // 10 分钟超时
|
|
492
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// 解析 rsync 输出统计文件数
|
|
496
|
-
const lines = result.split('\n');
|
|
497
|
-
let filesTransferred = 0;
|
|
498
|
-
for (const line of lines) {
|
|
499
|
-
if (line.trim() && !line.startsWith('sending') && !line.startsWith('receiving') && !line.startsWith('total')) {
|
|
500
|
-
filesTransferred++;
|
|
501
|
-
}
|
|
492
|
+
if (options.recursive === false) {
|
|
493
|
+
args.push('--dirs') // 不递归,只传输目录本身
|
|
502
494
|
}
|
|
495
|
+
if (options.exclude) {
|
|
496
|
+
for (const pattern of options.exclude) {
|
|
497
|
+
args.push(`--exclude=${escapeShellPath(pattern)}`)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 构建 rsync 命令(本地执行)
|
|
502
|
+
// 注意:这需要密钥认证或 ssh-agent,密码认证不支持
|
|
503
|
+
const sshCmd = `ssh -p ${sessionInfo.port} -o StrictHostKeyChecking=no -o BatchMode=yes`
|
|
504
|
+
const remoteSpec = `${sessionInfo.username}@${sessionInfo.host}:${escapeShellPath(remotePath)}`
|
|
505
|
+
const rsyncCmd = direction === 'upload'
|
|
506
|
+
? `rsync ${args.join(' ')} -e "${sshCmd}" ${escapeShellPath(localPath)} ${remoteSpec}`
|
|
507
|
+
: `rsync ${args.join(' ')} -e "${sshCmd}" ${remoteSpec} ${escapeShellPath(localPath)}`
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const result = execSync(rsyncCmd, {
|
|
511
|
+
encoding: 'utf-8',
|
|
512
|
+
timeout: 600000, // 10 分钟超时
|
|
513
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// 解析 rsync 输出统计文件数
|
|
517
|
+
const lines = result.split('\n')
|
|
518
|
+
let filesTransferred = 0
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
if (line.trim() &&
|
|
521
|
+
!line.startsWith('sending') &&
|
|
522
|
+
!line.startsWith('receiving') &&
|
|
523
|
+
!line.startsWith('total')) {
|
|
524
|
+
filesTransferred++
|
|
525
|
+
}
|
|
526
|
+
}
|
|
503
527
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
528
|
+
return {
|
|
529
|
+
success: true,
|
|
530
|
+
method: 'rsync',
|
|
531
|
+
filesTransferred,
|
|
532
|
+
output: result,
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// rsync 失败(可能是密码认证),回退到 SFTP
|
|
536
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options)
|
|
537
|
+
}
|
|
514
538
|
}
|
|
515
539
|
|
|
516
540
|
/**
|
|
517
541
|
* 使用 SFTP 同步文件
|
|
518
542
|
*/
|
|
519
543
|
async function syncWithSftp(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
544
|
+
alias: string,
|
|
545
|
+
localPath: string,
|
|
546
|
+
remotePath: string,
|
|
547
|
+
direction: 'upload' | 'download',
|
|
548
|
+
options: {
|
|
549
|
+
delete?: boolean;
|
|
550
|
+
dryRun?: boolean;
|
|
551
|
+
exclude?: string[];
|
|
552
|
+
recursive?: boolean;
|
|
553
|
+
},
|
|
530
554
|
): Promise<{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
success: boolean;
|
|
556
|
+
method: 'rsync' | 'sftp';
|
|
557
|
+
filesTransferred?: number;
|
|
558
|
+
bytesTransferred?: number;
|
|
559
|
+
output?: string;
|
|
536
560
|
}> {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
return {
|
|
545
|
-
success: true,
|
|
546
|
-
method: 'sftp',
|
|
547
|
-
output: 'Dry run mode: would transfer files via SFTP' + (warnings.length ? `. Warning: ${warnings.join('; ')}` : ''),
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
let result: { fileCount: number; totalSize: number } | { success: boolean; size: number };
|
|
553
|
-
|
|
554
|
-
if (direction === 'upload') {
|
|
555
|
-
// 检查是否是目录
|
|
556
|
-
const stats = fs.statSync(localPath);
|
|
557
|
-
if (stats.isDirectory() && options.recursive !== false) {
|
|
558
|
-
result = await uploadDirectory(alias, localPath, remotePath, options.exclude);
|
|
559
|
-
return {
|
|
560
|
-
success: true,
|
|
561
|
-
method: 'sftp',
|
|
562
|
-
filesTransferred: result.fileCount,
|
|
563
|
-
bytesTransferred: result.totalSize,
|
|
564
|
-
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
565
|
-
};
|
|
566
|
-
} else {
|
|
567
|
-
result = await uploadFile(alias, localPath, remotePath);
|
|
568
|
-
return {
|
|
569
|
-
success: result.success,
|
|
570
|
-
method: 'sftp',
|
|
571
|
-
filesTransferred: 1,
|
|
572
|
-
bytesTransferred: result.size,
|
|
573
|
-
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
// 下载
|
|
578
|
-
const info = await getFileInfo(alias, remotePath);
|
|
579
|
-
if (info.isDirectory && options.recursive !== false) {
|
|
580
|
-
result = await downloadDirectory(alias, remotePath, localPath, options.exclude);
|
|
561
|
+
// SFTP 模式不支持 delete 选项
|
|
562
|
+
const warnings: string[] = []
|
|
563
|
+
if (options.delete) {
|
|
564
|
+
warnings.push('delete option is not supported in SFTP mode (requires rsync)')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (options.dryRun) {
|
|
581
568
|
return {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
569
|
+
success: true,
|
|
570
|
+
method: 'sftp',
|
|
571
|
+
output: 'Dry run mode: would transfer files via SFTP' +
|
|
572
|
+
(warnings.length ? `. Warning: ${warnings.join('; ')}` : ''),
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
let result: { fileCount: number; totalSize: number } | { success: boolean; size: number }
|
|
578
|
+
|
|
579
|
+
if (direction === 'upload') {
|
|
580
|
+
// 检查是否是目录
|
|
581
|
+
const stats = fs.statSync(localPath)
|
|
582
|
+
if (stats.isDirectory() && options.recursive !== false) {
|
|
583
|
+
result = await uploadDirectory(alias, localPath, remotePath, options.exclude)
|
|
584
|
+
return {
|
|
585
|
+
success: true,
|
|
586
|
+
method: 'sftp',
|
|
587
|
+
filesTransferred: result.fileCount,
|
|
588
|
+
bytesTransferred: result.totalSize,
|
|
589
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
result = await uploadFile(alias, localPath, remotePath)
|
|
593
|
+
return {
|
|
594
|
+
success: result.success,
|
|
595
|
+
method: 'sftp',
|
|
596
|
+
filesTransferred: 1,
|
|
597
|
+
bytesTransferred: result.size,
|
|
598
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
// 下载
|
|
603
|
+
const info = await getFileInfo(alias, remotePath)
|
|
604
|
+
if (info.isDirectory && options.recursive !== false) {
|
|
605
|
+
result = await downloadDirectory(alias, remotePath, localPath, options.exclude)
|
|
606
|
+
return {
|
|
607
|
+
success: true,
|
|
608
|
+
method: 'sftp',
|
|
609
|
+
filesTransferred: result.fileCount,
|
|
610
|
+
bytesTransferred: result.totalSize,
|
|
611
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
result = await downloadFile(alias, remotePath, localPath)
|
|
615
|
+
return {
|
|
616
|
+
success: result.success,
|
|
617
|
+
method: 'sftp',
|
|
618
|
+
filesTransferred: 1,
|
|
619
|
+
bytesTransferred: result.size,
|
|
620
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} catch (err: any) {
|
|
590
625
|
return {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
626
|
+
success: false,
|
|
627
|
+
method: 'sftp',
|
|
628
|
+
output: err.message,
|
|
629
|
+
}
|
|
598
630
|
}
|
|
599
|
-
} catch (err: any) {
|
|
600
|
-
return {
|
|
601
|
-
success: false,
|
|
602
|
-
method: 'sftp',
|
|
603
|
-
output: err.message,
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
631
|
}
|
|
607
632
|
|
|
608
633
|
/**
|
|
609
634
|
* 递归上传目录
|
|
610
635
|
*/
|
|
611
636
|
async function uploadDirectory(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
637
|
+
alias: string,
|
|
638
|
+
localPath: string,
|
|
639
|
+
remotePath: string,
|
|
640
|
+
exclude?: string[],
|
|
616
641
|
): Promise<{ fileCount: number; totalSize: number }> {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
+
let fileCount = 0
|
|
643
|
+
let totalSize = 0
|
|
644
|
+
|
|
645
|
+
// 确保远程目录存在
|
|
646
|
+
await mkdir(alias, remotePath, true)
|
|
647
|
+
|
|
648
|
+
const items = fs.readdirSync(localPath)
|
|
649
|
+
for (const item of items) {
|
|
650
|
+
// 检查排除模式
|
|
651
|
+
if (exclude && exclude.some(pattern => matchPattern(item, pattern))) {
|
|
652
|
+
continue
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const itemLocalPath = path.join(localPath, item)
|
|
656
|
+
const itemRemotePath = path.posix.join(remotePath, item)
|
|
657
|
+
const stats = fs.statSync(itemLocalPath)
|
|
658
|
+
|
|
659
|
+
if (stats.isDirectory()) {
|
|
660
|
+
const result = await uploadDirectory(alias, itemLocalPath, itemRemotePath, exclude)
|
|
661
|
+
fileCount += result.fileCount
|
|
662
|
+
totalSize += result.totalSize
|
|
663
|
+
} else if (stats.isFile()) {
|
|
664
|
+
await uploadFile(alias, itemLocalPath, itemRemotePath)
|
|
665
|
+
fileCount++
|
|
666
|
+
totalSize += stats.size
|
|
667
|
+
}
|
|
642
668
|
}
|
|
643
|
-
}
|
|
644
669
|
|
|
645
|
-
|
|
670
|
+
return {fileCount, totalSize}
|
|
646
671
|
}
|
|
647
672
|
|
|
648
673
|
/**
|
|
649
674
|
* 递归下载目录
|
|
650
675
|
*/
|
|
651
676
|
async function downloadDirectory(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
677
|
+
alias: string,
|
|
678
|
+
remotePath: string,
|
|
679
|
+
localPath: string,
|
|
680
|
+
exclude?: string[],
|
|
656
681
|
): Promise<{ fileCount: number; totalSize: number }> {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// 确保本地目录存在
|
|
661
|
-
if (!fs.existsSync(localPath)) {
|
|
662
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const items = await listDir(alias, remotePath, true);
|
|
666
|
-
for (const item of items) {
|
|
667
|
-
// 检查排除模式
|
|
668
|
-
if (exclude && exclude.some(pattern => matchPattern(item.name, pattern))) {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
682
|
+
let fileCount = 0
|
|
683
|
+
let totalSize = 0
|
|
671
684
|
|
|
672
|
-
|
|
685
|
+
// 确保本地目录存在
|
|
686
|
+
if (!fs.existsSync(localPath)) {
|
|
687
|
+
fs.mkdirSync(localPath, {recursive: true})
|
|
688
|
+
}
|
|
673
689
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
690
|
+
const items = await listDir(alias, remotePath, true)
|
|
691
|
+
for (const item of items) {
|
|
692
|
+
// 检查排除模式
|
|
693
|
+
if (exclude && exclude.some(pattern => matchPattern(item.name, pattern))) {
|
|
694
|
+
continue
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const itemLocalPath = path.join(localPath, item.name)
|
|
698
|
+
|
|
699
|
+
if (item.isDirectory) {
|
|
700
|
+
const result = await downloadDirectory(alias, item.path, itemLocalPath, exclude)
|
|
701
|
+
fileCount += result.fileCount
|
|
702
|
+
totalSize += result.totalSize
|
|
703
|
+
} else if (item.isFile) {
|
|
704
|
+
await downloadFile(alias, item.path, itemLocalPath)
|
|
705
|
+
fileCount++
|
|
706
|
+
totalSize += item.size
|
|
707
|
+
}
|
|
682
708
|
}
|
|
683
|
-
}
|
|
684
709
|
|
|
685
|
-
|
|
710
|
+
return {fileCount, totalSize}
|
|
686
711
|
}
|
|
687
712
|
|
|
688
713
|
/**
|
|
689
714
|
* 简单的模式匹配(支持 * 和 ?)
|
|
690
715
|
*/
|
|
691
716
|
function matchPattern(name: string, pattern: string): boolean {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
717
|
+
const regexPattern = pattern
|
|
718
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
|
|
719
|
+
.replace(/\*/g, '.*')
|
|
720
|
+
.replace(/\?/g, '.')
|
|
721
|
+
return new RegExp(`^${regexPattern}$`).test(name)
|
|
697
722
|
}
|
|
698
723
|
|
|
699
724
|
/**
|
|
700
725
|
* 格式化权限字符串
|
|
701
726
|
*/
|
|
702
727
|
function formatPermissions(mode: number): string {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
let type = '-';
|
|
710
|
-
for (const [mask, char] of Object.entries(types)) {
|
|
711
|
-
if ((mode & parseInt(mask)) !== 0) {
|
|
712
|
-
type = char;
|
|
713
|
-
break;
|
|
728
|
+
const types: Record<number, string> = {
|
|
729
|
+
0o40000: 'd',
|
|
730
|
+
0o120000: 'l',
|
|
731
|
+
0o100000: '-',
|
|
714
732
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
733
|
+
|
|
734
|
+
let type = '-'
|
|
735
|
+
for (const [mask, char] of Object.entries(types)) {
|
|
736
|
+
if ((mode & parseInt(mask)) !== 0) {
|
|
737
|
+
type = char
|
|
738
|
+
break
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const perms = [
|
|
743
|
+
(mode & 0o400) ? 'r' : '-',
|
|
744
|
+
(mode & 0o200) ? 'w' : '-',
|
|
745
|
+
(mode & 0o100) ? 'x' : '-',
|
|
746
|
+
(mode & 0o040) ? 'r' : '-',
|
|
747
|
+
(mode & 0o020) ? 'w' : '-',
|
|
748
|
+
(mode & 0o010) ? 'x' : '-',
|
|
749
|
+
(mode & 0o004) ? 'r' : '-',
|
|
750
|
+
(mode & 0o002) ? 'w' : '-',
|
|
751
|
+
(mode & 0o001) ? 'x' : '-',
|
|
752
|
+
]
|
|
753
|
+
|
|
754
|
+
return type + perms.join('')
|
|
730
755
|
}
|