diff --git a/app/classes/logical/UniversalPath.js b/app/classes/logical/UniversalPath.js index cb9bf52..1dc8b7d 100644 --- a/app/classes/logical/UniversalPath.js +++ b/app/classes/logical/UniversalPath.js @@ -1,5 +1,6 @@ const { Injectable } = require('flitter-di') const moment = require('moment') +const path = require('path') class UniversalPath extends Injectable { static get services() { @@ -56,6 +57,12 @@ class UniversalPath extends Injectable { return this.is_file() || this.is_directory() } + async directory() { + await this.classify() + if ( this.is_directory() ) return this.path + return path.dirname(this.path) + } + async classify() { const dir_result = await this._host.execute(`${this._directory_classify_command} ${this._path}`) if ( dir_result.exit_code === 0 ) { diff --git a/app/classes/metal/Host.js b/app/classes/metal/Host.js index 28d81e7..3931caf 100644 --- a/app/classes/metal/Host.js +++ b/app/classes/metal/Host.js @@ -23,6 +23,7 @@ class Host extends Injectable { _file_directory_delete_command = `rm -rf "%%RESOURCE%%"` _resolve_path_command = `readlink -f "%%PATH%%"` _reboot_command = `reboot` + _change_directory_command = `cd "%%PATH%%"` constructor(config) { super() @@ -106,6 +107,11 @@ class Host extends Injectable { await this.execute(this._file_directory_delete_command.replace('%%RESOURCE%%', resource_path)) } + get_directory_change_command(path) { + if ( typeof path !== 'string' ) path = path.path + return this._change_directory_command.replace('%%PATH%%', 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)) diff --git a/app/classes/state/fs/DownloadState.js b/app/classes/state/fs/DownloadState.js new file mode 100644 index 0000000..540aefb --- /dev/null +++ b/app/classes/state/fs/DownloadState.js @@ -0,0 +1,51 @@ +const State = require('../State') +const axios = require('axios') + +class DownloadState extends State { + static get services() { + return [...super.services, 'output'] + } + + async apply() { + if ( !(await this.check()) ) { + const path = await this._path() + if ( !this._config.method ) this._config.method = 'get' + else this._config.method = this._config.method.toLowerCase() + + if ( !this._config.source ) throw new Error('Missing source config for DownloadState.') + try { + const res = await axios({ + method: this._config.method, + url: this._config.source, + responseType: 'stream', + }) + + const write_stream = await path.open_write_stream() + await res.data.pipe(write_stream) + } catch(e) { + this.output.error('Error encountered while fetching data for DownloadState.') + throw e + } + } + } + + 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() { + if ( !this._config.path ) throw new Error('Missing path config for DownloadState.') + return this._host.get_path(this._config.path) + } +} + +module.exports = exports = DownloadState diff --git a/app/classes/state/fs/FileState.js b/app/classes/state/fs/FileState.js index 51d33b4..45e5aaf 100644 --- a/app/classes/state/fs/FileState.js +++ b/app/classes/state/fs/FileState.js @@ -28,6 +28,7 @@ class FileState extends State { } async _path() { + if ( !this._config.path ) throw new Error('Missing path config for FileState.') return this._host.get_path(this._config.path) } } diff --git a/app/classes/state/fs/UnpackState.js b/app/classes/state/fs/UnpackState.js new file mode 100644 index 0000000..9ab379c --- /dev/null +++ b/app/classes/state/fs/UnpackState.js @@ -0,0 +1,63 @@ +const State = require('../State') + +class UnpackState extends State { + static get services() { + return [...super.services, 'output'] + } + + async apply() { + if ( !(await this.check()) ) { + const path = await this._path() + await path.classify() + if ( !path.is_file() ) throw new Error(`Invalid path for unpack: ${path}`) + const type = await this._get_type() + const destination = await this._destination() + const cd_cmd = await this._host.get_directory_change_command(await destination.directory()) + + if ( type === 'tar' ) { + const untar_cmd = `${cd_cmd} && tar -x -z -f "${path.path}"` + await this._host.run(untar_cmd) + + } else if ( type === 'zip' ) { + const unzip_cmd = `${cd_cmd} && unzip "${path.path}"` + await this._host.run(unzip_cmd) + } + } + } + + async check() { + return false + } + + async reverse() { + this.output.warn(`Unpack state does not currently support reversal. (Host: ${this._host.name})`) + } + + async _get_type() { + if ( this._config.type ) { + if ( this._config.type === 'tar' ) return 'tar' + else if ( this._config.type === 'zip' ) return 'zip' + throw new Error('Invalid unpack type: ' + this._config.type) + } else { + const path = await this._path() + if ( path.path.endsWith('.tar.gz') || path.path.endsWith('.tgz') ) return 'tar' + else if ( path.path.endsWith('.zip') ) return 'zip' + throw new Error(`Unable to determine unpack type from archive: ${path}`) + } + } + + async _path() { + if ( !this._config.path ) throw new Error('Missing path config for UnpackState.') + return this._host.get_path(this._config.path) + } + + async _destination() { + if ( !this._config.destination ) throw new Error('Missing destination config for UnpackState.') + const path = await this._host.get_path(this._config.destination) + await path.classify() + if ( !path.is_directory() ) throw new Error(`Invalid extraction path. Must be a directory: ${path}`) + return path + } +} + +module.exports = exports = UnpackState diff --git a/app/services/states.service.js b/app/services/states.service.js index 0d446df..08ef2cd 100644 --- a/app/services/states.service.js +++ b/app/services/states.service.js @@ -13,14 +13,14 @@ class StatesService extends Service { static #state_map = { // TODO apache and nginx states - virtual host, reverse proxy // TODO file/directory permissions state - chmod & chown - // TODO file download state - // TODO file unpack state - zips, tarballs + // TODO file pack state - zip, tarball // TODO package repository states - import keys, install repository // TODO service manager states - service enabled, service installed // TODO git states - clone repo, ref checked out 'fs.file': require('../classes/state/fs/FileState'), 'fs.directory': require('../classes/state/fs/DirectoryState'), + 'fs.unpack': require('../classes/state/fs/UnpackState'), 'package.present': require('../classes/state/os/PackageState'), 'package.updates': require('../classes/state/os/UpdateState'), @@ -29,6 +29,8 @@ class StatesService extends Service { 'service.running': require('../classes/state/os/ServiceState'), 'service.restarted': require('../classes/state/os/ServiceRestartState'), 'service.daemon.reloaded': require('../classes/state/os/ServiceDaemonReloadState'), + + 'web.download': require('../classes/state/fs/DownloadState'), } static get services() { diff --git a/package.json b/package.json index 714441d..2370ae8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "Garrett Mills (https://glmdev.tech/)", "license": "MIT", "dependencies": { + "axios": "^0.19.2", "cross-zip": "^2.1.6", "flitter-agenda": "^0.5.0", "flitter-auth": "^0.15.1", diff --git a/yarn.lock b/yarn.lock index 8c79d5a..a145b4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -208,6 +208,13 @@ axios@^0.19.0: follow-redirects "1.5.10" is-buffer "^2.0.2" +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-core@^5.4.7: version "5.8.38" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-5.8.38.tgz#1fcaee79d7e61b750b00b8e54f6dfc9d0af86558"