-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathConnection.ts
More file actions
241 lines (209 loc) · 8.05 KB
/
Copy pathConnection.ts
File metadata and controls
241 lines (209 loc) · 8.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
67
68
69
70
71
72
73
74
75
76
77
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import { pwd, readInBuffer } from './Local'
import { join } from 'node:path/posix'
import ReferenceCountMap from './utils/ReferenceCountMap'
import { FileSystem, FileAttributes } from './FileSystem'
import { URI, ConnectionID, LocalFileSystemID, Files, Path } from './types'
import { createURI, parseURI, connectionID } from './utils/URI'
import unqid from './utils/uniqid'
import logger, { LogFS } from './log'
import Local from './fs/Local'
import SFtp, { ATTRIBUTES as SFTP_ATTRIBUTES } from './fs/SFtp'
import Ftp, { ATTRIBUTES as FTP_ATTRIBUTES } from './fs/Ftp'
import S3, { ATTRIBUTES as S3_ATTRIBUTES } from './fs/S3'
import options from './options'
export default class {
static initialize() {
this.shared.set(LocalFileSystemID, new Local)
}
static async open(
protocol: string,
user: string,
host: string,
port: number,
password: string,
privatekey: string
): Promise<FileAttributes> {
const id = connectionID(protocol, user, host, port)
const attrs = {
sftp: SFTP_ATTRIBUTES,
ftp: FTP_ATTRIBUTES,
s3: S3_ATTRIBUTES
}[protocol]
if (!attrs) {
throw new Error(`Invalid protocol ${protocol}`)
}
if (this.shared.inc(id)) {
return attrs
}
this.protocols.set(id, protocol)
if (privatekey) {
if (privatekey.startsWith('~')) {
privatekey = join(pwd(), privatekey.substring(1))
}
this.credentials.set(id, ['key', await readInBuffer(privatekey)])
} else {
this.credentials.set(id, ['password', password])
}
const conn = await this.create(id)
await conn.open()
this.shared.set(id, conn)
return attrs
}
static get(id: ConnectionID) {
return this.shared.get(id)
}
static close(id: ConnectionID) {
this.shared.dec(id)?.close()
}
static async list(id: ConnectionID, path: Path): Promise<Files> {
const files = await this.get(id)?.ls(path) || []
return files.map(f => ({...f, URI: createURI(id, f.path)}))
}
static async transmit(id: ConnectionID): Promise<[FileSystem, () => void]> {
if (id == LocalFileSystemID) {
return [new Local, () => {}]
}
const conn = await this.hold(id)
if (conn) {
return [conn[0], () => this.release(id, conn[1])]
}
return new Promise((resolve) => {
const onRelease = (poolId: string) => {
const pool = this.pools.get(id)?.get(poolId)
pool && resolve([pool.fs, () => this.release(id, poolId)])
}
id in this.pending ? this.pending[id].push(onRelease) : (this.pending[id] = [onRelease])
})
}
private static async hold(id: ConnectionID): Promise<[FileSystem, string]|null> {
!this.pools.has(id) && this.pools.set(id, new Map())
const pool = this.pools.get(id)
const conn = this.getIdle(id)
if (conn) {
return conn
}
if (pool && pool.size < this.getLimit(id)) {
return new Promise((resolve) => {
const connect = async () => {
const conn = this.getIdle(id)
if (conn) {
resolve(conn)
return
}
const poolId = unqid()
try {
const onClose = () => this.pools.get(id)?.delete(poolId)
const fs = await this.create(id, onClose)
await fs.open()
this.pools.get(id)!.set(poolId, { fs, idle: false })
resolve([fs, poolId])
} catch (e) {
const conn = this.getIdle(id)
if (conn) {
resolve(conn)
return
}
}
resolve(null)
}
this.queue.push( connect )
if (this.queue.length == 1) {
this.connectNext()
}
})
}
return null
}
private static getIdle(id: ConnectionID): [FileSystem, string]|null {
const conn = Array.from(this.pools.get(id)?.entries() || []).find(([,{ idle }]) => idle !== false)
if (conn) {
conn[1].idle !== false && clearTimeout(conn[1].idle)
conn[1].idle = false
return [conn[1].fs, conn[0]]
}
return null
}
private static async connectNext() {
this.queue
.splice(0, Math.max(this.maxStartups - this.numOfStartups, 0))
.map(connect => async () => { await connect(); this.numOfStartups--; this.connectNext() })
.forEach(f => { this.numOfStartups++; f() })
}
private static numOfStartups = 0
private static maxStartups = 7 // for SFTP see MaxStartups in /etc/ssh/sshd_config
private static release(id: ConnectionID, poolId: string) {
const conn = this.pools.get(id)?.get(poolId)
if (conn) {
if (this.pending[id]?.length) {
this.pending[id].shift()!(poolId)
} else {
conn.idle = setTimeout(() => {
conn.fs.close()
this.pools.get(id)?.delete(poolId)
}, 2000)
}
}
}
private static getLimit(id: ConnectionID): number {
return this.limits.has(id) ? (this.limits.get(id) || 1024) : 1024 // for vsftpd max_per_ip in /etc/vsftpd.conf
}
private static async create(id: ConnectionID, onClose = () => {}): Promise<FileSystem> {
const fs = await this.createFS(id, onClose)
return options.log ? new LogFS(id, fs) : fs
}
private static async createFS(id: ConnectionID, onClose: () => void): Promise<FileSystem> {
const { scheme, user, host, port } = parseURI(id as URI)
if (!this.credentials.has(id)) {
onClose?.()
throw new Error(`No credentials for ${id}`)
}
const [authType, credential] = this.credentials.get(id)!
const protocol = this.protocols.get(id)
if (!protocol) {
throw new Error(`Protocol ${protocol} not found`)
}
switch (protocol) {
case 'sftp': {
return new SFtp(
host,
user,
authType == 'password' ? credential : '',
authType == 'key' ? credential : null,
port || 22,
error => logger.error('SFTP error:', error),
onClose
)
}
case 'ftp': {
return new Ftp(
host,
user,
credential as string,
port || 21,
error => logger.error('FTP error:', error),
onClose
)
}
case 's3': {
return new S3(
`${scheme}:\\${host}`,
user,
credential as string,
'us-east-1', // TODO
port || 443,
error => logger.error('S3 error:', error),
onClose
)
}
default:
throw new Error(`Unsupported protocol ${protocol}`)
}
}
private static shared = new ReferenceCountMap<ConnectionID, FileSystem>
private static pools: Map<string, Map<string, {fs: FileSystem, idle: false|ReturnType<typeof setTimeout>}>> = new Map()
private static queue: Function[] = []
private static pending: Record<string, ((id: string) => void)[]> = {}
private static limits = new Map<ConnectionID, number>()
private static protocols = new Map<ConnectionID, string>()
private static credentials = new Map<ConnectionID, ['password', string]|['key', Buffer]>()
}