restic-service 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +126 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/restic-service +9 -0
- data/install-dev.sh +31 -0
- data/install.sh +27 -0
- data/lib/restic/service.rb +14 -0
- data/lib/restic/service/cli.rb +88 -0
- data/lib/restic/service/conf.rb +259 -0
- data/lib/restic/service/ssh_keys.rb +129 -0
- data/lib/restic/service/targets/b2.rb +38 -0
- data/lib/restic/service/targets/base.rb +23 -0
- data/lib/restic/service/targets/rclone_b2.rb +49 -0
- data/lib/restic/service/targets/restic.rb +89 -0
- data/lib/restic/service/targets/restic_b2.rb +21 -0
- data/lib/restic/service/targets/restic_file.rb +27 -0
- data/lib/restic/service/targets/restic_sftp.rb +61 -0
- data/lib/restic/service/version.rb +5 -0
- data/restic-service.gemspec +29 -0
- data/restic-service.service +9 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5eb6ed2de697e518e95589565d8104e15aff50c4
|
4
|
+
data.tar.gz: afc4fd0c966455bb46920ce2ca5cd42902eb1e1c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c9e35b848ca8d1cb6627aefcc3c3073a3499b2770b995bf9d22ae673ff085f84193dbb52fa54a329ec6dd1dedf802dbe069ebcd8bd8f9ac932bd8806235ef9b
|
7
|
+
data.tar.gz: 31d8e7db6d33568552a33a90cef56c3c86996d6e1021a90f56b43224fda7a80e28a910465583a8c8186564784fb041abbb5c9302da66a2fbdda2c81d1ff380db
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Sylvain Joyeux
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# restic-service
|
2
|
+
|
3
|
+
A backup service using restic and rclone, meant to be run as a background
|
4
|
+
service.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
`restic-service` expects a YAML configuration file. By default, it is stored in
|
9
|
+
`/etc/restic-service/conf.yml`.
|
10
|
+
|
11
|
+
The template configuration file looks like this:
|
12
|
+
|
13
|
+
~~~ yaml
|
14
|
+
# The path to the underlying tools
|
15
|
+
tools:
|
16
|
+
restic: /opt/restic
|
17
|
+
rclone: /opt/rclone
|
18
|
+
|
19
|
+
# The targets. The only generic parts of a target definition
|
20
|
+
# are the name and type. The rest is target-specific
|
21
|
+
targets:
|
22
|
+
- name: a_restic_sftp_target
|
23
|
+
type: restic-sftp
|
24
|
+
~~~
|
25
|
+
|
26
|
+
## Remote host identification using SSH {#ssh_key_id}
|
27
|
+
|
28
|
+
Some target types recognize the remote host based on expected SSH keys.
|
29
|
+
Corresponding keys have to be stored locally in the `keys/` subfolder of
|
30
|
+
restic-service's configuration folder (i.e. /etc/restic-service/keys/ by
|
31
|
+
default). Targets that are recognized this way must have a file named
|
32
|
+
`${target_name}.keys`.
|
33
|
+
|
34
|
+
This file can be created using ssh's tools with
|
35
|
+
|
36
|
+
~~~
|
37
|
+
ssh-keyscan -H hostname > /etc/restic-service/keys/targetname.keys
|
38
|
+
~~~
|
39
|
+
|
40
|
+
## Target type: restic-sftp
|
41
|
+
|
42
|
+
The rest-sftp target backs files up using sftp on a remote target. The
|
43
|
+
target expects the SFTP authentication to be done [using SSH keys](#ssh_key_id).
|
44
|
+
|
45
|
+
The target takes the following arguments:
|
46
|
+
|
47
|
+
~~~ yaml
|
48
|
+
- name: name_of_target
|
49
|
+
type: restic-sftp
|
50
|
+
# The repo host. This is authenticated using SSH keys, so there must be a
|
51
|
+
# corresponding keys/name_of_target.keys file in $CONF_DIR/keys
|
52
|
+
host: host
|
53
|
+
# The username that should be used to connect to the host
|
54
|
+
username: the_user
|
55
|
+
# The repo path on the host
|
56
|
+
path: /
|
57
|
+
# The repository password (encryption password from Restic)
|
58
|
+
password: repo_password
|
59
|
+
# Mandatory, list of paths to backup. Needs at least one
|
60
|
+
includes:
|
61
|
+
- list
|
62
|
+
- of
|
63
|
+
- paths
|
64
|
+
- to
|
65
|
+
- backup
|
66
|
+
# Optional, list of excluded patterns
|
67
|
+
excludes:
|
68
|
+
- list/of/patterns
|
69
|
+
- to/not/backup
|
70
|
+
one_filesystem: false
|
71
|
+
# Optional, the IO class. Defaults to 3 (Idle)
|
72
|
+
io_class: 3
|
73
|
+
# Optional, the IO priority. Unused for IO class 3
|
74
|
+
io_priority: 0
|
75
|
+
# Optional, the CPU priority. Higher gets less CPU
|
76
|
+
cpu_priority: 19
|
77
|
+
~~~
|
78
|
+
|
79
|
+
## Target type: restic-b2
|
80
|
+
|
81
|
+
The rest-b2 target backs files up using sftp on a B2 bucket.
|
82
|
+
|
83
|
+
The target takes the following arguments:
|
84
|
+
|
85
|
+
~~~ yaml
|
86
|
+
- name: name_of_target
|
87
|
+
type: restic-b2
|
88
|
+
# The B2 bucket
|
89
|
+
bucket: mybucket
|
90
|
+
# The path within the bucket
|
91
|
+
path: path
|
92
|
+
# The B2 ID
|
93
|
+
id:
|
94
|
+
# The B2 Key
|
95
|
+
key:
|
96
|
+
# The repository password (encryption password from Restic)
|
97
|
+
password: repo_password
|
98
|
+
# Mandatory, list of paths to backup. Needs at least one
|
99
|
+
includes:
|
100
|
+
- list
|
101
|
+
- of
|
102
|
+
- paths
|
103
|
+
- to
|
104
|
+
- backup
|
105
|
+
# Optional, list of excluded patterns
|
106
|
+
excludes:
|
107
|
+
- list/of/patterns
|
108
|
+
- to/not/backup
|
109
|
+
one_filesystem: false
|
110
|
+
# Optional, the IO class. Defaults to 3 (Idle)
|
111
|
+
io_class: 3
|
112
|
+
# Optional, the IO priority. Unused for IO class 3
|
113
|
+
io_priority: 0
|
114
|
+
# Optional, the CPU priority. Higher gets less CPU
|
115
|
+
cpu_priority: 19
|
116
|
+
~~~
|
117
|
+
|
118
|
+
## Contributing
|
119
|
+
|
120
|
+
Bug reports and pull requests are welcome on GitHub at
|
121
|
+
https://github.com/ThirteenLtda/restic-service.
|
122
|
+
|
123
|
+
## License
|
124
|
+
|
125
|
+
The gem is available as open source under the terms of the [MIT
|
126
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "restic/service"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/restic-service
ADDED
data/install-dev.sh
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#! /bin/sh -ex
|
2
|
+
|
3
|
+
PROGRAM=restic-service
|
4
|
+
|
5
|
+
dev_path=$PWD
|
6
|
+
|
7
|
+
target=`mktemp -d`
|
8
|
+
cd $target
|
9
|
+
cat > Gemfile <<GEMFILE
|
10
|
+
source "https://rubygems.org"
|
11
|
+
gem '${PROGRAM}', path: "$dev_path"
|
12
|
+
GEMFILE
|
13
|
+
|
14
|
+
cat Gemfile
|
15
|
+
|
16
|
+
bundler install --standalone --binstubs
|
17
|
+
if test -d /opt/${PROGRAM}; then
|
18
|
+
sudo rm -rf /opt/${PROGRAM}
|
19
|
+
fi
|
20
|
+
sudo cp -r . /opt/${PROGRAM}
|
21
|
+
sudo chmod go+rX /opt/${PROGRAM}
|
22
|
+
|
23
|
+
if test -d /lib/systemd/system; then
|
24
|
+
target_gem=`bundler show ${PROGRAM}`
|
25
|
+
sudo cp $target_gem/${PROGRAM}.service /lib/systemd/system
|
26
|
+
( sudo systemctl stop ${PROGRAM}.service
|
27
|
+
sudo systemctl enable ${PROGRAM}.service
|
28
|
+
sudo systemctl start ${PROGRAM}.service )
|
29
|
+
fi
|
30
|
+
|
31
|
+
rm -rf $target
|
data/install.sh
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#! /bin/sh -ex
|
2
|
+
|
3
|
+
PROGRAM=restic-service
|
4
|
+
|
5
|
+
target=`mktemp -d`
|
6
|
+
cd $target
|
7
|
+
cat > Gemfile <<GEMFILE
|
8
|
+
source "https://rubygems.org"
|
9
|
+
gem '${PROGRAM}'
|
10
|
+
GEMFILE
|
11
|
+
|
12
|
+
bundler install --standalone --binstubs
|
13
|
+
if test -d /opt/${PROGRAM}; then
|
14
|
+
sudo rm -rf /opt/${PROGRAM}
|
15
|
+
fi
|
16
|
+
sudo cp -r . /opt/${PROGRAM}
|
17
|
+
sudo chmod go+rX /opt/${PROGRAM}
|
18
|
+
|
19
|
+
if test -d /lib/systemd/system; then
|
20
|
+
target_gem=`bundler show ${PROGRAM}`
|
21
|
+
sudo cp $target_gem/${PROGRAM}.service /lib/systemd/system
|
22
|
+
( sudo systemctl stop ${PROGRAM}.service
|
23
|
+
sudo systemctl enable ${PROGRAM}.service
|
24
|
+
sudo systemctl start ${PROGRAM}.service )
|
25
|
+
fi
|
26
|
+
|
27
|
+
rm -rf $target
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require "pathname"
|
3
|
+
require 'yaml'
|
4
|
+
require 'tempfile'
|
5
|
+
require "restic/service/version"
|
6
|
+
require "restic/service/targets/base"
|
7
|
+
require "restic/service/targets/restic"
|
8
|
+
require "restic/service/targets/b2"
|
9
|
+
require "restic/service/targets/restic_b2"
|
10
|
+
require "restic/service/targets/restic_file"
|
11
|
+
require "restic/service/targets/restic_sftp"
|
12
|
+
require "restic/service/targets/rclone_b2"
|
13
|
+
require "restic/service/ssh_keys"
|
14
|
+
require "restic/service/conf"
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'restic/service'
|
3
|
+
|
4
|
+
module Restic
|
5
|
+
module Service
|
6
|
+
DEFAULT_CONF = "/etc/restic-service"
|
7
|
+
|
8
|
+
class CLI < Thor
|
9
|
+
class_option :conf, desc: "path to the configuration file (#{DEFAULT_CONF})",
|
10
|
+
type: :string, default: DEFAULT_CONF
|
11
|
+
|
12
|
+
no_commands do
|
13
|
+
def conf_dir_path
|
14
|
+
@conf_dir_path ||= Pathname.new(options[:conf])
|
15
|
+
end
|
16
|
+
|
17
|
+
def conf_file_path
|
18
|
+
@conf_file_path ||= (conf_dir_path + "conf.yml")
|
19
|
+
end
|
20
|
+
|
21
|
+
def conf_keys_path
|
22
|
+
@conf_keys_path ||= (conf_dir_path + "keys")
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_conf
|
26
|
+
Conf.load(conf_file_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_sync(conf, *targets)
|
30
|
+
has_target = false
|
31
|
+
conf.each_target do |target|
|
32
|
+
has_target = true
|
33
|
+
if !targets.empty? && !targets.include?(target.name)
|
34
|
+
next
|
35
|
+
end
|
36
|
+
|
37
|
+
if !target.available?
|
38
|
+
puts "#{target.name} is not available"
|
39
|
+
next
|
40
|
+
end
|
41
|
+
|
42
|
+
puts
|
43
|
+
puts "-----"
|
44
|
+
puts "#{Time.now} - Synchronizing #{target.name}"
|
45
|
+
target.run
|
46
|
+
end
|
47
|
+
if !has_target
|
48
|
+
STDERR.puts "WARNING: no targets in #{options[:conf]}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc 'available-targets', 'finds the available backup targets'
|
54
|
+
def whereami
|
55
|
+
STDOUT.sync = true
|
56
|
+
conf = load_conf
|
57
|
+
conf.each_target do |target|
|
58
|
+
print "#{target.name}: "
|
59
|
+
puts(target.available? ? 'yes' : 'no')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'sync', 'synchronize all (some) targets'
|
64
|
+
def sync(*targets)
|
65
|
+
STDOUT.sync = true
|
66
|
+
conf = load_conf
|
67
|
+
run_sync(conf, *targets)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'auto', 'periodically runs the backups, pass target names to restrict to these'
|
71
|
+
def auto(*targets)
|
72
|
+
STDOUT.sync = true
|
73
|
+
conf = load_conf
|
74
|
+
loop do
|
75
|
+
puts "#{Time.now} Starting automatic synchronization pass"
|
76
|
+
puts ""
|
77
|
+
|
78
|
+
run_sync(conf, *targets)
|
79
|
+
|
80
|
+
puts ""
|
81
|
+
puts "#{Time.now} Finished automatic synchronization pass"
|
82
|
+
|
83
|
+
sleep conf.period
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
# The overall service configuration
|
4
|
+
#
|
5
|
+
# This is the API side of the service configuration. The configuration
|
6
|
+
# is usually stored on disk in YAML and
|
7
|
+
#
|
8
|
+
# The YAML format is as follows:
|
9
|
+
#
|
10
|
+
# # The path to the underlying tools
|
11
|
+
# tools:
|
12
|
+
# restic: /opt/restic
|
13
|
+
# rclone: /opt/rclone
|
14
|
+
#
|
15
|
+
# # The targets. The only generic parts of a target definition
|
16
|
+
# # are the name and type. The rest is target-specific
|
17
|
+
# targets:
|
18
|
+
# - name: a_restic_sftp_target
|
19
|
+
# type: restic-sftp
|
20
|
+
#
|
21
|
+
# See the README.md for more details about available targets
|
22
|
+
class Conf
|
23
|
+
# Exception raised when using a target name that does not exist
|
24
|
+
class NoSuchTarget < RuntimeError; end
|
25
|
+
# Exception raised when an invalid configuration file is loaded
|
26
|
+
class InvalidConfigurationFile < RuntimeError; end
|
27
|
+
|
28
|
+
# The default (empty) configuration
|
29
|
+
def self.default_conf
|
30
|
+
Hash['targets' => [],
|
31
|
+
'period' => 3600,
|
32
|
+
'bandwidth_limit' => nil,
|
33
|
+
'tools' => Hash.new]
|
34
|
+
end
|
35
|
+
|
36
|
+
TARGET_CLASS_FROM_TYPE = Hash[
|
37
|
+
'restic-b2' => Targets::ResticB2,
|
38
|
+
'restic-sftp' => Targets::ResticSFTP,
|
39
|
+
'restic-file' => Targets::ResticFile,
|
40
|
+
'rclone-b2' => Targets::RcloneB2]
|
41
|
+
|
42
|
+
TOOLS = %w{restic rclone}
|
43
|
+
|
44
|
+
# Returns the target class that will handle the given target type
|
45
|
+
#
|
46
|
+
# @param [String] type the type as represented in the YAML file
|
47
|
+
def self.target_class_from_type(type)
|
48
|
+
if target_class = TARGET_CLASS_FROM_TYPE[type]
|
49
|
+
return target_class
|
50
|
+
else
|
51
|
+
raise InvalidConfigurationFile, "target type #{type} does not exist, available targets: #{TARGET_CLASS_FROM_TYPE.keys.sort.join(", ")}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Normalizes and validates a configuration hash, as stored in YAML
|
56
|
+
#
|
57
|
+
# @raise [InvalidConfigurationFile]
|
58
|
+
def self.normalize_yaml(yaml)
|
59
|
+
yaml = default_conf.merge(yaml)
|
60
|
+
TOOLS.each do |tool_name|
|
61
|
+
yaml['tools'][tool_name] ||= tool_name
|
62
|
+
end
|
63
|
+
|
64
|
+
target_names = Array.new
|
65
|
+
yaml['targets'] = yaml['targets'].map do |target|
|
66
|
+
if !target['name']
|
67
|
+
raise InvalidConfigurationFile, "missing 'name' field in target"
|
68
|
+
elsif !target['type']
|
69
|
+
raise InvalidConfigurationFile, "missing 'type' field in target"
|
70
|
+
end
|
71
|
+
|
72
|
+
target_class = target_class_from_type(target['type'])
|
73
|
+
if !target_class
|
74
|
+
raise InvalidConfigurationFile, "target type #{target['type']} does not exist, available targets: #{TARGET_CLASS_FROM_TYPE.keys.sort.join(", ")}"
|
75
|
+
end
|
76
|
+
|
77
|
+
name = target['name'].to_s
|
78
|
+
if target_names.include?(name)
|
79
|
+
raise InvalidConfigurationFile, "duplicate target name '#{name}'"
|
80
|
+
end
|
81
|
+
|
82
|
+
target = target.dup
|
83
|
+
target['name'] = name
|
84
|
+
target = target_class.normalize_yaml(target)
|
85
|
+
target_names << name
|
86
|
+
target
|
87
|
+
end
|
88
|
+
yaml
|
89
|
+
end
|
90
|
+
|
91
|
+
# Load a configuration file
|
92
|
+
#
|
93
|
+
# @param [Pathname]
|
94
|
+
# @return [Conf]
|
95
|
+
# @raise (see normalize_yaml)
|
96
|
+
def self.load(path)
|
97
|
+
if !path.file?
|
98
|
+
return Conf.new(Pathname.new(""))
|
99
|
+
end
|
100
|
+
|
101
|
+
yaml = YAML.load(path.read) || Hash.new
|
102
|
+
yaml = normalize_yaml(yaml)
|
103
|
+
|
104
|
+
conf = Conf.new(path.dirname)
|
105
|
+
conf.load_from_yaml(yaml)
|
106
|
+
conf
|
107
|
+
end
|
108
|
+
|
109
|
+
# The configuration path
|
110
|
+
#
|
111
|
+
# @return [Pathname]
|
112
|
+
attr_reader :conf_path
|
113
|
+
|
114
|
+
# The polling period in seconds
|
115
|
+
#
|
116
|
+
# Default is 1h (3600s)
|
117
|
+
#
|
118
|
+
# @return [Integer]
|
119
|
+
attr_reader :period
|
120
|
+
|
121
|
+
# The bandwidth limit in bytes/s
|
122
|
+
#
|
123
|
+
# Default is nil (none)
|
124
|
+
#
|
125
|
+
# @return [nil,Integer]
|
126
|
+
attr_reader :bandwidth_limit
|
127
|
+
|
128
|
+
def initialize(conf_path)
|
129
|
+
@conf_path = conf_path
|
130
|
+
@targets = Hash.new
|
131
|
+
@period = 3600
|
132
|
+
@tools = Hash.new
|
133
|
+
TOOLS.each do |tool_name|
|
134
|
+
@tools[tool_name] = find_in_path(tool_name)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
BANDWIDTH_SCALES = Hash[
|
139
|
+
nil => 1,
|
140
|
+
'k' => 1_000,
|
141
|
+
'm' => 1_000_000,
|
142
|
+
'g' => 1_000_000_000]
|
143
|
+
|
144
|
+
def self.parse_bandwidth_limit(limit)
|
145
|
+
if !limit.respond_to?(:to_str)
|
146
|
+
return Integer(limit)
|
147
|
+
else
|
148
|
+
match = /^(\d+)\s*(k|m|g)?$/.match(limit.downcase)
|
149
|
+
if match
|
150
|
+
return Integer(match[1]) * BANDWIDTH_SCALES.fetch(match[2])
|
151
|
+
else
|
152
|
+
raise ArgumentError, "cannot interpret '#{limit}' as a valid bandwidth limit, give a plain number in bytes or use the k, M and G suffixes"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# The path to the key file for the given target
|
158
|
+
def conf_keys_path_for(target)
|
159
|
+
conf_path.join("keys", "#{target.name}.keys")
|
160
|
+
end
|
161
|
+
|
162
|
+
# Gets a target configuration
|
163
|
+
#
|
164
|
+
# @param [String] name the target name
|
165
|
+
# @return [Target]
|
166
|
+
# @raise NoSuchTarget
|
167
|
+
def target_by_name(name)
|
168
|
+
if target = @targets[name]
|
169
|
+
target
|
170
|
+
else
|
171
|
+
raise NoSuchTarget, "no target named '#{name}'"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Enumerates the targets
|
176
|
+
def each_target(&block)
|
177
|
+
@targets.each_value(&block)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Registers a target
|
181
|
+
def register_target(target)
|
182
|
+
@targets[target.name] = target
|
183
|
+
end
|
184
|
+
|
185
|
+
# @api private
|
186
|
+
#
|
187
|
+
# Helper that resolves a binary in PATH
|
188
|
+
def find_in_path(name)
|
189
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |p|
|
190
|
+
candidate = Pathname.new(p).join(name)
|
191
|
+
if candidate.file?
|
192
|
+
return candidate
|
193
|
+
end
|
194
|
+
end
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
# Checks whether a given tool is available
|
199
|
+
#
|
200
|
+
# @param [String]
|
201
|
+
# @return [Boolean]
|
202
|
+
def tool_available?(tool_name)
|
203
|
+
@tools.has_key?(tool_name)
|
204
|
+
end
|
205
|
+
|
206
|
+
# The full path of a given tool
|
207
|
+
#
|
208
|
+
# @param [String]
|
209
|
+
# @return [Pathname]
|
210
|
+
def tool_path(tool_name)
|
211
|
+
if tool = @tools[tool_name]
|
212
|
+
tool
|
213
|
+
else
|
214
|
+
raise ArgumentError, "cound not find '#{tool_name}'"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Add the information stored in a YAML-like hash into this
|
219
|
+
# configuration
|
220
|
+
#
|
221
|
+
# @param [Hash] the configuration, following the documented
|
222
|
+
# configuration format (see {Conf})
|
223
|
+
# @return [void]
|
224
|
+
def load_from_yaml(yaml)
|
225
|
+
load_tools_from_yaml(yaml['tools'])
|
226
|
+
@period = Integer(yaml['period'])
|
227
|
+
@bandwidth_limit = if limit_yaml = yaml['bandwidth_limit']
|
228
|
+
Conf.parse_bandwidth_limit(limit_yaml)
|
229
|
+
end
|
230
|
+
|
231
|
+
yaml['targets'].each do |yaml_target|
|
232
|
+
type = yaml_target['type']
|
233
|
+
target_class = Conf.target_class_from_type(type)
|
234
|
+
target = target_class.new(yaml_target['name'])
|
235
|
+
target.setup_from_conf(self, yaml_target)
|
236
|
+
register_target(target)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# @api private
|
241
|
+
#
|
242
|
+
# Helper for {#load_from_yaml}
|
243
|
+
def load_tools_from_yaml(yaml)
|
244
|
+
TOOLS.each do |tool_name|
|
245
|
+
tool_path = Pathname.new(yaml[tool_name])
|
246
|
+
if tool_path.relative?
|
247
|
+
tool_path = find_in_path(tool_path)
|
248
|
+
end
|
249
|
+
if tool_path && tool_path.file?
|
250
|
+
@tools[tool_name] = tool_path
|
251
|
+
else
|
252
|
+
STDERR.puts "cannot find path to #{tool_name}"
|
253
|
+
@tools.delete(tool_name)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
# Interface to the functionality of querying and verifying host keys
|
4
|
+
class SSHKeys
|
5
|
+
class SSHFailed < RuntimeError; end
|
6
|
+
class NoLocalKey < RuntimeError; end
|
7
|
+
class ValidationFailed < RuntimeError; end
|
8
|
+
|
9
|
+
PublicKey = Struct.new :type, :hash do
|
10
|
+
def ==(other)
|
11
|
+
other.type == type &&
|
12
|
+
other.hash == hash
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Load a set of SSH keys from a file
|
17
|
+
#
|
18
|
+
# @return [Array<PublicKey>]
|
19
|
+
def self.load_keys_from_file(path)
|
20
|
+
load_keys_from_string(path.read)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load a set of SSH keys from a string
|
24
|
+
#
|
25
|
+
# @return [Array<PublicKey>]
|
26
|
+
def self.load_keys_from_string(string)
|
27
|
+
string.each_line.map do |line|
|
28
|
+
_, key_type, *rest = line.chomp.split(" ")
|
29
|
+
PublicKey.new(key_type, rest.join(" "))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Query the list of keys for this host
|
34
|
+
#
|
35
|
+
# @param [#host]
|
36
|
+
def query_keys(host)
|
37
|
+
self.class.load_keys_from_string(ssh_keyscan_host(host))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Query the keys from a host and returns them as a SSH key string
|
41
|
+
def ssh_keyscan_host(host)
|
42
|
+
ssh_key_run('ssh-keyscan', '-H', host)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Run a ssh subprocess and returns its standard output
|
46
|
+
#
|
47
|
+
# @raise [SSHFailed] if the subcommand fails
|
48
|
+
def ssh_key_run(*args)
|
49
|
+
out_pipe_r, out_pipe_w = IO.pipe
|
50
|
+
err_pipe_r, err_pipe_w = IO.pipe
|
51
|
+
pid = spawn *args, in: :close, err: err_pipe_w, out: out_pipe_w
|
52
|
+
out_pipe_w.close
|
53
|
+
err_pipe_w.close
|
54
|
+
_, status = Process.waitpid2 pid
|
55
|
+
|
56
|
+
out = out_pipe_r.read
|
57
|
+
err_pipe_r.readlines.each do |line|
|
58
|
+
if line !~ /^#/
|
59
|
+
STDERR.puts line
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if !status.success?
|
64
|
+
t raise SSHFailed, "failed to run #{args}"
|
65
|
+
end
|
66
|
+
out
|
67
|
+
ensure
|
68
|
+
out_pipe_r.close
|
69
|
+
err_pipe_r.close
|
70
|
+
end
|
71
|
+
|
72
|
+
def ssh_config_path
|
73
|
+
Pathname.new(Dir.home) + ".ssh" + "config"
|
74
|
+
end
|
75
|
+
|
76
|
+
def ssh_setup_config(target_name, username, hostname, key_file, ssh_config_path: self.ssh_config_path)
|
77
|
+
ssh_config = ssh_cleanup_config(ssh_config_path: ssh_config_path)
|
78
|
+
|
79
|
+
if ssh_config[-1] && ssh_config[-1] != ''
|
80
|
+
ssh_config << ""
|
81
|
+
end
|
82
|
+
ssh_config_name = "restic-service-host-#{target_name}"
|
83
|
+
ssh_config << "# Added by restic-service"
|
84
|
+
ssh_config << "Host #{ssh_config_name}"
|
85
|
+
ssh_config << " User #{username}"
|
86
|
+
ssh_config << " Hostname #{hostname}"
|
87
|
+
ssh_config << " UserKnownHostsFile #{key_file}"
|
88
|
+
|
89
|
+
ssh_config_path.dirname.mkpath
|
90
|
+
ssh_config_path.dirname.chmod 0700
|
91
|
+
ssh_config_path.open('w') do |io|
|
92
|
+
io.puts ssh_config.join("\n")
|
93
|
+
end
|
94
|
+
ssh_config_path.chmod 0600
|
95
|
+
ssh_config_name
|
96
|
+
end
|
97
|
+
|
98
|
+
def ssh_cleanup_config(ssh_config_path: self.ssh_config_path)
|
99
|
+
ssh_config =
|
100
|
+
if ssh_config_path.file?
|
101
|
+
ssh_config_path.read.split("\n").map(&:chomp)
|
102
|
+
else
|
103
|
+
[]
|
104
|
+
end
|
105
|
+
|
106
|
+
_, host_line = ssh_config.each_with_index.
|
107
|
+
find { |line, line_i| line.start_with?("Host restic-service-host-") }
|
108
|
+
if host_line
|
109
|
+
ssh_config.delete_at(host_line)
|
110
|
+
while ssh_config[host_line - 1] && ssh_config[host_line - 1].start_with?("#")
|
111
|
+
ssh_config.delete_at(host_line - 1)
|
112
|
+
host_line -= 1
|
113
|
+
end
|
114
|
+
|
115
|
+
while ssh_config[host_line] && !ssh_config[host_line].start_with?("Host")
|
116
|
+
ssh_config.delete_at(host_line)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
if ssh_config_path.file?
|
120
|
+
ssh_config_path.open('w') do |io|
|
121
|
+
io.puts ssh_config.join("\n")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
ssh_config
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
module B2
|
5
|
+
def available?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.normalize_yaml(yaml)
|
10
|
+
%w{bucket path id key}.each do |required_field|
|
11
|
+
if !yaml[required_field]
|
12
|
+
raise Conf::InvalidConfigurationFile, "missing '#{required_field}' field in target"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
yaml
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
|
21
|
+
@bucket = nil
|
22
|
+
@path = nil
|
23
|
+
@id = nil
|
24
|
+
@key = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup_from_conf(conf, yaml)
|
28
|
+
super
|
29
|
+
|
30
|
+
@bucket = yaml['bucket']
|
31
|
+
@path = yaml['path']
|
32
|
+
@id = yaml['id']
|
33
|
+
@key = yaml['key']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
class Base
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
|
10
|
+
@bandwidth_limit = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup_from_conf(conf, yaml)
|
14
|
+
@bandwidth_limit =
|
15
|
+
if limit = yaml.fetch('bandwidth_limit', conf.bandwidth_limit)
|
16
|
+
Conf.parse_bandwidth_limit(limit)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
class RcloneB2 < Base
|
5
|
+
include B2
|
6
|
+
|
7
|
+
def self.normalize_yaml(yaml)
|
8
|
+
yaml = B2.normalize_yaml(yaml)
|
9
|
+
if !yaml['src']
|
10
|
+
raise Conf::InvalidConfigurationFile, "no src field provided for rclone-b2"
|
11
|
+
elsif !File.directory?(yaml['src'])
|
12
|
+
raise Conf::InvalidConfigurationFile, "provided rclone-b2 source #{yaml['src']} does not exist"
|
13
|
+
end
|
14
|
+
yaml
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup_from_conf(conf, yaml)
|
18
|
+
super
|
19
|
+
@rclone_path = conf.tool_path('rclone')
|
20
|
+
@src = yaml['src']
|
21
|
+
@conf_path = conf.conf_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
extra_args = []
|
26
|
+
if @bandwidth_limit
|
27
|
+
extra_args << '--bwlimit' << @bandwidth_limit.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
Tempfile.create "rclone-#{@name}", @conf_path.to_path, perm: 0600 do |io|
|
31
|
+
io.puts <<-EOCONF
|
32
|
+
[restic-service]
|
33
|
+
type = b2
|
34
|
+
account = #{@id}
|
35
|
+
key = #{@key}
|
36
|
+
endpoint =
|
37
|
+
EOCONF
|
38
|
+
io.flush
|
39
|
+
system(@rclone_path.to_path,
|
40
|
+
'--transfers', '16',
|
41
|
+
'--config', io.path,
|
42
|
+
*extra_args,
|
43
|
+
'sync', @src, "restic-service:#{@bucket}/#{@path}", in: :close)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
# Base class for all restic-based targets
|
5
|
+
#
|
6
|
+
# See README.md for the YAML configuration file format
|
7
|
+
class Restic < Base
|
8
|
+
def initialize(name)
|
9
|
+
super
|
10
|
+
|
11
|
+
@password = nil
|
12
|
+
@includes = []
|
13
|
+
@excludes = []
|
14
|
+
@one_filesystem = false
|
15
|
+
|
16
|
+
@io_class = 3
|
17
|
+
@io_priority = 0
|
18
|
+
@cpu_priority = 19
|
19
|
+
end
|
20
|
+
|
21
|
+
def one_filesystem?
|
22
|
+
@one_filesystem
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.normalize_yaml(yaml)
|
26
|
+
yaml = Hash['includes' => [],
|
27
|
+
'excludes' => [],
|
28
|
+
'one_filesystem' => false,
|
29
|
+
'io_class' => 3,
|
30
|
+
'io_priority' => 0,
|
31
|
+
'cpu_priority' => 19].merge(yaml)
|
32
|
+
if yaml['includes'].empty?
|
33
|
+
raise Conf::InvalidConfigurationFile, "nothing to backup"
|
34
|
+
elsif !yaml['password']
|
35
|
+
raise Conf::InvalidConfigurationFile, "no password field"
|
36
|
+
end
|
37
|
+
yaml
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_from_conf(conf, yaml)
|
41
|
+
super
|
42
|
+
|
43
|
+
@restic_path = conf.tool_path('restic')
|
44
|
+
@password = yaml['password']
|
45
|
+
@includes = yaml['includes'] || Array.new
|
46
|
+
@excludes = yaml['excludes'] || Array.new
|
47
|
+
@one_filesystem = !!yaml['one_filesystem']
|
48
|
+
@io_class = Integer(yaml['io_class'])
|
49
|
+
@io_priority = Integer(yaml['io_priority'])
|
50
|
+
@cpu_priority = Integer(yaml['cpu_priority'])
|
51
|
+
end
|
52
|
+
|
53
|
+
def run(*args)
|
54
|
+
old_home = ENV['HOME']
|
55
|
+
ENV['HOME'] = old_home || '/root'
|
56
|
+
|
57
|
+
env = if args.first.kind_of?(Hash)
|
58
|
+
env = args.shift
|
59
|
+
else
|
60
|
+
env = Hash.new
|
61
|
+
end
|
62
|
+
|
63
|
+
ionice_args = []
|
64
|
+
if @io_class != 3
|
65
|
+
ionice_args << '-n' << @io_priority.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
extra_args = []
|
69
|
+
if one_filesystem?
|
70
|
+
extra_args << '--one-file-system'
|
71
|
+
end
|
72
|
+
if @bandwidth_limit
|
73
|
+
limit_KiB = @bandwidth_limit / 1000
|
74
|
+
extra_args << '--limit-download' << limit_KiB.to_s << '--limit-upload' << limit_KiB.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
system(Hash['RESTIC_PASSWORD' => @password].merge(env),
|
78
|
+
'ionice', '-c', @io_class.to_s, *ionice_args,
|
79
|
+
'nice', "-#{@cpu_priority}",
|
80
|
+
@restic_path.to_path, *args, *extra_args,
|
81
|
+
*@excludes.flat_map { |e| ['--exclude', e] },
|
82
|
+
*@includes, in: :close)
|
83
|
+
ensure
|
84
|
+
ENV['HOME'] = old_home
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
# A target that backs up to a SFTP target using Restic
|
5
|
+
#
|
6
|
+
# See README.md for the YAML configuration file format
|
7
|
+
class ResticB2 < Restic
|
8
|
+
include B2
|
9
|
+
|
10
|
+
def self.normalize_yaml(yaml)
|
11
|
+
yaml = B2.normalize_yaml(yaml)
|
12
|
+
super(yaml)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
super(Hash['B2_ACCOUNT_ID' => @id, 'B2_ACCOUNT_KEY' => @key], '-r', "b2:#{@bucket}:#{@path}", 'backup')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
class ResticFile < Restic
|
5
|
+
def available?
|
6
|
+
@dest.directory?
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.normalize_yaml(yaml)
|
10
|
+
if !yaml['dest']
|
11
|
+
raise ArgumentError, "'dest' field not set in rest-file target"
|
12
|
+
end
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_from_conf(conf, target_yaml)
|
17
|
+
super
|
18
|
+
@dest = Pathname.new(target_yaml['dest'])
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
super('-r', @dest.to_path, 'backup')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Restic
|
2
|
+
module Service
|
3
|
+
module Targets
|
4
|
+
# A target that backs up to a SFTP target using Restic
|
5
|
+
#
|
6
|
+
# See README.md for the YAML configuration file format
|
7
|
+
class ResticSFTP < Restic
|
8
|
+
def initialize(name)
|
9
|
+
super
|
10
|
+
@host = nil
|
11
|
+
@username = nil
|
12
|
+
@path = nil
|
13
|
+
@host_keys = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def available?
|
17
|
+
ssh = SSHKeys.new
|
18
|
+
actual_keys = ssh.query_keys(@host)
|
19
|
+
valid?(actual_keys)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.normalize_yaml(yaml)
|
23
|
+
%w{host username path password}.each do |required_field|
|
24
|
+
if !yaml[required_field]
|
25
|
+
raise Conf::InvalidConfigurationFile, "missing '#{required_field}' field in target"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_from_conf(conf, yaml)
|
32
|
+
@target_name = yaml['name']
|
33
|
+
@key_path = conf.conf_keys_path_for(self)
|
34
|
+
@host_keys = SSHKeys.load_keys_from_file(@key_path)
|
35
|
+
@host = yaml['host'].to_str
|
36
|
+
@username = yaml['username'].to_str
|
37
|
+
@path = yaml['path'].to_str
|
38
|
+
@password = yaml['password'].to_str
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?(actual_keys)
|
43
|
+
actual_keys.any? { |k| @host_keys.include?(k) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
current_home = ENV['HOME']
|
48
|
+
ENV['HOME'] = current_home || '/root'
|
49
|
+
|
50
|
+
ssh = SSHKeys.new
|
51
|
+
ssh_config_name = ssh.ssh_setup_config(@target_name, @username, @host, @key_path)
|
52
|
+
|
53
|
+
super('-r', "sftp:#{ssh_config_name}:#{@path}", 'backup')
|
54
|
+
ensure
|
55
|
+
ssh.ssh_cleanup_config
|
56
|
+
ENV['HOME'] = current_home
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "restic/service/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "restic-service"
|
8
|
+
spec.version = Restic::Service::VERSION
|
9
|
+
spec.authors = ["Sylvain Joyeux"]
|
10
|
+
spec.email = ["sylvain.joyeux@m4x.org"]
|
11
|
+
|
12
|
+
spec.summary = %q{Higher-level management on top of restic to use it as a peridiodic backup tool}
|
13
|
+
spec.homepage = "https://github.com/thirteenltda/restic-service"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "thor"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
28
|
+
spec.add_development_dependency "flexmock"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: restic-service
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sylvain Joyeux
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-12-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: flexmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- sylvain.joyeux@m4x.org
|
86
|
+
executables:
|
87
|
+
- restic-service
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- exe/restic-service
|
100
|
+
- install-dev.sh
|
101
|
+
- install.sh
|
102
|
+
- lib/restic/service.rb
|
103
|
+
- lib/restic/service/cli.rb
|
104
|
+
- lib/restic/service/conf.rb
|
105
|
+
- lib/restic/service/ssh_keys.rb
|
106
|
+
- lib/restic/service/targets/b2.rb
|
107
|
+
- lib/restic/service/targets/base.rb
|
108
|
+
- lib/restic/service/targets/rclone_b2.rb
|
109
|
+
- lib/restic/service/targets/restic.rb
|
110
|
+
- lib/restic/service/targets/restic_b2.rb
|
111
|
+
- lib/restic/service/targets/restic_file.rb
|
112
|
+
- lib/restic/service/targets/restic_sftp.rb
|
113
|
+
- lib/restic/service/version.rb
|
114
|
+
- restic-service.gemspec
|
115
|
+
- restic-service.service
|
116
|
+
homepage: https://github.com/thirteenltda/restic-service
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata: {}
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubyforge_project:
|
136
|
+
rubygems_version: 2.5.1
|
137
|
+
signing_key:
|
138
|
+
specification_version: 4
|
139
|
+
summary: Higher-level management on top of restic to use it as a peridiodic backup
|
140
|
+
tool
|
141
|
+
test_files: []
|