Files
cloudron-box/src/storage/gcs.js

286 lines
9.6 KiB
JavaScript
Raw Normal View History

2017-09-17 17:51:00 +02:00
'use strict';
exports = module.exports = {
getBackupRootPath,
getProviderStatus,
getAvailableSize,
2021-02-01 14:23:15 -08:00
upload,
exists,
2021-02-01 14:23:15 -08:00
download,
copy,
2021-02-01 14:23:15 -08:00
listDir,
2018-07-27 17:08:53 -07:00
2021-02-01 14:23:15 -08:00
remove,
removeDir,
2017-09-17 17:51:00 +02:00
2021-02-01 14:23:15 -08:00
testConfig,
removePrivateFields,
injectPrivateFields,
2017-09-17 17:51:00 +02:00
// Used to mock GCS
_mockInject: mockInject,
_mockRestore: mockRestore
};
2022-04-14 07:59:50 -05:00
const 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'),
constants = require('../constants.js'),
2017-09-17 17:51:00 +02:00
debug = require('debug')('box:storage/gcs'),
2022-04-14 07:59:50 -05:00
path = require('path'),
safe = require('safetydance'),
util = require('util');
2022-04-14 07:59:50 -05:00
let GCS = require('@google-cloud/storage').Storage;
2017-09-17 17:51:00 +02:00
// test only
2022-04-14 20:43:04 -05:00
let originalGCS;
2017-09-17 17:51:00 +02:00
function mockInject(mock) {
originalGCS = GCS;
GCS = mock;
}
function mockRestore() {
GCS = originalGCS;
}
// internal only
function getBucket(apiConfig) {
assert.strictEqual(typeof apiConfig, 'object');
2017-09-17 17:51:00 +02:00
2022-04-14 20:43:04 -05:00
const 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
}
// storage api
function getBackupRootPath(apiConfig) {
assert.strictEqual(typeof apiConfig, 'object');
return apiConfig.prefix;
}
async function getProviderStatus(apiConfig) {
assert.strictEqual(typeof apiConfig, 'object');
return { state: 'active' };
}
async function getAvailableSize(apiConfig) {
assert.strictEqual(typeof apiConfig, 'object');
return Number.POSITIVE_INFINITY;
}
function upload(apiConfig, backupFilePath, sourceStream, callback) {
2017-09-17 17:51:00 +02:00
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof sourceStream, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`Uploading to ${backupFilePath}`);
function done(error) {
if (error) {
debug(`upload: [${backupFilePath}] gcp upload error. %o`, 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-09-17 17:51:00 +02:00
callback(null);
}
2017-09-17 17:51:00 +02:00
2022-04-14 20:43:04 -05:00
const 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
}
2022-04-14 08:07:03 -05:00
async function exists(apiConfig, backupFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
const bucket = getBucket(apiConfig);
if (!backupFilePath.endsWith('/')) {
const file = bucket.file(backupFilePath);
2022-04-14 08:07:03 -05:00
const [error] = await safe(file.getMetadata());
if (error && error.code === 404) return false;
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message);
2022-04-14 08:07:03 -05:00
return true;
} else {
const query = {
prefix: backupFilePath,
maxResults: 1,
autoPaginate: true
};
2022-04-14 08:07:03 -05:00
const [error, files] = await safe(bucket.getFiles(query));
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message);
2022-04-14 08:07:03 -05:00
return files.length !== 0;
}
}
async function download(apiConfig, backupFilePath) {
2017-09-17 17:51:00 +02:00
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
2017-09-17 17:51:00 +02:00
debug(`Download ${backupFilePath} starting`);
const file = getBucket(apiConfig).file(backupFilePath);
return file.createReadStream();
}
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);
var query = { prefix: backupFilePath, autoPaginate: batchSize === -1 };
if (batchSize > 0) {
query.maxResults = batchSize;
}
let done = false;
async.whilst((testDone) => testDone(null, !done), function listAndDownload(whilstCallback) {
bucket.getFiles(query, function (error, files, nextQuery) {
if (error) return whilstCallback(error);
if (files.length === 0) { done = true; return whilstCallback(); }
2018-07-27 17:08:53 -07:00
const entries = files.map(function (f) { return { fullPath: f.name }; });
iteratorCallback(entries, function (error) {
if (error) return whilstCallback(error);
if (!nextQuery) { done = true; return whilstCallback(); }
query = nextQuery;
2018-07-27 17:08:53 -07:00
whilstCallback();
});
});
}, callback);
2017-09-17 17:51:00 +02:00
}
2022-04-30 16:01:42 -07:00
async function copy(apiConfig, oldFilePath, newFilePath, progressCallback) {
2017-09-17 17:51:00 +02:00
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
2022-04-30 16:01:42 -07:00
assert.strictEqual(typeof progressCallback, 'function');
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. %o', error);
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
iteratorCallback(null);
});
}
2020-09-28 22:02:48 -07:00
const batchSize = 1000;
const concurrency = apiConfig.limits?.copyConcurrency || 10;
2022-04-30 16:01:42 -07:00
let total = 0;
const listDirAsync = util.promisify(listDir);
2022-04-30 16:01:42 -07:00
const [copyError] = await safe(listDirAsync(apiConfig, oldFilePath, batchSize, function (entries, done) {
2018-07-27 17:08:53 -07:00
total += entries.length;
2017-09-17 17:51:00 +02:00
2022-04-30 16:01:42 -07:00
progressCallback({ message: `Copying ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}` });
2018-07-27 17:08:53 -07:00
async.eachLimit(entries, concurrency, copyFile, done);
2022-04-30 16:01:42 -07:00
}));
2022-04-30 16:01:42 -07:00
progressCallback({ message: `Copied ${total} files with error: ${copyError}` });
}
async function remove(apiConfig, filename) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof filename, 'string');
2018-07-27 17:08:53 -07:00
const [error] = await safe(getBucket(apiConfig).file(filename).delete());
if (error) debug('removeBackups: Unable to remove %s (%s). Not fatal.', filename, error.message);
2017-09-17 17:51:00 +02:00
}
async function removeDir(apiConfig, pathPrefix, progressCallback) {
2017-09-17 17:51:00 +02:00
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof pathPrefix, 'string');
assert.strictEqual(typeof progressCallback, 'function');
2017-09-17 17:51:00 +02:00
const batchSize = 1000, concurrency = apiConfig.limits?.deleteConcurrency || 10; // https://googleapis.dev/nodejs/storage/latest/Bucket.html#deleteFiles
let total = 0;
const listDirAsync = util.promisify(listDir);
await listDirAsync(apiConfig, pathPrefix, batchSize, function (entries, done) {
total += entries.length;
progressCallback({ message: `Removing ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}` });
async.eachLimit(entries, concurrency, async (entry) => await remove(apiConfig, entry.fullPath), done);
2017-09-17 17:51:00 +02:00
});
progressCallback({ progress: `Deleted ${total} files` });
2017-09-17 17:51:00 +02:00
}
2022-04-14 07:59:50 -05:00
async function testConfig(apiConfig) {
2017-09-17 17:51:00 +02:00
assert.strictEqual(typeof apiConfig, 'object');
2022-04-14 07:59:50 -05:00
if (typeof apiConfig.projectId !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'projectId must be a string');
if (!apiConfig.credentials || typeof apiConfig.credentials !== 'object') throw new BoxError(BoxError.BAD_FIELD, 'credentials must be an object');
if (typeof apiConfig.credentials.client_email !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'credentials.client_email must be a string');
if (typeof apiConfig.credentials.private_key !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'credentials.private_key must be a string');
2017-09-17 17:51:00 +02:00
2022-04-14 07:59:50 -05:00
if (typeof apiConfig.bucket !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'bucket must be a string');
if (typeof apiConfig.prefix !== 'string') throw 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
2022-04-14 07:59:50 -05:00
const bucket = getBucket(apiConfig);
2022-04-14 07:59:50 -05:00
const testFile = bucket.file(path.join(apiConfig.prefix, 'cloudron-testfile'));
2022-04-14 07:59:50 -05:00
const uploadStream = testFile.createWriteStream({ resumable: false });
2022-04-14 07:59:50 -05:00
await new Promise((resolve, reject) => {
uploadStream.on('error', function(error) {
debug('testConfig: failed uploading cloudron-testfile. %o', error);
2022-04-14 07:59:50 -05:00
if (error && error.code && (error.code == 403 || error.code == 404)) {
return reject(new BoxError(BoxError.BAD_FIELD, error.message));
}
2017-09-17 17:51:00 +02:00
2022-04-14 07:59:50 -05:00
return reject(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
2022-04-14 07:59:50 -05:00
uploadStream.write('testfilecontents');
uploadStream.end();
uploadStream.on('finish', resolve);
});
2022-04-14 07:59:50 -05:00
debug('testConfig: uploaded cloudron-testfile');
const [delError] = await safe(bucket.file(path.join(apiConfig.prefix, 'cloudron-testfile')).delete());
if (delError) throw new BoxError(BoxError.EXTERNAL_ERROR, delError.message);
debug('testConfig: deleted cloudron-testfile');
2017-09-17 17:51:00 +02:00
}
function removePrivateFields(apiConfig) {
apiConfig.credentials.private_key = constants.SECRET_PLACEHOLDER;
return apiConfig;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}