restic-service 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|