diff --git a/src/api/vm.coffee b/src/api/vm.coffee index 6a05aa3d5..5485bda1c 100644 --- a/src/api/vm.coffee +++ b/src/api/vm.coffee @@ -493,13 +493,6 @@ exports.snapshot = snapshot #--------------------------------------------------------------------- rollingDeltaBackup = $coroutine ({vm, remote, tag, depth}) -> - _remote = yield @getRemote remote - if not _remote?.path? - throw new Error "No such Remote #{remote}" - if not _remote.enabled - throw new Error "Backup remote #{remote} is disabled" - if _remote.type == 'smb' - throw new Error "Delta Backup is not supported for smb remotes" return yield @rollingDeltaVmBackup({ vm, remoteId: remote, @@ -628,11 +621,6 @@ exports.importBackup = importBackup #--------------------------------------------------------------------- rollingBackup = $coroutine ({vm, remoteId, tag, depth, compress, onlyMetadata}) -> - remote = yield @getRemote remoteId - if not remote?.path? - throw new Error "No such Remote #{remoteId}" - if not remote.enabled - throw new Error "Backup remote #{remoteId} is disabled" return yield @rollingBackupVm({ vm, remoteId, diff --git a/src/remote-handlers/abstract.js b/src/remote-handlers/abstract.js index e871bfa50..95f292bfd 100644 --- a/src/remote-handlers/abstract.js +++ b/src/remote-handlers/abstract.js @@ -1,3 +1,5 @@ +import eventToPromise from 'event-to-promise' + export default class RemoteHandlerAbstract { constructor (remote) { this._remote = remote @@ -8,42 +10,92 @@ export default class RemoteHandlerAbstract { } async sync () { + return await this._sync() + } + + async _sync () { throw new Error('Not implemented') } async forget () { + return await this._forget() + } + + async _forget () { throw new Error('Not implemented') } async outputFile (file, data, options) { - throw new Error('Not implemented') + return await this._outputFile(file, data, options) + } + + async _outputFile (file, data, options) { + const stream = this.createOutputStream(file) + const promise = eventToPromise(stream, 'finish') + stream.end(data) + return promise } async readFile (file, options) { - throw new Error('Not implemented') + return await this._readFile(file, options) + } + + async _readFile (file, options) { + const stream = this.createReadStream(file, options) + let data = '' + stream.on('data', d => data += d) + await eventToPromise(stream, 'end') + return data } async rename (oldPath, newPath) { + return await this._rename(oldPath, newPath) + } + + async _rename (oldPath, newPath) { throw new Error('Not implemented') } - async list (dir = undefined) { + async list (dir = '.') { + return await this._list(dir) + } + + async _list (dir = '.') { throw new Error('Not implemented') } - async createReadStream (file) { + async createReadStream (file, options) { + const length = await this.getSize(file) + const stream = await this._createReadStream(file) + stream.length = length + return stream + } + + async _createReadStream (file, options) { throw new Error('Not implemented') } - async createOutputStream (file) { + async createOutputStream (file, options) { + return await this._createOutputStream(file) + } + + async _createOutputStream (file, options) { throw new Error('Not implemented') } async unlink (file) { + return await this._unlink(file) + } + + async _unlink (file) { throw new Error('Not implemented') } async getSize (file) { + return await this._getSize(file) + } + + async _getSize (file) { throw new Error('Not implement') } } diff --git a/src/remote-handlers/local.js b/src/remote-handlers/local.js index 2dc08e919..0c2c8530a 100644 --- a/src/remote-handlers/local.js +++ b/src/remote-handlers/local.js @@ -1,23 +1,23 @@ import fs from 'fs-promise' import RemoteHandlerAbstract from './abstract' -import {dirname} from 'path' +import startsWith from 'lodash.startswith' import {noop} from '../utils' +import {resolve} from 'path' export default class LocalHandler extends RemoteHandlerAbstract { - constructor (remote) { - super(remote) - this.forget = noop - } - _getFilePath (file) { const parts = [this._remote.path] if (file) { parts.push(file) } - return parts.join('/') + const path = resolve.apply(null, parts) + if (!startsWith(path, this._remote.path)) { + throw new Error('Remote path is unavailable') + } + return path } - async sync () { + async _sync () { if (this._remote.enabled) { try { await fs.ensureDir(this._remote.path) @@ -30,39 +30,39 @@ export default class LocalHandler extends RemoteHandlerAbstract { return this._remote } - async outputFile (file, data, options) { - const path = this._getFilePath(file) - await fs.ensureDir(dirname(path)) - await fs.writeFile(path, data, options) + async _forget () { + return noop() } - async readFile (file, options) { + async _outputFile (file, data, options) { + await fs.outputFile(this._getFilePath(file), data, options) + } + + async _readFile (file, options) { return await fs.readFile(this._getFilePath(file), options) } - async rename (oldPath, newPath) { + async _rename (oldPath, newPath) { return await fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath)) } - async list (dir = undefined) { + async _list (dir = '.') { return await fs.readdir(this._getFilePath(dir)) } - async createReadStream (file) { - return fs.createReadStream(this._getFilePath(file)) + async _createReadStream (file, options) { + return await fs.createReadStream(this._getFilePath(file), options) } - async createOutputStream (file, options) { - const path = this._getFilePath(file) - await fs.ensureDir(dirname(path)) - return fs.createWriteStream(path, options) + async _createOutputStream (file, options) { + return await fs.createOutputStream(this._getFilePath(file), options) } - async unlink (file) { - return fs.unlink(this._getFilePath(file)) + async _unlink (file) { + return await fs.unlink(this._getFilePath(file)) } - async getSize (file) { + async _getSize (file) { const stats = await fs.stat(this._getFilePath(file)) return stats.size } diff --git a/src/remote-handlers/nfs.js b/src/remote-handlers/nfs.js index c4b8d309d..b7f882eb2 100644 --- a/src/remote-handlers/nfs.js +++ b/src/remote-handlers/nfs.js @@ -1,20 +1,11 @@ import fs from 'fs-promise' -import RemoteHandlerAbstract from './abstract' -import {dirname} from 'path' +import LocalHandler from './local' import {exec} from 'child_process' import {forEach, promisify} from '../utils' const execAsync = promisify(exec) -export default class NfsHandler extends RemoteHandlerAbstract { - _getFilePath (file) { - const parts = [this._remote.path] - if (file) { - parts.push(file) - } - return parts.join('/') - } - +export default class NfsHandler extends LocalHandler { async _loadRealMounts () { let stdout try { @@ -48,7 +39,7 @@ export default class NfsHandler extends RemoteHandlerAbstract { return await execAsync(`mount -t nfs ${remote.host}:${remote.share} ${remote.path}`) } - async sync () { + async _sync () { await this._loadRealMounts() if (this._matchesRealMount(this._remote) && !this._remote.enabled) { try { @@ -68,7 +59,7 @@ export default class NfsHandler extends RemoteHandlerAbstract { return this._remote } - async forget () { + async _forget () { try { await this._umount(this._remote) } catch (_) { @@ -79,41 +70,4 @@ export default class NfsHandler extends RemoteHandlerAbstract { async _umount (remote) { await execAsync(`umount ${remote.path}`) } - - async outputFile (file, data, options) { - const path = this._getFilePath(file) - await fs.ensureDir(dirname(path)) - await fs.writeFile(path, data, options) - } - - async readFile (file, options) { - return await fs.readFile(this._getFilePath(file), options) - } - - async rename (oldPath, newPath) { - return await fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath)) - } - - async list (dir = undefined) { - return await fs.readdir(this._getFilePath(dir)) - } - - async createReadStream (file) { - return fs.createReadStream(this._getFilePath(file)) - } - - async createOutputStream (file, options) { - const path = this._getFilePath(file) - await fs.ensureDir(dirname(path)) - return fs.createWriteStream(path, options) - } - - async unlink (file) { - return fs.unlink(this._getFilePath(file)) - } - - async getSize (file) { - const stats = await fs.stat(this._getFilePath(file)) - return stats.size - } } diff --git a/src/remote-handlers/smb.js b/src/remote-handlers/smb.js index e6f8a6918..8eb74420e 100644 --- a/src/remote-handlers/smb.js +++ b/src/remote-handlers/smb.js @@ -5,7 +5,7 @@ import RemoteHandlerAbstract from './abstract' export default class SmbHandler extends RemoteHandlerAbstract { constructor (remote) { super(remote) - this.forget = noop + this._forget = noop } _getClient (remote) { @@ -19,6 +19,9 @@ export default class SmbHandler extends RemoteHandlerAbstract { } _getFilePath (file) { + if (file === '.') { + file = undefined + } const parts = [] if (this._remote.path !== '') { parts.push(this._remote.path) @@ -29,13 +32,13 @@ export default class SmbHandler extends RemoteHandlerAbstract { return parts.join('\\') } - _getDirname (file) { + _dirname (file) { const parts = file.split('\\') parts.pop() return parts.join('\\') } - async sync () { + async _sync () { if (this._remote.enabled) { try { // Check access (smb2 does not expose connect in public so far...) @@ -48,10 +51,10 @@ export default class SmbHandler extends RemoteHandlerAbstract { return this._remote } - async outputFile (file, data, options) { + async _outputFile (file, data, options) { const client = this._getClient(this._remote) const path = this._getFilePath(file) - const dir = this._getDirname(path) + const dir = this._dirname(path) try { if (dir) { await client.ensureDir(dir) @@ -62,7 +65,7 @@ export default class SmbHandler extends RemoteHandlerAbstract { } } - async readFile (file, options) { + async _readFile (file, options) { const client = this._getClient(this._remote) try { return await client.readFile(this._getFilePath(file), options) @@ -71,7 +74,7 @@ export default class SmbHandler extends RemoteHandlerAbstract { } } - async rename (oldPath, newPath) { + async _rename (oldPath, newPath) { const client = this._getClient(this._remote) try { return await client.rename(this._getFilePath(oldPath), this._getFilePath(newPath)) @@ -80,7 +83,7 @@ export default class SmbHandler extends RemoteHandlerAbstract { } } - async list (dir = undefined) { + async _list (dir = '.') { const client = this._getClient(this._remote) try { return await client.readdir(this._getFilePath(dir)) @@ -89,23 +92,23 @@ export default class SmbHandler extends RemoteHandlerAbstract { } } - async createReadStream (file) { + async _createReadStream (file, options) { const client = this._getClient(this._remote) - const stream = await client.createReadStream(this._getFilePath(file)) + const stream = await client.createReadStream(this._getFilePath(file), options) // FIXME ensure that options are properly handled by @marsaud/smb2 stream.on('end', () => client.close()) return stream } - async createOutputStream (file, options) { + async _createOutputStream (file, options) { const client = this._getClient(this._remote) const path = this._getFilePath(file) - const dir = this._getDirname(path) + const dir = this._dirname(path) let stream try { if (dir) { await client.ensureDir(dir) } - stream = await client.createWriteStream(path, options/* , { flags: 'wx' }*/) // TODO ensure that wx flag is properly handled by @marsaud/smb2 + stream = await client.createWriteStream(path, options) // FIXME ensure that options are properly handled by @marsaud/smb2 } catch (err) { client.close() throw err @@ -114,7 +117,7 @@ export default class SmbHandler extends RemoteHandlerAbstract { return stream } - async unlink (file) { + async _unlink (file) { const client = this._getClient(this._remote) try { return await client.unlink(this._getFilePath(file)) @@ -123,7 +126,7 @@ export default class SmbHandler extends RemoteHandlerAbstract { } } - async getSize (file) { + async _getSize (file) { const client = await this._getClient(this._remote) try { return await client.getSize(this._getFilePath(file)) diff --git a/src/xo-mixins/backups.js b/src/xo-mixins/backups.js index 93a7a9d7f..02def985a 100644 --- a/src/xo-mixins/backups.js +++ b/src/xo-mixins/backups.js @@ -378,6 +378,17 @@ export default class { async rollingDeltaVmBackup ({vm, remoteId, tag, depth}) { const remote = await this._xo.getRemote(remoteId) + + if (!remote) { + throw new Error(`No such Remote ${remoteId}`) + } + if (!remote.enabled) { + throw new Error(`Remote ${remoteId} is disabled`) + } + if (remote.type === 'smb') { + throw new Error('Delta Backup is not supported for smb remotes') + } + const dir = `vm_delta_${tag}_${vm.uuid}` const info = { @@ -589,6 +600,14 @@ export default class { async rollingBackupVm ({vm, remoteId, tag, depth, compress, onlyMetadata}) { const remote = await this._xo.getRemote(remoteId) + + if (!remote) { + throw new Error(`No such Remote s{remoteId}`) + } + if (!remote.enabled) { + throw new Error(`Backup remote ${remoteId} is disabled`) + } + const handler = this._xo.getRemoteHandler(remote) const files = await handler.list()