Compare commits

...

19 Commits
0.8 ... 0.11

Author SHA1 Message Date
cupcakearmy
678aa96c06 version bump 2019-12-21 23:38:07 +01:00
cupcakearmy
e51eacf13c support for tilde in optional arguments 2019-12-21 23:37:44 +01:00
12d2e010bb Update README.md 2019-12-10 14:03:13 +01:00
e25e65e052 Update README.md 2019-12-10 14:02:24 +01:00
4491cfd536 Update README.md 2019-12-10 14:00:44 +01:00
d0e82b47e1 Update README.md 2019-12-10 13:59:11 +01:00
cupcakearmy
90f9a998e8 Merge remote-tracking branch 'origin/master' 2019-12-10 13:45:09 +01:00
cupcakearmy
b40adcae1f added command to display some info about the config file 2019-12-10 13:44:59 +01:00
cupcakearmy
ad5afab355 version bump 2019-12-10 13:44:41 +01:00
cupcakearmy
5b0011330c now shows elapsed time on each backup and some depulication of code 2019-12-10 13:44:30 +01:00
fd2fd91635 Update README.md 2019-12-05 00:31:41 +01:00
9c09ce1d79 Update README.md 2019-12-05 00:31:05 +01:00
c2f6f51789 Update README.md 2019-12-05 00:27:01 +01:00
cupcakearmy
f09cf90653 hooks for backups 2019-12-05 00:24:20 +01:00
cupcakearmy
d352aced37 version bump 2019-12-05 00:24:11 +01:00
cupcakearmy
563d4ffb96 remove duplicate code 2019-12-05 00:23:49 +01:00
cupcakearmy
1c6a061dd1 cleanup types 2019-12-05 00:23:15 +01:00
cupcakearmy
504ad639ab function to convert a variable to an array if its not already 2019-12-05 00:23:06 +01:00
f7a15c6d86 Update README.md 2019-12-05 00:22:01 +01:00
10 changed files with 333 additions and 92 deletions

220
README.md
View File

