Compare commits

..

26 Commits

Author SHA1 Message Date
Girish Ramakrishnan f8d3a7cadd Bump mail container (fixes spam crash) 2017-10-06 16:45:21 -07:00
Girish Ramakrishnan d04a09b015 Add note on bumping major infra version 2017-10-06 15:52:04 -07:00
Girish Ramakrishnan 5d997bcc89 Just mark DO Spaces as experimental instead 2017-10-06 14:45:14 -07:00
Girish Ramakrishnan f0dd90a1f5 listObjectsV2 does not work on some S3 providers
specifically, cloudscale does not support it
2017-10-05 12:07:14 -07:00
Girish Ramakrishnan ee8ee8e786 KeyCount is not set on some S3 providers 2017-10-05 11:36:54 -07:00
Girish Ramakrishnan ee1a4411f8 Do not crash if prefix is empty string
('' || undefined) will return undefined ...
2017-10-05 11:08:01 -07:00
Girish Ramakrishnan df6e6cb071 Allow s3 backend to accept self-signed certs
Fixes #316
2017-10-05 10:14:55 -07:00
Girish Ramakrishnan ba5645a20e Disable DO spaces since it is not yet production ready 2017-10-05 09:21:26 -07:00
Girish Ramakrishnan ca502a2d55 Display error code 2017-10-04 22:34:44 -07:00
Girish Ramakrishnan ecd53b48db Display the backup format 2017-10-04 22:11:11 -07:00
Girish Ramakrishnan b9efb0b50b Fix callback invokation 2017-10-04 19:28:40 -07:00
Johannes Zellner 3fb5034ebd Ensure we setup the correct OAuth redirectURI if altDomain is used 2017-10-05 01:10:25 +02:00
Girish Ramakrishnan afed3f3725 Remove duplicate debug 2017-10-04 15:08:26 -07:00
Girish Ramakrishnan b4f14575d7 Add 1.7.1 changes 2017-10-04 14:31:41 -07:00
Johannes Zellner f437a1f48c Only allow dns setup with subdomain if enterprise query argument is provided 2017-10-04 22:25:14 +02:00
Girish Ramakrishnan c3d7d867be Do not set logCallback 2017-10-04 12:32:12 -07:00
Girish Ramakrishnan 96c16cd5d2 remove debug 2017-10-04 11:54:17 -07:00
Girish Ramakrishnan af182e3df6 caas: cache the creds, otherwise we bombard the server 2017-10-04 11:49:38 -07:00
Girish Ramakrishnan d70ff7cd5b Make copy() return event emitter
This way the storage logic does not need to rely on progress
2017-10-04 11:02:50 -07:00
Johannes Zellner 38331e71e2 Ensure all S3 CopySource properties are URI encoded 2017-10-04 19:07:08 +02:00
Johannes Zellner 322a9a18d7 Use multipart copy for s3 and files larger than 5GB 2017-10-04 18:56:23 +02:00
Johannes Zellner 423ef546a9 Merge branch 'user_agent' into 'master'
Added user agent to health checks

