mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 10:30:39 +00:00
Compare commits
186 Commits
Author | SHA1 | Date | |
---|---|---|---|
cfcc010bc5 | |||
1fd009b819 | |||
91e902d7ef | |||
b690c1c3a9 | |||
b025aeeeab | |||
01cafb9436 | |||
6a2c34df48 | |||
88e47efc68 | |||
60d7e0b561 | |||
e9a7e03af7 | |||
d959a50b5a | |||
b5bc1a4cee | |||
|
e61c940d23 | ||
7fb0fbd467 | |||
cbc1b33750 | |||
e7be01da37 | |||
|
309073fe4d | ||
|
1243721a7e | ||
ebb934d1c5 | |||
8417a6e0aa | |||
2b5efd0499 | |||
af6e715b27 | |||
9484299cd9 | |||
ef93fef36a | |||
995f2250a1 | |||
3498e6da7b | |||
7128983581 | |||
158407927a | |||
ea8bf83799 | |||
e8a8ccccd0 | |||
b332897713 | |||
369037ea77 | |||
|
52cd6fe3fd | ||
|
b6f7ef6577 | ||
1a891fffbd | |||
ed68160a9a | |||
20ecd21928 | |||
df6fd75ca4 | |||
c918fb6ded | |||
2bc8fe4ee7 | |||
757a134362 | |||
6bb36cbde0 | |||
5c0b67b7fb | |||
1520e10a59 | |||
f9c645120b | |||
8fd0240929 | |||
da7b70c7a7 | |||
c138ac9882 | |||
af04f1cded | |||
3a43b2b113 | |||
885c7ca778 | |||
44a48aab9b | |||
f5f550f5b1 | |||
f1d5a6b5dd | |||
734c962006 | |||
d20a2fda68 | |||
8dcd0b1f12 | |||
9acb6296e4 | |||
82f6942ff1 | |||
22f5f61ee0 | |||
ddce8bf8a7 | |||
abeaacf182 | |||
db436587ee | |||
32837e7e45 | |||
500abfbd27 | |||
4ee54110a6 | |||
92e5071343 | |||
7e577c439a | |||
bc36a39de4 | |||
9e6b393e62 | |||
de34396b93 | |||
ebbe10608a | |||
8a34270934 | |||
e459e393a9 | |||
b1a3074f33 | |||
ae63d8b12e | |||
7aa937dd41 | |||
|
37361727ba | ||
f1874438e5 | |||
|
066342a7b7 | ||
|
f620bb1764 | ||
|
e3506e44b5 | ||
|
f65a83991b | ||
f10b8c7990 | |||
|
a8af085d9c | ||
fa89d2941f | |||
|
bcabd467c9 | ||
|
005072b90f | ||
|
d13d4f7cf1 | ||
330e3254f7 | |||
38763ed919 | |||
|
886b6362cd | ||
|
9ece1d867d | ||
|
485ada6599 | ||
|
e80db74af4 | ||
|
2fd9e2dd22 | ||
0c654eacf1 | |||
|
8fdf5188ff | ||
|
22d93f0b9c | ||
|
f940f23338 | ||
|
678aa96c06 | ||
|
e51eacf13c | ||
12d2e010bb | |||
e25e65e052 | |||
4491cfd536 | |||
d0e82b47e1 | |||
|
90f9a998e8 | ||
|
b40adcae1f | ||
|
ad5afab355 | ||
|
5b0011330c | ||
fd2fd91635 | |||
9c09ce1d79 | |||
c2f6f51789 | |||
|
f09cf90653 | ||
|
d352aced37 | ||
|
563d4ffb96 | ||
|
1c6a061dd1 | ||
|
504ad639ab | ||
f7a15c6d86 | |||
|
2f0092befe | ||
|
1026e68b68 | ||
2389c59aa9 | |||
087aeaf578 | |||
3b7062f733 | |||
|
96b63c744b | ||
|
9669b70e20 | ||
|
bcb081234c | ||
|
336f44e9dc | ||
|
d0cda7f1d5 | ||
|
a8f4c23254 | ||
|
1c9f6d7d91 | ||
|
18c3f4a06f | ||
632062a23f | |||
3d1d7ba256 | |||
|
417c54db4d | ||
|
a9696bbc0c | ||
|
45f7506478 | ||
|
d7cdeafe60 | ||
|
cf09cdbb30 | ||
|
88059fe405 | ||
|
cdf18430b6 | ||
|
352754dad9 | ||
|
b68dc75053 | ||
|
6a055d3114 | ||
|
b5daff07eb | ||
b2d01d77d9 | |||
f41c042fce | |||
a81498ac42 | |||
1731ee30b3 | |||
1f4f1a1855 | |||
13cb764067 | |||
8058f37368 | |||
|
57ffa1e3fa | ||
|
671542cd30 | ||
|
322df9f0bd | ||
|
652158d1ed | ||
|
06ce8180fb | ||
81f513d77b | |||
e32521e6ea | |||
f5c5b39b30 | |||
e016c8defc | |||
a2e0a0c9cc | |||
|
f9b04ea342 | ||
770c9dd7d4 | |||
|
851bbe5776 | ||
|
8fb6bdb3c6 | ||
|
47f5d91e89 | ||
|
de27034b94 | ||
|
9dafe9d36a | ||
|
d47e7d0912 | ||
|
e47d6be854 | ||
|
993fe072e2 | ||
|
3d1e28e574 | ||
|
3c0ebdfb4a | ||
|
2653633c91 | ||
|
6a17444c4c | ||
|
9538881f9f | ||
|
edddcebcea | ||
|
793ab1c6fe | ||
|
9832eeab22 | ||
|
b1f2678dc1 | ||
|
64f2eaf16b | ||
|
b1b12f4592 | ||
|
24a364ce08 | ||
|
d59362e82c | ||
|
0802dedb47 |
15
.codedoc/build.ts
Normal file
15
.codedoc/build.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { build } from '@codedoc/core';
|
||||||
|
|
||||||
|
import { config } from './config';
|
||||||
|
import { installTheme$ } from './content/theme';
|
||||||
|
import { content } from './content';
|
||||||
|
|
||||||
|
|
||||||
|
build(config, content, installTheme$, {
|
||||||
|
resolve: {
|
||||||
|
modules: ['.codedoc/node_modules']
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
modules: ['.codedoc/node_modules']
|
||||||
|
}
|
||||||
|
});
|
24
.codedoc/config.ts
Normal file
24
.codedoc/config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { configuration } from '@codedoc/core'
|
||||||
|
|
||||||
|
export const config = configuration({
|
||||||
|
src: {
|
||||||
|
base: 'docs',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
html: './build',
|
||||||
|
assets: './build',
|
||||||
|
bundle: './_',
|
||||||
|
styles: './_',
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
title: {
|
||||||
|
base: 'Autorestic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
misc: {
|
||||||
|
github: {
|
||||||
|
user: 'cupcakearmy',
|
||||||
|
repo: 'autorestic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
19
.codedoc/content/footer.tsx
Normal file
19
.codedoc/content/footer.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { CodedocConfig } from '@codedoc/core';
|
||||||
|
import { Footer as _Footer, GitterToggle$, Watermark} from '@codedoc/core/components';
|
||||||
|
|
||||||
|
|
||||||
|
export function Footer(config: CodedocConfig, renderer: any) {
|
||||||
|
let github$;
|
||||||
|
if (config.misc?.github)
|
||||||
|
github$ = <a href={`https://github.com/${config.misc.github.user}/${config.misc.github.repo}/`}
|
||||||
|
target="_blank">GitHub</a>;
|
||||||
|
|
||||||
|
let community$;
|
||||||
|
if (config.misc?.gitter)
|
||||||
|
community$ = <GitterToggle$ room={config.misc.gitter.room}/>
|
||||||
|
|
||||||
|
if (github$ && community$) return <_Footer>{github$}<hr/>{community$}</_Footer>;
|
||||||
|
else if (github$) return <_Footer>{github$}</_Footer>;
|
||||||
|
else if (community$) return <_Footer>{community$}</_Footer>;
|
||||||
|
else return <_Footer><Watermark/></_Footer>;
|
||||||
|
}
|
21
.codedoc/content/header.tsx
Normal file
21
.codedoc/content/header.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CodedocConfig } from '@codedoc/core';
|
||||||
|
import { Header as _Header, GithubButton, Watermark } from '@codedoc/core/components';
|
||||||
|
|
||||||
|
|
||||||
|
export function Header(config: CodedocConfig, renderer: any) {
|
||||||
|
return (
|
||||||
|
<_Header>{config.misc?.github ?
|
||||||
|
<fragment>
|
||||||
|
<GithubButton action={config.misc.github.action || 'Star'}
|
||||||
|
repo={config.misc.github.repo}
|
||||||
|
user={config.misc.github.user}
|
||||||
|
large={config.misc.github.large === true}
|
||||||
|
count={config.misc.github.count !== false}
|
||||||
|
standardIcon={config.misc.github.standardIcon !== false}/>
|
||||||
|
<br/><br/>
|
||||||
|
</fragment>
|
||||||
|
: ''}
|
||||||
|
<Watermark/>
|
||||||
|
</_Header>
|
||||||
|
)
|
||||||
|
}
|
57
.codedoc/content/index.tsx
Normal file
57
.codedoc/content/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { RendererLike } from '@connectv/html'
|
||||||
|
import { File } from 'rxline/fs'
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Meta,
|
||||||
|
ContentNav,
|
||||||
|
Fonts,
|
||||||
|
ToC,
|
||||||
|
GithubSearch$,
|
||||||
|
} from '@codedoc/core/components'
|
||||||
|
|
||||||
|
import { config } from '../config'
|
||||||
|
import { Header } from './header'
|
||||||
|
import { Footer } from './footer'
|
||||||
|
|
||||||
|
export function content(
|
||||||
|
_content: HTMLElement,
|
||||||
|
toc: HTMLElement,
|
||||||
|
renderer: RendererLike<any, any>,
|
||||||
|
file: File<string>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
title={config.page.title.extractor(_content, config, file)}
|
||||||
|
favicon={config.page.favicon}
|
||||||
|
meta={<Meta {...config.page.meta} />}
|
||||||
|
fonts={<Fonts {...config.page.fonts} />}
|
||||||
|
scripts={config.page.scripts}
|
||||||
|
stylesheets={config.page.stylesheets}
|
||||||
|
header={<Header {...config} />}
|
||||||
|
footer={<Footer {...config} />}
|
||||||
|
toc={
|
||||||
|
<ToC
|
||||||
|
default={'open'}
|
||||||
|
search={
|
||||||
|
config.misc?.github ? (
|
||||||
|
<GithubSearch$
|
||||||
|
repo={config.misc.github.repo}
|
||||||
|
user={config.misc.github.user}
|
||||||
|
root={config.src.base}
|
||||||
|
pick={config.src.pick.source}
|
||||||
|
drop={config.src.drop.source}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{toc}
|
||||||
|
</ToC>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{_content}
|
||||||
|
<ContentNav content={_content} />
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
8
.codedoc/content/theme.ts
Normal file
8
.codedoc/content/theme.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { funcTransport } from '@connectv/sdh/transport';
|
||||||
|
import { useTheme } from '@codedoc/core/transport';
|
||||||
|
|
||||||
|
import { theme } from '../theme';
|
||||||
|
|
||||||
|
|
||||||
|
export function installTheme() { useTheme(theme); }
|
||||||
|
export const installTheme$ = /*#__PURE__*/funcTransport(installTheme);
|
4739
.codedoc/package-lock.json
generated
Normal file
4739
.codedoc/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
.codedoc/package.json
Normal file
5
.codedoc/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@codedoc/core": "^0.2.15"
|
||||||
|
}
|
||||||
|
}
|
18
.codedoc/serve.ts
Normal file
18
.codedoc/serve.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { serve } from '@codedoc/core';
|
||||||
|
|
||||||
|
import { config } from './config';
|
||||||
|
import { content } from './content';
|
||||||
|
import { installTheme$ } from './content/theme';
|
||||||
|
|
||||||
|
|
||||||
|
const root = join(__dirname, '../');
|
||||||
|
|
||||||
|
serve(root, config, content, installTheme$, {
|
||||||
|
resolve: {
|
||||||
|
modules: ['.codedoc/node_modules']
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
modules: ['.codedoc/node_modules']
|
||||||
|
}
|
||||||
|
});
|
11
.codedoc/theme.ts
Normal file
11
.codedoc/theme.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createTheme } from '@codedoc/core/transport';
|
||||||
|
|
||||||
|
|
||||||
|
export const theme = /*#__PURE__*/createTheme({
|
||||||
|
light: {
|
||||||
|
primary: '#1eb2a6'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#1eb2a6'
|
||||||
|
}
|
||||||
|
});
|
26
.codedoc/tsconfig.json
Normal file
26
.codedoc/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"declaration": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "renderer.create",
|
||||||
|
"lib": [
|
||||||
|
"es2017",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*"
|
||||||
|
]
|
||||||
|
}
|
22
.codedoc/watch.ts
Normal file
22
.codedoc/watch.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { exec, spawn } from 'child_process';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
|
||||||
|
const cmd = 'ts-node-dev';
|
||||||
|
const params = `--project .codedoc/tsconfig.json`
|
||||||
|
+ ` -T --watch ${config.src.base},.codedoc`
|
||||||
|
+ ` --ignore-watch .codedoc/node_modules`
|
||||||
|
+ ` .codedoc/serve`;
|
||||||
|
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const child = exec(cmd + ' ' + params);
|
||||||
|
|
||||||
|
child.stdout?.pipe(process.stdout);
|
||||||
|
child.stderr?.pipe(process.stderr);
|
||||||
|
child.on('close', () => {});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const child = spawn(cmd, [params], { stdio: 'inherit', shell: 'bash' });
|
||||||
|
child.on('close', () => {});
|
||||||
|
}
|
31
.drone.yml
Normal file
31
.drone.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: node
|
||||||
|
pull: always
|
||||||
|
commands:
|
||||||
|
- yarn
|
||||||
|
- yarn run bin
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
|
||||||
|
- name: publish
|
||||||
|
image: plugins/github-release
|
||||||
|
pull: always
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: github
|
||||||
|
files: bin/*
|
||||||
|
checksum:
|
||||||
|
- sha512
|
||||||
|
note: CHANGELOG.md
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: 3b1f235f6a6f0ee1aa3f572d0833c4f0eec931dbe0378f31b9efa336a7462912
|
||||||
|
|
||||||
|
...
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: cupcakearmy
|
BIN
.github/logo.png
vendored
Normal file
BIN
.github/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,8 +1,20 @@
|
|||||||
node_modules/
|
# Editors
|
||||||
package-lock.json
|
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
config.yml
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build & Runtime
|
||||||
bin
|
bin
|
||||||
lib
|
lib
|
||||||
data
|
data
|
||||||
|
restore
|
||||||
|
docker
|
||||||
|
Dockerfile
|
||||||
|
build
|
||||||
|
|
||||||
|
# Config
|
||||||
|
.autorestic.yml
|
||||||
|
.autorestic.lock
|
||||||
|
.docker.yml
|
4
.prettierrc.yml
Normal file
4
.prettierrc.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
semi: false
|
||||||
|
singleQuote: true
|
||||||
|
trailingComma: 'es5'
|
||||||
|
printWidth: 150
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 0.22
|
||||||
|
|
||||||
|
- New CI Flag for clean ci output
|
23
README.md
23
README.md
@@ -1,2 +1,21 @@
|
|||||||
# autorestic
|
<p align="center">
|
||||||
High level CLI utility for restic
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<img align="center" src="https://github.com/cupcakearmy/autorestic/raw/master/.github/logo.png" height="50" alt="autorestic logo">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Config driven, easy backup cli for <a href="https://restic.net/">restic</a>.
|
||||||
|
<br>
|
||||||
|
<strong><a href="https://autorestic.vercel.app/">»»» Docs & Getting Started »»»</a></strong>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Why / What?
|
||||||
|
|
||||||
|
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
|
||||||
|
9
RELEASE.md
Normal file
9
RELEASE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Releasing
|
||||||
|
|
||||||
|
Releases are handled by the CD server with includes checksums for each binary.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag 0.x
|
||||||
|
git push
|
||||||
|
git push origin --tags
|
||||||
|
```
|
@@ -1,18 +0,0 @@
|
|||||||
locations:
|
|
||||||
home:
|
|
||||||
from: /home/myUser
|
|
||||||
to: remote
|
|
||||||
important:
|
|
||||||
from: /path/to/important/stuff
|
|
||||||
to:
|
|
||||||
- remote
|
|
||||||
- hdd
|
|
||||||
backends:
|
|
||||||
remote:
|
|
||||||
type: b2
|
|
||||||
path: 'myBucket:backup/home'
|
|
||||||
B2_ACCOUNT_ID: account_id
|
|
||||||
B2_ACCOUNT_KEY: account_key
|
|
||||||
hdd:
|
|
||||||
type: local
|
|
||||||
path: /mnt/my_external_storage
|
|
38
docs/_toc.md
Normal file
38
docs/_toc.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[Home](/)
|
||||||
|
[Quick Start](/quick)
|
||||||
|
[Installation](/installation)
|
||||||
|
[Configuration](/config)
|
||||||
|
|
||||||
|
> :Collapse label=Locations
|
||||||
|
>
|
||||||
|
> [Overview](/location/overview)
|
||||||
|
> [Hooks](/location/hooks)
|
||||||
|
> [Excluding Files](/location/exclude)
|
||||||
|
> [Forget Policy](/location/forget)
|
||||||
|
> [Cron](/location/cron)
|
||||||
|
> [Docker Volumes](/location/docker)
|
||||||
|
|
||||||
|
> :Collapse label=Backend
|
||||||
|
>
|
||||||
|
> [Overview](/backend/overview)
|
||||||
|
> [Available Backends](/backend/available)
|
||||||
|
|
||||||
|
> :Collapse label=CLI
|
||||||
|
>
|
||||||
|
> [General](/cli/general)
|
||||||
|
> [Info](/cli/info)
|
||||||
|
> [Check](/cli/check)
|
||||||
|
> [Backup](/cli/backup)
|
||||||
|
> [Restore](/cli/restore)
|
||||||
|
> [Forget](/cli/forget)
|
||||||
|
> [Cron](/cli/cron)
|
||||||
|
> [Exec](/cli/exec)
|
||||||
|
> [Install](/cli/install)
|
||||||
|
> [Uninstall](/cli/uninstall)
|
||||||
|
> [Update](/cli/update)
|
||||||
|
|
||||||
|
[Examples](/examples)
|
||||||
|
|
||||||
|
[QA](/qa)
|
||||||
|
|
||||||
|
[Contributors](/contrib)
|
62
docs/backend/available.md
Normal file
62
docs/backend/available.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Available Backends
|
||||||
|
|
||||||
|
In theory [all the restic backends](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html) are supported.
|
||||||
|
|
||||||
|
Those tested are the following:
|
||||||
|
|
||||||
|
## Local
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: local
|
||||||
|
path: /data/my/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backblaze
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: b2
|
||||||
|
path: 'myAccount:myBucket/my/path'
|
||||||
|
B2_ACCOUNT_ID: backblaze_account_id
|
||||||
|
B2_ACCOUNT_KEY: backblaze_account_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## S3 / Minio
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: s3
|
||||||
|
path: s3.amazonaws.com/bucket_name
|
||||||
|
# Minio
|
||||||
|
# path: http://localhost:9000/bucket_name
|
||||||
|
AWS_ACCESS_KEY_ID: my_key
|
||||||
|
AWS_SECRET_ACCESS_KEY: my_secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## SFTP
|
||||||
|
|
||||||
|
For SFTP to work you need to use configure your host inside of ~/.ssh/config as password prompt is not supported. For more information on this topic please see the [official docs](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#sftp) on the matter.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: sftp
|
||||||
|
path: my-host:/remote/path/on/the/server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rest Server
|
||||||
|
|
||||||
|
See [here](https://github.com/restic/rest-server) for how to install a rest server backend and [here](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server) for further documentation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: rest
|
||||||
|
path: http://localhost:8000/repo_name
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
16
docs/backend/overview.md
Normal file
16
docs/backend/overview.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 💽 Backends
|
||||||
|
|
||||||
|
Backends are the ouputs of the backup process. Each location needs at least one.
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
backends:
|
||||||
|
name-of-backend:
|
||||||
|
type: local
|
||||||
|
path: /data/my/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
We restic supports multiple types of backends. See the [full list](/backend/available) for details.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
13
docs/cli/backup.md
Normal file
13
docs/cli/backup.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic backup [-l, --location] [-a, --all]
|
||||||
|
```
|
||||||
|
|
||||||
|
Performs a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic backup -l my-location
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
15
docs/cli/check.md
Normal file
15
docs/cli/check.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic check [-b, --backend] [-a, --all]
|
||||||
|
```
|
||||||
|
|
||||||
|
Cheks if one or more backend are configured properly and initializes them if they are not already.
|
||||||
|
|
||||||
|
This is mostly an internal command, but useful to verify if a backend is configured correctly.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic check -b my-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
11
docs/cli/cron.md
Normal file
11
docs/cli/cron.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Cron
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic cron
|
||||||
|
```
|
||||||
|
|
||||||
|
This command is mostly intended to be triggered by an automated system like systemd or crontab.
|
||||||
|
|
||||||
|
It will run cron jobs es [specified in the cron section](/locations/cron) of a specific location.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
15
docs/cli/exec.md
Normal file
15
docs/cli/exec.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Exec
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic exec -a -- snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
With `exec` you can basically run every cli command that you would be able to run with the restic cli. It only pre-fills path, key, etc.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
11
docs/cli/forget.md
Normal file
11
docs/cli/forget.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Forget
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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](/locations/forget)
|
||||||
|
|
||||||
|
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
29
docs/cli/general.md
Normal file
29
docs/cli/general.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# General
|
||||||
|
|
||||||
|
## `--version`
|
||||||
|
|
||||||
|
Prints the current version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## `--c, --config`
|
||||||
|
|
||||||
|
Specify the config file to be used.
|
||||||
|
If omitted `autorestic` will search for for a `.autorestic.yml` in the current directory and your home directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic -c /path/to/my/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## `--ci`
|
||||||
|
|
||||||
|
> Available since version 0.22
|
||||||
|
|
||||||
|
Run the CLI in CI Mode, which means there will be no interactivity.
|
||||||
|
This can be useful when you want to run cron e.g. as all the output will be saved.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic --ci
|
||||||
|
```
|
18
docs/cli/info.md
Normal file
18
docs/cli/info.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Info
|
||||||
|
|
||||||
|
Displays the config file that autorestic is referring to.
|
||||||
|
Useful when you want to quickly see what locations are being backed-up where.
|
||||||
|
|
||||||
|
**Pro tip:** if it gets a bit long you can read it more easily with `autorestic info | less` 😉
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic info
|
||||||
|
```
|
||||||
|
|
||||||
|
## With a custom file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic -c path/to/some/config.yml info
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
9
docs/cli/install.md
Normal file
9
docs/cli/install.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Install
|
||||||
|
|
||||||
|
Installs both restic and autorestic to `/usr/local/bin`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic install
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
17
docs/cli/restore.md
Normal file
17
docs/cli/restore.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
9
docs/cli/uninstall.md
Normal file
9
docs/cli/uninstall.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Uninstall
|
||||||
|
|
||||||
|
Installs both restic and autorestic from `/usr/local/bin`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
11
docs/cli/update.md
Normal file
11
docs/cli/update.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Update
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic update
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates both restic and autorestic automagically.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
42
docs/config.md
Normal file
42
docs/config.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 🎛 Config File
|
||||||
|
|
||||||
|
## Path
|
||||||
|
|
||||||
|
By default autorestic searches for a `.autorestic.yml` file in the current directory and your home folder.
|
||||||
|
|
||||||
|
- `./.autorestic.yml`
|
||||||
|
- `~/.autorestic.yml`
|
||||||
|
|
||||||
|
You can also specify a custom file with the `-c path/to/some/config.yml`
|
||||||
|
|
||||||
|
> **⚠️ WARNING ⚠️**
|
||||||
|
>
|
||||||
|
> 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 or config file somewhere in case your server dies**. Otherwise DATA IS LOST!
|
||||||
|
|
||||||
|
## Example configuration
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
home:
|
||||||
|
from: /home/me
|
||||||
|
to: remote
|
||||||
|
|
||||||
|
important:
|
||||||
|
from: /path/to/important/stuff
|
||||||
|
to:
|
||||||
|
- remote
|
||||||
|
- hdd
|
||||||
|
|
||||||
|
backends:
|
||||||
|
remote:
|
||||||
|
type: b2
|
||||||
|
path: 'myBucket:backup/home'
|
||||||
|
B2_ACCOUNT_ID: account_id
|
||||||
|
B2_ACCOUNT_KEY: account_key
|
||||||
|
|
||||||
|
hdd:
|
||||||
|
type: local
|
||||||
|
path: /mnt/my_external_storage
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
8
docs/contrib.md
Normal file
8
docs/contrib.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 🙋♀️🙋♂️ Contributors
|
||||||
|
|
||||||
|
This amazing people helped the project!
|
||||||
|
|
||||||
|
- @ChanceM [Docs]
|
||||||
|
- @EliotBerriot [Docs, Pruning, S3]
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
21
docs/examples.md
Normal file
21
docs/examples.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 🐣 Examples
|
||||||
|
|
||||||
|
## Exec
|
||||||
|
|
||||||
|
### List all the snapshots for all the backends
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic exec -a -- snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unlock a locked repository
|
||||||
|
|
||||||
|
If you accidentally cancelled a running operation this could be useful.
|
||||||
|
|
||||||
|
Only do this if you know what you are doing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic exec -b my-backend -- unlock
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
27
docs/index.md
Normal file
27
docs/index.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# `autorestic`
|
||||||
|
|
||||||
|
High backup level CLI utility for [restic](https://restic.net/).
|
||||||
|
|
||||||
|
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
|
||||||
|
## ✈️ Roadmap
|
||||||
|
|
||||||
|
~~I would like to make the official `1.0` release in the coming months. Until then please feel free to file issues or feature requests so that the tool is as flexible as possible :)~~
|
||||||
|
|
||||||
|
As of version `0.18` crons are supported wich where the last feature missing for a `1.0`. Will test this for a few weeks and then it's time for the first "real" release! 🎉 Also we now have waaay better docs 📒
|
||||||
|
|
||||||
|
## 🌈 Features
|
||||||
|
|
||||||
|
- YAML config files, no CLI
|
||||||
|
- Incremental -> Minimal space is used
|
||||||
|
- Backup locations to multiple backends
|
||||||
|
- Snapshot policies and pruning
|
||||||
|
- Fully encrypted
|
||||||
|
- Pre/After hooks
|
||||||
|
- Exclude pattern/files
|
||||||
|
- Cron jobs for automatic backup
|
||||||
|
- Backup & Restore docker volumes
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
11
docs/installation.md
Normal file
11
docs/installation.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 🛳 Installation
|
||||||
|
|
||||||
|
Linux & macOS. Windows is not supported. If you have problems installing please open an issue :)
|
||||||
|
|
||||||
|
Autorestic requires `curl`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
49
docs/location/cron.md
Normal file
49
docs/location/cron.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Cron
|
||||||
|
|
||||||
|
Often it is usefull to trigger backups autmatically. For this we can specify a `cron` attribute to each location.
|
||||||
|
|
||||||
|
> Available since version 0.18
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
my-location:
|
||||||
|
from: /data
|
||||||
|
to: my-backend
|
||||||
|
cron: '0 3 * * 0' # Every Sunday at 3:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is a awesome website with [some examples](https://crontab.guru/examples.html) and an [explorer](https://crontab.guru/)
|
||||||
|
|
||||||
|
## Installing the cron
|
||||||
|
|
||||||
|
**This has to be done only once, regadless of now many cros you have in your config file.**
|
||||||
|
|
||||||
|
To actually enable cron jobs you need something to call `autorestic cron` on a timed shedule.
|
||||||
|
Note that the shedule has nothing to do with the `cron` attribute in each location.
|
||||||
|
My advise would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to shedule it once a day.
|
||||||
|
|
||||||
|
### Crontab
|
||||||
|
|
||||||
|
Here is an example using crontab, but systemd would do too.
|
||||||
|
|
||||||
|
First, open your crontab in edit mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Then paste this at the bottom of the file and save it. Note that in this specific example the `.autorestic.yml` is located in `/srv/`. You need to modify that part of course to fit your config file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This is required, as it otherwise cannot find restic as a command.
|
||||||
|
PATH="/usr/local/bin:/usr/bin:/bin"
|
||||||
|
|
||||||
|
# Example running every 5 minutes
|
||||||
|
*/5 * * * * autorestic -c /srv/.autorestic.yml --ci cron
|
||||||
|
```
|
||||||
|
|
||||||
|
> The `--ci` option is not required, but recommended
|
||||||
|
|
||||||
|
Now you can add as many `cron` attributes as you wish in the config file ⏱
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
58
docs/location/docker.md
Normal file
58
docs/location/docker.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
|
||||||
|
|
||||||
|
> Available since version 0.13
|
||||||
|
|
||||||
|
Let see an example.
|
||||||
|
|
||||||
|
```yaml | docker-compose.yml
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
name: my-data
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: alpine
|
||||||
|
volumes:
|
||||||
|
- data:/foo/bar
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
hello:
|
||||||
|
from: 'volume:my-data'
|
||||||
|
to:
|
||||||
|
- remote
|
||||||
|
options:
|
||||||
|
forget:
|
||||||
|
keep-last: 14 # Useful for limitations explained belowd
|
||||||
|
|
||||||
|
backends:
|
||||||
|
remote: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can backup and restore as always.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic -l hello backup
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic -l hello restore
|
||||||
|
```
|
||||||
|
|
||||||
|
If the volume does not exist on restore, autorestic will create it for you and then fill it with the data.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Unfortunately there are some limitations when backing up directly from a docker volume without mounting the volume to the host:
|
||||||
|
|
||||||
|
1. Incremental updates are not possible right now due to how the current docker mounting works. This means that it will take significantely more space.
|
||||||
|
2. Exclude patterns and files also do not work as restic only sees a compressed tarball as source and not the actual data.
|
||||||
|
|
||||||
|
If you are curious or have ideas how to improve this, please [read more here](https://github.com/cupcakearmy/autorestic/issues/4#issuecomment-568771951). Any help is welcomed 🙂
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
20
docs/location/exclude.md
Normal file
20
docs/location/exclude.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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) so you can use any flag used there.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
locations:
|
||||||
|
my-location:
|
||||||
|
from: /data
|
||||||
|
to: my-backend
|
||||||
|
options:
|
||||||
|
backup:
|
||||||
|
exclude:
|
||||||
|
- '*.nope'
|
||||||
|
- '*.abc'
|
||||||
|
exclude-file: .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
25
docs/location/forget.md
Normal file
25
docs/location/forget.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Forget/Prune Policies
|
||||||
|
|
||||||
|
Autorestic supports declaring snapshot policies for location to avoid keeping old snapshot around if you don't need them.
|
||||||
|
|
||||||
|
This is based on [Restic's snapshots policies](https://restic.readthedocs.io/en/latest/060_forget.html#removing-snapshots-according-to-a-policy), and can be enabled for each location as shown below:
|
||||||
|
|
||||||
|
> **Note** This is a full example, of course you also can specify only one of them
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
etc:
|
||||||
|
from: /etc
|
||||||
|
to: local
|
||||||
|
options:
|
||||||
|
forget:
|
||||||
|
keep-last: 5 # always keep at least 5 snapshots
|
||||||
|
keep-hourly: 3 # keep 3 last hourly shapshots
|
||||||
|
keep-daily: 4 # keep 4 last daily shapshots
|
||||||
|
keep-weekly: 1 # keep 1 last weekly shapshots
|
||||||
|
keep-monthly: 12 # keep 12 last monthly shapshots
|
||||||
|
keep-yearly: 7 # keep 7 last yearly shapshots
|
||||||
|
keep-within: '2w' # keep snapshots from the last 2 weeks
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
18
docs/location/hooks.md
Normal file
18
docs/location/hooks.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
```yml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
my-location:
|
||||||
|
from: /data
|
||||||
|
to: my-backend
|
||||||
|
hooks:
|
||||||
|
before:
|
||||||
|
- echo "Hello"
|
||||||
|
- echo "Human"
|
||||||
|
after:
|
||||||
|
- echo "kthxbye"
|
||||||
|
```
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
27
docs/location/overview.md
Normal file
27
docs/location/overview.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 🗂 Locations
|
||||||
|
|
||||||
|
Locations can be seen as the input to the backup process. Generally this is simply a folder.
|
||||||
|
The paths can be relative from the config file. A location can have multiple backends, so that the data is secured across multiple servers.
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
my-location-name:
|
||||||
|
from: path/to/backup
|
||||||
|
to:
|
||||||
|
- name-of-backend
|
||||||
|
- also-backup-to-this-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## `from`
|
||||||
|
|
||||||
|
This is the source of the location.
|
||||||
|
|
||||||
|
#### How are paths resolved?
|
||||||
|
|
||||||
|
Paths can be absolute or relative. If relative they are resolved relative to the location of the config file. Tilde `~` paths are also supported for home folder resolution.
|
||||||
|
|
||||||
|
## `to`
|
||||||
|
|
||||||
|
This is einther a single backend or an array of backends. The backends have to be configured in the same config file.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
8
docs/qa.md
Normal file
8
docs/qa.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# ❓ QA
|
||||||
|
|
||||||
|
## My config file was moved?
|
||||||
|
|
||||||
|
This happens when autorestic needs to write to the config file. This happend e.g. when we are generating a key for you.
|
||||||
|
Unfortunately during this process formatting and comments are lost. That is why autorestic will place a copy of your old config next to the one we are writing to.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
72
docs/quick.md
Normal file
72
docs/quick.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 🚀 Quickstart
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Write a simple config file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vim .autorestic.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
For a quick overview:
|
||||||
|
|
||||||
|
- `locations` can be seen as the inputs and `backends` the output where the data is stored and backed up.
|
||||||
|
- One `location` can have one or multiple `backends` for redudancy.
|
||||||
|
- One `backend` can also be the target for multiple `locations`.
|
||||||
|
- **Backup the config file as it will contain the generated keys**. If you don't have a copy of that keys, the backups are useless as they are encrypted and data will be not recoverable.
|
||||||
|
|
||||||
|
```yaml | .autorestic.yml
|
||||||
|
locations:
|
||||||
|
home:
|
||||||
|
from: /home/me
|
||||||
|
to: remote
|
||||||
|
|
||||||
|
important:
|
||||||
|
from: /path/to/important/stuff
|
||||||
|
to:
|
||||||
|
- remote
|
||||||
|
- hdd
|
||||||
|
|
||||||
|
backends:
|
||||||
|
remote:
|
||||||
|
type: s3
|
||||||
|
path: 's3.amazonaws.com/bucket_name'
|
||||||
|
AWS_ACCESS_KEY_ID: account_id
|
||||||
|
AWS_SECRET_ACCESS_KEY: account_key
|
||||||
|
|
||||||
|
hdd:
|
||||||
|
type: local
|
||||||
|
path: /mnt/my_external_storage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic check -a
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks if the config file has any issues. If this is the first time this can take longer as autorestic will setup the backends.
|
||||||
|
|
||||||
|
Now is good time to **backup the config**. After you run autorestic at least once we will add the generated encryption keys to the config.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic backup -a
|
||||||
|
```
|
||||||
|
|
||||||
|
This will do a backup of all locations.
|
||||||
|
|
||||||
|
## Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autorestic restore -l home --from hdd --to /path/where/to/restore
|
||||||
|
```
|
||||||
|
|
||||||
|
This will restore the location `home` from the backend `hdd` to the given path.
|
||||||
|
|
||||||
|
> :ToCPrevNext
|
22
install.sh
Executable file
22
install.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
OUT_FILE=/usr/local/bin/autorestic
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||||
|
TYPE=linux
|
||||||
|
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
TYPE=macos
|
||||||
|
else
|
||||||
|
echo "Unsupported OS"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
|
||||||
|
| grep "browser_download_url.*_${TYPE}" \
|
||||||
|
| cut -d : -f 2,3 \
|
||||||
|
| tr -d \" \
|
||||||
|
| wget -O ${OUT_FILE} -i -
|
||||||
|
chmod +x ${OUT_FILE}
|
||||||
|
|
||||||
|
autorestic install
|
||||||
|
autorestic
|
32
package.json
32
package.json
@@ -2,23 +2,27 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"build:watch": "tsc -w",
|
"dev": "tsc -w",
|
||||||
"dev": "tsnd --no-notify --respawn ./src/autorestic.ts",
|
"move": "mv bin/index-linux bin/autorestic_linux_x64 && mv bin/index-macos bin/autorestic_macos_x64",
|
||||||
"bin": "npm run build && pkg lib/autorestic.js --out-path bin"
|
"bin": "yarn run build && pkg lib/index.js --targets latest-macos-x64,latest-linux-x64 --out-path bin && yarn run move",
|
||||||
|
"docs:build": "codedoc install && codedoc build",
|
||||||
|
"docs:dev": "codedoc serve"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/decompress": "^4.2.3",
|
"@codedoc/cli": "^0.2",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3",
|
||||||
"@types/minimist": "^1.2.0",
|
"@types/node": "^14",
|
||||||
"pkg": "^4.4.0",
|
"pkg": "^4.4",
|
||||||
"ts-node-dev": "^1.0.0-pre.40",
|
"ts-node-dev": "^1",
|
||||||
"typescript": "^3.5.1"
|
"typescript": "^3.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19",
|
||||||
"clitastic": "0.0.1",
|
"clitastic": "^0.1.2",
|
||||||
"colors": "^1.3.3",
|
"colors": "^1",
|
||||||
"js-yaml": "^3.13.1",
|
"commander": "^6.2",
|
||||||
"minimist": "^1.2.0"
|
"cron-parser": "2.x.x",
|
||||||
|
"js-yaml": "3.x.x",
|
||||||
|
"uhrwerk": "1.x.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
import 'colors'
|
|
||||||
import minimist from 'minimist'
|
|
||||||
import { resolve } from 'path'
|
|
||||||
|
|
||||||
import { init } from './config'
|
|
||||||
import handlers, { error, help } from './handlers'
|
|
||||||
import { Config } from './types'
|
|
||||||
|
|
||||||
|
|
||||||
process.on('uncaughtException', err => {
|
|
||||||
console.log(err.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
|
|
||||||
alias: {
|
|
||||||
'c': 'config',
|
|
||||||
'v': 'verbose',
|
|
||||||
'h': 'help',
|
|
||||||
'a': 'all',
|
|
||||||
'l': 'location',
|
|
||||||
'b': 'backend',
|
|
||||||
},
|
|
||||||
boolean: ['a'],
|
|
||||||
string: ['l', 'b'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = 'config.yml'
|
|
||||||
export const INSTALL_DIR = '/usr/local/bin'
|
|
||||||
export const CONFIG_FILE: string = resolve(flags.config || DEFAULT_CONFIG)
|
|
||||||
export const VERBOSE = flags.verbose
|
|
||||||
|
|
||||||
export const config: Config = init()
|
|
||||||
|
|
||||||
if (commands.length < 1)
|
|
||||||
help()
|
|
||||||
else {
|
|
||||||
const command: string = commands[0]
|
|
||||||
const args: string[] = commands.slice(1)
|
|
||||||
;(handlers[command] || error)(args, flags)
|
|
||||||
}
|
|
@@ -1,30 +1,27 @@
|
|||||||
import { Writer } from 'clitastic'
|
import { Writer } from 'clitastic'
|
||||||
|
|
||||||
import { config, VERBOSE } from './autorestic'
|
import { config, VERBOSE } from './'
|
||||||
import { Backend, Backends } from './types'
|
import { Backend, Backends, Locations } from './types'
|
||||||
import { exec } from './utils'
|
import { exec, pathRelativeToConfigFile, filterObjectByKey } from './utils'
|
||||||
|
|
||||||
const ALREADY_EXISTS = /(?=.*exists)(?=.*already)(?=.*config).*/
|
|
||||||
|
|
||||||
|
const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/
|
||||||
|
|
||||||
export const getPathFromBackend = (backend: Backend): string => {
|
export const getPathFromBackend = (backend: Backend): string => {
|
||||||
switch (backend.type) {
|
switch (backend.type) {
|
||||||
case 'local':
|
case 'local':
|
||||||
return backend.path
|
return pathRelativeToConfigFile(backend.path)
|
||||||
case 'b2':
|
case 'b2':
|
||||||
case 'azure':
|
case 'azure':
|
||||||
case 'gs':
|
case 'gs':
|
||||||
case 's3':
|
case 's3':
|
||||||
return `${backend.type}:${backend.path}`
|
|
||||||
case 'sftp':
|
case 'sftp':
|
||||||
case 'rest':
|
case 'rest':
|
||||||
throw new Error(`Unsupported backend type: "${backend.type}"`)
|
return `${backend.type}:${backend.path}`
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown backend type.`)
|
throw new Error(`Unknown backend type.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getEnvFromBackend = (backend: Backend) => {
|
export const getEnvFromBackend = (backend: Backend) => {
|
||||||
const { type, path, key, ...rest } = backend
|
const { type, path, key, ...rest } = backend
|
||||||
return {
|
return {
|
||||||
@@ -34,24 +31,36 @@ export const getEnvFromBackend = (backend: Backend) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getBackendsFromLocations = (locations: Locations): string[] => {
|
||||||
|
const backends = new Set<string>()
|
||||||
|
for (const to of Object.values(locations).map((location) => location.to)) Array.isArray(to) ? to.forEach((t) => backends.add(t)) : backends.add(to)
|
||||||
|
return Array.from(backends)
|
||||||
|
}
|
||||||
|
|
||||||
export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
||||||
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
||||||
|
try {
|
||||||
const env = getEnvFromBackend(backend)
|
const env = getEnvFromBackend(backend)
|
||||||
|
|
||||||
const { out, err } = exec('restic', ['init'], { env })
|
const { out, err } = exec('restic', ['init'], { env })
|
||||||
|
|
||||||
if (err.length > 0 && !ALREADY_EXISTS.test(err))
|
if (err.length > 0 && !ALREADY_EXISTS.test(err)) throw new Error(`Could not load the backend "${name}": ${err}`)
|
||||||
throw new Error(`Could not load the backend "${name}": ${err}`)
|
|
||||||
|
|
||||||
if (VERBOSE && out.length > 0) console.log(out)
|
if (VERBOSE && out.length > 0) console.log(out)
|
||||||
|
|
||||||
writer.done(name.blue + ' : ' + 'Done ✓'.green)
|
writer.done(name.blue + ' : ' + 'Done ✓'.green)
|
||||||
|
} catch (e) {
|
||||||
|
writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const checkAndConfigureBackends = (backends?: Backends) => {
|
||||||
|
if (!backends) backends = config.backends
|
||||||
|
|
||||||
export const checkAndConfigureBackends = (backends: Backends = config.backends) => {
|
|
||||||
console.log('\nConfiguring Backends'.grey.underline)
|
console.log('\nConfiguring Backends'.grey.underline)
|
||||||
for (const [name, backend] of Object.entries(backends))
|
for (const [name, backend] of Object.entries(backends)) checkAndConfigureBackend(name, backend)
|
||||||
checkAndConfigureBackend(name, backend)
|
}
|
||||||
|
|
||||||
|
export const checkAndConfigureBackendsForLocations = (locations: Locations) => {
|
||||||
|
checkAndConfigureBackends(filterObjectByKey(config.backends, getBackendsFromLocations(locations)))
|
||||||
}
|
}
|
104
src/backup.ts
104
src/backup.ts
@@ -1,37 +1,103 @@
|
|||||||
import { Writer } from 'clitastic'
|
import { Writer } from 'clitastic'
|
||||||
|
import { mkdirSync } from 'fs'
|
||||||
|
|
||||||
import { config, VERBOSE } from './autorestic'
|
import { config, VERBOSE } from './'
|
||||||
import { getEnvFromBackend } from './backend'
|
import { getEnvFromBackend } from './backend'
|
||||||
import { Locations, Location } from './types'
|
import { LocationFromPrefixes } from './config'
|
||||||
import { exec } from './utils'
|
import { Locations, Location, Backend } from './types'
|
||||||
|
import {
|
||||||
|
exec,
|
||||||
|
pathRelativeToConfigFile,
|
||||||
|
getFlagsFromLocation,
|
||||||
|
makeArrayIfIsNot,
|
||||||
|
execPlain,
|
||||||
|
MeasureDuration,
|
||||||
|
fill,
|
||||||
|
decodeLocationFromPrefix,
|
||||||
|
checkIfDockerVolumeExistsOrFail,
|
||||||
|
getPathFromVolume,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
export const backupFromFilesystem = (from: string, location: Location, backend: Backend, tags?: string[]) => {
|
||||||
|
const path = pathRelativeToConfigFile(from)
|
||||||
|
|
||||||
export const backupSingle = (name: string, from: string, to: string) => {
|
const { out, err, status } = exec('restic', ['backup', '.', ...getFlagsFromLocation(location, 'backup')], {
|
||||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
env: getEnvFromBackend(backend),
|
||||||
const backend = config.backends[to]
|
cwd: path,
|
||||||
const cmd = exec('restic', ['backup', from], { env: getEnvFromBackend(backend) })
|
})
|
||||||
|
|
||||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
if (VERBOSE) console.log(out, err)
|
||||||
writer.done(name + to.blue + ' : ' + 'Done ✓'.green)
|
if (status != 0 || err.length > 0) throw new Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const backupFromVolume = (volume: string, location: Location, backend: Backend) => {
|
||||||
|
const tmp = getPathFromVolume(volume)
|
||||||
|
try {
|
||||||
|
mkdirSync(tmp)
|
||||||
|
checkIfDockerVolumeExistsOrFail(volume)
|
||||||
|
|
||||||
export const backupLocation = (name: string, backup: Location) => {
|
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
||||||
|
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`)
|
||||||
|
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`)
|
||||||
|
|
||||||
|
backupFromFilesystem(tmp, location, backend)
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
execPlain(`rm -rf ${tmp}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||||
|
const delta = new MeasureDuration()
|
||||||
|
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const backend = config.backends[to]
|
||||||
|
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case LocationFromPrefixes.Filesystem:
|
||||||
|
backupFromFilesystem(value, location, backend)
|
||||||
|
break
|
||||||
|
|
||||||
|
case LocationFromPrefixes.DockerVolume:
|
||||||
|
backupFromVolume(value, location, backend)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||||
|
} catch (e) {
|
||||||
|
writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backupLocation = (name: string, location: Location) => {
|
||||||
const display = name.yellow + ' ▶ '
|
const display = name.yellow + ' ▶ '
|
||||||
if (Array.isArray(backup.to)) {
|
const filler = fill(name.length + 3)
|
||||||
let first = true
|
let first = true
|
||||||
for (const t of backup.to) {
|
|
||||||
const nameOrBlankSpaces: string = first ? display : new Array(name.length + 3).fill(' ').join('')
|
if (location.hooks && location.hooks.before)
|
||||||
backupSingle(nameOrBlankSpaces, backup.from, t)
|
for (const command of makeArrayIfIsNot(location.hooks.before)) {
|
||||||
|
const cmd = execPlain(command, {})
|
||||||
|
console.log(cmd.out, cmd.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const t of makeArrayIfIsNot(location.to)) {
|
||||||
|
backupSingle(first ? display : filler, t, location)
|
||||||
if (first) first = false
|
if (first) first = false
|
||||||
}
|
}
|
||||||
} else
|
|
||||||
backupSingle(display, backup.from, backup.to)
|
if (location.hooks && location.hooks.after)
|
||||||
|
for (const command of makeArrayIfIsNot(location.hooks.after)) {
|
||||||
|
const cmd = execPlain(command)
|
||||||
|
console.log(cmd.out, cmd.err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const backupAll = (locations?: Locations) => {
|
||||||
|
if (!locations) locations = config.locations
|
||||||
|
|
||||||
export const backupAll = (backups: Locations = config.locations) => {
|
|
||||||
console.log('\nBacking Up'.underline.grey)
|
console.log('\nBacking Up'.underline.grey)
|
||||||
for (const [name, backup] of Object.entries(backups))
|
for (const [name, location] of Object.entries(locations)) backupLocation(name, location)
|
||||||
backupLocation(name, backup)
|
|
||||||
}
|
}
|
@@ -1,15 +1,22 @@
|
|||||||
import { readFileSync, writeFileSync } from 'fs'
|
import { readFileSync, writeFileSync, statSync, copyFileSync } from 'fs'
|
||||||
import yaml from 'js-yaml'
|
import { resolve } from 'path'
|
||||||
import { CONFIG_FILE } from './autorestic'
|
import { homedir } from 'os'
|
||||||
import { Backend, Config } from './types'
|
|
||||||
import { makeObjectKeysLowercase, rand } from './utils'
|
|
||||||
|
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import CronParser from 'cron-parser'
|
||||||
|
|
||||||
|
import { Backend, Config } from './types'
|
||||||
|
import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
|
||||||
|
|
||||||
|
export enum LocationFromPrefixes {
|
||||||
|
Filesystem,
|
||||||
|
DockerVolume,
|
||||||
|
}
|
||||||
|
|
||||||
export const normalizeAndCheckBackends = (config: Config) => {
|
export const normalizeAndCheckBackends = (config: Config) => {
|
||||||
config.backends = makeObjectKeysLowercase(config.backends)
|
config.backends = makeObjectKeysLowercase(config.backends)
|
||||||
|
|
||||||
for (const [name, { type, path, key, ...rest }] of Object.entries(config.backends)) {
|
for (const [name, { type, path, key, ...rest }] of Object.entries(config.backends)) {
|
||||||
|
|
||||||
if (!type || !path) throw new Error(`The backend "${name}" is missing some required attributes`)
|
if (!type || !path) throw new Error(`The backend "${name}" is missing some required attributes`)
|
||||||
|
|
||||||
const tmp: any = {
|
const tmp: any = {
|
||||||
@@ -17,42 +24,81 @@ export const normalizeAndCheckBackends = (config: Config) => {
|
|||||||
path,
|
path,
|
||||||
key: key || rand(128),
|
key: key || rand(128),
|
||||||
}
|
}
|
||||||
for (const [key, value] of Object.entries(rest))
|
for (const [key, value] of Object.entries(rest)) tmp[key.toUpperCase()] = value
|
||||||
tmp[key.toUpperCase()] = value
|
|
||||||
|
|
||||||
config.backends[name] = tmp as Backend
|
config.backends[name] = tmp as Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeAndCheckLocations = (config: Config) => {
|
||||||
export const normalizeAndCheckBackups = (config: Config) => {
|
|
||||||
config.locations = makeObjectKeysLowercase(config.locations)
|
config.locations = makeObjectKeysLowercase(config.locations)
|
||||||
const backends = Object.keys(config.backends)
|
const backends = Object.keys(config.backends)
|
||||||
|
|
||||||
const checkDestination = (backend: string, backup: string) => {
|
const checkDestination = (backend: string, location: string) => {
|
||||||
if (!backends.includes(backend))
|
if (!backends.includes(backend)) throw new Error(`Cannot find the backend "${backend}" for "${location}"`)
|
||||||
throw new Error(`Cannot find the backend "${backend}" for "${backup}"`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, { from, to, ...rest }] of Object.entries(config.locations)) {
|
for (const [name, { from, to, cron, ...rest }] of Object.entries(config.locations)) {
|
||||||
if (!from || !to) throw new Error(`The backup "${name}" is missing some required attributes`)
|
if (!from) throw new Error(`The location "${name.blue}" is missing the "${'from'.underline.red}" source folder. See https://git.io/Jf0xw`)
|
||||||
|
if (!to || (Array.isArray(to) && !to.length))
|
||||||
|
throw new Error(`The location "${name.blue}" has no backend "${'to'.underline.red}" to save the backups. See https://git.io/Jf0xw`)
|
||||||
|
|
||||||
if (Array.isArray(to))
|
for (const t of makeArrayIfIsNot(to)) checkDestination(t, name)
|
||||||
for (const t of to)
|
|
||||||
checkDestination(t, name)
|
if (cron) {
|
||||||
else
|
try {
|
||||||
checkDestination(to, name)
|
CronParser.parseExpression(cron)
|
||||||
|
} catch {
|
||||||
|
throw new Error(`The location "${name.blue}" has an invalid ${'cron'.underline.red} entry. See https://git.io/Jf0xP`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findConfigFile = (custom: string): string => {
|
||||||
|
const config = '.autorestic.yml'
|
||||||
|
const paths = [resolve(custom || ''), resolve('./' + config), homedir() + '/' + config]
|
||||||
|
for (const path of paths) {
|
||||||
|
try {
|
||||||
|
const file = statSync(path)
|
||||||
|
if (file.isFile()) return path
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
throw new Error('Config file not found')
|
||||||
|
}
|
||||||
|
|
||||||
export const init = (): Config => {
|
export let CONFIG_FILE: string = ''
|
||||||
const raw: Config = makeObjectKeysLowercase(yaml.safeLoad(readFileSync(CONFIG_FILE).toString()))
|
|
||||||
|
export const init = (custom: string): Config => {
|
||||||
|
const file = findConfigFile(custom)
|
||||||
|
CONFIG_FILE = file
|
||||||
|
|
||||||
|
const parsed = yaml.safeLoad(readFileSync(CONFIG_FILE).toString())
|
||||||
|
if (!parsed || typeof parsed === 'string') throw new Error('Could not parse the config file')
|
||||||
|
const raw: Config = makeObjectKeysLowercase(parsed)
|
||||||
|
|
||||||
|
const current = JSON.stringify(raw)
|
||||||
|
|
||||||
normalizeAndCheckBackends(raw)
|
normalizeAndCheckBackends(raw)
|
||||||
normalizeAndCheckBackups(raw)
|
normalizeAndCheckLocations(raw)
|
||||||
|
|
||||||
|
const changed = JSON.stringify(raw) !== current
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
const OLD_CONFIG_FILE = CONFIG_FILE + '.old'
|
||||||
|
copyFileSync(CONFIG_FILE, OLD_CONFIG_FILE)
|
||||||
writeFileSync(CONFIG_FILE, yaml.safeDump(raw))
|
writeFileSync(CONFIG_FILE, yaml.safeDump(raw))
|
||||||
|
console.log(
|
||||||
|
'\n' +
|
||||||
|
'⚠️ MOVED OLD CONFIG FILE TO: ⚠️'.red.underline.bold +
|
||||||
|
'\n' +
|
||||||
|
OLD_CONFIG_FILE +
|
||||||
|
'\n' +
|
||||||
|
'What? Why? '.grey +
|
||||||
|
'https://git.io/Jf0xK'.underline.grey +
|
||||||
|
'\n'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
31
src/cron.ts
Normal file
31
src/cron.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import CronParser from 'cron-parser'
|
||||||
|
|
||||||
|
import { config } from './'
|
||||||
|
import { checkAndConfigureBackendsForLocations } from './backend'
|
||||||
|
import { Location } from './types'
|
||||||
|
import { backupLocation } from './backup'
|
||||||
|
import { readLock, writeLock } from './lock'
|
||||||
|
|
||||||
|
const runCronForLocation = (name: string, location: Location) => {
|
||||||
|
const lock = readLock()
|
||||||
|
const parsed = CronParser.parseExpression(location.cron || '')
|
||||||
|
const last = parsed.prev()
|
||||||
|
|
||||||
|
if (!lock.crons[name] || last.toDate().getTime() > lock.crons[name].lastRun) {
|
||||||
|
backupLocation(name, location)
|
||||||
|
lock.crons[name] = { lastRun: Date.now() }
|
||||||
|
writeLock(lock)
|
||||||
|
} else {
|
||||||
|
console.log(`${name.yellow} ▶ Skipping. Scheduled for: ${parsed.next().toString().underline.blue}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runCron = () => {
|
||||||
|
const locationsWithCron = Object.entries(config.locations).filter(([name, { cron }]) => !!cron)
|
||||||
|
checkAndConfigureBackendsForLocations(Object.fromEntries(locationsWithCron))
|
||||||
|
|
||||||
|
console.log('\nRunning cron jobs'.underline.gray)
|
||||||
|
for (const [name, location] of locationsWithCron) runCronForLocation(name, location)
|
||||||
|
|
||||||
|
console.log('\nFinished!'.underline + ' 🎉')
|
||||||
|
}
|
61
src/forget.ts
Normal file
61
src/forget.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Writer } from 'clitastic'
|
||||||
|
|
||||||
|
import { config, VERBOSE } from './'
|
||||||
|
import { getEnvFromBackend } from './backend'
|
||||||
|
import { LocationFromPrefixes } from './config'
|
||||||
|
import { Locations, Location, Flags } from './types'
|
||||||
|
import { exec, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, fill, decodeLocationFromPrefix, getPathFromVolume } from './utils'
|
||||||
|
|
||||||
|
export const forgetSingle = (name: string, to: string, location: Location, dryRun: boolean) => {
|
||||||
|
const base = name + to.blue + ' : '
|
||||||
|
const writer = new Writer(base + 'Removing old snapshots… ⏳')
|
||||||
|
|
||||||
|
const backend = config.backends[to]
|
||||||
|
const flags = getFlagsFromLocation(location, 'forget')
|
||||||
|
|
||||||
|
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||||
|
let path: string
|
||||||
|
switch (type) {
|
||||||
|
case LocationFromPrefixes.Filesystem:
|
||||||
|
path = pathRelativeToConfigFile(value)
|
||||||
|
break
|
||||||
|
case LocationFromPrefixes.DockerVolume:
|
||||||
|
path = getPathFromVolume(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.length == 0) {
|
||||||
|
writer.done(base + 'Skipping, no policy declared')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dryRun) flags.push('--dry-run')
|
||||||
|
|
||||||
|
writer.replaceLn(base + 'Forgetting old snapshots… ⏳')
|
||||||
|
const cmd = exec('restic', ['forget', '--path', path, '--prune', ...flags], { env: getEnvFromBackend(backend) })
|
||||||
|
|
||||||
|
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||||
|
writer.done(base + 'Done ✓'.green)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
|
||||||
|
const display = name.yellow + ' ▶ '
|
||||||
|
const filler = fill(name.length + 3)
|
||||||
|
let first = true
|
||||||
|
|
||||||
|
for (const t of makeArrayIfIsNot(backup.to)) {
|
||||||
|
const nameOrBlankSpaces: string = first ? display : filler
|
||||||
|
forgetSingle(nameOrBlankSpaces, t, backup, dryRun)
|
||||||
|
if (first) first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const forgetAll = (backups?: Locations, dryRun = false) => {
|
||||||
|
if (!backups) {
|
||||||
|
backups = config.locations
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nRemoving old snapshots according to policy'.underline.grey)
|
||||||
|
if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow)
|
||||||
|
|
||||||
|
for (const [name, backup] of Object.entries(backups)) forgetLocation(name, backup, dryRun)
|
||||||
|
}
|
196
src/handlers.ts
196
src/handlers.ts
@@ -1,196 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { Writer } from 'clitastic'
|
|
||||||
import { createWriteStream, unlinkSync } from 'fs'
|
|
||||||
import { arch, platform, tmpdir } from 'os'
|
|
||||||
import { join, resolve } from 'path'
|
|
||||||
|
|
||||||
import { config, INSTALL_DIR } from './autorestic'
|
|
||||||
import { checkAndConfigureBackends, getEnvFromBackend } from './backend'
|
|
||||||
import { backupAll } from './backup'
|
|
||||||
import { Backends, Flags, Locations } from './types'
|
|
||||||
import { checkIfCommandIsAvailable, checkIfResticIsAvailable, exec, filterObjectByKey, singleToArray } from './utils'
|
|
||||||
|
|
||||||
export type Handlers = { [command: string]: (args: string[], flags: Flags) => void }
|
|
||||||
|
|
||||||
const parseBackend = (flags: Flags): Backends => {
|
|
||||||
if (!flags.all && !flags.backend)
|
|
||||||
throw new Error('No backends specified.'.red
|
|
||||||
+ '\n--all [-a]\t\t\t\tCheck all.'
|
|
||||||
+ '\n--backend [-b] myBackend\t\tSpecify one or more backend',
|
|
||||||
)
|
|
||||||
if (flags.all)
|
|
||||||
return config.backends
|
|
||||||
else {
|
|
||||||
const backends = singleToArray<string>(flags.backend)
|
|
||||||
for (const backend of backends)
|
|
||||||
if (!config.backends[backend])
|
|
||||||
throw new Error('Invalid backend: '.red + backend)
|
|
||||||
return filterObjectByKey(config.backends, backends)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseLocations = (flags: Flags): Locations => {
|
|
||||||
if (!flags.all && !flags.location)
|
|
||||||
throw new Error('No locations specified.'.red
|
|
||||||
+ '\n--all [-a]\t\t\t\tBackup all.'
|
|
||||||
+ '\n--location [-l] site1\t\t\tSpecify one or more locations',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (flags.all) {
|
|
||||||
return config.locations
|
|
||||||
} else {
|
|
||||||
const locations = singleToArray<string>(flags.location)
|
|
||||||
for (const location of locations)
|
|
||||||
if (!config.locations[location])
|
|
||||||
throw new Error('Invalid location: '.red + location)
|
|
||||||
return filterObjectByKey(config.locations, locations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers: Handlers = {
|
|
||||||
check(args, flags) {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
const backends = parseBackend(flags)
|
|
||||||
checkAndConfigureBackends(backends)
|
|
||||||
},
|
|
||||||
backup(args, flags) {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
const locations: Locations = parseLocations(flags)
|
|
||||||
|
|
||||||
const backends = new Set<string>()
|
|
||||||
for (const to of Object.values(locations).map(location => location.to))
|
|
||||||
Array.isArray(to) ? to.forEach(t => backends.add(t)) : backends.add(to)
|
|
||||||
|
|
||||||
checkAndConfigureBackends(filterObjectByKey(config.backends, Array.from(backends)))
|
|
||||||
backupAll(locations)
|
|
||||||
|
|
||||||
console.log('\nFinished!'.underline + ' 🎉')
|
|
||||||
},
|
|
||||||
restore(args, flags) {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
const locations = parseLocations(flags)
|
|
||||||
for (const [name, location] of Object.entries(locations)) {
|
|
||||||
const w = new Writer(name.green + `\t\tRestoring... ⏳`)
|
|
||||||
const env = getEnvFromBackend(config.backends[Array.isArray(location.to) ? location.to[0] : location.to])
|
|
||||||
|
|
||||||
exec(
|
|
||||||
'restic',
|
|
||||||
['restore', 'latest', '--path', resolve(location.from), ...args],
|
|
||||||
{ env },
|
|
||||||
)
|
|
||||||
w.done(name.green + '\t\tDone 🎉')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exec(args, flags) {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
const backends = parseBackend(flags)
|
|
||||||
for (const [name, backend] of Object.entries(backends)) {
|
|
||||||
console.log(`\n${name}:\n`.grey.underline)
|
|
||||||
const env = getEnvFromBackend(backend)
|
|
||||||
|
|
||||||
const { out, err } = exec('restic', args, { env })
|
|
||||||
console.log(out, err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async install() {
|
|
||||||
try {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
console.log('Restic is already installed')
|
|
||||||
return
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = new Writer('Checking latest version... ⏳')
|
|
||||||
checkIfCommandIsAvailable('bzip2')
|
|
||||||
const { data: json } = await axios({
|
|
||||||
method: 'get',
|
|
||||||
url: 'https://api.github.com/repos/restic/restic/releases/latest',
|
|
||||||
responseType: 'json',
|
|
||||||
})
|
|
||||||
|
|
||||||
const archMap: { [a: string]: string } = {
|
|
||||||
'x32': '386',
|
|
||||||
'x64': 'amd64',
|
|
||||||
}
|
|
||||||
|
|
||||||
w.replaceLn('Downloading binary... 🌎')
|
|
||||||
const name = `${json.name.replace(' ', '_')}_${platform()}_${archMap[arch()]}.bz2`
|
|
||||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
|
||||||
if (!dl) return console.log(
|
|
||||||
'Cannot get the right binary.'.red,
|
|
||||||
'Please see https://bit.ly/2Y1Rzai',
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: file } = await axios({
|
|
||||||
method: 'get',
|
|
||||||
url: dl.browser_download_url,
|
|
||||||
responseType: 'stream',
|
|
||||||
})
|
|
||||||
|
|
||||||
const from = join(tmpdir(), name)
|
|
||||||
const to = from.slice(0, -4)
|
|
||||||
|
|
||||||
w.replaceLn('Decompressing binary... 📦')
|
|
||||||
const stream = createWriteStream(from)
|
|
||||||
await new Promise(res => {
|
|
||||||
const writer = file.pipe(stream)
|
|
||||||
writer.on('close', res)
|
|
||||||
})
|
|
||||||
stream.close()
|
|
||||||
|
|
||||||
w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`)
|
|
||||||
// TODO: Native bz2
|
|
||||||
// Decompress
|
|
||||||
exec('bzip2', ['-dk', from])
|
|
||||||
// Remove .bz2
|
|
||||||
exec('chmod', ['+x', to])
|
|
||||||
exec('mv', [to, INSTALL_DIR + '/restic'])
|
|
||||||
|
|
||||||
unlinkSync(from)
|
|
||||||
|
|
||||||
w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉')
|
|
||||||
},
|
|
||||||
uninstall() {
|
|
||||||
try {
|
|
||||||
unlinkSync(INSTALL_DIR + '/restic')
|
|
||||||
console.log(`Finished! restic was uninstalled`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('restic is already uninstalled'.red)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
checkIfResticIsAvailable()
|
|
||||||
const w = new Writer('Checking for new restic version... ⏳')
|
|
||||||
exec('restic', ['self-update'])
|
|
||||||
w.done('All up to date! 🚀')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const help = () => {
|
|
||||||
console.log('\nAutorestic'.blue + ' - Easy Restic CLI Utility'
|
|
||||||
+ '\n'
|
|
||||||
+ '\nOptions:'.yellow
|
|
||||||
+ '\n -c, --config [default=config.yml] Specify config file'
|
|
||||||
+ '\n'
|
|
||||||
+ '\nCommands:'.yellow
|
|
||||||
+ '\n check [-b, --backend] [-a, --all] Check backends'
|
|
||||||
+ '\n backup [-l, --location] [-a, --all] Backup all or specified locations'
|
|
||||||
+ '\n restore [-l, --location] [-- --target <out dir>] Check backends'
|
|
||||||
+ '\n'
|
|
||||||
+ '\n exec [-b, --backend] [-a, --all] <command> -- [native options] Execute native restic command'
|
|
||||||
+ '\n'
|
|
||||||
+ '\n install install restic'
|
|
||||||
+ '\n uninstall uninstall restic'
|
|
||||||
+ '\n update update restic'
|
|
||||||
+ '\n help Show help'
|
|
||||||
+ '\n'
|
|
||||||
+ '\nExamples: '.yellow + 'https://git.io/fjVbg'
|
|
||||||
+ '\n',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export const error = () => {
|
|
||||||
help()
|
|
||||||
console.log(`Invalid Command:`.red.underline, `${process.argv.slice(2).join(' ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default handlers
|
|
13
src/handlers/backup.ts
Normal file
13
src/handlers/backup.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||||
|
import { backupAll } from '../backup'
|
||||||
|
import { Flags, Locations } from '../types'
|
||||||
|
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||||
|
|
||||||
|
export default function backup({ location, all }: Flags) {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
const locations: Locations = parseLocations(location, all)
|
||||||
|
checkAndConfigureBackendsForLocations(locations)
|
||||||
|
backupAll(locations)
|
||||||
|
|
||||||
|
console.log('\nFinished!'.underline + ' 🎉')
|
||||||
|
}
|
9
src/handlers/check.ts
Normal file
9
src/handlers/check.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { checkAndConfigureBackends } from '../backend'
|
||||||
|
import { Flags } from '../types'
|
||||||
|
import { checkIfResticIsAvailable, parseBackend } from '../utils'
|
||||||
|
|
||||||
|
export default function check({ backend, all }: Flags) {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
const backends = parseBackend(backend, all)
|
||||||
|
checkAndConfigureBackends(backends)
|
||||||
|
}
|
7
src/handlers/cron.ts
Normal file
7
src/handlers/cron.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { runCron } from '../cron'
|
||||||
|
import { checkIfResticIsAvailable } from '../utils'
|
||||||
|
|
||||||
|
export function cron() {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
runCron()
|
||||||
|
}
|
14
src/handlers/exec.ts
Normal file
14
src/handlers/exec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { getEnvFromBackend } from '../backend'
|
||||||
|
import { Flags } from '../types'
|
||||||
|
import { checkIfResticIsAvailable, exec as execCLI, parseBackend } from '../utils'
|
||||||
|
|
||||||
|
export default function exec({ backend, all }: Flags, args: string[]) {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
const backends = parseBackend(backend, all)
|
||||||
|
for (const [name, backend] of Object.entries(backends)) {
|
||||||
|
console.log(`\n${name}:\n`.grey.underline)
|
||||||
|
const env = getEnvFromBackend(backend)
|
||||||
|
const { out, err } = execCLI('restic', args, { env })
|
||||||
|
console.log(out, err)
|
||||||
|
}
|
||||||
|
}
|
13
src/handlers/forget.ts
Normal file
13
src/handlers/forget.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||||
|
import { forgetAll } from '../forget'
|
||||||
|
import { Flags, Locations } from '../types'
|
||||||
|
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||||
|
|
||||||
|
export default function forget({ location, all, dryRun }: Flags) {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
const locations: Locations = parseLocations(location, all)
|
||||||
|
checkAndConfigureBackendsForLocations(locations)
|
||||||
|
forgetAll(locations, dryRun)
|
||||||
|
|
||||||
|
console.log('\nFinished!'.underline + ' 🎉')
|
||||||
|
}
|
18
src/handlers/info.ts
Normal file
18
src/handlers/info.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { config } from '../'
|
||||||
|
import { fill, treeToString } from '../utils'
|
||||||
|
|
||||||
|
const showAll = () => {
|
||||||
|
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:', 'cron:']))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
50
src/handlers/install.ts
Normal file
50
src/handlers/install.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { chmodSync, renameSync, unlinkSync } from 'fs'
|
||||||
|
import { tmpdir } from 'os'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Writer } from 'clitastic'
|
||||||
|
|
||||||
|
import { INSTALL_DIR } from '..'
|
||||||
|
import { checkIfCommandIsAvailable, checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||||
|
|
||||||
|
export default async function install() {
|
||||||
|
try {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
console.log('Restic is already installed')
|
||||||
|
return
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const w = new Writer('Checking latest version... ⏳')
|
||||||
|
checkIfCommandIsAvailable('bzip2')
|
||||||
|
const { data: json } = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: 'https://api.github.com/repos/restic/restic/releases/latest',
|
||||||
|
responseType: 'json',
|
||||||
|
})
|
||||||
|
|
||||||
|
const archMap: { [a: string]: string } = {
|
||||||
|
x32: '386',
|
||||||
|
x64: 'amd64',
|
||||||
|
}
|
||||||
|
|
||||||
|
w.replaceLn('Downloading binary... 🌎')
|
||||||
|
const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2`
|
||||||
|
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||||
|
if (!dl) return console.log('Cannot get the right binary.'.red, 'Please see https://bit.ly/2Y1Rzai')
|
||||||
|
|
||||||
|
const tmp = join(tmpdir(), name)
|
||||||
|
const extracted = tmp.slice(0, -4) //without the .bz2
|
||||||
|
|
||||||
|
await downloadFile(dl.browser_download_url, tmp)
|
||||||
|
|
||||||
|
w.replaceLn('Decompressing binary... 📦')
|
||||||
|
exec('bzip2', ['-dk', tmp])
|
||||||
|
unlinkSync(tmp)
|
||||||
|
|
||||||
|
w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`)
|
||||||
|
chmodSync(extracted, 0o755)
|
||||||
|
renameSync(extracted, INSTALL_DIR + '/restic')
|
||||||
|
|
||||||
|
w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉')
|
||||||
|
}
|
9
src/handlers/restore.ts
Normal file
9
src/handlers/restore.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { restoreSingle } from '../restore'
|
||||||
|
import { Flags } from '../types'
|
||||||
|
import { checkIfResticIsAvailable, checkIfValidLocation } from '../utils'
|
||||||
|
|
||||||
|
export default function restore({ location, to, from }: Flags) {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
checkIfValidLocation(location)
|
||||||
|
restoreSingle(location, from, to)
|
||||||
|
}
|
13
src/handlers/uninstall.ts
Normal file
13
src/handlers/uninstall.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { unlinkSync } from 'fs'
|
||||||
|
|
||||||
|
import { INSTALL_DIR } from '..'
|
||||||
|
|
||||||
|
export function uninstall() {
|
||||||
|
for (const bin of ['restic', 'autorestic'])
|
||||||
|
try {
|
||||||
|
unlinkSync(INSTALL_DIR + '/' + bin)
|
||||||
|
console.log(`Finished! ${bin} was uninstalled`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`${bin} is already uninstalled`.red)
|
||||||
|
}
|
||||||
|
}
|
37
src/handlers/upgrade.ts
Normal file
37
src/handlers/upgrade.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { chmodSync } from 'fs'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Writer } from 'clitastic'
|
||||||
|
|
||||||
|
import { INSTALL_DIR, VERSION } from '..'
|
||||||
|
import { checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||||
|
|
||||||
|
export async function upgrade() {
|
||||||
|
checkIfResticIsAvailable()
|
||||||
|
const w = new Writer('Checking for latest restic version... ⏳')
|
||||||
|
exec('restic', ['self-update'])
|
||||||
|
|
||||||
|
w.replaceLn('Checking for latest autorestic version... ⏳')
|
||||||
|
const { data: json } = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: 'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest',
|
||||||
|
responseType: 'json',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (json.tag_name != VERSION) {
|
||||||
|
const platformMap: { [key: string]: string } = {
|
||||||
|
darwin: 'macos',
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
||||||
|
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||||
|
|
||||||
|
const to = INSTALL_DIR + '/autorestic'
|
||||||
|
w.replaceLn('Downloading binary... 🌎')
|
||||||
|
await downloadFile(dl.browser_download_url, to)
|
||||||
|
|
||||||
|
chmodSync(to, 0o755)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.done('All up to date! 🚀')
|
||||||
|
}
|
114
src/index.ts
Normal file
114
src/index.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import 'colors'
|
||||||
|
import { program } from 'commander'
|
||||||
|
import { setCIMode } from 'clitastic'
|
||||||
|
|
||||||
|
import { unlock, readLock, writeLock } from './lock'
|
||||||
|
import { Config } from './types'
|
||||||
|
import { init } from './config'
|
||||||
|
|
||||||
|
import info from './handlers/info'
|
||||||
|
import check from './handlers/check'
|
||||||
|
import backup from './handlers/backup'
|
||||||
|
import restore from './handlers/restore'
|
||||||
|
import forget from './handlers/forget'
|
||||||
|
import { cron } from './handlers/cron'
|
||||||
|
import exec from './handlers/exec'
|
||||||
|
import install from './handlers/install'
|
||||||
|
import { uninstall } from './handlers/uninstall'
|
||||||
|
import { upgrade } from './handlers/upgrade'
|
||||||
|
|
||||||
|
export const VERSION = '0.22'
|
||||||
|
export const INSTALL_DIR = '/usr/local/bin'
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.log(err.message)
|
||||||
|
unlock()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
let queue: Function = () => {}
|
||||||
|
const enqueue = (fn: Function) => (cmd: any) => {
|
||||||
|
queue = () => fn(cmd.opts())
|
||||||
|
}
|
||||||
|
|
||||||
|
program.storeOptionsAsProperties()
|
||||||
|
program.name('autorestic').description('Easy Restic CLI Utility').version(VERSION)
|
||||||
|
|
||||||
|
program.option('-c, --config <path>', 'Config file')
|
||||||
|
program.option('-v, --verbose', 'Verbosity', false)
|
||||||
|
program.option('--ci', 'CI Mode. Removes interactivity from the shell', false)
|
||||||
|
|
||||||
|
program.command('info').action(enqueue(info))
|
||||||
|
|
||||||
|
program.on('--help', () => {
|
||||||
|
console.log('')
|
||||||
|
console.log(`${'Docs:'.yellow}\t\thttps://autorestic.vercel.app`)
|
||||||
|
console.log(`${'Examples:'.yellow}\thttps://autorestic.vercel.app/examples`)
|
||||||
|
})
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('check')
|
||||||
|
.description('Checks and initializes backend as needed')
|
||||||
|
.option('-b, --backend <backends...>')
|
||||||
|
.option('-a, --all')
|
||||||
|
.action(enqueue(check))
|
||||||
|
|
||||||
|
program.command('backup').description('Performs a backup').option('-b, --backend <backends...>').option('-a, --all').action(enqueue(backup))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('restore')
|
||||||
|
.description('Restores data to a specified folder from a location')
|
||||||
|
.requiredOption('-l, --location <location>')
|
||||||
|
.option('--from <backend>')
|
||||||
|
.requiredOption('--to <path>', 'Path to save the restored data to')
|
||||||
|
.action(enqueue(restore))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('forget')
|
||||||
|
.description('This will prune and remove data according to your policies')
|
||||||
|
.option('-l, --location <locations...>')
|
||||||
|
.option('-a, --all')
|
||||||
|
.option('--dry-run')
|
||||||
|
.action(enqueue(forget))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('cron')
|
||||||
|
.description('Intended to be triggered by an automated system like systemd or crontab.')
|
||||||
|
.option('-a, --all')
|
||||||
|
.action(enqueue(cron))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('exec')
|
||||||
|
.description('Run any native restic command on desired backends')
|
||||||
|
.option('-b, --backend <backends...>')
|
||||||
|
.option('-a, --all')
|
||||||
|
.action(({ args, all, backend }) => {
|
||||||
|
queue = () => exec({ all, backend }, args)
|
||||||
|
})
|
||||||
|
|
||||||
|
program.command('install').description('Installs both restic and autorestic to /usr/local/bin').action(enqueue(install))
|
||||||
|
|
||||||
|
program.command('uninstall').description('Uninstalls autorestic from the system').action(enqueue(uninstall))
|
||||||
|
|
||||||
|
program.command('upgrade').alias('update').description('Checks and installs new autorestic versions').action(enqueue(upgrade))
|
||||||
|
|
||||||
|
const { verbose, config: configFile, ci } = program.parse(process.argv)
|
||||||
|
|
||||||
|
export const VERBOSE = verbose
|
||||||
|
export let config: Config = init(configFile)
|
||||||
|
setCIMode(ci)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lock = readLock()
|
||||||
|
if (lock.running) throw new Error('An instance of autorestic is already running for this config file'.red)
|
||||||
|
|
||||||
|
writeLock({
|
||||||
|
...lock,
|
||||||
|
running: true,
|
||||||
|
})
|
||||||
|
queue()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message)
|
||||||
|
} finally {
|
||||||
|
unlock()
|
||||||
|
}
|
18
src/info.ts
Normal file
18
src/info.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { config } from './'
|
||||||
|
import { fill, treeToString } from './utils'
|
||||||
|
|
||||||
|
const showAll = () => {
|
||||||
|
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:', 'cron:']))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
32
src/lock.ts
Normal file
32
src/lock.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import { pathRelativeToConfigFile } from './utils'
|
||||||
|
import { Lockfile } from './types'
|
||||||
|
|
||||||
|
export const getLockFileName = () => {
|
||||||
|
const LOCK_FILE = '.autorestic.lock'
|
||||||
|
return pathRelativeToConfigFile(LOCK_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readLock = (): Lockfile => {
|
||||||
|
const name = getLockFileName()
|
||||||
|
let lock = {
|
||||||
|
running: false,
|
||||||
|
crons: {},
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
lock = JSON.parse(fs.readFileSync(name, { encoding: 'utf-8' }))
|
||||||
|
} catch {}
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
export const writeLock = (lock: Lockfile) => {
|
||||||
|
const name = getLockFileName()
|
||||||
|
fs.writeFileSync(name, JSON.stringify(lock, null, 2), { encoding: 'utf-8' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unlock = () => {
|
||||||
|
writeLock({
|
||||||
|
...readLock(),
|
||||||
|
running: false,
|
||||||
|
})
|
||||||
|
}
|
63
src/restore.ts
Normal file
63
src/restore.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Writer } from 'clitastic'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
import { config } from './'
|
||||||
|
import { getEnvFromBackend } from './backend'
|
||||||
|
import { LocationFromPrefixes } from './config'
|
||||||
|
import { Backend } from './types'
|
||||||
|
import { checkIfDockerVolumeExistsOrFail, decodeLocationFromPrefix, exec, execPlain, getPathFromVolume } from './utils'
|
||||||
|
|
||||||
|
export const restoreToFilesystem = (from: string, to: string, backend: Backend) => {
|
||||||
|
exec('restic', ['restore', 'latest', '--path', resolve(from), '--target', to], { env: getEnvFromBackend(backend) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreToVolume = (volume: string, backend: Backend) => {
|
||||||
|
const tmp = getPathFromVolume(volume)
|
||||||
|
try {
|
||||||
|
restoreToFilesystem(tmp, tmp, backend)
|
||||||
|
try {
|
||||||
|
checkIfDockerVolumeExistsOrFail(volume)
|
||||||
|
} catch {
|
||||||
|
execPlain(`docker volume create ${volume}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
||||||
|
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /backup /data`)
|
||||||
|
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar xf /backup/archive.tar -C /data`)
|
||||||
|
} finally {
|
||||||
|
execPlain(`rm -rf ${tmp}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreSingle = (locationName: string, from: string, to?: string) => {
|
||||||
|
const location = config.locations[locationName]
|
||||||
|
|
||||||
|
const baseText = locationName.green + '\t\t'
|
||||||
|
const w = new Writer(baseText + `Restoring...`)
|
||||||
|
|
||||||
|
let backendName: string = Array.isArray(location.to) ? location.to[0] : location.to
|
||||||
|
if (from) {
|
||||||
|
if (!location.to.includes(from)) {
|
||||||
|
w.done(baseText + `Backend ${from} is not a valid location for ${locationName}`.red)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backendName = from
|
||||||
|
w.replaceLn(baseText + `Restoring from ${backendName.blue}...`)
|
||||||
|
} else if (Array.isArray(location.to) && location.to.length > 1) {
|
||||||
|
w.replaceLn(baseText + `Restoring from ${backendName.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`)
|
||||||
|
}
|
||||||
|
const backend = config.backends[backendName]
|
||||||
|
|
||||||
|
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||||
|
switch (type) {
|
||||||
|
case LocationFromPrefixes.Filesystem:
|
||||||
|
if (!to) throw new Error(`You need to specify the restore path with --to`.red)
|
||||||
|
restoreToFilesystem(value, to, backend)
|
||||||
|
break
|
||||||
|
|
||||||
|
case LocationFromPrefixes.DockerVolume:
|
||||||
|
restoreToVolume(value, backend)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w.done(locationName.green + '\t\tDone 🎉')
|
||||||
|
}
|
85
src/types.ts
85
src/types.ts
@@ -1,53 +1,57 @@
|
|||||||
|
export type StringOrArray = string | string[]
|
||||||
|
|
||||||
|
// BACKENDS
|
||||||
|
|
||||||
type BackendLocal = {
|
type BackendLocal = {
|
||||||
type: 'local',
|
type: 'local'
|
||||||
key: string,
|
key: string
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendSFTP = {
|
type BackendSFTP = {
|
||||||
type: 'sftp',
|
type: 'sftp'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
password?: string,
|
password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendREST = {
|
type BackendREST = {
|
||||||
type: 'rest',
|
type: 'rest'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
user?: string,
|
user?: string
|
||||||
password?: string
|
password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendS3 = {
|
type BackendS3 = {
|
||||||
type: 's3',
|
type: 's3'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
aws_access_key_id: string,
|
aws_access_key_id: string
|
||||||
aws_secret_access_key: string,
|
aws_secret_access_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendB2 = {
|
type BackendB2 = {
|
||||||
type: 'b2',
|
type: 'b2'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
b2_account_id: string,
|
b2_account_id: string
|
||||||
b2_account_key: string
|
b2_account_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendAzure = {
|
type BackendAzure = {
|
||||||
type: 'azure',
|
type: 'azure'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
azure_account_name: string,
|
azure_account_name: string
|
||||||
azure_account_key: string
|
azure_account_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackendGS = {
|
type BackendGS = {
|
||||||
type: 'gs',
|
type: 'gs'
|
||||||
key: string,
|
key: string
|
||||||
path: string,
|
path: string
|
||||||
google_project_id: string,
|
google_project_id: string
|
||||||
google_application_credentials: string
|
google_application_credentials: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,16 +59,39 @@ export type Backend = BackendAzure | BackendB2 | BackendGS | BackendLocal | Back
|
|||||||
|
|
||||||
export type Backends = { [name: string]: Backend }
|
export type Backends = { [name: string]: Backend }
|
||||||
|
|
||||||
|
// LOCATIONS
|
||||||
|
|
||||||
export type Location = {
|
export type Location = {
|
||||||
from: string,
|
from: string
|
||||||
to: string | string[]
|
to: StringOrArray
|
||||||
|
cron?: string
|
||||||
|
hooks?: {
|
||||||
|
before?: StringOrArray
|
||||||
|
after?: StringOrArray
|
||||||
|
}
|
||||||
|
options?: {
|
||||||
|
[key: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Lockfile = {
|
||||||
|
running: boolean
|
||||||
|
crons: {
|
||||||
|
[name: string]: {
|
||||||
|
lastRun: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Flags = { [arg: string]: any }
|
export type Flags = { [arg: string]: any }
|
194
src/utils.ts
194
src/utils.ts
@@ -1,9 +1,18 @@
|
|||||||
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
||||||
import { randomBytes } from 'crypto'
|
import { createHash, randomBytes } from 'crypto'
|
||||||
|
import { createWriteStream, renameSync, unlinkSync } from 'fs'
|
||||||
|
import { homedir, tmpdir } from 'os'
|
||||||
|
import { dirname, isAbsolute, join, resolve } from 'path'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Duration, Humanizer } from 'uhrwerk'
|
||||||
|
|
||||||
|
import { CONFIG_FILE, LocationFromPrefixes } from './config'
|
||||||
|
import { Backends, Location, Locations } from './types'
|
||||||
|
import { config } from '.'
|
||||||
|
|
||||||
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
|
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
|
||||||
|
const { stdout, stderr, status } = spawnSync(command, args, {
|
||||||
const cmd = spawnSync(command, args, {
|
|
||||||
...rest,
|
...rest,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -11,35 +20,182 @@ export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyn
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const out = cmd.stdout && cmd.stdout.toString().trim()
|
const out = stdout && stdout.toString().trim()
|
||||||
const err = cmd.stderr && cmd.stderr.toString().trim()
|
const err = stderr && stderr.toString().trim()
|
||||||
|
|
||||||
return { out, err }
|
return { out, err, status }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkIfResticIsAvailable = () => checkIfCommandIsAvailable(
|
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
|
||||||
|
const split = command.split(' ')
|
||||||
|
if (split.length < 1) throw new Error(`The command ${command} is not valid`.red)
|
||||||
|
|
||||||
|
return exec(split[0], split.slice(1), { shell: true, ...opt })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkIfResticIsAvailable = () =>
|
||||||
|
checkIfCommandIsAvailable(
|
||||||
'restic',
|
'restic',
|
||||||
'Restic is not installed'.red + ' https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases',
|
'restic is not installed'.red +
|
||||||
)
|
'\nEither run ' +
|
||||||
|
'autorestic install'.green +
|
||||||
|
'\nOr go to https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases'
|
||||||
|
)
|
||||||
|
|
||||||
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
||||||
if (require('child_process').spawnSync(cmd).error)
|
const error = spawnSync(cmd, { shell: true }).stderr
|
||||||
throw new Error(errorMsg ? errorMsg : `"${errorMsg}" is not installed`.red)
|
if (error.length) throw new Error(errorMsg ? errorMsg : `"${cmd}" is not installed`.red)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeObjectKeysLowercase = (object: Object): any =>
|
export const makeObjectKeysLowercase = (object: Object): any =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(Object.entries(object).map(([key, value]) => [key.toLowerCase(), value]))
|
||||||
Object.entries(object)
|
|
||||||
.map(([key, value]) => [key.toLowerCase(), value]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export function rand(length = 32): string {
|
export function rand(length = 32): string {
|
||||||
return randomBytes(length / 2).toString('hex')
|
return randomBytes(length / 2).toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const singleToArray = <T>(singleOrArray: T | T[]): T[] => Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray]
|
export const filterObject = <T>(obj: { [key: string]: T }, filter: (item: [string, T]) => boolean): { [key: string]: T } =>
|
||||||
|
Object.fromEntries(Object.entries(obj).filter(filter))
|
||||||
export const filterObject = <T>(obj: { [key: string]: T }, filter: (item: [string, T]) => boolean): { [key: string]: T } => Object.fromEntries(Object.entries(obj).filter(filter))
|
|
||||||
|
|
||||||
export const filterObjectByKey = <T>(obj: { [key: string]: T }, keys: string[]) => filterObject(obj, ([key]) => keys.includes(key))
|
export const filterObjectByKey = <T>(obj: { [key: string]: T }, keys: string[]) => filterObject(obj, ([key]) => keys.includes(key))
|
||||||
|
|
||||||
|
export const downloadFile = async (url: string, to: string) =>
|
||||||
|
new Promise<void>(async (res) => {
|
||||||
|
const { data: file } = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: url,
|
||||||
|
responseType: 'stream',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tmp = join(tmpdir(), rand(64))
|
||||||
|
const stream = createWriteStream(tmp)
|
||||||
|
|
||||||
|
const writer = file.pipe(stream)
|
||||||
|
writer.on('close', () => {
|
||||||
|
stream.close()
|
||||||
|
try {
|
||||||
|
// Delete file if already exists. Needed if the binary wants to replace itself.
|
||||||
|
// Unix does not allow to overwrite a file that is being executed, but you can remove it and save other one at its place
|
||||||
|
unlinkSync(to)
|
||||||
|
} catch {}
|
||||||
|
renameSync(tmp, to)
|
||||||
|
res()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if is an absolute path, otherwise get the path relative to the config file
|
||||||
|
export const pathRelativeToConfigFile = (path: string): string => (isAbsolute(path) ? 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 getFlagsFromLocation = (location: Location, command?: string): string[] => {
|
||||||
|
if (!location.options) return []
|
||||||
|
|
||||||
|
const all = {
|
||||||
|
...location.options.global,
|
||||||
|
...(location.options[command || ''] || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags: string[] = []
|
||||||
|
// Map the flags to an array for the exec function.
|
||||||
|
for (let [flag, values] of Object.entries(all))
|
||||||
|
for (const value of makeArrayIfIsNot(values)) {
|
||||||
|
const stringValue = String(value)
|
||||||
|
const resolvedTilde = resolveTildePath(stringValue)
|
||||||
|
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBackend(backends: string[] = [], all: boolean = false): Backends {
|
||||||
|
if (all) return config.backends
|
||||||
|
if (backends.length) {
|
||||||
|
for (const backend of backends) if (!config.backends[backend]) throw new Error('Invalid backend: '.red + backend)
|
||||||
|
return filterObjectByKey(config.backends, backends)
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'No backends specified.'.red + '\n-a, --all, -a\t\t\tSelect all.' + '\n-b, --backend <backends...>\t\tSpecify one or more backend'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkIfValidLocation(location: string) {
|
||||||
|
if (!config.locations[location]) throw new Error('Invalid location: '.red + location)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLocations(locations: string[] = [], all: boolean = false): Locations {
|
||||||
|
if (all) {
|
||||||
|
return config.locations
|
||||||
|
}
|
||||||
|
if (locations.length) {
|
||||||
|
for (const location of locations) checkIfValidLocation(location)
|
||||||
|
return filterObjectByKey(config.locations, locations)
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'No locations specified.'.red + '\n-a, --all\t\t\tSelect all.' + '\n-l, --location <locations...>\t\t\tSpecify one or more location'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}/gm, '')
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodeLocationFromPrefix = (from: string): [LocationFromPrefixes, string] => {
|
||||||
|
const firstDelimiter = from.indexOf(':')
|
||||||
|
if (firstDelimiter === -1) return [LocationFromPrefixes.Filesystem, from]
|
||||||
|
|
||||||
|
const type = from.substr(0, firstDelimiter)
|
||||||
|
const value = from.substr(firstDelimiter + 1)
|
||||||
|
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'volume':
|
||||||
|
return [LocationFromPrefixes.DockerVolume, value]
|
||||||
|
case 'path':
|
||||||
|
return [LocationFromPrefixes.Filesystem, value]
|
||||||
|
default:
|
||||||
|
throw new Error(`Could not decode the location from: ${from}`.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hash = (plain: string): string => createHash('sha1').update(plain).digest().toString('hex')
|
||||||
|
|
||||||
|
export const getPathFromVolume = (volume: string) => pathRelativeToConfigFile(hash(volume))
|
||||||
|
|
||||||
|
export const checkIfDockerVolumeExistsOrFail = (volume: string) => {
|
||||||
|
const cmd = exec('docker', ['volume', 'inspect', volume])
|
||||||
|
if (cmd.err.length > 0) throw new Error('Volume not found')
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
|
3
vercel.json
Normal file
3
vercel.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cleanUrls": true
|
||||||
|
}
|
Reference in New Issue
Block a user