Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8d3a7cadd | |||
| d04a09b015 | |||
| 5d997bcc89 | |||
| f0dd90a1f5 | |||
| ee8ee8e786 | |||
| ee1a4411f8 | |||
| df6e6cb071 | |||
| ba5645a20e | |||
| ca502a2d55 | |||
| ecd53b48db | |||
| b9efb0b50b | |||
| 3fb5034ebd | |||
| afed3f3725 | |||
| b4f14575d7 | |||
| f437a1f48c | |||
| c3d7d867be | |||
| 96c16cd5d2 | |||
| af182e3df6 | |||
| d70ff7cd5b | |||
| 38331e71e2 | |||
| 322a9a18d7 | |||
| 423ef546a9 | |||
| e3f3241966 | |||
| eaef384ea5 | |||
| b85bc3aa01 | |||
| 01154d0ae6 |
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+7
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}]);
|
||||
|
||||
Reference in New Issue
Block a user