2017-04-17 14:46:39 +02:00
|
|
|
/* global it:false */
|
|
|
|
|
/* global describe:false */
|
|
|
|
|
/* global before:false */
|
|
|
|
|
/* global after:false */
|
2021-08-19 13:24:38 -07:00
|
|
|
/* global xit:false */
|
2017-04-17 14:46:39 +02:00
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2025-09-12 09:48:37 +02:00
|
|
|
const backupSites = require('../backupsites.js'),
|
2023-08-04 11:24:28 +05:30
|
|
|
BoxError = require('../boxerror.js'),
|
2021-07-14 19:03:12 -07:00
|
|
|
common = require('./common.js'),
|
2025-08-02 01:46:29 +02:00
|
|
|
consumers = require('node:stream/consumers'),
|
2025-08-14 11:17:38 +05:30
|
|
|
execSync = require('node:child_process').execSync,
|
2017-09-27 10:25:36 -07:00
|
|
|
expect = require('expect.js'),
|
|
|
|
|
filesystem = require('../storage/filesystem.js'),
|
2025-08-14 11:17:38 +05:30
|
|
|
fs = require('node:fs'),
|
2022-04-15 08:07:46 -05:00
|
|
|
gcs = require('../storage/gcs.js'),
|
2025-08-14 11:17:38 +05:30
|
|
|
os = require('node:os'),
|
|
|
|
|
path = require('node:path'),
|
2017-09-17 17:51:00 +02:00
|
|
|
s3 = require('../storage/s3.js'),
|
2024-07-08 22:29:45 +02:00
|
|
|
safe = require('safetydance'),
|
|
|
|
|
stream = require('stream/promises');
|
2017-04-17 14:46:39 +02:00
|
|
|
|
|
|
|
|
describe('Storage', function () {
|
2025-09-12 09:48:37 +02:00
|
|
|
const { setup, cleanup, getDefaultBackupSite, auditSource } = common;
|
2021-07-14 19:03:12 -07:00
|
|
|
|
|
|
|
|
before(setup);
|
|
|
|
|
after(cleanup);
|
2017-10-02 20:08:00 -07:00
|
|
|
|
2021-07-14 19:03:12 -07:00
|
|
|
describe('filesystem', function () {
|
|
|
|
|
let gTmpFolder;
|
2017-10-02 20:08:00 -07:00
|
|
|
|
2021-07-14 19:03:12 -07:00
|
|
|
const gBackupConfig = {
|
2017-04-17 14:46:39 +02:00
|
|
|
key: 'key',
|
2025-08-02 10:37:37 +02:00
|
|
|
backupDir: null,
|
2025-10-13 09:45:29 +02:00
|
|
|
prefix: 'someprefix',
|
|
|
|
|
_provider: 'filesystem' // need this internal variable since we call the backend logic directly for testing
|
2017-04-17 14:46:39 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-12 09:48:37 +02:00
|
|
|
let defaultBackupSite;
|
2025-07-24 19:02:02 +02:00
|
|
|
|
|
|
|
|
before(async function () {
|
2017-10-02 20:08:00 -07:00
|
|
|
gTmpFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'filesystem-storage-test_'));
|
2025-09-12 09:48:37 +02:00
|
|
|
defaultBackupSite = await getDefaultBackupSite();
|
2025-08-02 10:37:37 +02:00
|
|
|
gBackupConfig.backupDir = path.join(gTmpFolder, 'backups/');
|
2017-10-02 20:08:00 -07:00
|
|
|
});
|
2017-04-17 14:46:39 +02:00
|
|
|
|
2017-10-02 20:08:00 -07:00
|
|
|
after(function (done) {
|
2022-11-06 11:48:56 +01:00
|
|
|
fs.rmSync(gTmpFolder, { recursive: true, force: true });
|
2017-10-02 20:08:00 -07:00
|
|
|
done();
|
|
|
|
|
});
|
2017-09-20 09:57:16 -07:00
|
|
|
|
2023-08-15 20:24:54 +05:30
|
|
|
it('fails to set backup storage for bad folder', async function () {
|
2025-08-02 10:37:37 +02:00
|
|
|
const tmp = Object.assign({}, gBackupConfig, { backupDir: '/root/oof' });
|
2025-09-12 09:48:37 +02:00
|
|
|
const [error] = await safe(backupSites.setConfig(defaultBackupSite, tmp, auditSource));
|
2021-08-19 13:24:38 -07:00
|
|
|
expect(error.reason).to.equal(BoxError.BAD_FIELD);
|
2021-07-14 19:03:12 -07:00
|
|
|
});
|
|
|
|
|
|
2023-08-15 20:24:54 +05:30
|
|
|
it('succeeds to set backup storage', async function () {
|
2025-09-12 09:48:37 +02:00
|
|
|
await backupSites.setConfig(defaultBackupSite, gBackupConfig, auditSource);
|
2025-08-02 10:37:37 +02:00
|
|
|
expect(fs.existsSync(path.join(gBackupConfig.backupDir, 'someprefix/snapshot'))).to.be(true); // auto-created
|
2021-07-14 19:03:12 -07:00
|
|
|
});
|
|
|
|
|
|
2024-07-08 22:29:45 +02:00
|
|
|
it('can upload', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
|
|
|
|
const sourceStream = fs.createReadStream(sourceFile);
|
2025-08-02 10:37:37 +02:00
|
|
|
const destFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test.txt');
|
2025-11-28 17:40:13 +01:00
|
|
|
const uploader = await filesystem.upload(gBackupConfig, {}, 'uploadtest/test.txt');
|
2024-07-08 22:29:45 +02:00
|
|
|
await stream.pipeline(sourceStream, uploader.stream);
|
|
|
|
|
await uploader.finish();
|
|
|
|
|
expect(fs.existsSync(destFile));
|
|
|
|
|
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
2017-04-18 17:34:42 +02:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
xit('upload waits for empty file to be created', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/empty');
|
|
|
|
|
const sourceStream = fs.createReadStream(sourceFile);
|
2025-08-02 10:37:37 +02:00
|
|
|
const destFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/empty');
|
2025-11-28 17:40:13 +01:00
|
|
|
const uploader = await filesystem.upload(gBackupConfig, {}, destFile);
|
2024-07-08 22:29:45 +02:00
|
|
|
await stream.pipeline(sourceStream, uploader.stream);
|
|
|
|
|
await uploader.finish();
|
|
|
|
|
expect(fs.existsSync(destFile));
|
|
|
|
|
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
|
2017-04-21 17:21:10 +02:00
|
|
|
});
|
|
|
|
|
|
2024-07-08 22:29:45 +02:00
|
|
|
it('upload unlinks old file', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
|
|
|
|
const sourceStream = fs.createReadStream(sourceFile);
|
2025-08-02 10:37:37 +02:00
|
|
|
const destFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test.txt');
|
2022-04-14 17:41:41 -05:00
|
|
|
const oldStat = fs.statSync(destFile);
|
2025-11-28 17:40:13 +01:00
|
|
|
const uploader = await filesystem.upload(gBackupConfig, {}, 'uploadtest/test.txt');
|
2024-07-08 22:29:45 +02:00
|
|
|
await stream.pipeline(sourceStream, uploader.stream);
|
|
|
|
|
await uploader.finish();
|
|
|
|
|
expect(fs.existsSync(destFile)).to.be(true);
|
|
|
|
|
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
|
|
|
|
|
expect(oldStat.inode).to.not.be(fs.statSync(destFile).size);
|
2017-04-21 17:21:10 +02:00
|
|
|
});
|
|
|
|
|
|
2023-07-28 13:15:08 +05:30
|
|
|
it('can download file', async function () {
|
2025-08-02 10:37:37 +02:00
|
|
|
const sourceFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test.txt');
|
2025-08-02 01:46:29 +02:00
|
|
|
const [error, stream] = await safe(filesystem.download(gBackupConfig, 'uploadtest/test.txt'));
|
2023-07-28 13:15:08 +05:30
|
|
|
expect(error).to.be(null);
|
|
|
|
|
expect(stream).to.be.an('object');
|
2025-08-02 01:46:29 +02:00
|
|
|
const data = await consumers.buffer(stream);
|
|
|
|
|
expect(fs.readFileSync(sourceFile)).to.eql(data); // buffer compare
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
|
|
|
|
|
2023-07-28 13:15:08 +05:30
|
|
|
it('download errors for missing file', async function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
const [error] = await safe(filesystem.download(gBackupConfig, 'uploadtest/missing'));
|
2023-07-28 13:15:08 +05:30
|
|
|
expect(error.reason).to.be(BoxError.NOT_FOUND);
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
|
|
|
|
|
2025-02-12 18:46:54 +01:00
|
|
|
it('list dir lists the source dir', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceDir = path.join(__dirname, 'storage');
|
2025-08-02 10:37:37 +02:00
|
|
|
execSync(`cp -r ${sourceDir} ${gBackupConfig.backupDir}/${gBackupConfig.prefix}`, { encoding: 'utf8' });
|
2017-04-17 14:46:39 +02:00
|
|
|
|
2025-02-12 18:46:54 +01:00
|
|
|
let allFiles = [], marker = null;
|
|
|
|
|
while (true) {
|
2025-08-02 01:46:29 +02:00
|
|
|
const result = await filesystem.listDir(gBackupConfig, 'storage', 1, marker);
|
2025-02-12 18:46:54 +01:00
|
|
|
allFiles = allFiles.concat(result.entries);
|
|
|
|
|
if (!result.marker) break;
|
|
|
|
|
marker = result.marker;
|
|
|
|
|
}
|
2018-08-02 14:59:50 -07:00
|
|
|
|
2025-08-16 07:43:43 +05:30
|
|
|
const expectedFiles = execSync(`find . -type f -printf '%P\n'`, { cwd: sourceDir, encoding: 'utf8' }).trim().split('\n').map(p => `storage/${p}`);
|
2025-08-02 10:24:51 +02:00
|
|
|
expect(allFiles.map(function (f) { return f.path; }).sort()).to.eql(expectedFiles.sort());
|
2017-10-02 20:08:00 -07:00
|
|
|
});
|
2017-04-21 17:21:10 +02:00
|
|
|
|
2022-04-30 16:01:42 -07:00
|
|
|
it('can copy', async function () {
|
2025-08-02 10:37:37 +02:00
|
|
|
// const sourceFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test.txt'); // keep the test within same device
|
|
|
|
|
const destFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test-hardlink.txt');
|
2017-09-20 09:57:16 -07:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
await filesystem.copy(gBackupConfig, 'uploadtest/test.txt', 'uploadtest/test-hardlink.txt', () => {});
|
2022-04-30 16:01:42 -07:00
|
|
|
expect(fs.statSync(destFile).nlink).to.be(2); // created a hardlink
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
|
|
|
|
|
2022-04-14 20:43:04 -05:00
|
|
|
it('can remove file', async function () {
|
2025-08-02 10:37:37 +02:00
|
|
|
const sourceFile = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, '/uploadtest/test-hardlink.txt');
|
2017-04-17 14:46:39 +02:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
await filesystem.remove(gBackupConfig, 'uploadtest/test-hardlink.txt');
|
2022-04-14 20:43:04 -05:00
|
|
|
expect(fs.existsSync(sourceFile)).to.be(false);
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
|
|
|
|
|
2022-04-14 20:43:04 -05:00
|
|
|
it('can remove empty dir', async function () {
|
2025-08-02 10:37:37 +02:00
|
|
|
const sourceDir = path.join(gBackupConfig.backupDir, gBackupConfig.prefix, 'emptydir');
|
2017-10-02 20:08:00 -07:00
|
|
|
fs.mkdirSync(sourceDir);
|
2017-09-20 09:57:16 -07:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
await filesystem.remove(gBackupConfig, 'emptydir', () => {});
|
2022-04-14 20:43:04 -05:00
|
|
|
expect(fs.existsSync(sourceDir)).to.be(false);
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|
2017-10-02 20:08:00 -07:00
|
|
|
});
|
2017-04-17 14:46:39 +02:00
|
|
|
|
2017-04-18 17:34:42 +02:00
|
|
|
describe('s3', function () {
|
2025-02-12 20:56:46 +01:00
|
|
|
const basePath = path.join(os.tmpdir(), 's3-backup-test-buckets');
|
|
|
|
|
const backupConfig = {
|
2017-04-18 17:34:42 +02:00
|
|
|
provider: 's3',
|
|
|
|
|
key: 'key',
|
2025-08-16 07:43:43 +05:30
|
|
|
prefix: 'prefix-test',
|
2017-04-18 17:34:42 +02:00
|
|
|
bucket: 'cloudron-storage-test',
|
2017-04-18 19:15:56 +02:00
|
|
|
accessKeyId: 'testkeyid',
|
|
|
|
|
secretAccessKey: 'testsecret',
|
2017-09-23 14:27:35 -07:00
|
|
|
region: 'eu-central-1',
|
|
|
|
|
format: 'tgz'
|
2017-04-18 17:34:42 +02:00
|
|
|
};
|
2025-08-02 01:46:29 +02:00
|
|
|
const bucketPath = path.join(basePath, backupConfig.bucket, backupConfig.prefix);
|
|
|
|
|
const bucketPathNoPrefix = path.join(basePath, backupConfig.bucket);
|
2025-02-12 20:56:46 +01:00
|
|
|
|
|
|
|
|
class S3MockUpload {
|
|
|
|
|
constructor(args) { // { client: s3, params, partSize, queueSize: 3, leavePartsOnError: false }
|
2025-08-02 01:46:29 +02:00
|
|
|
// console.log('S3MockUpload constructor:', basePath, args.params.Bucket, args.params.Key);
|
2025-02-12 20:56:46 +01:00
|
|
|
const destFilePath = path.join(basePath, args.params.Bucket, args.params.Key);
|
|
|
|
|
fs.mkdirSync(path.dirname(destFilePath), { recursive: true });
|
|
|
|
|
this.pipeline = stream.pipeline(args.params.Body, fs.createWriteStream(destFilePath));
|
|
|
|
|
}
|
2017-04-18 17:34:42 +02:00
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
on() {}
|
|
|
|
|
|
|
|
|
|
async done() {
|
|
|
|
|
await this.pipeline;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class S3Mock {
|
|
|
|
|
constructor(cfg) {
|
|
|
|
|
expect(cfg.credentials).to.eql({ // retryDelayOptions is a function
|
|
|
|
|
accessKeyId: backupConfig.accessKeyId,
|
|
|
|
|
secretAccessKey: backupConfig.secretAccessKey
|
|
|
|
|
});
|
|
|
|
|
expect(cfg.region).to.be(backupConfig.region);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 14:51:30 +02:00
|
|
|
async headObject(params) {
|
|
|
|
|
expect(params.Bucket).to.be(backupConfig.bucket);
|
|
|
|
|
const stat = await fs.promises.stat(path.join(bucketPathNoPrefix, params.Key));
|
|
|
|
|
return { ContentLength: stat.size };
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 08:34:21 +02:00
|
|
|
async listObjectsV2(params) {
|
2025-02-12 20:56:46 +01:00
|
|
|
expect(params.Bucket).to.be(backupConfig.bucket);
|
|
|
|
|
return {
|
|
|
|
|
Contents: [{
|
2025-08-02 01:46:29 +02:00
|
|
|
Key: `${backupConfig.prefix}/uploadtest/test.txt`,
|
2025-02-12 20:56:46 +01:00
|
|
|
Size: 23
|
|
|
|
|
}, {
|
2025-08-02 01:46:29 +02:00
|
|
|
Key: `${backupConfig.prefix}/uploadtest/C++.gitignore`,
|
2025-02-12 20:56:46 +01:00
|
|
|
Size: 23
|
|
|
|
|
}]
|
|
|
|
|
};
|
|
|
|
|
}
|
2017-04-18 19:15:56 +02:00
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
async copyObject(params) {
|
2025-08-02 01:46:29 +02:00
|
|
|
// CopySource already has the bucket path!
|
2025-09-13 14:51:30 +02:00
|
|
|
const source = path.join(basePath, params.CopySource.replace(/%2B/g, '+'));
|
2025-08-02 01:46:29 +02:00
|
|
|
// Key already has prefix but no bucket ptah!
|
2025-09-13 14:51:30 +02:00
|
|
|
const dest = path.join(bucketPathNoPrefix, params.Key);
|
|
|
|
|
console.log('Copying:', source, dest, path.dirname(dest));
|
|
|
|
|
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
|
|
|
await fs.promises.copyFile(source, dest);
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-15 09:07:23 +02:00
|
|
|
async deleteObject(params) {
|
|
|
|
|
expect(params.Bucket).to.be(backupConfig.bucket);
|
2025-08-02 01:46:29 +02:00
|
|
|
fs.rmSync(path.join(bucketPathNoPrefix, params.Key));
|
2025-07-15 09:07:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
async deleteObjects(params) {
|
|
|
|
|
expect(params.Bucket).to.be(backupConfig.bucket);
|
2025-08-02 01:46:29 +02:00
|
|
|
params.Delete.Objects.forEach(o => fs.rmSync(path.join(bucketPathNoPrefix, o.Key)));
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
before(function () {
|
|
|
|
|
fs.rmSync(basePath, { recursive: true, force: true });
|
|
|
|
|
globalThis.S3Mock = S3Mock;
|
|
|
|
|
globalThis.S3MockUpload = S3MockUpload;
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2017-10-02 20:08:00 -07:00
|
|
|
after(function () {
|
2025-02-12 20:56:46 +01:00
|
|
|
// fs.rmSync(basePath, { recursive: true, force: true });
|
|
|
|
|
delete globalThis.S3Mock;
|
|
|
|
|
delete globalThis.S3MockUpload;
|
2017-10-02 20:08:00 -07:00
|
|
|
});
|
2017-04-18 19:15:56 +02:00
|
|
|
|
2024-07-08 22:29:45 +02:00
|
|
|
it('can upload', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
|
|
|
|
const sourceStream = fs.createReadStream(sourceFile);
|
|
|
|
|
const destKey = 'uploadtest/test.txt';
|
2025-11-28 17:40:13 +01:00
|
|
|
const uploader = await s3.upload(backupConfig, {}, destKey);
|
2024-07-08 22:29:45 +02:00
|
|
|
await stream.pipeline(sourceStream, uploader.stream);
|
|
|
|
|
await uploader.finish();
|
2025-02-12 20:56:46 +01:00
|
|
|
expect(fs.existsSync(path.join(bucketPath, destKey))).to.be(true);
|
|
|
|
|
expect(fs.statSync(path.join(bucketPath, destKey)).size).to.be(fs.statSync(sourceFile).size);
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2023-07-28 13:15:08 +05:30
|
|
|
it('can download file', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceKey = 'uploadtest/test.txt';
|
2025-02-12 20:56:46 +01:00
|
|
|
const [error, outstream] = await safe(s3.download(backupConfig, sourceKey));
|
2023-07-28 13:15:08 +05:30
|
|
|
expect(error).to.be(null);
|
2025-02-12 20:56:46 +01:00
|
|
|
expect(outstream).to.be.an('object');
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2025-02-12 18:46:54 +01:00
|
|
|
it('list dir lists contents of source dir', async function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
let allFiles = [], marker = null;
|
2025-02-12 18:46:54 +01:00
|
|
|
|
|
|
|
|
while (true) {
|
2025-02-12 20:56:46 +01:00
|
|
|
const result = await s3.listDir(backupConfig, '', 1, marker);
|
2025-02-12 18:46:54 +01:00
|
|
|
allFiles = allFiles.concat(result.entries);
|
|
|
|
|
if (!result.marker) break;
|
|
|
|
|
marker = result.marker;
|
|
|
|
|
}
|
2017-04-21 17:21:10 +02:00
|
|
|
|
2025-08-02 10:24:51 +02:00
|
|
|
expect(allFiles.map(function (f) { return f.path; })).to.contain('uploadtest/test.txt');
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2022-04-30 16:01:42 -07:00
|
|
|
it('can copy', async function () {
|
2025-02-12 20:56:46 +01:00
|
|
|
fs.writeFileSync(path.join(bucketPath, 'uploadtest/C++.gitignore'), 'special', 'utf8');
|
2017-10-03 15:40:01 -07:00
|
|
|
|
2025-11-28 17:40:13 +01:00
|
|
|
await s3.copyDir(backupConfig, {}, 'uploadtest', 'uploadtest-copy', () => {});
|
2022-04-30 16:01:42 -07:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
2025-02-12 20:56:46 +01:00
|
|
|
expect(fs.statSync(path.join(bucketPath, 'uploadtest-copy/test.txt')).size).to.be(fs.statSync(sourceFile).size);
|
|
|
|
|
expect(fs.statSync(path.join(bucketPath, 'uploadtest-copy/C++.gitignore')).size).to.be(7);
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2024-07-08 22:29:45 +02:00
|
|
|
it('can remove file', async function () {
|
2025-02-12 20:56:46 +01:00
|
|
|
await s3.remove(backupConfig, 'uploadtest/test.txt');
|
|
|
|
|
expect(fs.existsSync(path.join(bucketPath, 'uploadtest/test.txt'))).to.be(false);
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
it('cannot remove non-existent file', async function () {
|
|
|
|
|
const [error] = await safe(s3.remove(backupConfig, 'blah'));
|
|
|
|
|
expect(error).to.be.ok();
|
2017-04-18 17:34:42 +02:00
|
|
|
});
|
|
|
|
|
});
|
2017-09-17 17:51:00 +02:00
|
|
|
|
|
|
|
|
describe('gcs', function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
const backupConfig = {
|
2017-09-17 17:51:00 +02:00
|
|
|
provider: 'gcs',
|
|
|
|
|
key: '',
|
|
|
|
|
prefix: 'unit.test',
|
|
|
|
|
bucket: 'cloudron-storage-test',
|
2025-02-12 20:56:46 +01:00
|
|
|
projectId: 'some-project',
|
2017-09-17 17:51:00 +02:00
|
|
|
credentials: {
|
2025-02-12 20:56:46 +01:00
|
|
|
client_email: 'some-client',
|
|
|
|
|
private_key: 'some-key'
|
2017-09-17 17:51:00 +02:00
|
|
|
}
|
|
|
|
|
};
|
2025-02-12 20:56:46 +01:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
const basePath = path.join(os.tmpdir(), 'gcs-backup-test-buckets/');
|
|
|
|
|
const bucketPath = path.join(basePath, backupConfig.bucket, backupConfig.prefix);
|
|
|
|
|
const bucketPathNoPrefix = path.join(basePath, backupConfig.bucket);
|
|
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
class GCSMockBucket {
|
|
|
|
|
constructor(name) {
|
2025-08-02 01:46:29 +02:00
|
|
|
expect(name).to.be(backupConfig.bucket);
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
2025-08-02 01:46:29 +02:00
|
|
|
file(key) { // already has prefix
|
|
|
|
|
// console.log('gcs file object:', key);
|
|
|
|
|
function getFullWritablePath(key) {
|
|
|
|
|
const fullPath = path.join(bucketPathNoPrefix, key);
|
|
|
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
|
|
|
return fullPath;
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2022-04-14 20:43:04 -05:00
|
|
|
return {
|
2025-08-02 01:46:29 +02:00
|
|
|
name: key,
|
2025-02-12 20:56:46 +01:00
|
|
|
createReadStream: function() {
|
2025-08-02 01:46:29 +02:00
|
|
|
return fs.createReadStream(getFullWritablePath(key))
|
2025-02-12 20:56:46 +01:00
|
|
|
.on('error', function(e){
|
2025-08-02 01:46:29 +02:00
|
|
|
console.log('error createReadStream: '+key);
|
2025-02-12 20:56:46 +01:00
|
|
|
if (e.code == 'ENOENT') { e.code = 404; }
|
|
|
|
|
this.emit('error', e);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
createWriteStream: function() {
|
2025-08-02 01:46:29 +02:00
|
|
|
return fs.createWriteStream(getFullWritablePath(key));
|
2025-02-12 20:56:46 +01:00
|
|
|
},
|
|
|
|
|
delete: async function() {
|
2025-08-02 01:46:29 +02:00
|
|
|
await fs.promises.unlink(getFullWritablePath(key));
|
2025-02-12 20:56:46 +01:00
|
|
|
},
|
2025-08-02 01:46:29 +02:00
|
|
|
copy: async function(destKey) {
|
|
|
|
|
// console.log('gcs copy:', key, destKey);
|
|
|
|
|
await fs.promises.mkdir(path.dirname(path.join(bucketPathNoPrefix, destKey)), { recursive: true });
|
|
|
|
|
await fs.promises.copyFile(path.join(bucketPathNoPrefix, key), path.join(bucketPathNoPrefix, destKey));
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
async getFiles(q) {
|
2025-08-02 01:46:29 +02:00
|
|
|
expect(q.maxResults).to.be.a('number');
|
|
|
|
|
expect(q.prefix).to.be.a('string');
|
2025-02-12 20:56:46 +01:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
const files = [{
|
|
|
|
|
name: `${backupConfig.prefix}/uploadtest/test.txt`,
|
|
|
|
|
}, {
|
|
|
|
|
name: `${backupConfig.prefix}/uploadtest/C++.gitignore`,
|
|
|
|
|
}];
|
2025-02-12 20:56:46 +01:00
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
return [ files, null ];
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class GCSMock {
|
|
|
|
|
constructor(config) {
|
2025-08-02 01:46:29 +02:00
|
|
|
expect(config.projectId).to.be(backupConfig.projectId);
|
|
|
|
|
expect(config.credentials.private_key).to.be(backupConfig.credentials.private_key);
|
2025-02-12 20:56:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket(name) {
|
|
|
|
|
return new GCSMockBucket(name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
before(function () {
|
|
|
|
|
globalThis.GCSMock = GCSMock;
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2025-02-12 20:56:46 +01:00
|
|
|
after(function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
fs.rmSync(basePath, { recursive: true, force: true });
|
2025-02-12 20:56:46 +01:00
|
|
|
delete globalThis.GCSMock;
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
it('can upload', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
|
|
|
|
const sourceStream = fs.createReadStream(sourceFile);
|
|
|
|
|
const destKey = 'uploadtest/test.txt';
|
2025-11-28 17:40:13 +01:00
|
|
|
const uploader = await gcs.upload(backupConfig, {}, destKey);
|
2024-07-08 22:29:45 +02:00
|
|
|
await stream.pipeline(sourceStream, uploader.stream);
|
|
|
|
|
await uploader.finish();
|
2025-08-02 01:46:29 +02:00
|
|
|
expect(fs.existsSync(path.join(bucketPath, destKey))).to.be(true);
|
|
|
|
|
expect(fs.statSync(path.join(bucketPath, destKey)).size).to.be(fs.statSync(sourceFile).size);
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2023-07-28 13:15:08 +05:30
|
|
|
it('can download file', async function () {
|
2022-04-14 17:41:41 -05:00
|
|
|
const sourceKey = 'uploadtest/test.txt';
|
2025-08-02 01:46:29 +02:00
|
|
|
const [error, stream] = await safe(gcs.download(backupConfig, sourceKey));
|
2023-07-28 13:15:08 +05:30
|
|
|
expect(error).to.be(null);
|
|
|
|
|
expect(stream).to.be.an('object');
|
2017-10-29 11:10:50 +01:00
|
|
|
});
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2025-02-12 18:46:54 +01:00
|
|
|
it('list dir lists contents of source dir', async function () {
|
|
|
|
|
let allFiles = [ ], marker = null;
|
|
|
|
|
|
|
|
|
|
while (true) {
|
2025-08-02 01:46:29 +02:00
|
|
|
const result = await gcs.listDir(backupConfig, '', 1, marker);
|
2025-02-12 18:46:54 +01:00
|
|
|
allFiles = allFiles.concat(result.entries);
|
|
|
|
|
if (!result.marker) break;
|
|
|
|
|
marker = result.marker;
|
|
|
|
|
}
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2025-08-02 10:24:51 +02:00
|
|
|
expect(allFiles.map(function (f) { return f.path; })).to.contain('uploadtest/test.txt');
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2025-08-02 01:46:29 +02:00
|
|
|
it('can copy', async function () {
|
|
|
|
|
fs.writeFileSync(path.join(bucketPath, 'uploadtest/C++.gitignore'), 'special', 'utf8');
|
2017-09-17 17:51:00 +02:00
|
|
|
|
2025-11-28 17:40:13 +01:00
|
|
|
await gcs.copyDir(backupConfig, {}, 'uploadtest', 'uploadtest-copy', () => {});
|
2025-08-02 01:46:29 +02:00
|
|
|
const sourceFile = path.join(__dirname, 'storage/data/test.txt');
|
|
|
|
|
expect(fs.statSync(path.join(bucketPath, 'uploadtest-copy/test.txt')).size).to.be(fs.statSync(sourceFile).size);
|
|
|
|
|
expect(fs.statSync(path.join(bucketPath, 'uploadtest-copy/C++.gitignore')).size).to.be(7);
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2022-04-14 20:43:04 -05:00
|
|
|
it('can remove file', async function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
await gcs.remove(backupConfig, 'uploadtest-copy/test.txt');
|
|
|
|
|
expect(fs.existsSync(path.join(basePath, 'uploadtest-copy/test.txt'))).to.be(false);
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
|
2022-04-14 20:43:04 -05:00
|
|
|
it('can remove non-existent dir', async function () {
|
2025-08-02 01:46:29 +02:00
|
|
|
await gcs.remove(backupConfig, 'blah', () => {});
|
2017-09-17 17:51:00 +02:00
|
|
|
});
|
|
|
|
|
});
|
2017-04-17 14:46:39 +02:00
|
|
|
});
|