@defra-fish/fulfilment-job 1.55.0-rc.5 → 1.55.0
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/package.json +6 -5
- package/src/__mocks__/ssh2-sftp-client.js +9 -0
- package/src/__tests__/config.spec.js +32 -1
- package/src/config.js +67 -1
- package/src/staging/__tests__/deliver-fulfilment-files.spec.js +36 -4
- package/src/staging/deliver-fulfilment-files.js +7 -1
- package/src/transport/__tests__/ftp.spec.js +63 -0
- package/src/transport/ftp.js +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/fulfilment-job",
|
|
3
|
-
"version": "1.55.0
|
|
3
|
+
"version": "1.55.0",
|
|
4
4
|
"description": "Rod Licensing Sales Fulfilment Job",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,14 +35,15 @@
|
|
|
35
35
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defra-fish/connectors-lib": "1.55.0
|
|
39
|
-
"@defra-fish/dynamics-lib": "1.55.0
|
|
38
|
+
"@defra-fish/connectors-lib": "1.55.0",
|
|
39
|
+
"@defra-fish/dynamics-lib": "1.55.0",
|
|
40
40
|
"commander": "^7.2.0",
|
|
41
41
|
"debug": "^4.3.3",
|
|
42
42
|
"merge2": "^1.4.1",
|
|
43
43
|
"moment": "^2.29.1",
|
|
44
44
|
"openpgp": "^5.0.0-1",
|
|
45
|
-
"pluralize": "^8.0.0"
|
|
45
|
+
"pluralize": "^8.0.0",
|
|
46
|
+
"ssh2-sftp-client": "^6.0.1"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "3405322b81a2016ba22e530de8d096a49be0bea4"
|
|
48
49
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const ssh2sftpClient = jest.genMockFromModule('ssh2-sftp-client')
|
|
2
|
+
|
|
3
|
+
export const mockedFtpMethods = {
|
|
4
|
+
connect: jest.fn(async () => {}),
|
|
5
|
+
put: jest.fn(async () => {}),
|
|
6
|
+
end: jest.fn()
|
|
7
|
+
}
|
|
8
|
+
ssh2sftpClient.mockImplementation(() => mockedFtpMethods)
|
|
9
|
+
export default ssh2sftpClient
|
|
@@ -17,6 +17,11 @@ const clearEnvVars = () => {
|
|
|
17
17
|
|
|
18
18
|
const envVars = Object.freeze({
|
|
19
19
|
FULFILMENT_FILE_SIZE: 1234,
|
|
20
|
+
FULFILMENT_FTP_HOST: 'test-host',
|
|
21
|
+
FULFILMENT_FTP_PORT: 2222,
|
|
22
|
+
FULFILMENT_FTP_PATH: '/remote/share',
|
|
23
|
+
FULFILMENT_FTP_USERNAME: 'test-user',
|
|
24
|
+
FULFILMENT_FTP_KEY_SECRET_ID: 'test-secret-id',
|
|
20
25
|
FULFILMENT_S3_BUCKET: 'test-bucket',
|
|
21
26
|
FULFILMENT_PGP_PUBLIC_KEY_SECRET_ID: 'pgp-key-secret-id',
|
|
22
27
|
FULFILMENT_SEND_UNENCRYPTED_FILE: 'false'
|
|
@@ -39,6 +44,32 @@ describe('config', () => {
|
|
|
39
44
|
})
|
|
40
45
|
})
|
|
41
46
|
|
|
47
|
+
describe('ftp', () => {
|
|
48
|
+
it('provides properties relating the use of SFTP', async () => {
|
|
49
|
+
expect(config.ftp).toEqual(
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
host: 'test-host',
|
|
52
|
+
port: '2222',
|
|
53
|
+
path: '/remote/share',
|
|
54
|
+
username: 'test-user',
|
|
55
|
+
privateKey: 'test-ssh-key',
|
|
56
|
+
algorithms: { cipher: expect.any(Array), kex: expect.any(Array) },
|
|
57
|
+
// Wait up to 60 seconds for the SSH handshake
|
|
58
|
+
readyTimeout: expect.any(Number),
|
|
59
|
+
// Retry 5 times over a minute
|
|
60
|
+
retries: expect.any(Number),
|
|
61
|
+
retry_minTimeout: expect.any(Number),
|
|
62
|
+
debug: expect.any(Function)
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
it('defaults the sftp port to 22 if the environment variable is not configured', async () => {
|
|
67
|
+
delete process.env.FULFILMENT_FTP_PORT
|
|
68
|
+
await config.initialise()
|
|
69
|
+
expect(config.ftp.port).toEqual('22')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
42
73
|
describe('s3', () => {
|
|
43
74
|
it('provides properties relating the use of Amazon S3', async () => {
|
|
44
75
|
expect(config.s3.bucket).toEqual('test-bucket')
|
|
@@ -48,7 +79,7 @@ describe('config', () => {
|
|
|
48
79
|
|
|
49
80
|
describe('pgp config', () => {
|
|
50
81
|
const init = async (samplePublicKey = 'sample-pgp-key') => {
|
|
51
|
-
AwsMock.SecretsManager.__setNextResponses('getSecretValue', { SecretString: samplePublicKey })
|
|
82
|
+
AwsMock.SecretsManager.__setNextResponses('getSecretValue', { SecretString: 'test-ssh-key' }, { SecretString: samplePublicKey })
|
|
52
83
|
await config.initialise()
|
|
53
84
|
}
|
|
54
85
|
beforeAll(setEnvVars)
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { AWS } from '@defra-fish/connectors-lib'
|
|
2
|
-
|
|
2
|
+
import db from 'debug'
|
|
3
3
|
const { secretsManager } = AWS()
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Key exchange algorithms for public key authentication - in descending order of priority
|
|
7
|
+
* @type {string[]}
|
|
8
|
+
*/
|
|
9
|
+
export const SFTP_KEY_EXCHANGE_ALGORITHMS = [
|
|
10
|
+
'curve25519-sha256@libssh.org',
|
|
11
|
+
'curve25519-sha256',
|
|
12
|
+
'ecdh-sha2-nistp521',
|
|
13
|
+
'ecdh-sha2-nistp384',
|
|
14
|
+
'ecdh-sha2-nistp256',
|
|
15
|
+
'diffie-hellman-group-exchange-sha256',
|
|
16
|
+
'diffie-hellman-group14-sha256',
|
|
17
|
+
'diffie-hellman-group16-sha512',
|
|
18
|
+
'diffie-hellman-group18-sha512',
|
|
19
|
+
'diffie-hellman-group14-sha1',
|
|
20
|
+
'diffie-hellman-group-exchange-sha1',
|
|
21
|
+
'diffie-hellman-group1-sha1'
|
|
22
|
+
]
|
|
23
|
+
/**
|
|
24
|
+
* Ciphers for SFTP support - in descending order of priority
|
|
25
|
+
* @type {string[]}
|
|
26
|
+
*/
|
|
27
|
+
export const SFTP_CIPHERS = [
|
|
28
|
+
// http://tools.ietf.org/html/rfc4344#section-4
|
|
29
|
+
'aes256-ctr',
|
|
30
|
+
'aes192-ctr',
|
|
31
|
+
'aes128-ctr',
|
|
32
|
+
'aes256-gcm',
|
|
33
|
+
'aes256-gcm@openssh.com',
|
|
34
|
+
'aes128-gcm',
|
|
35
|
+
'aes128-gcm@openssh.com',
|
|
36
|
+
'aes256-cbc',
|
|
37
|
+
'aes192-cbc',
|
|
38
|
+
'aes128-cbc',
|
|
39
|
+
'blowfish-cbc',
|
|
40
|
+
'3des-cbc',
|
|
41
|
+
'cast128-cbc'
|
|
42
|
+
]
|
|
4
43
|
const falseRegEx = /(false|0)/i
|
|
5
44
|
const trueRegEx = /(true|1)/i
|
|
6
45
|
const toBoolean = val => {
|
|
@@ -15,6 +54,7 @@ const toBoolean = val => {
|
|
|
15
54
|
|
|
16
55
|
class Config {
|
|
17
56
|
_file
|
|
57
|
+
_ftp
|
|
18
58
|
_s3
|
|
19
59
|
_pgp
|
|
20
60
|
|
|
@@ -28,6 +68,20 @@ class Config {
|
|
|
28
68
|
*/
|
|
29
69
|
partFileSize: Math.min(Number.parseInt(process.env.FULFILMENT_FILE_SIZE), 999)
|
|
30
70
|
}
|
|
71
|
+
this.ftp = {
|
|
72
|
+
host: process.env.FULFILMENT_FTP_HOST,
|
|
73
|
+
port: process.env.FULFILMENT_FTP_PORT || '22',
|
|
74
|
+
path: process.env.FULFILMENT_FTP_PATH,
|
|
75
|
+
username: process.env.FULFILMENT_FTP_USERNAME,
|
|
76
|
+
privateKey: (await secretsManager.getSecretValue({ SecretId: process.env.FULFILMENT_FTP_KEY_SECRET_ID }).promise()).SecretString,
|
|
77
|
+
algorithms: { cipher: SFTP_CIPHERS, kex: SFTP_KEY_EXCHANGE_ALGORITHMS },
|
|
78
|
+
// Wait up to 60 seconds for the SSH handshake
|
|
79
|
+
readyTimeout: 60000,
|
|
80
|
+
// Retry 5 times over a minute
|
|
81
|
+
retries: 5,
|
|
82
|
+
retry_minTimeout: 12000,
|
|
83
|
+
debug: db('fulfilment:ftp')
|
|
84
|
+
}
|
|
31
85
|
this.s3 = {
|
|
32
86
|
bucket: process.env.FULFILMENT_S3_BUCKET
|
|
33
87
|
}
|
|
@@ -50,6 +104,18 @@ class Config {
|
|
|
50
104
|
this._file = cfg
|
|
51
105
|
}
|
|
52
106
|
|
|
107
|
+
/**
|
|
108
|
+
* FTP configuration settings
|
|
109
|
+
* @type {object}
|
|
110
|
+
*/
|
|
111
|
+
get ftp () {
|
|
112
|
+
return this._ftp
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set ftp (cfg) {
|
|
116
|
+
this._ftp = cfg
|
|
117
|
+
}
|
|
118
|
+
|
|
53
119
|
/**
|
|
54
120
|
* S3 configuration settings
|
|
55
121
|
* @type {object}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable, PassThrough, Writable } from 'stream'
|
|
2
2
|
import { deliverFulfilmentFiles } from '../deliver-fulfilment-files.js'
|
|
3
3
|
import { createS3WriteStream, readS3PartFiles } from '../../transport/s3.js'
|
|
4
|
+
import { createFtpWriteStream } from '../../transport/ftp.js'
|
|
4
5
|
import { FULFILMENT_FILE_STATUS_OPTIONSET, getOptionSetEntry } from '../staging-common.js'
|
|
5
6
|
import { FulfilmentRequestFile, executeQuery, persist } from '@defra-fish/dynamics-lib'
|
|
6
7
|
import openpgp from 'openpgp'
|
|
@@ -9,6 +10,7 @@ import streamHelper from '../streamHelper.js'
|
|
|
9
10
|
import merge2 from 'merge2'
|
|
10
11
|
|
|
11
12
|
jest.mock('../../transport/s3.js')
|
|
13
|
+
jest.mock('../../transport/ftp.js')
|
|
12
14
|
jest.mock('openpgp', () => ({
|
|
13
15
|
readKey: jest.fn(() => ({})),
|
|
14
16
|
encrypt: jest.fn(({ message: readableStream }) => readableStream),
|
|
@@ -44,10 +46,20 @@ describe('deliverFulfilmentFiles', () => {
|
|
|
44
46
|
executeQuery.mockResolvedValue([{ entity: mockFulfilmentRequestFile2 }, { entity: mockFulfilmentRequestFile1 }])
|
|
45
47
|
|
|
46
48
|
// Streams for file1
|
|
47
|
-
const {
|
|
49
|
+
const {
|
|
50
|
+
s3DataStreamFile: s3DataStreamFile1,
|
|
51
|
+
ftpDataStreamFile: ftpDataStreamFile1,
|
|
52
|
+
s3HashStreamFile: s3HashStreamFile1,
|
|
53
|
+
ftpHashStreamFile: ftpHashStreamFile1
|
|
54
|
+
} = createMockFileStreams()
|
|
48
55
|
|
|
49
56
|
// Streams for file2
|
|
50
|
-
const {
|
|
57
|
+
const {
|
|
58
|
+
s3DataStreamFile: s3DataStreamFile2,
|
|
59
|
+
ftpDataStreamFile: ftpDataStreamFile2,
|
|
60
|
+
s3HashStreamFile: s3HashStreamFile2,
|
|
61
|
+
ftpHashStreamFile: ftpHashStreamFile2
|
|
62
|
+
} = createMockFileStreams()
|
|
51
63
|
|
|
52
64
|
// Run the delivery
|
|
53
65
|
await expect(deliverFulfilmentFiles()).resolves.toBeUndefined()
|
|
@@ -57,14 +69,22 @@ describe('deliverFulfilmentFiles', () => {
|
|
|
57
69
|
// File 1 expectations
|
|
58
70
|
expect(createS3WriteStream).toHaveBeenNthCalledWith(1, 'EAFF202006180001.json')
|
|
59
71
|
expect(createS3WriteStream).toHaveBeenNthCalledWith(3, 'EAFF202006180001.json.sha256')
|
|
72
|
+
expect(createFtpWriteStream).toHaveBeenNthCalledWith(1, 'EAFF202006180001.json')
|
|
73
|
+
expect(createFtpWriteStream).toHaveBeenNthCalledWith(3, 'EAFF202006180001.json.sha256')
|
|
60
74
|
expect(JSON.parse(s3DataStreamFile1.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] })
|
|
75
|
+
expect(JSON.parse(ftpDataStreamFile1.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] })
|
|
61
76
|
expect(s3HashStreamFile1.dataProcessed).toEqual(fileShaHash) // validated
|
|
77
|
+
expect(ftpHashStreamFile1.dataProcessed).toEqual(fileShaHash) // validated
|
|
62
78
|
|
|
63
79
|
// File 2 expectations
|
|
64
80
|
expect(createS3WriteStream).toHaveBeenNthCalledWith(4, 'EAFF202006180002.json')
|
|
65
81
|
expect(createS3WriteStream).toHaveBeenNthCalledWith(6, 'EAFF202006180002.json.sha256')
|
|
82
|
+
expect(createFtpWriteStream).toHaveBeenNthCalledWith(4, 'EAFF202006180002.json')
|
|
83
|
+
expect(createFtpWriteStream).toHaveBeenNthCalledWith(6, 'EAFF202006180002.json.sha256')
|
|
66
84
|
expect(JSON.parse(s3DataStreamFile2.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] })
|
|
85
|
+
expect(JSON.parse(ftpDataStreamFile2.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] })
|
|
67
86
|
expect(s3HashStreamFile2.dataProcessed).toEqual(fileShaHash) // validated
|
|
87
|
+
expect(ftpHashStreamFile2.dataProcessed).toEqual(fileShaHash) // validated
|
|
68
88
|
|
|
69
89
|
// Persist to dynamics for file 1
|
|
70
90
|
expect(persist).toHaveBeenNthCalledWith(1, [
|
|
@@ -177,7 +197,10 @@ describe('deliverFulfilmentFiles', () => {
|
|
|
177
197
|
const s3 = createTestableStream()
|
|
178
198
|
streamHelper.pipelinePromise.mockResolvedValue()
|
|
179
199
|
openpgp.encrypt.mockResolvedValue(s2)
|
|
180
|
-
merge2
|
|
200
|
+
merge2
|
|
201
|
+
.mockReturnValueOnce(s1)
|
|
202
|
+
.mockReturnValueOnce(s2)
|
|
203
|
+
.mockReturnValueOnce(s3)
|
|
181
204
|
await mockExecuteQuery()
|
|
182
205
|
createMockFileStreams()
|
|
183
206
|
|
|
@@ -204,18 +227,27 @@ const createMockFulfilmentRequestFile = async (fileName, date) =>
|
|
|
204
227
|
|
|
205
228
|
const createMockFileStreams = () => {
|
|
206
229
|
const s3DataStreamFile = createTestableStream()
|
|
230
|
+
const ftpDataStreamFile = createTestableStream()
|
|
207
231
|
createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3DataStreamFile, managedUpload: Promise.resolve() })
|
|
232
|
+
createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpDataStreamFile, managedUpload: Promise.resolve() })
|
|
208
233
|
|
|
209
234
|
const s3EncryptedDataStreamFile = createTestableStream()
|
|
235
|
+
const ftpEncryptedDataStreamFile = createTestableStream()
|
|
210
236
|
createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3EncryptedDataStreamFile, managedUpload: Promise.resolve() })
|
|
237
|
+
createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpEncryptedDataStreamFile, managedUpload: Promise.resolve() })
|
|
211
238
|
|
|
212
239
|
const s3HashStreamFile = createTestableStream()
|
|
240
|
+
const ftpHashStreamFile = createTestableStream()
|
|
213
241
|
createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3HashStreamFile, managedUpload: Promise.resolve() })
|
|
242
|
+
createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpHashStreamFile, managedUpload: Promise.resolve() })
|
|
214
243
|
|
|
215
244
|
return {
|
|
216
245
|
s3DataStreamFile,
|
|
246
|
+
ftpDataStreamFile,
|
|
217
247
|
s3EncryptedDataStreamFile,
|
|
218
|
-
|
|
248
|
+
ftpEncryptedDataStreamFile,
|
|
249
|
+
s3HashStreamFile,
|
|
250
|
+
ftpHashStreamFile
|
|
219
251
|
}
|
|
220
252
|
}
|
|
221
253
|
|
|
@@ -4,6 +4,7 @@ import merge2 from 'merge2'
|
|
|
4
4
|
import moment from 'moment'
|
|
5
5
|
import { executeQuery, persist, findFulfilmentFiles } from '@defra-fish/dynamics-lib'
|
|
6
6
|
import { createS3WriteStream, readS3PartFiles } from '../transport/s3.js'
|
|
7
|
+
import { createFtpWriteStream } from '../transport/ftp.js'
|
|
7
8
|
import { FULFILMENT_FILE_STATUS_OPTIONSET, getOptionSetEntry } from './staging-common.js'
|
|
8
9
|
import db from 'debug'
|
|
9
10
|
import openpgp from 'openpgp'
|
|
@@ -68,6 +69,11 @@ const createEncryptedDataReadStream = async file => {
|
|
|
68
69
|
*/
|
|
69
70
|
const deliver = async (targetFileName, readableStream, ...transforms) => {
|
|
70
71
|
const { s3WriteStream: s3DataStream, managedUpload: s3DataManagedUpload } = createS3WriteStream(targetFileName)
|
|
72
|
+
const { ftpWriteStream: ftpDataStream, managedUpload: ftpDataManagedUpload } = createFtpWriteStream(targetFileName)
|
|
71
73
|
|
|
72
|
-
await Promise.all([
|
|
74
|
+
await Promise.all([
|
|
75
|
+
streamHelper.pipelinePromise([readableStream, ...transforms, s3DataStream, ftpDataStream]),
|
|
76
|
+
s3DataManagedUpload,
|
|
77
|
+
ftpDataManagedUpload
|
|
78
|
+
])
|
|
73
79
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createFtpWriteStream } from '../ftp.js'
|
|
2
|
+
import { mockedFtpMethods } from 'ssh2-sftp-client'
|
|
3
|
+
|
|
4
|
+
jest.mock('stream')
|
|
5
|
+
jest.mock('../../config.js', () => ({
|
|
6
|
+
ftp: {
|
|
7
|
+
host: 'testhost',
|
|
8
|
+
port: 2222,
|
|
9
|
+
path: 'testpath/',
|
|
10
|
+
username: 'testusername',
|
|
11
|
+
privateKey: 'testprivatekey'
|
|
12
|
+
}
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('ftp', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('createFtpWriteStream', () => {
|
|
21
|
+
it('creates a stream to write to the configured FTP server', async () => {
|
|
22
|
+
const { ftpWriteStream, managedUpload } = createFtpWriteStream('testfile.json')
|
|
23
|
+
ftpWriteStream.write('Some data')
|
|
24
|
+
ftpWriteStream.end()
|
|
25
|
+
await managedUpload
|
|
26
|
+
expect(mockedFtpMethods.connect).toHaveBeenCalledWith(
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
host: 'testhost',
|
|
29
|
+
port: 2222,
|
|
30
|
+
username: 'testusername',
|
|
31
|
+
privateKey: 'testprivatekey'
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
expect(mockedFtpMethods.put).toHaveBeenCalledWith(ftpWriteStream, 'testpath/testfile.json', {
|
|
35
|
+
flags: 'w',
|
|
36
|
+
encoding: 'UTF-8',
|
|
37
|
+
autoClose: false
|
|
38
|
+
})
|
|
39
|
+
expect(mockedFtpMethods.end).toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('rejects the managed upload promise if an FTP upload error occurs', async () => {
|
|
43
|
+
const testError = new Error('Test error')
|
|
44
|
+
mockedFtpMethods.put.mockImplementationOnce(() => Promise.reject(testError))
|
|
45
|
+
const { ftpWriteStream, managedUpload } = createFtpWriteStream('testfile.json')
|
|
46
|
+
await expect(managedUpload).rejects.toThrow('Test error')
|
|
47
|
+
expect(mockedFtpMethods.connect).toHaveBeenCalledWith(
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
host: 'testhost',
|
|
50
|
+
port: 2222,
|
|
51
|
+
username: 'testusername',
|
|
52
|
+
privateKey: 'testprivatekey'
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
expect(mockedFtpMethods.put).toHaveBeenCalledWith(ftpWriteStream, 'testpath/testfile.json', {
|
|
56
|
+
flags: 'w',
|
|
57
|
+
encoding: 'UTF-8',
|
|
58
|
+
autoClose: false
|
|
59
|
+
})
|
|
60
|
+
expect(mockedFtpMethods.end).toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import FtpClient from 'ssh2-sftp-client'
|
|
2
|
+
import Path from 'path'
|
|
3
|
+
import { PassThrough } from 'stream'
|
|
4
|
+
import config from '../config.js'
|
|
5
|
+
import db from 'debug'
|
|
6
|
+
const debug = db('fulfilment:transport')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a stream to write to the configured FTP server
|
|
10
|
+
*
|
|
11
|
+
* @param {string} filename The name of the file to be written to the remote server
|
|
12
|
+
* @returns {{ftpWriteStream: module:stream.internal.PassThrough, managedUpload: Promise<*>}}
|
|
13
|
+
*/
|
|
14
|
+
export const createFtpWriteStream = filename => {
|
|
15
|
+
const sftp = new FtpClient()
|
|
16
|
+
const passThrough = new PassThrough()
|
|
17
|
+
const remoteFilePath = Path.join(config.ftp.path, filename)
|
|
18
|
+
return {
|
|
19
|
+
ftpWriteStream: passThrough,
|
|
20
|
+
managedUpload: sftp
|
|
21
|
+
.connect(config.ftp)
|
|
22
|
+
.then(() => sftp.put(passThrough, remoteFilePath, { flags: 'w', encoding: 'UTF-8', autoClose: false }))
|
|
23
|
+
.then(() =>
|
|
24
|
+
debug('File successfully uploaded to fulfilment provider at sftp://%s:%s%s', config.ftp.host, config.ftp.port, remoteFilePath)
|
|
25
|
+
)
|
|
26
|
+
.finally(() => sftp.end())
|
|
27
|
+
}
|
|
28
|
+
}
|