@@ -14,7 +14,18 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
- Simple interface - Simple interface
- Fully encrypted - Fully encrypted
## Installation ### 📒 Docs
* [Locations](#-locations)
* [Pruning & Deleting old files](#pruning-and-snapshot-policies)
* [Excluding files](#excluding-filesfolders)
* [Hooks](#before--after-hooks)
* [Backends](#-backends)
* [Commands](#-commands)
## 🛳 Installation
Linux & macOS. Windows is not supported.
``` ```
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
@@ -60,27 +71,21 @@ If we would check only one location we could run the following: `autorestic chec
Note that the data is automatically encrypted on the server. The key will be generated and added to your config file. Every backend will have a separate key. You should keep a copy of the keys somewhere in case your server dies. Otherwise DATA IS LOST! Note that the data is automatically encrypted on the server. The key will be generated and added to your config file. Every backend will have a separate key. You should keep a copy of the keys somewhere in case your server dies. Otherwise DATA IS LOST!
### Backup ### 📦 Backup
``` ```
autorestic backup -a autorestic backup -a
``` ```
### Restore ### 📼 Restore
```
autorestic restore -a --to /path/where/to/restore
```
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one.
Lets see a more realistic example (from the config above)
``` ```
autorestic restore -l home --from hdd --to /path/where/to/restore autorestic restore -l home --from hdd --to /path/where/to/restore
``` ```
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd` ### 📲 Updates
Autorestic can update itself! Super handy right? Simply run `autorestic update` and we will check for you if there are updates for restic and autorestic and install them if necessary.
## 🗂 Locations ## 🗂 Locations
@@ -95,43 +100,7 @@ locations:
- also-backup-to-this-backend - also-backup-to-this-backend
``` ```
## 💽 Backends #### Pruning and snapshot policies
Backends are the place where you data will be saved. Backups are incremental and encrypted.
### Fields
##### `type`
Type of the backend see a list [here](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html)
Supported are:
- [Local](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local)
- [Backblaze B2](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2)
- [Amazon S3](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3)
- [Minio](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#minio-server)
- [Google Cloud Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage)
- [Microsoft Azure Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage)
For each backend you need to specify the right variables as shown in the example below.
##### `path`
The path on the remote server.
For object storages as
##### Example
```yaml
backends:
name-of-backend:
type: b2
path: 'myAccount:myBucket/my/path'
B2_ACCOUNT_ID: backblaze_account_id
B2_ACCOUNT_KEY: backblaze_account_key
```
### Pruning and snapshot policies
Autorestic supports declaring snapshot policies for location to avoid keeping old snapshot around if you don't need them. Autorestic supports declaring snapshot policies for location to avoid keeping old snapshot around if you don't need them.
@@ -176,7 +145,7 @@ f8f8f976 2019-12-02 12:11:08 computer within 2w /etc
3 snapshots 3 snapshots
``` ```
### Excluding files/folders #### Excluding files/folders
If you want to exclude certain files or folders it done easily by specifiyng the right flags in the location you desire to filter. The flags are taken straight from the [restic cli exclude rules](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files). If you want to exclude certain files or folders it done easily by specifiyng the right flags in the location you desire to filter. The flags are taken straight from the [restic cli exclude rules](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files).
@@ -201,6 +170,159 @@ backends:
... ...
``` ```
#### Before / After hooks
Sometimes you might want to stop an app/db before backing up data and start the service again after the backup has completed. This is what the hooks are made for. Simply add them to your location config. You can have as many commands as you wish.
```yaml
locations:
my-location:
from: /data
to:
- local
- remote
hooks:
before:
- echo "Hello"
- echo "Human"
after:
- echo "kthxbye"
```
## 💽 Backends
Backends are the place where you data will be saved. Backups are incremental and encrypted.
### Fields
##### `type`
Type of the backend see a list [here](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html)
Supported are:
- [Local](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local)
- [Backblaze B2](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2)
- [Amazon S3](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3)
- [Minio](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#minio-server)
- [Google Cloud Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage)
- [Microsoft Azure Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage)
For each backend you need to specify the right variables as shown in the example below.
##### `path`
The path on the remote server.
For object storages as
##### Example
```yaml
backends:
name-of-backend:
type: b2
path: 'myAccount:myBucket/my/path'
B2_ACCOUNT_ID: backblaze_account_id
B2_ACCOUNT_KEY: backblaze_account_key
```
## 👉 Commands
* [info](#info)
* [check](#check)
* [backup](#backup)
* [forget](#forget)
* [restore](#restore)
* [exec](#exec)
* [intall](#install)
* [uninstall](#uninstall)
* [upgrade](#upgrade)
### Info
```
autorestic info
```
Shows all the information in the config file. Usefull for a quick overview of what location backups where.
Pro tip: if it gets a bit long you can read it more easily with `autorestic info | less` 😉
### Check
```
autorestic check [-b, --backend] [-a, --all]
```
Checks the backends and configures them if needed. Can be applied to all with the `-a` flag or by specifying one or more backends with the `-b` or `--backend` flag.
### Backup
```
autorestic backup [-l, --location] [-a, --all]
```
Performes a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
### Restore
```
autorestic restore [-l, --location] [--from backend] [--to <out dir>]
```
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one.
Lets see a more realistic example (from the config above)
```
autorestic restore -l home --from hdd --to /path/where/to/restore
```
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd`
```
autorestic restore
```
Performes a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
### Forget
```
autorestic forget [-l, --location] [-a, --all] [--dry-run]
```
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](#pruning-and-snapshot-policies)
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
### Exec
```
autorestic exec [-b, --backend] [-a, --all] <command> -- [native options]
```
This is avery handy command which enables you to run any native restic command on desired backends. An example would be listing all the snapshots of all your backends:
```
autorestic exec -a -- snapshots
```
#### Install
Installs both restic and autorestic
#### Uninstall
Uninstall both restic and autorestic
#### Upgrade
Upgrades both restic and autorestic automagically
## Contributors ## Contributors
This amazing people helped the project! This amazing people helped the project!

View File

@@ -19,6 +19,7 @@
"clitastic": "0.0.1", "clitastic": "0.0.1",
"colors": "^1.3.3", "colors": "^1.3.3",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"minimist": "^1.2.0" "minimist": "^1.2.0",
"uhrwerk": "^1.0.0"
} }
} }

View File

@@ -3,7 +3,6 @@ import minimist from 'minimist'
import { init } from './config' import { init } from './config'
import handlers, { error, help } from './handlers' import handlers, { error, help } from './handlers'
import { Config } from './types'
@@ -26,7 +25,7 @@ export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
string: ['l', 'b'], string: ['l', 'b'],
}) })
export const VERSION = '0.8' export const VERSION = '0.11'
export const INSTALL_DIR = '/usr/local/bin' export const INSTALL_DIR = '/usr/local/bin'
export const VERBOSE = flags.verbose export const VERBOSE = flags.verbose

View File

@@ -3,12 +3,21 @@ import { Writer } from 'clitastic'
import { config, VERBOSE } from './autorestic' import { config, VERBOSE } from './autorestic'
import { getEnvFromBackend } from './backend' import { getEnvFromBackend } from './backend'
import { Locations, Location } from './types' import { Locations, Location } from './types'
import { exec, ConfigError, pathRelativeToConfigFile, getFlagsFromLocation } from './utils' import {
exec,
ConfigError,
pathRelativeToConfigFile,
getFlagsFromLocation,
makeArrayIfIsNot,
execPlain,
MeasureDuration, fill,
} from './utils'
export const backupSingle = (name: string, to: string, location: Location) => { export const backupSingle = (name: string, to: string, location: Location) => {
if (!config) throw ConfigError if (!config) throw ConfigError
const delta = new MeasureDuration()
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳') const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
const backend = config.backends[to] const backend = config.backends[to]
@@ -21,18 +30,30 @@ export const backupSingle = (name: string, to: string, location: Location) => {
) )
if (VERBOSE) console.log(cmd.out, cmd.err) if (VERBOSE) console.log(cmd.out, cmd.err)
writer.done(name + to.blue + ' : ' + 'Done ✓'.green) writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
} }
export const backupLocation = (name: string, location: Location) => { export const backupLocation = (name: string, location: Location) => {
const display = name.yellow + ' ▶ ' const display = name.yellow + ' ▶ '
const filler = new Array(name.length + 3).fill(' ').join('') const filler = fill(name.length + 3)
let first = true let first = true
for (const t of Array.isArray(location.to) ? location.to : [location.to]) { if (location.hooks && location.hooks.before)
for (const command of makeArrayIfIsNot(location.hooks.before)) {
const cmd = execPlain(command)
if (cmd) console.log(cmd.out, cmd.err)
}
for (const t of makeArrayIfIsNot(location.to)) {
backupSingle(first ? display : filler, t, location) backupSingle(first ? display : filler, t, location)
if (first) first = false if (first) first = false
} }
if (location.hooks && location.hooks.after)
for (const command of makeArrayIfIsNot(location.hooks.after)) {
const cmd = execPlain(command)
if (cmd) console.log(cmd.out, cmd.err)
}
} }
export const backupAll = (locations?: Locations) => { export const backupAll = (locations?: Locations) => {

View File

@@ -3,7 +3,7 @@ import { resolve } from 'path'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { flags } from './autorestic' import { flags } from './autorestic'
import { Backend, Config } from './types' import { Backend, Config } from './types'
import { makeObjectKeysLowercase, rand } from './utils' import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
import { homedir } from 'os' import { homedir } from 'os'
@@ -48,8 +48,8 @@ export const normalizeAndCheckBackups = (config: Config) => {
`The backup "${name}" is missing some required attributes`, `The backup "${name}" is missing some required attributes`,
) )
if (Array.isArray(to)) for (const t of to) checkDestination(t, name) for (const t of makeArrayIfIsNot(to))
else checkDestination(to, name) checkDestination(t, name)
} }
} }

View File

@@ -3,7 +3,14 @@ import { Writer } from 'clitastic'
import { config, VERBOSE } from './autorestic' import { config, VERBOSE } from './autorestic'
import { getEnvFromBackend } from './backend' import { getEnvFromBackend } from './backend'
import { Locations, Location, Flags } from './types' import { Locations, Location, Flags } from './types'
import { exec, ConfigError, pathRelativeToConfigFile, getFlagsFromLocation } from './utils' import {
exec,
ConfigError,
pathRelativeToConfigFile,
getFlagsFromLocation,
makeArrayIfIsNot,
fill,
} from './utils'
@@ -35,10 +42,10 @@ export const forgetSingle = (name: string, to: string, location: Location, dryRu
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => { export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
const display = name.yellow + ' ▶ ' const display = name.yellow + ' ▶ '
const filler = new Array(name.length + 3).fill(' ').join('') const filler = fill(name.length + 3)
let first = true let first = true
for (const t of Array.isArray(backup.to) ? backup.to : [backup.to]) { for (const t of makeArrayIfIsNot(backup.to)) {
const nameOrBlankSpaces: string = first ? display : filler const nameOrBlankSpaces: string = first ? display : filler
forgetSingle(nameOrBlankSpaces, t, backup, dryRun) forgetSingle(nameOrBlankSpaces, t, backup, dryRun)
if (first) first = false if (first) first = false

View File

@@ -8,15 +8,15 @@ import { config, INSTALL_DIR, VERSION } from './autorestic'
import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend } from './backend' import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend } from './backend'
import { backupAll } from './backup' import { backupAll } from './backup'
import { forgetAll } from './forget' import { forgetAll } from './forget'
import { Backend, Backends, Flags, Locations } from './types' import showAll from './info'
import { Backends, Flags, Locations } from './types'
import { import {
checkIfCommandIsAvailable, checkIfCommandIsAvailable,
checkIfResticIsAvailable, checkIfResticIsAvailable,
downloadFile, downloadFile,
exec, exec,
filterObjectByKey, filterObjectByKey,
singleToArray, ConfigError, makeArrayIfIsNot,
ConfigError,
} from './utils' } from './utils'
@@ -35,7 +35,7 @@ const parseBackend = (flags: Flags): Backends => {
) )
if (flags.all) return config.backends if (flags.all) return config.backends
else { else {
const backends = singleToArray<string>(flags.backend) const backends = makeArrayIfIsNot<string>(flags.backend)
for (const backend of backends) for (const backend of backends)
if (!config.backends[backend]) if (!config.backends[backend])
throw new Error('Invalid backend: '.red + backend) throw new Error('Invalid backend: '.red + backend)
@@ -55,7 +55,7 @@ const parseLocations = (flags: Flags): Locations => {
if (flags.all) { if (flags.all) {
return config.locations return config.locations
} else { } else {
const locations = singleToArray<string>(flags.location) const locations = makeArrayIfIsNot<string>(flags.location)
for (const location of locations) for (const location of locations)
if (!config.locations[location]) if (!config.locations[location])
throw new Error('Invalid location: '.red + location) throw new Error('Invalid location: '.red + location)
@@ -139,6 +139,9 @@ const handlers: Handlers = {
console.log(out, err) console.log(out, err)
} }
}, },
async info() {
showAll()
},
async install() { async install() {
try { try {
checkIfResticIsAvailable() checkIfResticIsAvailable()
@@ -241,6 +244,7 @@ export const help = () => {
`\n -c, --config Specify config file. Default: .autorestic.yml` + `\n -c, --config Specify config file. Default: .autorestic.yml` +
'\n' + '\n' +
'\nCommands:'.yellow + '\nCommands:'.yellow +
'\n info Show all locations and backends' +
'\n check [-b, --backend] [-a, --all] Check backends' + '\n check [-b, --backend] [-a, --all] Check backends' +
'\n backup [-l, --location] [-a, --all] Backup all or specified locations' + '\n backup [-l, --location] [-a, --all] Backup all or specified locations' +
'\n forget [-l, --location] [-a, --all] [--dry-run] Forget old snapshots according to declared policies' + '\n forget [-l, --location] [-a, --all] [--dry-run] Forget old snapshots according to declared policies' +

28
src/info.ts Normal file
View File

@@ -0,0 +1,28 @@
import { config } from './autorestic'
import { ConfigError, fill, treeToString } from './utils'
const showAll = () => {
if (!config) throw ConfigError
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
for (const [key, data] of Object.entries(config.locations)) {
console.log(`\n${key.blue.underline}:`)
console.log(treeToString(
data,
['to:', 'from:', 'hooks:', 'options:'],
))
}
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
for (const [key, data] of Object.entries(config.backends)) {
console.log(`\n${key.blue.underline}:`)
console.log(treeToString(
data,
['type:', 'path:', 'key:'],
))
}
}
export default showAll

View File

@@ -1,3 +1,7 @@
export type StringOrArray = string | string[]
// BACKENDS
type BackendLocal = { type BackendLocal = {
type: 'local' type: 'local'
key: string key: string
@@ -62,30 +66,26 @@ export type Backend =
export type Backends = { [name: string]: Backend } export type Backends = { [name: string]: Backend }
export type ForgetPolicy = { // LOCATIONS
last?: number,
hourly?: number,
daily?: number,
weekly?: number,
monthly?: number,
yearly?: number,
within?: string,
tags?: string[],
}
export type Location = { export type Location = {
from: string from: string
to: string | string[] to: StringOrArray
keep?: ForgetPolicy hooks?: {
before?: StringOrArray
after?: StringOrArray
}
options?: { options?: {
[key: string]: { [key: string]: {
[key: string]: string | string[] [key: string]: StringOrArray
} }
} }
} }
export type Locations = { [name: string]: Location } export type Locations = { [name: string]: Location }
// OTHER
export type Config = { export type Config = {
locations: Locations locations: Locations
backends: Backends backends: Backends

View File

@@ -1,8 +1,12 @@
import axios from 'axios'
import { spawnSync, SpawnSyncOptions } from 'child_process' import { spawnSync, SpawnSyncOptions } from 'child_process'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import { createWriteStream } from 'fs' import { createWriteStream } from 'fs'
import { isAbsolute, resolve, dirname } from 'path' import { dirname, isAbsolute, join, resolve } from 'path'
import { homedir } from 'os'
import axios from 'axios'
import { Duration, Humanizer } from 'uhrwerk'
import { CONFIG_FILE } from './config' import { CONFIG_FILE } from './config'
import { Location } from './types' import { Location } from './types'
@@ -27,6 +31,16 @@ export const exec = (
return { out, err } return { out, err }
} }
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
const split = command.split(' ')
if (split.length < 1) {
console.log(`The command ${command} is not valid`.red)
return
}
return exec(split[0], split.slice(1), opt)
}
export const checkIfResticIsAvailable = () => export const checkIfResticIsAvailable = () =>
checkIfCommandIsAvailable( checkIfCommandIsAvailable(
'restic', 'restic',
@@ -50,9 +64,6 @@ export function rand(length = 32): string {
} }
export const singleToArray = <T>(singleOrArray: T | T[]): T[] =>
Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray]
export const filterObject = <T>( export const filterObject = <T>(
obj: { [key: string]: T }, obj: { [key: string]: T },
filter: (item: [string, T]) => boolean, filter: (item: [string, T]) => boolean,
@@ -86,6 +97,11 @@ export const pathRelativeToConfigFile = (path: string): string => isAbsolute(pat
? path ? path
: resolve(dirname(CONFIG_FILE), path) : resolve(dirname(CONFIG_FILE), path)
export const resolveTildePath = (path: string): string | null =>
(path.length === 0 || path[0] !== '~')
? null
: join(homedir(), path.slice(1))
export const ConfigError = new Error('Config file not found') export const ConfigError = new Error('Config file not found')
export const getFlagsFromLocation = (location: Location, command?: string): string[] => { export const getFlagsFromLocation = (location: Location, command?: string): string[] => {
@@ -98,12 +114,55 @@ export const getFlagsFromLocation = (location: Location, command?: string): stri
let flags: string[] = [] let flags: string[] = []
// Map the flags to an array for the exec function. // Map the flags to an array for the exec function.
for (let [flag, values] of Object.entries(all)) { for (let [flag, values] of Object.entries(all))
if (!Array.isArray(values)) for (const value of makeArrayIfIsNot(values)) {
values = [values] const stringValue = String(value)
const resolvedTilde = resolveTildePath(stringValue)
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
}
for (const value of values)
flags = [...flags, `--${String(flag)}`, String(value)]
}
return flags return flags
} }
export const makeArrayIfIsNot = <T>(maybeArray: T | T[]): T[] => Array.isArray(maybeArray) ? maybeArray : [maybeArray]
export const fill = (length: number, filler = ' '): string => new Array(length).fill(filler).join('')
export const capitalize = (string: string): string => string.charAt(0).toUpperCase() + string.slice(1)
export const treeToString = (obj: Object, highlight = [] as string[]): string => {
let cleaned = JSON.stringify(obj, null, 2)
.replace(/[{}"\[\],]/g, '')
.replace(/^ {2}/mg, '')
.replace(/\n\s*\n/g, '\n')
.trim()
for (const word of highlight)
cleaned = cleaned.replace(word, capitalize(word).green)
return cleaned
}
export class MeasureDuration {
private static Humanizer: Humanizer = [
[d => d.hours() > 0, d => `${d.hours()}h ${d.minutes()}min`],
[d => d.minutes() > 0, d => `${d.minutes()}min ${d.seconds()}s`],
[d => d.seconds() > 0, d => `${d.seconds()}s`],
[() => true, d => `${d.milliseconds()}ms`],
]
private start = Date.now()
finished(human?: false): number
finished(human?: true): string
finished(human?: boolean): number | string {
const delta = Date.now() - this.start
return human
? new Duration(delta, 'ms').humanize(MeasureDuration.Humanizer)
: delta
}
}