See merge request !19
2017-10-04 11:48:02 +00:00
Dennis Schwerdel e3f3241966 Added user agent to health checks 2017-10-04 13:05:00 +02:00
Johannes Zellner eaef384ea5 Improve the invite link display
Fixes #445
2017-10-04 13:03:32 +02:00
Girish Ramakrishnan b85bc3aa01 s3: Must encode copySource
https://github.com/aws/aws-sdk-js/issues/1302
2017-10-03 15:51:05 -07:00
Girish Ramakrishnan 01154d0ae6 s3: better error messages 2017-10-03 14:46:59 -07:00
20 changed files with 259 additions and 73 deletions
+29 -2
View File
@@ -1017,9 +1017,36 @@
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message
'Send MAIL FROM first'
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
[1.7.1]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add DigitalOcean spaces backup storage backend
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
[1.7.2]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
+1 -1
View File
@@ -248,7 +248,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + config.appFqdn(app.location);
var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location));
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
+11 -11
View File
@@ -94,19 +94,20 @@ function checkAppHealth(app, callback) {
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
});
}
@@ -156,7 +157,6 @@ function processDockerEvents() {
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
debug('Container ' + ev.id + ' went OOM');
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
+10 -4
View File
@@ -170,9 +170,9 @@ function getBackupFilePath(backupConfig, backupId, format) {
if (format === 'tgz') {
const fileType = backupConfig.key ? '.tar.gz.enc' : '.tar.gz';
return path.join(backupConfig.prefix || backupConfig.backupFolder, backupId+fileType);
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId+fileType);
} else {
return path.join(backupConfig.prefix || backupConfig.backupFolder, backupId);
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId);
}
}
@@ -491,7 +491,10 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
progress.setDetail(progress.BACKUP, 'Rotating box snapshot');
api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format), function (copyBackupError) {
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', function (detail) { progress.setDetail(progress.BACKUP, detail); });
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
backupdb.update(backupId, { state: state }, function (error) {
@@ -590,7 +593,10 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
progress.setDetail(progress.BACKUP, 'Rotating app snapshot');
api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format), function (copyBackupError) {
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', function (detail) { progress.setDetail(progress.BACKUP, detail); });
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
debugApp(app, 'rotateAppBackup: successful id:%s', backupId);
+2 -2
View File
@@ -5,7 +5,7 @@
// Do not require anything here!
exports = module.exports = {
// a major version makes all apps restore from backup
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.6.0',
@@ -18,7 +18,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.37.1' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.37.2' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
}
};
-3
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:progress');
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
@@ -42,8 +41,6 @@ function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (config.TEST && !progress[tag]) progress[tag] = { };
progress[tag].detail = detail;
}
+1
View File
@@ -275,6 +275,7 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
settings.setBackupConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
+9 -5
View File
@@ -18,6 +18,7 @@ exports = module.exports = {
var assert = require('assert'),
BackupsError = require('../backups.js').BackupsError,
debug = require('debug')('box:storage/filesystem'),
EventEmitter = require('events'),
fs = require('fs'),
mkdirp = require('mkdirp'),
path = require('path'),
@@ -90,24 +91,27 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
});
}
function copy(apiConfig, oldFilePath, newFilePath, callback) {
function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
debug('copy: %s -> %s', oldFilePath, newFilePath);
var events = new EventEmitter();
mkdirp(path.dirname(newFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
// this will hardlink backups saving space
shell.exec('copy', '/bin/cp', [ '-al', oldFilePath, newFilePath ], { }, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback(null);
events.emit('done', null);
});
});
return events;
}
function remove(apiConfig, filename, callback) {
+6 -6
View File
@@ -20,7 +20,8 @@ exports = module.exports = {
testConfig: testConfig
};
var assert = require('assert');
var assert = require('assert'),
EventEmitter = require('events');
function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof apiConfig, 'object');
@@ -51,15 +52,14 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
callback(new Error('not implemented'));
}
function copy(apiConfig, oldFilePath, newFilePath, callback) {
function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
// Result: none
callback(new Error('not implemented'));
var events = new EventEmitter();
process.nextTick(function () { events.emit('done', null); });
return events;
}
function remove(apiConfig, filename, callback) {
+6 -4
View File
@@ -15,7 +15,8 @@ exports = module.exports = {
};
var assert = require('assert'),
debug = require('debug')('box:storage/noop');
debug = require('debug')('box:storage/noop'),
EventEmitter = require('events');
function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof apiConfig, 'object');
@@ -49,15 +50,16 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
callback(new Error('Cannot download from noop backend'));
}
function copy(apiConfig, oldFilePath, newFilePath, callback) {
function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
debug('copy: %s -> %s', oldFilePath, newFilePath);
callback(null);
var events = new EventEmitter();
process.nextTick(function () { events.emit('done', null); });
return events;
}
function remove(apiConfig, filename, callback) {
+103 -26
View File
@@ -22,14 +22,15 @@ var assert = require('assert'),
async = require('async'),
AWS = require('aws-sdk'),
BackupsError = require('../backups.js').BackupsError,
chunk = require('lodash.chunk'),
config = require('../config.js'),
debug = require('debug')('box:storage/s3'),
EventEmitter = require('events'),
fs = require('fs'),
chunk = require('lodash.chunk'),
https = require('https'),
mkdirp = require('mkdirp'),
PassThrough = require('stream').PassThrough,
path = require('path'),
progress = require('../progress.js'),
S3BlockReadStream = require('s3-block-read-stream'),
superagent = require('superagent');
@@ -44,12 +45,19 @@ function mockRestore() {
AWS = originalAWS;
}
// TODO: If we decide to use rsync backups for CaaS, we should cache the credentials below
var gCachedCaasCredentials = { issueDate: null, credentials: null };
function getCaasCredentials(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
assert(apiConfig.token);
if ((new Date() - gCachedCaasCredentials.issueDate) <= (1.75 * 60 * 60 * 1000)) { // caas gives tokens with 2 hour limit
return callback(null, gCachedCaasCredentials.credentials);
}
debug('getCaasCredentials: getting new credentials');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(error);
@@ -66,6 +74,11 @@ function getCaasCredentials(apiConfig, callback) {
if (apiConfig.endpoint) credentials.endpoint = new AWS.Endpoint(apiConfig.endpoint);
gCachedCaasCredentials = {
issueDate: new Date(),
credentials: credentials
};
callback(null, credentials);
});
}
@@ -86,6 +99,11 @@ function getBackupCredentials(apiConfig, callback) {
if (apiConfig.endpoint) credentials.endpoint = apiConfig.endpoint;
if (apiConfig.acceptSelfSignedCerts === true) {
credentials.httpOptions = {
agent: new https.Agent({ rejectUnauthorized: false })
};
}
callback(null, credentials);
}
@@ -110,7 +128,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error) {
if (error) {
debug('[%s] upload: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
callback(null);
@@ -123,8 +141,6 @@ function download(apiConfig, backupFilePath, callback) {
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
debug('download: %s', backupFilePath);
getBackupCredentials(apiConfig, function (error, credentials) {
if (error) return callback(error);
@@ -136,7 +152,7 @@ function download(apiConfig, backupFilePath, callback) {
var s3 = new AWS.S3(credentials);
var ps = new PassThrough();
var multipartDownload = new S3BlockReadStream(s3, params, { blockSize: 64 * 1024 * 1024, logCallback: debug });
var multipartDownload = new S3BlockReadStream(s3, params, { blockSize: 64 * 1024 * 1024 /*, logCallback: debug */ });
multipartDownload.on('error', function (error) {
if (error.code === 'NoSuchKey' || error.code === 'ENOENT') {
@@ -166,24 +182,24 @@ function listDir(apiConfig, backupFilePath, options, iteratorCallback, callback)
var total = 0;
async.forever(function listAndDownload(foreverCallback) {
s3.listObjectsV2(listParams, function (error, listData) {
s3.listObjects(listParams, function (error, listData) {
if (error) {
debug('remove: Failed to list %s. Not fatal.', error);
return foreverCallback(error);
}
debug('listDir: processing %s files (processed %s so far)', listData.Contents.length, total);
debug('listDir: processing %s files (processed %s so far). From "%s"', listData.Contents.length, total, listParams.Marker || '');
var arr = options.batchSize === 1 ? listData.Contents : chunk(listData.Contents, options.batchSize);
async.eachLimit(arr, 10, iteratorCallback.bind(null, s3), function iteratorDone(error) {
if (error) return foreverCallback(error);
total += listData.KeyCount;
total += listData.Contents.length;
if (!listData.IsTruncated) return foreverCallback(new Error('Done'));
listParams.StartAfter = listData.Contents[listData.Contents.length - 1].Key; // NextMarker is returned only with delimiter
listParams.Marker = listData.Contents[listData.Contents.length - 1].Key; // NextMarker is returned only with delimiter
foreverCallback();
});
@@ -227,33 +243,94 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
}, callback);
}
function copy(apiConfig, oldFilePath, newFilePath, callback) {
function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var events = new EventEmitter();
listDir(apiConfig, oldFilePath, { batchSize: 1 }, function copyFile(s3, content, iteratorCallback) {
var relativePath = path.relative(oldFilePath, content.Key);
var copyParams = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
CopySource: path.join(apiConfig.bucket, content.Key)
};
progress.setDetail(progress.BACKUP, 'Copying ' + content.Key.slice(oldFilePath.length+1));
s3.copyObject(copyParams, function (error) {
if (error && error.code === 'NoSuchKey') return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
function done(error) {
if (error && error.code === 'NoSuchKey') return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, `Old backup not found: ${content.Key}`));
if (error) {
debug('copy: s3 copy error.', error);
return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error copying ${content.Key} : ${error.message}`));
}
iteratorCallback();
iteratorCallback(null);
}
var copyParams = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath)
};
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
if (content.Size < 5 * 1024 * 1024 * 1024) {
events.emit('progress', 'Copying ' + content.Key.slice(oldFilePath.length+1));
copyParams.CopySource = encodeURIComponent(path.join(apiConfig.bucket, content.Key)); // See aws-sdk-js/issues/1302
return s3.copyObject(copyParams, done);
}
events.emit('progress', 'Copying (multipart) ' + content.Key.slice(oldFilePath.length+1));
s3.createMultipartUpload(copyParams, function (error, result) {
if (error) return done(error);
const CHUNK_SIZE = 1024 * 1024 * 1024; // 1GB - rather random size
var uploadId = result.UploadId;
var uploadedParts = [];
var partNumber = 1;
var startBytes = 0;
var endBytes = 0;
var size = content.Size-1;
function copyNextChunk() {
endBytes = startBytes + CHUNK_SIZE;
if (endBytes > size) endBytes = size;
var params = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
CopySource: encodeURIComponent(path.join(apiConfig.bucket, content.Key)),
CopySourceRange: 'bytes=' + startBytes + '-' + endBytes,
PartNumber: partNumber,
UploadId: uploadId
};
s3.uploadPartCopy(params, function (error, result) {
if (error) return done(error);
uploadedParts.push({ ETag: result.CopyPartResult.ETag, PartNumber: partNumber });
if (endBytes < size) {
startBytes = endBytes + 1;
partNumber++;
return copyNextChunk();
}
var params = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
MultipartUpload: { Parts: uploadedParts },
UploadId: uploadId
};
s3.completeMultipartUpload(params, done);
});
}
copyNextChunk();
});
}, callback);
}, function (error) {
events.emit('done', error);
});
return events;
}
function remove(apiConfig, filename, callback) {
+11 -3
View File
@@ -112,7 +112,8 @@ describe('Storage', function () {
var sourceFile = gTmpFolder + '/uploadtest/test.txt'; // keep the test within save device
var destFile = gTmpFolder + '/uploadtest/test-hardlink.txt';
filesystem.copy(gBackupConfig, sourceFile, destFile, function (error) {
var events = filesystem.copy(gBackupConfig, sourceFile, destFile);
events.on('done', function (error) {
expect(error).to.be(null);
expect(fs.statSync(destFile).nlink).to.be(2); // created a hardlink
done();
@@ -169,7 +170,8 @@ describe('Storage', function () {
});
it('can copy', function (done) {
noop.copy(gBackupConfig, 'sourceFile', 'destFile', function (error) {
var events = noop.copy(gBackupConfig, 'sourceFile', 'destFile');
events.on('done', function (error) {
expect(error).to.be(null);
done();
});
@@ -252,12 +254,18 @@ describe('Storage', function () {
});
it('can copy', function (done) {
fs.writeFileSync(path.join(gS3Folder, 'uploadtest/C++.gitignore'), 'special', 'utf8');
var sourceKey = 'uploadtest';
s3.copy(gBackupConfig, sourceKey, 'uploadtest-copy', function (error) {
var events = s3.copy(gBackupConfig, sourceKey, 'uploadtest-copy');
events.on('done', function (error) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
expect(error).to.be(null);
expect(fs.statSync(path.join(gS3Folder, 'uploadtest-copy/test.txt')).size).to.be(fs.statSync(sourceFile).size);
expect(fs.statSync(path.join(gS3Folder, 'uploadtest-copy/C++.gitignore')).size).to.be(7);
done();
});
});
File diff suppressed because one or more lines are too long
+1
View File
@@ -52,6 +52,7 @@
<script src="3rdparty/js/Chart.js"></script>
<script src="3rdparty/js/ansi_up.js"></script>
<script src="3rdparty/js/clipboard.min.js"></script>
<!-- Showdown (markdown converter) -->
<script src="3rdparty/js/showdown-1.6.4.min.js"></script>
+1
View File
@@ -21,6 +21,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
$scope.showDNSSetup = false;
$scope.instanceId = '';
$scope.explicitZone = search.zone || '';
$scope.isEnterprise = !!search.enterprise;
$scope.isDomain = false;
$scope.isSubdomain = false;
+2 -2
View File
@@ -58,7 +58,7 @@
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
</div>
<p ng-show="isSubdomain" class="text-bold">Installing on a subdomain requires an enterprise subscription.</p>
<p ng-show="isSubdomain" class="text-bold">Installing on a subdomain requires an enterprise subscription. <a href="mailto:support@cloudron.io">Contact us</a></p>
</div>
</div>
<div class="row">
@@ -137,7 +137,7 @@
</div>
<div class="row">
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid || (isSubdomain && !isEnterprise)"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
</div>
</div>
</form>
+17
View File
@@ -152,6 +152,14 @@
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL of Minio/S3 Compatible" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'">
</div>
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
<label>
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts" id="inputConfigureBackupSelfSigned">
Accept Self-signed certificate
</input>
</label>
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider)">
<label class="control-label" for="inputConfigureBackupBucket">Bucket name</label>
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
@@ -346,6 +354,15 @@
</div>
</div>
<div class="row" ng-show="backupConfig.provider !== 'caas'">
<div class="col-xs-6">
<span class="text-muted">Format</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ backupConfig.format }}</span>
</div>
</div>
<br/>
<div class="row">
+5 -1
View File
@@ -42,7 +42,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.storageProvider = [
{ name: 'Amazon S3', value: 's3' },
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
{ name: 'DigitalOcean Spaces NYC3 (Experimental)', value: 'digitalocean-spaces' },
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Minio', value: 'minio' },
@@ -326,6 +326,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
endpoint: '',
backupFolder: '',
retentionSecs: -1,
acceptSelfSignedCerts: false,
format: 'tgz',
clearForm: function () {
@@ -338,6 +339,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.configureBackup.backupFolder = '';
$scope.configureBackup.retentionSecs = -1;
$scope.configureBackup.format = 'tgz';
$scope.configureBackup.acceptSelfSignedCerts = false;
},
show: function () {
@@ -355,6 +357,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder;
$scope.configureBackup.retentionSecs = $scope.backupConfig.retentionSecs;
$scope.configureBackup.format = $scope.backupConfig.format;
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
$('#configureBackupModal').modal('show');
},
@@ -383,6 +386,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
if ($scope.configureBackup.region) backupConfig.region = $scope.configureBackup.region;
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
backupConfig.region = 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.endpoint = 'https://sos.exo.io';
backupConfig.region = 'us-east-1';
+7 -2
View File
@@ -222,10 +222,15 @@
<div class="modal-body">
<p>An email has been sent to <b>{{ inviteSent.email }}</b>.</p>
<p>You can also share this invite link directly:</p>
<p style="overflow: auto; white-space: nowrap;" eng-click-select>{{ inviteSent.setupLink }}</p>
<div class="input-group">
<input type="text" id="setupLinkInput" class="form-control" ng-value="inviteSent.setupLink" readonly/>
<span class="input-group-btn">
<button class="btn btn-default" id="setupLinkButton" type="button" data-clipboard-target="#setupLinkInput"><i class="fa fa-clipboard"></i></button>
</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
</div>
</div>
</div>
+30 -1
View File
@@ -1,6 +1,8 @@
'use strict';
angular.module('Application').controller('UsersController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
/* global Clipboard */
angular.module('Application').controller('UsersController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.ready = false;
@@ -380,6 +382,11 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
});
};
$scope.copyToClipboard = function (value) {
document.execCommand('copy');
};
function refresh() {
Client.getGroups(function (error, result) {
if (error) return console.error('Unable to get group listing.', error);
@@ -420,5 +427,27 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
});
});
var clipboard = new Clipboard('#setupLinkButton');
clipboard.on('success', function(e) {
$('#setupLinkButton').tooltip({
title: 'Copied!',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#setupLinkButton').tooltip('hide'); }, 2000);
e.clearSelection();
});
clipboard.on('error', function(e) {
$('#setupLinkButton').tooltip({
title: 'Press Ctrl+C to copy',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#setupLinkButton').tooltip('hide'); }, 2000);
});
$('.modal-backdrop').remove();
}]);