2017-09-17 17:51:00 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
2020-06-05 13:27:18 +02:00
|
|
|
getBackupPath: getBackupPath,
|
2020-06-08 16:25:00 +02:00
|
|
|
checkPreconditions: checkPreconditions,
|
2020-06-05 13:27:18 +02:00
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
upload: upload,
|
|
|
|
|
download: download,
|
|
|
|
|
copy: copy,
|
|
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
listDir: listDir,
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
remove: remove,
|
|
|
|
|
removeDir: removeDir,
|
2017-09-17 17:51:00 +02:00
|
|
|
|
|
|
|
|
testConfig: testConfig,
|
2019-02-09 18:08:10 -08:00
|
|
|
removePrivateFields: removePrivateFields,
|
|
|
|
|
injectPrivateFields: injectPrivateFields,
|
2017-09-17 17:51:00 +02:00
|
|
|
|
|
|
|
|
// Used to mock GCS
|
|
|
|
|
_mockInject: mockInject,
|
|
|
|
|
_mockRestore: mockRestore
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2017-12-15 17:28:45 +05:30
|
|
|
async = require('async'),
|
2019-10-22 20:36:20 -07:00
|
|
|
BoxError = require('../boxerror.js'),
|
2020-05-14 23:01:44 +02:00
|
|
|
constants = require('../constants.js'),
|
2020-06-08 16:25:00 +02:00
|
|
|
DataLayout = require('../datalayout.js'),
|
2017-09-17 17:51:00 +02:00
|
|
|
debug = require('debug')('box:storage/gcs'),
|
2017-10-29 11:10:50 +01:00
|
|
|
EventEmitter = require('events'),
|
2019-05-12 18:05:48 -07:00
|
|
|
GCS = require('@google-cloud/storage').Storage,
|
2017-12-15 17:28:45 +05:30
|
|
|
PassThrough = require('stream').PassThrough,
|
|
|
|
|
path = require('path');
|
2017-09-17 17:51:00 +02:00
|
|
|
|
|
|
|
|
// test only
|
|
|
|
|
var originalGCS;
|
|
|
|
|
function mockInject(mock) {
|
|
|
|
|
originalGCS = GCS;
|
|
|
|
|
GCS = mock;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockRestore() {
|
|
|
|
|
GCS = originalGCS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// internal only
|
2017-12-16 07:09:15 +05:30
|
|
|
function getBucket(apiConfig) {
|
|
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-12-16 07:09:15 +05:30
|
|
|
var gcsConfig = {
|
|
|
|
|
projectId: apiConfig.projectId,
|
|
|
|
|
|
|
|
|
|
credentials: {
|
|
|
|
|
client_email: apiConfig.credentials.client_email,
|
|
|
|
|
private_key: apiConfig.credentials.private_key
|
|
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
};
|
|
|
|
|
|
2019-05-12 18:05:48 -07:00
|
|
|
return new GCS(gcsConfig).bucket(apiConfig.bucket);
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
// storage api
|
2020-06-05 13:27:18 +02:00
|
|
|
function getBackupPath(apiConfig) {
|
|
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
|
|
|
|
|
|
|
|
|
return apiConfig.prefix;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-08 16:25:00 +02:00
|
|
|
function checkPreconditions(apiConfig, dataLayout, callback) {
|
|
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
|
|
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
function upload(apiConfig, backupFilePath, sourceStream, callback) {
|
2017-09-17 17:51:00 +02:00
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
2017-10-29 11:10:50 +01:00
|
|
|
assert.strictEqual(typeof backupFilePath, 'string');
|
|
|
|
|
assert.strictEqual(typeof sourceStream, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-10-31 11:40:00 +01:00
|
|
|
debug(`Uploading to ${backupFilePath}`);
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
function done(error) {
|
|
|
|
|
if (error) {
|
|
|
|
|
debug('[%s] upload: gcp upload error.', backupFilePath, error);
|
2019-10-22 20:36:20 -07:00
|
|
|
return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
|
2017-10-29 11:10:50 +01:00
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
callback(null);
|
|
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-12-16 07:09:15 +05:30
|
|
|
var uploadStream = getBucket(apiConfig).file(backupFilePath)
|
|
|
|
|
.createWriteStream({resumable: false})
|
|
|
|
|
.on('finish', done)
|
|
|
|
|
.on('error', done);
|
|
|
|
|
|
|
|
|
|
sourceStream.pipe(uploadStream);
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
function download(apiConfig, backupFilePath, callback) {
|
2017-09-17 17:51:00 +02:00
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
2017-10-29 11:10:50 +01:00
|
|
|
assert.strictEqual(typeof backupFilePath, 'string');
|
2017-09-17 17:51:00 +02:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2017-10-31 11:40:00 +01:00
|
|
|
debug(`Download ${backupFilePath} starting`);
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
var file = getBucket(apiConfig).file(backupFilePath);
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
var ps = new PassThrough();
|
|
|
|
|
var readStream = file.createReadStream()
|
2017-12-15 17:33:24 +05:30
|
|
|
.on('error', function(error) {
|
2017-10-29 11:10:50 +01:00
|
|
|
if (error && error.code == 404){
|
2019-10-22 20:36:20 -07:00
|
|
|
ps.emit('error', new BoxError(BoxError.NOT_FOUND));
|
2017-10-29 11:10:50 +01:00
|
|
|
} else {
|
|
|
|
|
debug('[%s] download: gcp stream error.', backupFilePath, error);
|
2019-10-22 20:36:20 -07:00
|
|
|
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error));
|
2017-10-29 11:10:50 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
;
|
|
|
|
|
readStream.pipe(ps);
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
callback(null, ps);
|
|
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-12-15 17:33:24 +05:30
|
|
|
function listDir(apiConfig, backupFilePath, batchSize, iteratorCallback, callback) {
|
2017-09-17 17:51:00 +02:00
|
|
|
var bucket = getBucket(apiConfig);
|
|
|
|
|
|
2017-12-16 07:09:15 +05:30
|
|
|
var query = { prefix: backupFilePath, autoPaginate: batchSize === -1 };
|
2017-10-31 11:40:00 +01:00
|
|
|
if (batchSize > 0) {
|
|
|
|
|
query.maxResults = batchSize;
|
|
|
|
|
}
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2019-12-04 11:17:42 -08:00
|
|
|
let done = false;
|
|
|
|
|
|
|
|
|
|
async.whilst(() => !done, function listAndDownload(whilstCallback) {
|
2017-10-29 11:10:50 +01:00
|
|
|
bucket.getFiles(query, function (error, files, nextQuery) {
|
2019-12-04 11:17:42 -08:00
|
|
|
if (error) return whilstCallback(error);
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2019-12-04 11:17:42 -08:00
|
|
|
if (files.length === 0) { done = true; return whilstCallback(); }
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
const entries = files.map(function (f) { return { fullPath: f.name }; });
|
|
|
|
|
iteratorCallback(entries, function (error) {
|
2019-12-04 11:17:42 -08:00
|
|
|
if (error) return whilstCallback(error);
|
|
|
|
|
if (!nextQuery) { done = true; return whilstCallback(); }
|
2017-10-29 11:10:50 +01:00
|
|
|
|
|
|
|
|
query = nextQuery;
|
2018-07-27 17:08:53 -07:00
|
|
|
|
2019-12-04 11:17:42 -08:00
|
|
|
whilstCallback();
|
2017-10-29 11:10:50 +01:00
|
|
|
});
|
|
|
|
|
});
|
2019-12-04 11:17:42 -08:00
|
|
|
}, callback);
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
function copy(apiConfig, oldFilePath, newFilePath) {
|
2017-09-17 17:51:00 +02:00
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
2017-10-29 11:10:50 +01:00
|
|
|
assert.strictEqual(typeof oldFilePath, 'string');
|
|
|
|
|
assert.strictEqual(typeof newFilePath, 'string');
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2019-10-18 18:38:37 -07:00
|
|
|
var events = new EventEmitter();
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
function copyFile(entry, iteratorCallback) {
|
|
|
|
|
var relativePath = path.relative(oldFilePath, entry.fullPath);
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
getBucket(apiConfig).file(entry.fullPath).copy(path.join(newFilePath, relativePath), function(error) {
|
|
|
|
|
if (error) debug('copyBackup: gcs copy error', error);
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2019-10-22 20:36:20 -07:00
|
|
|
if (error && error.code === 404) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, 'Old backup not found'));
|
|
|
|
|
if (error) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
2018-07-27 17:08:53 -07:00
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
iteratorCallback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 22:02:48 -07:00
|
|
|
const batchSize = 1000;
|
|
|
|
|
const concurrency = apiConfig.copyConcurrency || 10;
|
2019-10-18 18:38:37 -07:00
|
|
|
var total = 0;
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
listDir(apiConfig, oldFilePath, batchSize, function (entries, done) {
|
|
|
|
|
total += entries.length;
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2019-10-18 18:38:37 -07:00
|
|
|
events.emit('progress', `Copying ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}`);
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
async.eachLimit(entries, concurrency, copyFile, done);
|
2017-10-29 11:10:50 +01:00
|
|
|
}, function (error) {
|
2017-10-31 11:40:00 +01:00
|
|
|
events.emit('progress', `Copied ${total} files`);
|
2020-02-11 11:48:46 -08:00
|
|
|
process.nextTick(() => events.emit('done', error));
|
2017-10-29 11:10:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return events;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function remove(apiConfig, filename, callback) {
|
|
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof filename, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
getBucket(apiConfig)
|
|
|
|
|
.file(filename)
|
2018-07-27 17:08:53 -07:00
|
|
|
.delete(function (error) {
|
|
|
|
|
if (error) debug('removeBackups: Unable to remove %s (%s). Not fatal.', filename, error.message);
|
|
|
|
|
|
2017-09-17 17:51:00 +02:00
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
function removeDir(apiConfig, pathPrefix) {
|
2017-09-17 17:51:00 +02:00
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
2017-10-29 11:10:50 +01:00
|
|
|
assert.strictEqual(typeof pathPrefix, 'string');
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2019-10-18 18:38:37 -07:00
|
|
|
var events = new EventEmitter();
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2019-10-18 18:38:37 -07:00
|
|
|
const batchSize = 1000, concurrency = 10; // https://googleapis.dev/nodejs/storage/latest/Bucket.html#deleteFiles
|
|
|
|
|
var total = 0;
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
listDir(apiConfig, pathPrefix, batchSize, function (entries, done) {
|
|
|
|
|
total += entries.length;
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2019-10-18 18:38:37 -07:00
|
|
|
events.emit('progress', `Removing ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}`);
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2018-07-27 17:08:53 -07:00
|
|
|
async.eachLimit(entries, concurrency, function (entry, iteratorCallback) {
|
|
|
|
|
remove(apiConfig, entry.fullPath, iteratorCallback);
|
|
|
|
|
}, done);
|
2017-10-29 11:10:50 +01:00
|
|
|
}, function (error) {
|
2017-10-31 11:40:00 +01:00
|
|
|
events.emit('progress', `Deleted ${total} files`);
|
2017-10-29 11:10:50 +01:00
|
|
|
|
2020-02-11 11:48:46 -08:00
|
|
|
process.nextTick(() => events.emit('done', error));
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2017-10-29 11:10:50 +01:00
|
|
|
return events;
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testConfig(apiConfig, callback) {
|
|
|
|
|
assert.strictEqual(typeof apiConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-10-22 20:36:20 -07:00
|
|
|
if (typeof apiConfig.projectId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'projectId must be a string'));
|
|
|
|
|
if (!apiConfig.credentials || typeof apiConfig.credentials !== 'object') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials must be an object'));
|
|
|
|
|
if (typeof apiConfig.credentials.client_email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.client_email must be a string'));
|
|
|
|
|
if (typeof apiConfig.credentials.private_key !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.private_key must be a string'));
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2019-10-22 20:36:20 -07:00
|
|
|
if (typeof apiConfig.bucket !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'bucket must be a string'));
|
|
|
|
|
if (typeof apiConfig.prefix !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must be a string'));
|
2017-09-17 17:51:00 +02:00
|
|
|
|
|
|
|
|
// attempt to upload and delete a file with new credentials
|
|
|
|
|
var bucket = getBucket(apiConfig);
|
2017-10-31 11:40:00 +01:00
|
|
|
|
2017-09-17 17:51:00 +02:00
|
|
|
var testFile = bucket.file(path.join(apiConfig.prefix, 'cloudron-testfile'));
|
2017-10-31 11:40:00 +01:00
|
|
|
|
2017-12-16 21:24:37 +05:30
|
|
|
var uploadStream = testFile.createWriteStream({ resumable: false });
|
|
|
|
|
uploadStream.write('testfilecontents');
|
|
|
|
|
uploadStream.end();
|
|
|
|
|
|
|
|
|
|
uploadStream.on('error', function(error) {
|
|
|
|
|
debug('testConfig: failed uploading cloudron-testfile', error);
|
|
|
|
|
if (error && error.code && (error.code == 403 || error.code == 404)) {
|
2019-10-22 20:36:20 -07:00
|
|
|
return callback(new BoxError(BoxError.BAD_FIELD, error.message));
|
2017-12-16 21:24:37 +05:30
|
|
|
}
|
2017-10-31 11:40:00 +01:00
|
|
|
|
2019-10-22 20:36:20 -07:00
|
|
|
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
2017-12-16 21:24:37 +05:30
|
|
|
});
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2017-12-16 21:24:37 +05:30
|
|
|
uploadStream.on('finish', function() {
|
|
|
|
|
debug('testConfig: uploaded cloudron-testfile ' + JSON.stringify(arguments));
|
|
|
|
|
bucket.file(path.join(apiConfig.prefix, 'cloudron-testfile')).delete(function(error) {
|
2019-10-22 20:36:20 -07:00
|
|
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
2017-12-16 21:24:37 +05:30
|
|
|
debug('testConfig: deleted cloudron-testfile');
|
|
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
});
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
2019-02-09 18:08:10 -08:00
|
|
|
function removePrivateFields(apiConfig) {
|
2020-05-14 23:01:44 +02:00
|
|
|
apiConfig.credentials.private_key = constants.SECRET_PLACEHOLDER;
|
2019-02-09 18:08:10 -08:00
|
|
|
return apiConfig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
2020-05-14 23:01:44 +02:00
|
|
|
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
2019-02-09 18:08:10 -08:00
|
|
|
}
|