2022-04-28 18:43:14 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
getBackupFilePath,
|
|
|
|
|
download,
|
|
|
|
|
upload
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const assert = require('assert'),
|
|
|
|
|
async = require('async'),
|
|
|
|
|
BoxError = require('../boxerror.js'),
|
|
|
|
|
DataLayout = require('../datalayout.js'),
|
|
|
|
|
debug = require('debug')('box:backupformat/tgz'),
|
|
|
|
|
{ DecryptStream, EncryptStream } = require('../hush.js'),
|
|
|
|
|
once = require('../once.js'),
|
|
|
|
|
path = require('path'),
|
2022-11-06 10:17:14 +01:00
|
|
|
ProgressStream = require('../progress-stream.js'),
|
2023-07-25 09:56:58 +05:30
|
|
|
promiseRetry = require('../promise-retry.js'),
|
2024-07-04 14:10:59 +02:00
|
|
|
stream = require('stream/promises'),
|
2022-04-28 18:43:14 -07:00
|
|
|
storage = require('../storage.js'),
|
|
|
|
|
tar = require('tar-fs'),
|
2024-07-04 14:10:59 +02:00
|
|
|
tar2 = require('tar'),
|
2022-04-28 18:43:14 -07:00
|
|
|
zlib = require('zlib');
|
|
|
|
|
|
|
|
|
|
function getBackupFilePath(backupConfig, remotePath) {
|
|
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
|
|
|
|
2023-12-18 13:23:40 +01:00
|
|
|
const rootPath = backupConfig.rootPath;
|
|
|
|
|
const fileType = backupConfig.encryption ? '.tar.gz.enc' : '.tar.gz';
|
|
|
|
|
|
2023-10-16 16:36:31 +02:00
|
|
|
// we don't have a rootPath for noop
|
|
|
|
|
if (backupConfig.provider === 'noop') return remotePath + fileType;
|
|
|
|
|
|
2022-04-28 18:43:14 -07:00
|
|
|
return path.join(rootPath, remotePath + fileType);
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 21:58:00 -07:00
|
|
|
function tarPack(dataLayout, encryption) {
|
2022-04-28 18:43:14 -07:00
|
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
|
assert.strictEqual(typeof encryption, 'object');
|
|
|
|
|
|
|
|
|
|
const pack = tar.pack('/', {
|
|
|
|
|
dereference: false, // pack the symlink and not what it points to
|
|
|
|
|
entries: dataLayout.localPaths(),
|
|
|
|
|
ignoreStatError: (path, err) => {
|
|
|
|
|
debug(`tarPack: error stat'ing ${path} - ${err.code}`);
|
|
|
|
|
return err.code === 'ENOENT'; // ignore if file or dir got removed (probably some temporary file)
|
|
|
|
|
},
|
|
|
|
|
map: function(header) {
|
|
|
|
|
header.name = dataLayout.toRemotePath(header.name);
|
|
|
|
|
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
|
|
|
|
|
// https://www.systutorials.com/docs/linux/man/5-star/
|
|
|
|
|
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
|
|
|
|
|
return header;
|
|
|
|
|
},
|
|
|
|
|
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const gzip = zlib.createGzip({});
|
2022-11-06 10:17:14 +01:00
|
|
|
const ps = new ProgressStream({ interval: 10000 }); // emit 'progress' every 10 seconds
|
2022-04-28 18:43:14 -07:00
|
|
|
|
|
|
|
|
pack.on('error', function (error) {
|
2023-04-16 10:49:59 +02:00
|
|
|
debug('tarPack: tar stream error. %o', error);
|
2022-04-28 18:43:14 -07:00
|
|
|
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gzip.on('error', function (error) {
|
2023-04-16 10:49:59 +02:00
|
|
|
debug('tarPack: gzip stream error. %o', error);
|
2022-04-28 18:43:14 -07:00
|
|
|
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (encryption) {
|
|
|
|
|
const encryptStream = new EncryptStream(encryption);
|
|
|
|
|
encryptStream.on('error', function (error) {
|
2023-04-16 10:49:59 +02:00
|
|
|
debug('tarPack: encrypt stream error. %o', error);
|
2022-04-28 18:43:14 -07:00
|
|
|
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pack.pipe(gzip).pipe(encryptStream).pipe(ps);
|
|
|
|
|
} else {
|
|
|
|
|
pack.pipe(gzip).pipe(ps);
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 21:58:00 -07:00
|
|
|
return ps;
|
2022-04-28 18:43:14 -07:00
|
|
|
}
|
|
|
|
|
|
2024-07-04 14:10:59 +02:00
|
|
|
async function tarExtract(inStream, dataLayout, encryption, progressCallback) {
|
2022-04-28 18:43:14 -07:00
|
|
|
assert.strictEqual(typeof inStream, 'object');
|
|
|
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
|
assert.strictEqual(typeof encryption, 'object');
|
2024-07-04 14:10:59 +02:00
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
2022-04-28 18:43:14 -07:00
|
|
|
|
2022-11-06 10:17:14 +01:00
|
|
|
const ps = new ProgressStream({ interval: 10000 }); // display a progress every 10 seconds
|
2022-04-28 18:43:14 -07:00
|
|
|
|
2024-07-04 14:10:59 +02:00
|
|
|
const extract = tar2.extract({
|
|
|
|
|
cwd: '/tmp',
|
|
|
|
|
dmode: 500, // ensure directory is writable
|
|
|
|
|
preserveOwner: false, // use uid/gid of current process
|
|
|
|
|
strict: true, // error on warnings,
|
|
|
|
|
preservePaths: true, // allow absolute paths. otherwise will sandbox to cwd
|
|
|
|
|
|
|
|
|
|
// to map paths, we cannot use transform hook because it is only used for files and not directories
|
|
|
|
|
// we can use filter hook as well - https://github.com/isaacs/node-tar/issues/357#issuecomment-1416491212
|
|
|
|
|
onReadEntry(entry) {
|
|
|
|
|
// debug(entry.header.path, entry.header.type, entry.header.size);
|
|
|
|
|
entry.path = dataLayout.toLocalPath(entry.header.path);
|
|
|
|
|
},
|
|
|
|
|
onwarn(code, message /*, data */) {
|
|
|
|
|
debug(`extract warning:${message} ${code}`);
|
|
|
|
|
},
|
2022-04-28 18:43:14 -07:00
|
|
|
});
|
|
|
|
|
|
2024-07-04 14:10:59 +02:00
|
|
|
ps.on('progress', function (progress) {
|
|
|
|
|
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
|
|
|
|
if (!transferred && !speed) return progressCallback({ message: 'Downloading backup' }); // 0M@0MBps looks wrong
|
|
|
|
|
progressCallback({ message: `Downloading ${transferred}M@${speed}MBps` });
|
2022-04-28 18:43:14 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (encryption) {
|
2022-04-28 21:58:00 -07:00
|
|
|
const decrypt = new DecryptStream(encryption);
|
2024-07-04 14:10:59 +02:00
|
|
|
await stream.pipeline(inStream, ps, decrypt, extract);
|
2022-04-28 18:43:14 -07:00
|
|
|
} else {
|
2024-07-04 14:10:59 +02:00
|
|
|
await stream.pipeline(inStream, ps, extract);
|
2022-04-28 18:43:14 -07:00
|
|
|
}
|
|
|
|
|
|
2022-04-28 21:58:00 -07:00
|
|
|
return ps;
|
2022-04-28 18:43:14 -07:00
|
|
|
}
|
|
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
async function download(backupConfig, remotePath, dataLayout, progressCallback) {
|
2022-04-28 18:43:14 -07:00
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
|
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
|
|
|
|
|
|
debug(`download: Downloading ${remotePath} to ${dataLayout.toString()}`);
|
|
|
|
|
|
|
|
|
|
const backupFilePath = getBackupFilePath(backupConfig, remotePath);
|
|
|
|
|
|
2023-07-25 09:56:58 +05:30
|
|
|
await promiseRetry({ times: 5, interval: 20000, debug }, async () => {
|
2023-08-02 09:50:02 +05:30
|
|
|
progressCallback({ message: `Downloading backup ${backupFilePath}` });
|
2023-07-25 09:56:58 +05:30
|
|
|
const sourceStream = await storage.api(backupConfig.provider).download(backupConfig, backupFilePath);
|
2024-07-04 14:10:59 +02:00
|
|
|
await tarExtract(sourceStream, dataLayout, backupConfig.encryption, progressCallback);
|
2022-04-30 16:42:14 -07:00
|
|
|
});
|
2022-04-28 18:43:14 -07:00
|
|
|
}
|
|
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
async function upload(backupConfig, remotePath, dataLayout, progressCallback) {
|
2022-04-28 18:43:14 -07:00
|
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
|
|
|
assert.strictEqual(typeof dataLayout, 'object');
|
|
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
|
|
2022-11-05 08:43:02 +01:00
|
|
|
debug(`upload: Uploading ${dataLayout.toString()} to ${remotePath}`);
|
|
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
|
|
|
|
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
2022-04-28 18:43:14 -07:00
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
const tarStream = tarPack(dataLayout, backupConfig.encryption);
|
2022-04-28 18:43:14 -07:00
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
tarStream.on('progress', function (progress) {
|
|
|
|
|
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
|
|
|
|
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0MBps looks wrong
|
|
|
|
|
progressCallback({ message: `Uploading backup ${transferred}M@${speed}MBps` });
|
|
|
|
|
});
|
|
|
|
|
tarStream.on('error', retryCallback); // already returns BoxError
|
2022-04-28 21:58:00 -07:00
|
|
|
|
2022-04-30 16:42:14 -07:00
|
|
|
storage.api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, remotePath), tarStream, retryCallback);
|
|
|
|
|
}, (error) => {
|
|
|
|
|
if (error) return reject(error);
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
});
|
2022-04-28 18:43:14 -07:00
|
|
|
}
|