feat({fs,xo-server,xo-web}/remotes): show free space and usage (#3767)

Fixes #3055
This commit is contained in:
Enishowk
2019-01-11 10:07:22 +01:00
committed by Pierre Donias
parent f3c3889531
commit e34a0a6e33
12 changed files with 163 additions and 22 deletions

View File

@@ -21,6 +21,7 @@
},
"dependencies": {
"@marsaud/smb2": "^0.13.0",
"@sindresorhus/df": "^2.1.0",
"@xen-orchestra/async-map": "^0.0.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",

View File

@@ -19,6 +19,7 @@ type Data = Buffer | Readable | string
type FileDescriptor = {| fd: mixed, path: string |}
type LaxReadable = Readable & Object
type LaxWritable = Writable & Object
type RemoteInfo = { used?: number, size?: number }
type File = FileDescriptor | string
@@ -219,6 +220,10 @@ export default class RemoteHandlerAbstract {
await this._forget()
}
async getInfo(): Promise<RemoteInfo> {
return timeout.call(this._getInfo(), this._timeout)
}
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
@@ -427,6 +432,10 @@ export default class RemoteHandlerAbstract {
// called to finalize the remote
async _forget(): Promise<void> {}
async _getInfo(): Promise<Object> {
return {}
}
async _getSize(file: File): Promise<number> {
throw new Error('Not implemented')
}

View File

@@ -52,6 +52,18 @@ describe('createReadStream()', () => {
})
})
describe('getInfo()', () => {
it('throws in case of timeout', async () => {
const testHandler = new TestHandler({
getInfo: () => new Promise(() => {}),
})
const promise = testHandler.getInfo()
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getSize()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({

View File

@@ -116,6 +116,26 @@ handlers.forEach(url => {
})
})
describe('#getInfo()', () => {
let info
beforeAll(async () => {
info = await handler.getInfo()
})
it('should return an object with info', async () => {
expect(typeof info).toBe('object')
})
it('should return correct type of attribute', async () => {
if (info.size !== undefined) {
expect(typeof info.size).toBe('number')
}
if (info.used !== undefined) {
expect(typeof info.used).toBe('number')
}
})
})
describe('#getSize()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))

View File

@@ -1,3 +1,4 @@
import df from '@sindresorhus/df'
import fs from 'fs-extra'
import { fromEvent } from 'promise-toolbox'
@@ -46,6 +47,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
})
}
_getInfo() {
return df.file(this._getFilePath('/'))
}
async _getSize(file) {
const stats = await fs.stat(
this._getFilePath(typeof file === 'string' ? file : file.path)

View File

@@ -11,6 +11,7 @@
- [Backup NG / Health] Show number of lone snapshots in tab label [#3500](https://github.com/vatesfr/xen-orchestra/issues/3500) (PR [#3824](https://github.com/vatesfr/xen-orchestra/pull/3824))
- [Login] Add autofocus on username input on login page [#3835](https://github.com/vatesfr/xen-orchestra/issues/3835) (PR [#3836](https://github.com/vatesfr/xen-orchestra/pull/3836))
- [Home/VM] Bulk snapshot: specify snapshots' names [#3778](https://github.com/vatesfr/xen-orchestra/issues/3778) (PR [#3787](https://github.com/vatesfr/xen-orchestra/pull/3787))
- [Remotes] Show free space and disk usage on remote [#3055](https://github.com/vatesfr/xen-orchestra/issues/3055) (PR [#3767](https://github.com/vatesfr/xen-orchestra/pull/3767))
### Bug fixes

View File

@@ -15,6 +15,13 @@ get.params = {
id: { type: 'string' },
}
export async function getAllInfo() {
return this.getAllRemotesInfo()
}
getAllInfo.permission = 'admin'
getAllInfo.description = 'Gets all info of remote'
export async function test({ id }) {
return this.testRemote(id)
}

View File

@@ -1,7 +1,8 @@
import asyncMap from '@xen-orchestra/async-map'
import synchronized from 'decorator-synchronized'
import { format, parse } from 'xo-remote-parser'
import { getHandler } from '@xen-orchestra/fs'
import { ignoreErrors } from 'promise-toolbox'
import { ignoreErrors, timeout } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
@@ -18,13 +19,14 @@ const obfuscateRemote = ({ url, ...remote }) => {
export default class {
constructor(xo, { remoteOptions }) {
this._handlers = { __proto__: null }
this._remoteOptions = remoteOptions
this._remotes = new Remotes({
connection: xo._redis,
prefix: 'xo:remote',
indexes: ['enabled'],
})
this._handlers = { __proto__: null }
this._remotesInfo = {}
xo.on('clean', () => this._remotes.rebuildIndexes())
xo.on('start', async () => {
@@ -84,6 +86,23 @@ export default class {
return handler.test()
}
async getAllRemotesInfo() {
const remotes = await this._remotes.get()
await asyncMap(remotes, async remote => {
try {
const handler = await this.getRemoteHandler(remote.id)
await timeout.call(
handler.getInfo().then(info => {
this._remotesInfo[remote.id] = info
}),
this._remoteOptions.timeoutInfo
)
} catch (_) {}
})
return this._remotesInfo
}
async getAllRemotes() {
return (await this._remotes.get()).map(_ => obfuscateRemote(_))
}

View File

@@ -482,6 +482,7 @@ const messages = {
remotePath: 'Path',
remoteState: 'State',
remoteDevice: 'Device',
remoteDisk: 'Disk (Used / Total)',
remoteOptions: 'Options',
remoteShare: 'Share',
remoteAction: 'Action',

View File

@@ -312,6 +312,10 @@ export const subscribePlugins = createSubscription(() => _call('plugin.get'))
export const subscribeRemotes = createSubscription(() => _call('remote.getAll'))
export const subscribeRemotesInfo = createSubscription(() =>
_call('remote.getAllInfo')
)
export const subscribeResourceSets = createSubscription(() =>
_call('resourceSet.getAll')
)

View File

@@ -6,7 +6,7 @@ import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { addSubscriptions, generateRandomId, noop } from 'utils'
import { addSubscriptions, formatSize, generateRandomId, noop } from 'utils'
import { alert } from 'modal'
import { format, parse } from 'xo-remote-parser'
import { groupBy, map, isEmpty } from 'lodash'
@@ -21,6 +21,7 @@ import {
editRemote,
enableRemote,
subscribeRemotes,
subscribeRemotesInfo,
testRemote,
} from 'xo'
@@ -34,6 +35,7 @@ const _showError = remote => alert(_('remoteConnectionFailed'), remote.error)
const _editRemoteName = (name, { remote }) => editRemote(remote, { name })
const _editRemoteOptions = (options, { remote }) =>
editRemote(remote, { options: options !== '' ? options : null })
const COLUMN_NAME = {
itemRenderer: (remote, { formatMessage }) => (
<Text
@@ -74,6 +76,17 @@ const COLUMN_STATE = {
),
name: _('remoteState'),
}
const COLUMN_DISK = {
itemRenderer: (remote, { formatMessage }) =>
remote.info !== undefined &&
remote.info.used !== undefined &&
remote.info.size !== undefined && (
<span>
{`${formatSize(remote.info.used)} / ${formatSize(remote.info.size)}`}
</span>
),
name: _('remoteDisk'),
}
const fixRemoteUrl = remote => editRemote(remote, { url: format(remote) })
const COLUMNS_LOCAL_REMOTE = [
@@ -91,6 +104,7 @@ const COLUMNS_LOCAL_REMOTE = [
name: _('remotePath'),
},
COLUMN_STATE,
COLUMN_DISK,
]
const COLUMNS_NFS_REMOTE = [
COLUMN_NAME,
@@ -151,6 +165,7 @@ const COLUMNS_NFS_REMOTE = [
),
},
COLUMN_STATE,
COLUMN_DISK,
]
const COLUMNS_SMB_REMOTE = [
COLUMN_NAME,
@@ -272,18 +287,8 @@ const FILTERS = {
export default decorate([
addSubscriptions({
remotes: cb =>
subscribeRemotes(remotes => {
cb(
groupBy(
map(remotes, remote => ({
...parse(remote.url),
...remote,
})),
'type'
)
)
}),
remotes: subscribeRemotes,
remotesInfo: subscribeRemotesInfo,
}),
injectIntl,
provideState({
@@ -300,15 +305,26 @@ export default decorate([
remote,
}),
},
computed: {
remoteWithInfo: (_, { remotes, remotesInfo }) =>
groupBy(
map(remotes, remote => ({
...parse(remote.url),
...remote,
info: remotesInfo !== undefined ? remotesInfo[remote.id] : {},
})),
'type'
),
},
}),
injectState,
({ state, effects, remotes = {}, intl: { formatMessage } }) => (
<div>
{!isEmpty(remotes.file) && (
{!isEmpty(state.remoteWithInfo.file) && (
<div>
<h2>{_('remoteTypeLocal')}</h2>
<SortedTable
collection={remotes.file}
collection={state.remoteWithInfo.file}
columns={COLUMNS_LOCAL_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
@@ -320,11 +336,11 @@ export default decorate([
</div>
)}
{!isEmpty(remotes.nfs) && (
{!isEmpty(state.remoteWithInfo.nfs) && (
<div>
<h2>{_('remoteTypeNfs')}</h2>
<SortedTable
collection={remotes.nfs}
collection={state.remoteWithInfo.nfs}
columns={COLUMNS_NFS_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}
@@ -336,11 +352,11 @@ export default decorate([
</div>
)}
{!isEmpty(remotes.smb) && (
{!isEmpty(state.remoteWithInfo.smb) && (
<div>
<h2>{_('remoteTypeSmb')}</h2>
<SortedTable
collection={remotes.smb}
collection={state.remoteWithInfo.smb}
columns={COLUMNS_SMB_REMOTE}
data-editRemote={effects.editRemote}
data-formatMessage={formatMessage}

View File

@@ -849,6 +849,13 @@
dependencies:
safe-buffer "^5.1.2"
"@sindresorhus/df@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/df/-/df-2.1.0.tgz#d208cf27e06f0bb476d14d7deccd7d726e9aa389"
integrity sha1-0gjPJ+BvC7R20U197M19cm6ao4k=
dependencies:
execa "^0.2.2"
"@types/babel-types@*", "@types/babel-types@^7.0.0":
version "7.0.4"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.4.tgz#bfd5b0d0d1ba13e351dff65b6e52783b816826c8"
@@ -3668,6 +3675,14 @@ cross-env@^5.1.1, cross-env@^5.1.3, cross-env@^5.1.4:
cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-spawn-async@^2.1.1:
version "2.2.5"
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc"
integrity sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=
dependencies:
lru-cache "^4.0.0"
which "^1.2.8"
cross-spawn@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
@@ -5084,6 +5099,17 @@ exec-sh@^0.2.0:
dependencies:
merge "^1.2.0"
execa@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.2.2.tgz#e2ead472c2c31aad6f73f1ac956eef45e12320cb"
integrity sha1-4urUcsLDGq1vc/GslW7vReEjIMs=
dependencies:
cross-spawn-async "^2.1.1"
npm-run-path "^1.0.0"
object-assign "^4.0.1"
path-key "^1.0.0"
strip-eof "^1.0.0"
execa@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
@@ -8527,6 +8553,14 @@ lower-case@^1.1.1:
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
lru-cache@^4.0.0:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
lru-cache@^4.0.1, lru-cache@^4.1.2:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -9381,6 +9415,13 @@ npm-packlist@^1.1.6:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-run-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f"
integrity sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=
dependencies:
path-key "^1.0.0"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -9989,6 +10030,11 @@ path-is-inside@^1.0.2:
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
path-key@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af"
integrity sha1-XVPVeAGWRsDWiADbThRua9wqx68=
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@@ -13379,7 +13425,7 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0:
which@1, which@^1.2.12, which@^1.2.14, which@^1.2.8, which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==