-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy paths3-folder-sync.mjs
344 lines (314 loc) · 13.5 KB
/
s3-folder-sync.mjs
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
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
268
269
270
271
272
273
274
275
276
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import _ from 'lodash'
import fs from 'fs-extra'
import fg from 'fast-glob';
import __ from './attempt.mjs'
import S3Bucket from './s3bucket.mjs'
import PgpEncryption from './pgp-encryption.mjs'
export default class S3FolderSync {
constructor (config, logger) {
this.config = config
this.logger = logger
this.s3Bucket = null
this.pgp = null
}
/**
* Starts the S3 Folder Sync App
* @returns {Promise<unknown>}
*/
start() {
return new Promise(async (resolve, reject) => {
this.logger.info('Info: Configuration loaded.')
this.checkConfigAndArguments()
const folder = this.cleanFolderPath(this.config.args.folder)
const bucketName = this.config.args.bucket
this.pgp = new PgpEncryption(this.config, this.logger)
if(this.config.args.encrypt === 'yes') {
const [initPgpError, initPgpResult] = await __(this.pgp.init())
if (initPgpError) {
return reject(initPgpError)
}
}
this.s3Bucket = new S3Bucket(this.config, this.logger)
this.s3Bucket.init()
if(this.config.args.mode === 'upload') {
const [testError, testResult] = await __(this.s3Bucket.testBucketAccess(bucketName))
if (testError) {
return reject(testError)
}
if (!testResult){
return reject('Error: Object Storage Bucket Access Failed.')
}
this.logger.info('Connection to S3 Bucket initialised.')
const [getDirListError, getDirListResult] = await __(this.getDirectoryList(folder, this.config.args.filter, this.config.args.exclude, this.config.args.dotFiles, this.config.args.followSymbolicLinks))
if (getDirListError) {
return reject(getDirListError)
}
this.logger.info(`Found ${_.size(getDirListResult)} files to upload in the folder: ${folder}`)
const [error, result] = await __(this.uploadFileListToS3(getDirListResult, bucketName))
if (error) {
return reject(getDirListError)
}
} else if(this.config.args.mode === 'download') {
const [testError, testResult] = await __(this.s3Bucket.testBucketAccess(bucketName))
if (testError) {
return reject(testError)
}
if (!testResult){
return reject('Error: S3 Object Storage Bucket Access Failed.')
}
this.logger.info('Connection to S3 Bucket initialised.')
const [listError, listResult] = await __(this.s3Bucket.listObjects(bucketName))
if (listError) {
return reject(listError)
}
if (_.isEmpty(listResult.Contents)){
this.logger.warn('No files available to download from S3 Bucket.')
return resolve('No files available to download from S3 Bucket.')
}
const [downloadError, downloadResult] = await __(this.downloadListOfObjects(bucketName, listResult.Contents, folder))
if (downloadError) {
this.logger.error(listError)
return reject(listError)
}
} else {
this.logger.error('Error: Invalid --mode argument. Must be either "upload" or "download".')
return reject('Error: Invalid --mode argument. Must be either "upload" or "download".')
}
this.logger.info('S3 Folder Sync completed.')
return resolve(true)
})
}
/**
* Checks provided Config options and Arguments and returns any errors
* guiding the user on how to configure the app
*/
checkConfigAndArguments () {
let configErrorAndExit = false
let argsErrorAndExit = false
if (_.isEmpty(this.config.bucketSecretKey)) {
this.logger.error(`Error: Config file is missing the bucketSecretKey option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.bucketAccessKey)) {
this.logger.error(`Error: Config file is missing the bucketAccessKey option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.bucketEndpoint)) {
this.logger.error(`Error: Config file is missing the bucketEndpoint option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.bucketRegion)) {
this.logger.error(`Error: Config file is missing the bucketRegion option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.pgpPassphrase)) {
this.logger.error(`Error: Config file is missing the pgpPassphrase option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.pgpPrivateKeyArmored)) {
this.logger.error(`Error: Config file is missing the pgpPrivateKeyArmored option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.pgpPublicKeyArmored)) {
this.logger.error(`Error: Config file is missing the pgpPublicKeyArmored option.`)
configErrorAndExit = true
}
if (_.isEmpty(this.config.args.mode)) {
this.logger.error(`Missing the --mode argument.`)
argsErrorAndExit = true
}
if (_.isEmpty(this.config.args.encrypt)) {
this.logger.error(`Missing the --encrypt argument.`)
argsErrorAndExit = true
}
if (_.isEmpty(this.config.args.bucket)) {
this.logger.error(`Missing the --bucket argument.`)
argsErrorAndExit = true
}
if (_.isEmpty(this.config.args.folder)) {
this.logger.error(`Missing the --folder argument.`)
argsErrorAndExit = true
}
if (_.isEmpty(this.config.args.dotFiles)) {
this.config.args.dotFiles = true
} else {
this.config.args.dotFiles = this.config.args.dotFiles === 'yes'
}
if (_.isEmpty(this.config.args.followSymbolicLinks)) {
this.config.args.followSymbolicLinks = true
} else {
this.config.args.followSymbolicLinks = this.config.args.followSymbolicLinks === 'yes'
}
if (_.isEmpty(this.config.args.filter)) {
this.config.args.filter = ['*']
}
if (_.isEmpty(this.config.args.exclude)) {
this.config.args.exclude = []
}
if(configErrorAndExit) {
this.logger.info('The config file should have the following options: \n' +
'bucketSecretKey=XXXXXX : (Required) This is the Secret Key or Password for your Object Storage (S3) account. \n' +
'bucketAccessKey=XXXXXX : (Required) This is the Access Key for your Object Storage (S3) account. \n' +
'bucketEndpoint=XXXXXX : (Required) This is URL that points to your Object Storage account. Do not add the https:// prefix. \n' +
'bucketRegion=XX : (Required) This is Region code for your Object Storage account. Typically "US".')
process.exit(1)
}
if(argsErrorAndExit) {
this.logger.info('You can provide the following arguments: \n' +
'--mode=XXXXXX : (Required) This selects the sync direction. --mode=upload would upload the local directory to the bucket. --mode=download would do the opposite. \n' +
'--encrypt=XXXXXX : (Required) This enables or disables PGP file encryption. --encrypt=yes would enable --encrypt=no would disable. \n' +
'--bucket=XXXXXX : (Required) This is the name of the bucket you want to upload to or download from in your Object Storage (S3). \n' +
'--folder=XXXXXX : (Required) This is local folder you want to upload from or download to. \n' +
'--import-key=XXXXXX : (Optional) Use this option on its own to import a PGP key from a PEM file. Specify the file with full path to import. \n' +
'Example: # ./s3-folder-sync --mode=upload --encrypt=yes --bucket=nginx-configs --folder=/etc/nginx \n')
process.exit(1)
}
}
/**
* Downloads a list of fileObjects from a given bucket to a destination folder.
* If the file is encrypted then it will attempt to decrypt if the option is enabled.
* @param {string} bucket - Name of S3 / Object Storage bucket
* @param {array} objectsList - An array of objects in S3 lib format
* @param {string} destinationFolder - Full path of destination folder
* @returns {Promise<unknown>}
*/
downloadListOfObjects(bucket, objectsList, destinationFolder) {
return new Promise(async (resolve, reject) => {
for (const object of objectsList) {
const [downloadError, downloadResult] = await __(this.s3Bucket.downloadObject(object.Key, bucket))
if (downloadError) {
this.logger.error('Error:', downloadError)
return reject(downloadError)
}
// Check if object is encrypted with pgp
if (object.Key.endsWith('.pgp')) {
if(this.config.args.encrypt !== 'yes') {
this.logger.warn(`File ${object.Key} is encrypted with pgp but --encrypt=yes argument was not specified. Not downloading file.`)
continue
}
const fileName = object.Key.replace('.pgp', '')
const destinationFilePath = destinationFolder + '/' + fileName
this.logger.info('Destination File Path:', destinationFilePath)
const [decryptAndSaveError, decryptAndSaveResult] = await __(this.pgp.decryptAndSaveFile(downloadResult, destinationFilePath))
if (decryptAndSaveError) {
this.logger.error('Decrypting and saving file failed.', decryptAndSaveError)
return reject(decryptAndSaveError)
}
} else {
const destinationFilePath = destinationFolder + '/' + object.Key
this.logger.info(`Downloading unencrypted file: ${object.Key} to ${destinationFilePath}.`)
const [writeFileError, writeFileResult] = await __(fs.writeFile(destinationFilePath, downloadResult, {encoding: 'utf8'}))
if (writeFileError) {
this.logger.error('Error saving Object to file: ', writeFileError)
return reject(writeFileError)
}
}
this.logger.info(`Downloaded file: ${object.Key} successfully.`)
}
resolve (true)
})
}
/**
* Returns the folder listing for a given path and excludes sub-folders and ingore File Patterns
* This function also allows for dot files and following symbolic links to be enabled or disabled.
* @param {string} path - Full path of folder
* @param {string} filterFilePatterns - An array of glob file patterns to filter by
* @param {string} ignoreFilePatterns - An array of glob file patterns to specifically ignore
* @param {boolean} showHiddenFiles - Show dot files
* @param {boolean} followSymbolicLinks - Follow symbolic links
* @returns {Promise<[]>}
*/
async getDirectoryList (path, filterFilePatterns, ignoreFilePatterns, showHiddenFiles, followSymbolicLinks ) {
const { glob } = fg
const onlyFiles = true
const objectMode = true
const cwd = path
const dot = showHiddenFiles
const ignore = ignoreFilePatterns
const options = { cwd, dot, onlyFiles, followSymbolicLinks, objectMode, ignore }
const [error, result] = await __(glob(filterFilePatterns, options))
if (error) {
throw new Error(error)
}
if(_.isEmpty(result)) {
return null
}
const dirEntries = _.map(result, (file) => {
return file.dirent
})
const fileList = this.returnFilePathAndName(path, dirEntries)
return fileList
}
/**
* Returns an array of objects with path and filename
* Excludes any sub-folders in the provided fileList
* @param {string} path - Full path to folder
* @param {array} fileList - An array of Dirent objects
* @returns {[]}
*/
returnFilePathAndName (path, fileList) {
let returnList = []
_.forEach(fileList, (file) => {
if (!file.isDirectory()) {
returnList.push({ path, name: file.name })
}
})
return returnList
}
/**
* Removes the trailing "/" from a folder path.
* @param {string} path
*/
cleanFolderPath (path) {
if (_.endsWith(path, '/')) {
return _.trimEnd(path, '/')
}
return path
}
/**
* Uploads a given list of files with paths to the provided bucket.
* Files are encrypted prior to being uploaded if enabled.
* @param {array} fileList - Array of objects {path: "/example", name: "test.txt"}
* @param {string} bucketName - Name of the S3 / Object Storage bucket
* @returns {Promise<unknown>}
*/
uploadFileListToS3 (fileList, bucketName) {
return new Promise(async (resolve, reject) => {
if(_.isEmpty(fileList)) {
return resolve(false)
}
for (const file of fileList) {
let filePath = file.path
let fileName = file.name
if(this.config.args.encrypt === 'yes') {
const [encryptFileError, encryptFileResult] = await __(this.pgp.encryptFile(filePath, fileName))
if (encryptFileError) {
this.logger.error('Error while encrypting file:', encryptFileError)
return reject(encryptFileError)
}
filePath = encryptFileResult.path
fileName = encryptFileResult.pgpName
}
const [error, result] = await __(this.s3Bucket.uploadObject(filePath, fileName, bucketName))
if (error) {
this.logger.error('Object Storage upload error:', error)
if(this.config.args.encrypt === 'yes') {
const [removeFileError] = await __(fs.remove(filePath + '/' + fileName))
if (removeFileError) {
this.logger.error('Error while deleting temporary encrypted file:', removeFileError)
}
}
return reject(error)
}
this.logger.info(`Successfully uploaded ${filePath}/${fileName} to S3 bucket.`)
if(this.config.args.encrypt === 'yes') {
const [removeFileError] = await __(fs.remove(filePath + '/' + fileName))
if (removeFileError) {
this.logger.error('Error while deleting temporary encrypted file:', removeFileError)
}
}
}
return resolve(true)
})
}
}