Initial Commit
This commit is contained in:
29
app/classes/logical/ExecutionResult.js
Normal file
29
app/classes/logical/ExecutionResult.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
class ExecutionResult extends Injectable {
|
||||
stdout = []
|
||||
stderr = []
|
||||
exit_code = 0
|
||||
|
||||
out(data) {
|
||||
this.stdout = this.stdout.concat(String(data).split('\n'))
|
||||
}
|
||||
|
||||
error(data) {
|
||||
this.stderr = this.stderr.concat(String(data).split('\n'))
|
||||
}
|
||||
|
||||
exit(code) {
|
||||
this.exit_code = Number(code)
|
||||
}
|
||||
|
||||
get clean_out() {
|
||||
return this.stdout.filter(Boolean)
|
||||
}
|
||||
|
||||
get clean_err() {
|
||||
return this.stdout.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ExecutionResult
|
||||
24
app/classes/logical/SystemMetrics.js
Normal file
24
app/classes/logical/SystemMetrics.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
class SystemMetrics extends Injectable {
|
||||
_cpu = 0
|
||||
_ram = 0
|
||||
_mounts = {}
|
||||
|
||||
cpu(set = false) {
|
||||
if ( set !== false ) this._cpu = set
|
||||
else return this._cpu
|
||||
}
|
||||
|
||||
ram(set = false) {
|
||||
if ( set !== false ) this._ram = set
|
||||
else return this._ram
|
||||
}
|
||||
|
||||
mount(point, set = false) {
|
||||
if ( set !== false ) this._mounts[point] = set
|
||||
else return this._mounts[point]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = SystemMetrics
|
||||
167
app/classes/logical/UniversalPath.js
Normal file
167
app/classes/logical/UniversalPath.js
Normal file
@@ -0,0 +1,167 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const moment = require('moment')
|
||||
|
||||
class UniversalPath extends Injectable {
|
||||
static get services() {
|
||||
return [...super.services, 'hosts']
|
||||
}
|
||||
|
||||
static PATH_TYPE_FILE = Symbol('file')
|
||||
static PATH_TYPE_DIRECTORY = Symbol('directory')
|
||||
static PATH_TYPE_UNKNOWN = Symbol('unknown')
|
||||
_directory_classify_command = 'test -d'
|
||||
_file_classify_command = 'test -f'
|
||||
_datetime_format = 'Y-MM-DD_HH-mm-ss-SSSS'
|
||||
|
||||
static fromString(path_string) {
|
||||
const parts = path_string.split('::')
|
||||
const hostname = parts.length > 1 ? parts[0] : 'localhost'
|
||||
const path = parts.length > 1 ? parts[1] : parts[0]
|
||||
const host = this.prototype.hosts.get(hostname)
|
||||
return new this(host, path)
|
||||
}
|
||||
|
||||
constructor(host, host_path, type) {
|
||||
super()
|
||||
this._host = host
|
||||
this._path = host_path
|
||||
this._type = type ? type : this.constructor.PATH_TYPE_UNKNOWN
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this._host
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this._path
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this._type
|
||||
}
|
||||
|
||||
async resolve() {
|
||||
return this._host.resolve_path(this._path)
|
||||
}
|
||||
|
||||
is_directory() {
|
||||
return this.type === this.constructor.PATH_TYPE_DIRECTORY
|
||||
}
|
||||
|
||||
is_file() {
|
||||
return this.type === this.constructor.PATH_TYPE_FILE
|
||||
}
|
||||
|
||||
is_valid() {
|
||||
return this.is_file() || this.is_directory()
|
||||
}
|
||||
|
||||
async classify() {
|
||||
const dir_result = await this._host.execute(`${this._directory_classify_command} ${this._path}`)
|
||||
if ( dir_result.exit_code === 0 ) {
|
||||
this._type = this.constructor.PATH_TYPE_DIRECTORY
|
||||
return
|
||||
}
|
||||
|
||||
const file_result = await this._host.execute(`${this._file_classify_command} ${this._path}`)
|
||||
if ( file_result.exit_code === 0 ) {
|
||||
this._type = this.constructor.PATH_TYPE_FILE
|
||||
return
|
||||
}
|
||||
|
||||
this._type = this.constructor.PATH_TYPE_UNKNOWN
|
||||
}
|
||||
|
||||
async open_read_stream() {
|
||||
await this.classify()
|
||||
if ( this._type === this.constructor.PATH_TYPE_FILE ) {
|
||||
return this._host.open_file_read_stream(this._path)
|
||||
} else {
|
||||
throw new Error('Maestro does not support generation of read streams for this type of path: '+this._type.description)
|
||||
}
|
||||
}
|
||||
|
||||
async open_write_stream() {
|
||||
await this.classify()
|
||||
if (
|
||||
[this.constructor.PATH_TYPE_FILE, this.constructor.PATH_TYPE_UNKNOWN].includes(this._type)
|
||||
) {
|
||||
return this._host.open_file_write_stream(this._path)
|
||||
} else {
|
||||
throw new Error('Maestro does not support generation of write streams for this type of path: '+this._type.description)
|
||||
}
|
||||
}
|
||||
|
||||
async copy_from(other_path) {
|
||||
const read = await other_path.open_read_stream()
|
||||
const write = await this.open_write_stream()
|
||||
await read.pipe(write)
|
||||
}
|
||||
|
||||
async copy_to(other_path) {
|
||||
const read = await this.open_read_stream()
|
||||
const write = await other_path.open_write_stream()
|
||||
await read.pipe(write)
|
||||
}
|
||||
|
||||
async unlink() {
|
||||
await this.host.delete_path(this._path)
|
||||
}
|
||||
|
||||
async move_from(other_path) {
|
||||
await this.copy_from(other_path)
|
||||
await other_path.unlink()
|
||||
}
|
||||
|
||||
async move_to(other_path) {
|
||||
await this.copy_to(other_path)
|
||||
await this.unlink()
|
||||
}
|
||||
|
||||
async touch(directory = false) {
|
||||
await this.classify()
|
||||
if ( !directory ) {
|
||||
const base_dir = this._path.split('/').slice(0, -1).join('/')
|
||||
await this._host.run(`mkdir -p ${base_dir}`)
|
||||
await this._host.run(`touch ${this._path}`)
|
||||
} else {
|
||||
await this._host.run(`mkdir -p ${this._path}`)
|
||||
}
|
||||
}
|
||||
|
||||
async echo(text) {
|
||||
await this.classify()
|
||||
if ( this.is_directory() ) {
|
||||
throw new Error('Cannot echo to a directory.')
|
||||
}
|
||||
|
||||
if ( !this.is_valid() ) {
|
||||
await this.touch()
|
||||
}
|
||||
|
||||
const ws = await this.open_write_stream()
|
||||
ws.write(text)
|
||||
return new Promise(resolve => {
|
||||
ws.on('finish', () => resolve())
|
||||
ws.end()
|
||||
})
|
||||
}
|
||||
|
||||
async hash() {
|
||||
const line = await this._host.run_line_result(`sha256sum ${this._path}`)
|
||||
return line.split(' ')[0]
|
||||
}
|
||||
|
||||
async backup() {
|
||||
const now = moment().format(this._datetime_format)
|
||||
const backup_path = `${this}.maestro-backup-${now}`
|
||||
const backup_target = this.constructor.fromString(backup_path)
|
||||
await this.copy_to(backup_target)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this._host.name}::${this._path}`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = UniversalPath
|
||||
177
app/classes/logical/packages/APTManager.js
Normal file
177
app/classes/logical/packages/APTManager.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const PackageManager = require('./PackageManager')
|
||||
|
||||
class APTManager extends PackageManager {
|
||||
_command_install_package = 'apt-get install -y %%PACKAGE%%'
|
||||
_command_uninstall_package = 'apt-get purge -y %%PACKAGE%%'
|
||||
_command_update_package = 'apt-get upgrade -y %%PACKAGE%%'
|
||||
_command_reinstall_package = 'apt-get install -y --reinstall %%PACKAGE%%'
|
||||
_command_clear_cache = 'apt-get clean'
|
||||
_command_count_installed = 'dpkg --list | wc -l'
|
||||
_command_count_available = 'apt-cache pkgnames | wc -l'
|
||||
_command_add_repo = `add-apt-repository '%%URI%%`
|
||||
_command_preflight = 'apt-get update'
|
||||
|
||||
_status_keymap = {
|
||||
package: 'name',
|
||||
filename: 'source',
|
||||
section: 'repository',
|
||||
'description-en': 'summary',
|
||||
}
|
||||
|
||||
async status(pkg) {
|
||||
await this.preflight()
|
||||
const result = await this._host.execute(`apt-cache show ${pkg}`)
|
||||
if ( ![0, 100].includes(result.exit_code) ) {
|
||||
throw new Error('Unable to determine package information: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
if ( result.exit_code === 100 ) {
|
||||
return {
|
||||
name: pkg,
|
||||
state: this.constructor.PACKAGE_STATE_UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
const data = {}
|
||||
let last_data_key = false
|
||||
for ( const line of result.clean_out ) {
|
||||
if ( last_data_key && line.startsWith(' ') ) {
|
||||
data[last_data_key] += line
|
||||
} else {
|
||||
const parts = line.trim().split(':')
|
||||
const key = parts[0].toLowerCase()
|
||||
const value = parts.slice(1).join(':').trim()
|
||||
data[key] = value
|
||||
last_data_key = key
|
||||
}
|
||||
}
|
||||
|
||||
for ( const key in this._status_keymap ) {
|
||||
if ( !this._status_keymap.hasOwnProperty(key) ) continue
|
||||
if ( data[key] ) {
|
||||
data[this._status_keymap[key]] = data[key]
|
||||
delete data[key]
|
||||
}
|
||||
}
|
||||
|
||||
// check installation state
|
||||
const install_result = await this._host.execute(`dpkg -s ${pkg}`)
|
||||
data.state = install_result.exit_code === 0 ? this.constructor.PACKAGE_STATE_INSTALLED : this.constructor.PACKAGE_STATE_AVAILABLE
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async list_updates() {
|
||||
await this.preflight()
|
||||
const result = await this._host.execute(`apt-get --just-print upgrade`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine packages for update: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
let start_index = -1
|
||||
let end_index = -1
|
||||
for ( let i in result.clean_out ) {
|
||||
const line = result.clean_out[i]
|
||||
if ( line.toLowerCase().indexOf('packages will be upgraded') > -1 ) {
|
||||
start_index = Number(i)+1
|
||||
} else if ( start_index > -1 && i >= start_index ) {
|
||||
if ( line.indexOf('newly installed,') > -1 ) break
|
||||
else {
|
||||
end_index = Number(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const package_strings = result.clean_out.slice(start_index, end_index+1).join(' ').split(' ').filter(Boolean).map(x => x.trim())
|
||||
const updates = result.clean_out.filter(x => x.startsWith('Inst ')).map(x => {
|
||||
x = x.substring(5)
|
||||
x = x.split(' ')
|
||||
const data = {
|
||||
name: x[0],
|
||||
version: x[1].slice(1, -1),
|
||||
repository: x[3].replace(/,/g, ''),
|
||||
architecture: x.reverse()[0].replace(/[\[\])(]/g, '')
|
||||
}
|
||||
return data
|
||||
}).filter(x => package_strings.includes(x.name))
|
||||
return updates
|
||||
}
|
||||
|
||||
async list_repos() {
|
||||
await this.preflight()
|
||||
const result = await this._host.execute(`apt-cache policy`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to read repository list: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const lines = []
|
||||
for ( const line of result.clean_out ) {
|
||||
if ( line.trim().startsWith('500') ) {
|
||||
lines.push(line)
|
||||
} else if ( line.toLowerCase().startsWith('pinned packages') ) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lines.map(x => x.trim()).filter(x => x.startsWith('500')).map(x => x.split(' ')[2])
|
||||
}
|
||||
|
||||
async list_installed() {
|
||||
const result = await this._host.execute(`dpkg --list`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine installed packages: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
let header_line = result.clean_out.filter(x => x.trim().startsWith('+'))[0]
|
||||
let drop_count = 0
|
||||
while ( header_line.startsWith('+') ) {
|
||||
drop_count += 1
|
||||
header_line = header_line.substring(1)
|
||||
}
|
||||
|
||||
let start_at = 0
|
||||
for ( let i in result.clean_out ) {
|
||||
if ( result.clean_out[i].startsWith('+') ) {
|
||||
start_at = Number(i)+1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result.clean_out.slice(start_at).map(x => {
|
||||
x = x.substring(drop_count).trim().split(' ').filter(Boolean)
|
||||
return {
|
||||
name: x[0],
|
||||
version: x[1],
|
||||
architecture: x[2],
|
||||
summary: x.slice(3).join(' ')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async search(pkg) {
|
||||
await this.preflight()
|
||||
const result = await this._host.execute(`apt-cache search ${pkg}`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to search the package cache: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const results = []
|
||||
for ( const line of result.clean_out ) {
|
||||
const parts = line.trim().split(' - ').map(x => x.trim())
|
||||
results.push({
|
||||
name: parts[0],
|
||||
summary: parts[1]
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async preflight() {
|
||||
const result = await this._host.execute(this._command_preflight)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Error encountered during preflight for APTManager: '+result.stderr.join('\n'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = APTManager
|
||||
128
app/classes/logical/packages/DNFManager.js
Normal file
128
app/classes/logical/packages/DNFManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const PackageManager = require('./PackageManager')
|
||||
|
||||
class DNFManager extends PackageManager {
|
||||
_command_install_package = 'dnf install -y %%PACKAGE%%'
|
||||
_command_uninstall_package = 'dnf remove -y %%PACKAGE%%'
|
||||
_command_update_package = 'dnf update -y %%PACKAGE%%'
|
||||
_command_reinstall_package = 'dnf reinstall -y %%PACKAGE%%'
|
||||
_command_add_repo = 'dnf config-manager -y --add-repo="%%URI%%"'
|
||||
_command_clear_cache = 'dnf clean all'
|
||||
_command_count_installed = 'dnf list installed -q | wc -l'
|
||||
_command_count_available = 'dnf list available -q | wc -l'
|
||||
|
||||
async status(pkg) {
|
||||
const result = await this._host.execute(`dnf info -q --installed ${pkg}`)
|
||||
if ( result.exit_code === 0 ) {
|
||||
const data = this._data_from_result(result.clean_out)
|
||||
data.state = this.constructor.PACKAGE_STATE_INSTALLED
|
||||
return data
|
||||
} else {
|
||||
const available_result = await this._host.execute(`dnf info -q --available ${pkg}`)
|
||||
if ( available_result.exit_code === 0 ) {
|
||||
const data = this._data_from_result(available_result.clean_out)
|
||||
data.state = this.constructor.PACKAGE_STATE_AVAILABLE
|
||||
return data
|
||||
} else {
|
||||
return {
|
||||
name: pkg,
|
||||
state: this.constructor.PACKAGE_STATE_UNKNOWN,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async list_updates() {
|
||||
const result = await this._host.execute(`dnf check-update -q`)
|
||||
// 100 is the known exit code for successful result, but updates pending
|
||||
if ( ![0, 100].includes(result.exit_code) ) {
|
||||
throw new Error('Unable to check for updates: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const updates = []
|
||||
for ( const line of result.clean_out ) {
|
||||
const parts = line.trim().split(' ').filter(Boolean)
|
||||
const data = this._package_info_from_line_parts(parts)
|
||||
updates.push(data)
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
async list_repos() {
|
||||
const result = await this._host.execute(`dnf repolist all -q`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine repositories: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const offset_length = result.clean_out[0].toLowerCase().indexOf('repo name')
|
||||
return result.clean_out.slice(1).map(line => line.substr(0, offset_length).trim())
|
||||
}
|
||||
|
||||
async list_installed() {
|
||||
const result = await this._host.execute(`dnf list installed -q`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine installed packages: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const results = []
|
||||
for ( const line of result.clean_out.slice(1) ) {
|
||||
const parts = line.trim().split(' ').filter(Boolean)
|
||||
const data = this._package_info_from_line_parts(parts)
|
||||
results.push(data)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async search(term) {
|
||||
const result = await this._host.execute(`dnf search ${term} -q`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to complete search: '+result.stderr.join('\n'))
|
||||
}
|
||||
|
||||
const results = []
|
||||
for ( const line of result.clean_out ) {
|
||||
if ( !line.trim().startsWith('=') ) {
|
||||
const parts = line.split(':').map(x => x.trim())
|
||||
const data = this._package_info_from_line_parts(parts)
|
||||
if ( data.version ) {
|
||||
data.summary = data.version
|
||||
delete data.version
|
||||
}
|
||||
results.push(data)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
_data_from_result(stdout) {
|
||||
const data = {}
|
||||
for ( const line of stdout ) {
|
||||
const parts = line.split(':').map(x => String(x).trim())
|
||||
if ( parts.length > 1 && parts[0] ) {
|
||||
let key = parts[0].toLowerCase()
|
||||
if ( key === 'from repo' ) key = 'repo'
|
||||
data[key] = parts.slice(1).join(':')
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
_package_info_from_line_parts(parts) {
|
||||
const data = {}
|
||||
if ( parts.length > 0 ) {
|
||||
data.name = parts[0]
|
||||
if ( data.name.indexOf('.') >= 0 ) {
|
||||
const arch_parts = data.name.split('.').reverse()
|
||||
data.architecture = arch_parts[0]
|
||||
data.name = arch_parts.slice(1).reverse().join('.')
|
||||
}
|
||||
}
|
||||
if ( parts.length > 1 ) data.version = parts[1]
|
||||
if ( parts.length > 2 ) data.repository = parts[2]
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = DNFManager
|
||||
104
app/classes/logical/packages/PackageManager.js
Normal file
104
app/classes/logical/packages/PackageManager.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const ImplementationError = require('libflitter/errors/ImplementationError')
|
||||
|
||||
class PackageManager extends Injectable {
|
||||
static get services() {
|
||||
return [...super.services, 'hosts']
|
||||
}
|
||||
|
||||
static PACKAGE_STATE_INSTALLED = 'installed'
|
||||
static PACKAGE_STATE_AVAILABLE = 'available'
|
||||
static PACKAGE_STATE_UNKNOWN = 'unknown'
|
||||
|
||||
_command_install_package = '%%PACKAGE%%'
|
||||
_command_uninstall_package = '%%PACKAGE%%'
|
||||
_command_update_package = '%%PACKAGE%%'
|
||||
_command_reinstall_package = '%%PACKAGE%%'
|
||||
_command_add_repo = '%%URI%%'
|
||||
_command_clear_cache = ''
|
||||
_command_count_installed = ''
|
||||
_command_count_available = ''
|
||||
|
||||
_group_packages_by = ' '
|
||||
|
||||
constructor(host_or_hostname) {
|
||||
super()
|
||||
if ( typeof host_or_hostname === 'string' ) {
|
||||
host_or_hostname = this.hosts.get(host_or_hostname)
|
||||
}
|
||||
|
||||
this._host = host_or_hostname
|
||||
}
|
||||
|
||||
async list_updates() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async list_repos() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async status(pkg) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async search(term) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async list_installed() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async is_installed(pkg) {
|
||||
const result = await this.status(pkg)
|
||||
return result.state === this.constructor.PACKAGE_STATE_INSTALLED
|
||||
}
|
||||
|
||||
async clear_cache() {
|
||||
await this._host.run_line_result(this._command_clear_cache)
|
||||
}
|
||||
|
||||
async count_installed() {
|
||||
return this._host.run_line_result(this._command_count_installed)
|
||||
}
|
||||
|
||||
async count_available() {
|
||||
return this._host.run_line_result(this._command_count_available)
|
||||
}
|
||||
|
||||
async install(...packages) {
|
||||
await this._standard_format_execution(packages, this._command_install_package)
|
||||
}
|
||||
|
||||
async uninstall(...packages) {
|
||||
await this._standard_format_execution(packages, this._command_uninstall_package)
|
||||
}
|
||||
|
||||
async update(...packages) {
|
||||
await this._standard_format_execution(packages, this._command_update_package)
|
||||
}
|
||||
|
||||
async reinstall(...packages) {
|
||||
await this._standard_format_execution(packages, this._command_reinstall_package)
|
||||
}
|
||||
|
||||
async add_repo(uri) {
|
||||
await this._standard_format_execution([uri], this._command_add_repo, '%%URI%%')
|
||||
}
|
||||
|
||||
async _standard_format_execution(packages, command, placeholder = '%%PACKAGE%%') {
|
||||
if ( this._group_packages_by ) {
|
||||
packages = [packages.join(this._group_packages_by)]
|
||||
}
|
||||
|
||||
for ( const package_name of packages ) {
|
||||
const result = await this._host.execute(command.replace(placeholder, package_name))
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error(`Error encountered while executing command (${command.replace(placeholder, package_name)}): ${result.stderr.join('\n')}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PackageManager
|
||||
111
app/classes/logical/services/ServiceConfig.js
Normal file
111
app/classes/logical/services/ServiceConfig.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
class ServiceConfig extends Injectable {
|
||||
static get services() {
|
||||
return [...super.services, 'utility']
|
||||
}
|
||||
|
||||
_data = {
|
||||
after: ['syslog.target', 'network.target'],
|
||||
type: 'simple',
|
||||
restart: 'always',
|
||||
restart_after: '2s',
|
||||
install_to: 'multi-user.target',
|
||||
}
|
||||
|
||||
name(set) { return this._get_or_set('name', set) }
|
||||
|
||||
description(set) { return this._get_or_set('description', set) }
|
||||
|
||||
after(set) { return this._get_or_set('after', set, true) }
|
||||
|
||||
restart(set) { return this._get_or_set('restart', set) }
|
||||
|
||||
restart_after(set) { return this._get_or_set('restart_after', set) }
|
||||
|
||||
type(set) { return this._get_or_set('type', set) }
|
||||
|
||||
user(set) { return this._get_or_set('user', set) }
|
||||
|
||||
group(set) { return this._get_or_set('group', set) }
|
||||
|
||||
directory(set) { return this._get_or_set('directory', set) }
|
||||
|
||||
execute(set) { return this._get_or_set('execute', set) }
|
||||
|
||||
env(set) { return this._get_or_set('env', set) }
|
||||
|
||||
install_to(set) { return this._get_or_set('install_to', set) }
|
||||
|
||||
_export() {
|
||||
return this.utility.deep_copy(this._data)
|
||||
}
|
||||
|
||||
_build() {
|
||||
this._validate()
|
||||
const lines = []
|
||||
lines.push('[Unit]')
|
||||
if ( this.description() ) lines.push(`Description=${this.description()}`)
|
||||
if ( this.after() ) {
|
||||
for ( const item of this.after() ) {
|
||||
lines.push(`After=${item}`)
|
||||
}
|
||||
}
|
||||
lines.push('\n[Service]')
|
||||
if ( this.restart() ) {
|
||||
lines.push(`Restart=${this.restart()}`)
|
||||
|
||||
if ( this.restart_after() ) {
|
||||
lines.push(`RestartSec=${this.restart_after()}`)
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.user() ) {
|
||||
lines.push(`User=${this.user()}`)
|
||||
}
|
||||
|
||||
if ( this.group() ) {
|
||||
lines.push(`Group=${this.group()}`)
|
||||
}
|
||||
|
||||
if ( this.directory() ) {
|
||||
lines.push(`WorkingDirectory=${this.directory()}`)
|
||||
}
|
||||
|
||||
if ( this.execute() ) {
|
||||
lines.push(`ExecStart=${this.execute()}`)
|
||||
}
|
||||
|
||||
if ( this.env() ) {
|
||||
lines.push(`Environment=${this.env()}`)
|
||||
}
|
||||
|
||||
if ( this.install_to() ) {
|
||||
lines.push('\n[Install]')
|
||||
lines.push(`WantedBy=${this.install_to()}`)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
_validate() {
|
||||
const required_fields = ['name', 'description', 'after', 'type', 'user', 'group', 'directory', 'execute']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !this[field]() ) throw new Error(`Missing required service config field: ${field}`)
|
||||
}
|
||||
}
|
||||
|
||||
_get_or_set(field, value, arr = false) {
|
||||
if ( value ) {
|
||||
if ( arr ) {
|
||||
if ( !this._data[field] ) this._data[field] = []
|
||||
this._data[field].push(value)
|
||||
} else this._data[field] = value
|
||||
return this
|
||||
}
|
||||
|
||||
return this._data[field]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceConfig
|
||||
79
app/classes/logical/services/ServiceManager.js
Normal file
79
app/classes/logical/services/ServiceManager.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const ImplementationError = require('libflitter/errors/ImplementationError')
|
||||
const ServiceConfig = require('./ServiceConfig')
|
||||
|
||||
class ServiceManager extends Injectable {
|
||||
static SERVICE_STATE_STOPPED = 'stopped'
|
||||
static SERVICE_STATE_ERROR = 'error'
|
||||
static SERVICE_STATE_RUNNING = 'running'
|
||||
|
||||
_command_restart_service = '%%SERVICE%%'
|
||||
_command_start_service = '%%SERVICE%%'
|
||||
_command_stop_service = '%%SERVICE%%'
|
||||
_command_reload_service = '%%SERVICE%%'
|
||||
_command_enable_service = '%%SERVICE%%'
|
||||
_command_disable_service = '%%SERVICE%%'
|
||||
_command_daemon_reload = ''
|
||||
|
||||
_group_services_by = ' '
|
||||
|
||||
constructor(host) {
|
||||
super()
|
||||
this._host = host
|
||||
}
|
||||
|
||||
async status(...services) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async install(config) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
new_config() {
|
||||
return new ServiceConfig()
|
||||
}
|
||||
|
||||
async start(...services) {
|
||||
await this._standard_format_execution(services, this._command_start_service)
|
||||
}
|
||||
|
||||
async stop(...services) {
|
||||
await this._standard_format_execution(services, this._command_stop_service)
|
||||
}
|
||||
|
||||
async restart(...services) {
|
||||
await this._standard_format_execution(services, this._command_restart_service)
|
||||
}
|
||||
|
||||
async reload(...services) {
|
||||
await this._standard_format_execution(services, this._command_reload_service)
|
||||
}
|
||||
|
||||
async enable(...services) {
|
||||
await this._standard_format_execution(services, this._command_enable_service)
|
||||
}
|
||||
|
||||
async disable(...services) {
|
||||
await this._standard_format_execution(services, this._command_disable_service)
|
||||
}
|
||||
|
||||
async daemon_reload() {
|
||||
await this._host.run_line_result(this._command_daemon_reload)
|
||||
}
|
||||
|
||||
async _standard_format_execution(items, command, replace = '%%SERVICE%%') {
|
||||
if ( this._group_services_by ) {
|
||||
items = [items.join(this._group_services_by)]
|
||||
}
|
||||
|
||||
for ( const item of items ) {
|
||||
const result = await this._host.execute(command.replace(replace, item))
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Error encountered while executing command: '+result.stderr.join('\n'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceManager
|
||||
55
app/classes/logical/services/SystemDManager.js
Normal file
55
app/classes/logical/services/SystemDManager.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const ServiceManager = require('./ServiceManager')
|
||||
|
||||
class SystemDManager extends ServiceManager {
|
||||
_command_restart_service = 'systemctl restart %%SERVICE%%'
|
||||
_command_start_service = 'systemctl start %%SERVICE%%'
|
||||
_command_stop_service = 'systemctl stop %%SERVICE%%'
|
||||
_command_reload_service = 'systemctl reload %%SERVICE%%'
|
||||
_command_enable_service = 'systemctl enable %%SERVICE%%'
|
||||
_command_disable_service = 'systemctl disable %%SERVICE%%'
|
||||
_command_daemon_reload = 'systemctl daemon-reload'
|
||||
|
||||
async status(...services) {
|
||||
const statuses = []
|
||||
for ( const service_name of services ) {
|
||||
const result = await this._host.execute(`systemctl status -l --value --no-pager ${service_name}`)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error(`Unable to determine service state: ${result.stderr.join('\n')}`)
|
||||
}
|
||||
|
||||
const status = result.clean_out.map(x => x.trim().toLowerCase())
|
||||
.filter(x => x.startsWith('active:'))[0]
|
||||
.split(':')[1]
|
||||
.trim()
|
||||
|
||||
|
||||
statuses.push({
|
||||
name: service_name,
|
||||
status: this._determine_status_from_string(status)
|
||||
})
|
||||
}
|
||||
|
||||
return statuses.length === 1 ? statuses[0] : statuses
|
||||
}
|
||||
|
||||
async install(config) {
|
||||
if ( !config._build || !config.name ) throw new Error('Config must be instance of classes/logical/services/ServiceConfig')
|
||||
const file_contents = config._build()
|
||||
const name = config.name()
|
||||
|
||||
const service_file = await this._host.get_path(`/etc/systemd/system/${name}.service`)
|
||||
await service_file.echo(file_contents)
|
||||
}
|
||||
|
||||
_determine_status_from_string(str) {
|
||||
if ( str.startsWith('active') ) {
|
||||
return this.constructor.SERVICE_STATE_RUNNING
|
||||
} else if ( str.startsWith('fail') ) {
|
||||
return this.constructor.SERVICE_STATE_ERROR
|
||||
} else {
|
||||
return this.constructor.SERVICE_STATE_STOPPED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = SystemDManager
|
||||
160
app/classes/metal/Host.js
Normal file
160
app/classes/metal/Host.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const ImplementationError = require('libflitter/errors/ImplementationError')
|
||||
const uuid = require('uuid/v4')
|
||||
const UniversalPath = require('../logical/UniversalPath')
|
||||
const SystemMetrics = require('../logical/SystemMetrics')
|
||||
|
||||
const DNFManager = require('../logical/packages/DNFManager')
|
||||
const APTManager = require('../logical/packages/APTManager')
|
||||
|
||||
const SystemDManager = require('../logical/services/SystemDManager')
|
||||
|
||||
class Host extends Injectable {
|
||||
static get services() {
|
||||
return [...super.services, 'utility']
|
||||
}
|
||||
|
||||
_temp_path_command = 'mktemp -d'
|
||||
_temp_file_command = 'mktemp'
|
||||
_cpu_percentage_command = `grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage}'`
|
||||
_ram_percentage_command = `free | grep Mem | awk '{print $3/$2 * 100.0}'`
|
||||
_mount_point_percentage_command = `df -hl | grep -w '%%MOUNTPOINT%%$' | awk '{print $5}'`
|
||||
_list_mount_points_command = `df -hl | grep '/' | awk '{print $6}'`
|
||||
_file_directory_delete_command = `rm -rf "%%RESOURCE%%"`
|
||||
_resolve_path_command = `readlink -f "%%PATH%%"`
|
||||
_reboot_command = `reboot`
|
||||
|
||||
constructor(config) {
|
||||
super()
|
||||
this.config = config
|
||||
this.name = config.name
|
||||
|
||||
if ( config.packages && config.packages.type ) {
|
||||
if ( config.packages.type === 'dnf' ) {
|
||||
this.packages = new DNFManager(this)
|
||||
} else if ( config.packages.type === 'apt' ) {
|
||||
this.packages = new APTManager(this)
|
||||
} else {
|
||||
throw new Error(`Invalid or unknown package manager type: ${config.packages.type}`)
|
||||
}
|
||||
}
|
||||
|
||||
if ( config.services && config.services.type ) {
|
||||
if ( config.services.type === 'systemd' ) {
|
||||
this.services = new SystemDManager(this)
|
||||
} else {
|
||||
throw new Error(`Invalid or unknown service manager type: ${config.services.type}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute(command) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async open_file_read_stream(file_path) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async open_file_write_stream(file_path) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async is_alive() {
|
||||
try {
|
||||
const unique_id = uuid()
|
||||
const result = await this.execute(`echo "${unique_id}"`)
|
||||
return (result.exit_code === 0 && (result.clean_out.length > 0 && result.clean_out[0] === unique_id))
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async get_temp_path() {
|
||||
const path_string = await this.run_line_result(this._temp_path_command)
|
||||
return new UniversalPath(this, path_string, UniversalPath.PATH_TYPE_DIRECTORY)
|
||||
}
|
||||
|
||||
async get_temp_file() {
|
||||
const file_string = await this.run_line_result(this._temp_file_command)
|
||||
return new UniversalPath(this, file_string, UniversalPath.PATH_TYPE_FILE)
|
||||
}
|
||||
|
||||
async get_path(local_path) {
|
||||
const host_path = new UniversalPath(this, local_path)
|
||||
await host_path.classify()
|
||||
return host_path
|
||||
}
|
||||
|
||||
async metrics() {
|
||||
const metric = new SystemMetrics()
|
||||
const cpu_percent = Number(await this.run_line_result(this._cpu_percentage_command))
|
||||
const ram_percent = Number(await this.run_line_result(this._ram_percentage_command))
|
||||
metric.cpu(cpu_percent)
|
||||
metric.ram(ram_percent)
|
||||
|
||||
const mount_points = await this.get_mount_points()
|
||||
for ( const point of mount_points ) {
|
||||
metric.mount(point, await this.get_mountpoint_utilization(point))
|
||||
}
|
||||
|
||||
return metric
|
||||
}
|
||||
|
||||
async delete_path(resource_path) {
|
||||
resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path
|
||||
await this.execute(this._file_directory_delete_command.replace('%%RESOURCE%%', resource_path))
|
||||
}
|
||||
|
||||
async resolve_path(resource_path) {
|
||||
resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path
|
||||
return this.run_line_result(this._resolve_path_command.replace('%%PATH%%', resource_path))
|
||||
}
|
||||
|
||||
async get_mount_points() {
|
||||
const result = await this.execute(this._list_mount_points_command)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine mount points. Command execution error.')
|
||||
}
|
||||
|
||||
return result.clean_out
|
||||
}
|
||||
|
||||
async get_mountpoint_utilization(mountpoint) {
|
||||
const cmd = this._mount_point_percentage_command.replace('%%MOUNTPOINT%%', mountpoint)
|
||||
const result = await this.execute(cmd)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to determine mount utilization. Command execution error.')
|
||||
}
|
||||
|
||||
return Number(result.clean_out[0].replace('%', ''))/100
|
||||
}
|
||||
|
||||
async run_line_result(command) {
|
||||
const result = await this.execute(command)
|
||||
if ( result.exit_code !== 0 || result.clean_out.length < 1 ) {
|
||||
throw new Error('Unable to get line output from command: '+command)
|
||||
}
|
||||
return this.utility.infer(result.clean_out[0].trim())
|
||||
}
|
||||
|
||||
async run(command) {
|
||||
const result = await this.execute(command)
|
||||
if ( result.exit_code !== 0 ) {
|
||||
throw new Error('Unable to run command: '+command)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async list_files_in_directory(local_path) {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async _cleanup() {}
|
||||
|
||||
async reboot() {
|
||||
await this.run_line_result(this._reboot_command)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = Host
|
||||
30
app/classes/metal/LocalHost.js
Normal file
30
app/classes/metal/LocalHost.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const Host = require('./Host')
|
||||
const child_process = require('child_process')
|
||||
const ExecutionResult = require('../logical/ExecutionResult')
|
||||
const fs = require('fs')
|
||||
|
||||
class LocalHost extends Host {
|
||||
async execute(command) {
|
||||
const result = new ExecutionResult()
|
||||
return new Promise((resolve) => {
|
||||
child_process.exec(command, (error, stdout, stderr) => {
|
||||
result.exit(error ? error.code : 0)
|
||||
result.out(stdout)
|
||||
result.error(stderr)
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async open_file_read_stream(file_path) {
|
||||
file_path = typeof file_path === 'string' ? file_path : file_path.path
|
||||
return fs.createReadStream(file_path)
|
||||
}
|
||||
|
||||
async open_file_write_stream(file_path) {
|
||||
file_path = typeof file_path === 'string' ? file_path : file_path.path
|
||||
return fs.createWriteStream(file_path)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = LocalHost
|
||||
77
app/classes/metal/RemoteHost.js
Normal file
77
app/classes/metal/RemoteHost.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const Host = require('./Host')
|
||||
const ssh = require('ssh2')
|
||||
const ExecutionResult = require('../logical/ExecutionResult')
|
||||
const fs = require('fs').promises
|
||||
|
||||
class RemoteHost extends Host {
|
||||
_ssh = false
|
||||
|
||||
async execute(command) {
|
||||
const conn = await this._get_ssh_connection()
|
||||
const result = new ExecutionResult()
|
||||
await new Promise((resolve, reject) => {
|
||||
conn.exec(command, (error, stream) => {
|
||||
if ( error ) reject(error)
|
||||
else {
|
||||
stream.on('close', (code, signal) => {
|
||||
result.exit(code)
|
||||
resolve()
|
||||
}).on('data', (data) => {
|
||||
result.out(data)
|
||||
}).stderr.on('data', (data) => {
|
||||
result.error(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async open_file_read_stream(file_path) {
|
||||
file_path = typeof file_path === 'string' ? file_path : file_path.path
|
||||
const sftp = await this._get_sftp_connection()
|
||||
return sftp.createReadStream(file_path)
|
||||
}
|
||||
|
||||
async open_file_write_stream(file_path) {
|
||||
file_path = typeof file_path === 'string' ? file_path : file_path.path
|
||||
const sftp = await this._get_sftp_connection()
|
||||
return sftp.createWriteStream(file_path)
|
||||
}
|
||||
|
||||
async _get_ssh_connection() {
|
||||
if ( this._ssh ) return this._ssh
|
||||
|
||||
if ( this.config.ssh_params && this.config.ssh_params.key_file && !this.config.ssh_params.privateKey ) {
|
||||
this.config.ssh_params.privateKey = await fs.readFile(this.config.ssh_params.key_file)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = new ssh.Client()
|
||||
client.on('ready', () => {
|
||||
this._ssh = client
|
||||
resolve(client)
|
||||
}).connect(this.config.ssh_params)
|
||||
client.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async _get_sftp_connection() {
|
||||
const ssh = await this._get_ssh_connection()
|
||||
return new Promise((resolve, reject) => {
|
||||
ssh.sftp((err, sftp) => {
|
||||
if ( err ) reject(err)
|
||||
else resolve(sftp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async _cleanup() {
|
||||
if ( this._ssh ) {
|
||||
this._ssh.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RemoteHost
|
||||
24
app/classes/state/State.js
Normal file
24
app/classes/state/State.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const ImplementationError = require('libflitter/errors/ImplementationError')
|
||||
|
||||
class State extends Injectable {
|
||||
constructor(host, config) {
|
||||
super()
|
||||
this._host = host
|
||||
this._config = config
|
||||
}
|
||||
|
||||
async apply() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async check() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
async reverse() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = State
|
||||
29
app/classes/state/fs/DirectoryState.js
Normal file
29
app/classes/state/fs/DirectoryState.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const State = require('../State')
|
||||
|
||||
class DirectoryState extends State {
|
||||
async apply() {
|
||||
if ( !(await this.check()) ) {
|
||||
const path = await this._path()
|
||||
await path.touch(true)
|
||||
}
|
||||
}
|
||||
|
||||
async check() {
|
||||
const path = await this._path()
|
||||
await path.classify()
|
||||
return path.is_directory()
|
||||
}
|
||||
|
||||
async reverse() {
|
||||
if ( await this.check() ) {
|
||||
const path = await this._path()
|
||||
await path.unlink()
|
||||
}
|
||||
}
|
||||
|
||||
async _path() {
|
||||
return this._host.get_path(this._config.path)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = DirectoryState
|
||||
64
app/classes/state/fs/FileContentState.js
Normal file
64
app/classes/state/fs/FileContentState.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const State = require('../State')
|
||||
const UniversalPath = require('../../logical/UniversalPath')
|
||||
|
||||
class FileContentState extends State {
|
||||
static get services() {
|
||||
return [...super.services, 'hosts']
|
||||
}
|
||||
|
||||
async apply() {
|
||||
const target_path = await this._target()
|
||||
if ( target_path.is_valid() ) {
|
||||
|
||||
}
|
||||
|
||||
if ( this._config.source ) {
|
||||
const source_path = await this._source()
|
||||
await source_path.copy_to(target_path)
|
||||
} else {
|
||||
await target_path.echo(this._config.contents)
|
||||
}
|
||||
}
|
||||
|
||||
async check() {
|
||||
if ( this._config.source ) {
|
||||
const source_path = await this._source()
|
||||
const source_hash = await source_path.hash()
|
||||
|
||||
const target_path = await this._target()
|
||||
if ( !target_path.is_file() ) return false
|
||||
const target_hash = await target_path.hash()
|
||||
|
||||
return source_hash === target_hash
|
||||
} else {
|
||||
const localhost = this.hosts.get('localhost')
|
||||
const temp_path = await localhost.get_temp_file()
|
||||
await temp_path.echo(this._config.contents)
|
||||
const source_hash = await temp_path.hash()
|
||||
|
||||
const target_path = await this._target()
|
||||
if ( !target_path.is_file() ) return false
|
||||
const target_hash = await target_path.hash()
|
||||
|
||||
return source_hash === target_hash
|
||||
}
|
||||
}
|
||||
|
||||
async reverse() {
|
||||
await (await this._target()).echo('')
|
||||
}
|
||||
|
||||
async _target() {
|
||||
if ( typeof this._config.target === 'string' ) {
|
||||
return UniversalPath.fromString(this._config.target)
|
||||
} else return this._config.target
|
||||
}
|
||||
|
||||
async _source() {
|
||||
if ( typeof this._config.target === 'string' ) {
|
||||
return UniversalPath.fromString(this._config.target)
|
||||
} else return this._config.target
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = FileContentState
|
||||
29
app/classes/state/fs/FileState.js
Normal file
29
app/classes/state/fs/FileState.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const State = require('../State')
|
||||
|
||||
class FileState extends State {
|
||||
async apply() {
|
||||
if ( !(await this.check()) ) {
|
||||
const path = await this._path()
|
||||
await path.touch()
|
||||
}
|
||||
}
|
||||
|
||||
async check() {
|
||||
const path = await this._path()
|
||||
await path.classify()
|
||||
return path.is_file()
|
||||
}
|
||||
|
||||
async reverse() {
|
||||
if ( await this.check() ) {
|
||||
const path = await this._path()
|
||||
await path.unlink()
|
||||
}
|
||||
}
|
||||
|
||||
async _path() {
|
||||
return this._host.get_path(this._config.path)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = FileState
|
||||
Reference in New Issue
Block a user