ghost-backup 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +40 -0
- data/LICENSE +28 -0
- data/README.md +69 -0
- data/bin/ghost-backup +224 -0
- data/ghost-backup.gemspec +19 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc4061b469cecc705d4778038378fb28d280d4dd
|
4
|
+
data.tar.gz: f6bf6f78cf4a3f84d0e37993951d937df7eef8c5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 70f52837c26735267a5618fe138628cf102dd070c5aa349c5dfd8a4d599af28f4eb8be59055163fd9bc39f446c3c590c2847b4e3236b026c652589317144c198
|
7
|
+
data.tar.gz: 3a5fe36c990bfba123e7d322d1bd8f189fb3cb009a8e3afaaea30c1a6368c39ce6ae38f82edb3f58bdbdb50eabefcd9ea137c3e48f5f688990ef2621689c216f
|
data/.gitignore
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/vendor/bundle
|
26
|
+
/lib/bundler/man/
|
27
|
+
|
28
|
+
# for a library or gem, you might want to ignore these files since the code is
|
29
|
+
# intended to run in multiple environments; otherwise, check them in:
|
30
|
+
# Gemfile.lock
|
31
|
+
# .ruby-version
|
32
|
+
# .ruby-gemset
|
33
|
+
|
34
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
35
|
+
.rvmrc
|
36
|
+
|
37
|
+
Gemfile.lock
|
38
|
+
*.yml
|
39
|
+
*.json
|
40
|
+
contents
|
data/LICENSE
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
Copyright (c) 2015, Min RK
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
* Neither the name of ghost-backup nor the names of its
|
15
|
+
contributors may be used to endorse or promote products derived from
|
16
|
+
this software without specific prior written permission.
|
17
|
+
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# ghost-backup
|
2
|
+
|
3
|
+
Backup [ghost](https://ghost.org/) blog data.
|
4
|
+
|
5
|
+
This does two things:
|
6
|
+
|
7
|
+
1. Download the JSON export of the ghost post database
|
8
|
+
2. Make a copy of the ghost content directory (image assets, etc.)
|
9
|
+
|
10
|
+
Part 2 is only run
|
11
|
+
|
12
|
+
## Install
|
13
|
+
|
14
|
+
gem install ghost-backup
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
### Config
|
19
|
+
|
20
|
+
The first step is to generate `ghost-backup.yml`:
|
21
|
+
|
22
|
+
$> ghost-backup config
|
23
|
+
Writing config to ghost-backup.yml
|
24
|
+
|
25
|
+
The edit this file:
|
26
|
+
|
27
|
+
```yaml
|
28
|
+
---
|
29
|
+
username:
|
30
|
+
backup_dir: ghost-backup/%Y-%m-%d-%H%M%S
|
31
|
+
ghost_dir: /var/www/ghost
|
32
|
+
refresh_token:
|
33
|
+
base_url: https://localhost
|
34
|
+
```
|
35
|
+
|
36
|
+
- **username:** the username you use to login to the ghost server (optional)
|
37
|
+
- **backup_dir:** where backups should go. This passed to `strftime` at expor time
|
38
|
+
- **ghost_dir:** where on the local filesystem ghost is installed. If this location does not exist,
|
39
|
+
local assets will not be backed up, only the post database.
|
40
|
+
- **refresh_token:** ignore; this will be populated by the program when you login.
|
41
|
+
- **base_url:** the base URL of your ghost instance, e.g. `https://myblog.plumbing`
|
42
|
+
|
43
|
+
### Login
|
44
|
+
|
45
|
+
Once you have your `ghost-backup.yml`, you can login to ghost:
|
46
|
+
|
47
|
+
$> ghost-backup login
|
48
|
+
Logging in to https://my.blog/ghost/api/v0.1/authentication/token
|
49
|
+
Need username, password to generate new token (only used once, not stored).
|
50
|
+
Username: |you@email.com|
|
51
|
+
Password (not stored): *****
|
52
|
+
Writing config to ghost-backup.yml
|
53
|
+
|
54
|
+
Ghost tokens expire after a week, so to keep them alive you have to login at least once a week:
|
55
|
+
|
56
|
+
$> ghost-backup login
|
57
|
+
Logging in to https://my.blog/ghost/api/v0.1/authentication/token
|
58
|
+
|
59
|
+
Unless your token expires, logins after the first will not require a username or password.
|
60
|
+
|
61
|
+
### Backup
|
62
|
+
|
63
|
+
Now you can run a backup:
|
64
|
+
|
65
|
+
$> ghost-backup
|
66
|
+
Backing up 'https://my.blog' to '/backup/ghost-backup-2015-09-15-1306'
|
67
|
+
Copying /var/www/ghost/content → /backup/ghost-backup-2015-09-15-1306/content
|
68
|
+
Downloading ghost database https://blog.jupyter.org/ghost/api/v0.1/db/
|
69
|
+
Backing up database to '/backup/ghost-backup-2015-09-15-1306/ghost-db.json'
|
data/bin/ghost-backup
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) Min RK.
|
4
|
+
# Distributed under the terms of the Modified BSD License.
|
5
|
+
|
6
|
+
require 'date'
|
7
|
+
require 'json'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
# gems
|
11
|
+
require 'rest-client'
|
12
|
+
require 'commander/import'
|
13
|
+
require 'highline/import'
|
14
|
+
|
15
|
+
program :version, '0.0.1'
|
16
|
+
program :description, 'Backup ghost blog data'
|
17
|
+
|
18
|
+
class GhostBackup
|
19
|
+
CONFIG_KEYS = [
|
20
|
+
'username',
|
21
|
+
'backup_dir',
|
22
|
+
'ghost_dir',
|
23
|
+
'refresh_token',
|
24
|
+
'base_url',
|
25
|
+
]
|
26
|
+
|
27
|
+
API_VERSION = 'v0.1'
|
28
|
+
|
29
|
+
def initialize(config_file: "ghost-backup.yml")
|
30
|
+
@dirty = false
|
31
|
+
if config_file.nil?
|
32
|
+
@config_file = "ghost-backup.yml"
|
33
|
+
else
|
34
|
+
@config_file = config_file
|
35
|
+
end
|
36
|
+
@username = nil
|
37
|
+
# backup_dir is passed to strftime
|
38
|
+
@backup_dir = 'ghost-backup/%Y-%m-%d-%H%M%S'
|
39
|
+
@ghost_dir = "/var/www/ghost"
|
40
|
+
@base_url = 'https://localhost'
|
41
|
+
@refresh_token = nil
|
42
|
+
load_config
|
43
|
+
unless @base_url.include? '://'
|
44
|
+
@base_url = 'https://' + @base_url
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_accessor :config_file
|
49
|
+
|
50
|
+
# gen config as a hash
|
51
|
+
def cfg
|
52
|
+
Hash[
|
53
|
+
CONFIG_KEYS.map { |key|
|
54
|
+
[key, self.instance_variable_get("@#{key}")]
|
55
|
+
}
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
# load yaml config file
|
60
|
+
def load_config
|
61
|
+
if not File.exists? @config_file
|
62
|
+
@dirty = true
|
63
|
+
return
|
64
|
+
end
|
65
|
+
d = YAML.load_file @config_file
|
66
|
+
if not d
|
67
|
+
return
|
68
|
+
end
|
69
|
+
if not d.instance_of? Hash
|
70
|
+
throw Error("Bad config: #{config_file}")
|
71
|
+
end
|
72
|
+
d.each_pair do |key, value|
|
73
|
+
self.instance_variable_set("@#{key}", value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# save yaml config file, if there have been changes
|
78
|
+
def save_config
|
79
|
+
return unless @dirty
|
80
|
+
puts "Writing config to #{@config_file}"
|
81
|
+
File.open(@config_file, 'w') do |f|
|
82
|
+
f.write(YAML.dump(cfg))
|
83
|
+
end
|
84
|
+
@dirty = false
|
85
|
+
end
|
86
|
+
|
87
|
+
# the base API url
|
88
|
+
def api_url
|
89
|
+
"#{@base_url}/ghost/api/#{API_VERSION}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# the url for getting a new token
|
93
|
+
def token_url
|
94
|
+
"#{api_url}/authentication/token"
|
95
|
+
end
|
96
|
+
|
97
|
+
# the url for downloading the database as JSON
|
98
|
+
def db_url
|
99
|
+
"#{api_url}/db/"
|
100
|
+
end
|
101
|
+
|
102
|
+
# generate a new token with username/password via API request
|
103
|
+
def new_token(username, password)
|
104
|
+
reply = JSON.parse RestClient.post token_url, {
|
105
|
+
:grant_type => 'password',
|
106
|
+
:username => username,
|
107
|
+
:password => password,
|
108
|
+
:client_id => 'ghost-admin',
|
109
|
+
}
|
110
|
+
@refresh_token = reply['refresh_token']
|
111
|
+
@access_token = reply['access_token']
|
112
|
+
@dirty = true
|
113
|
+
end
|
114
|
+
|
115
|
+
# refresh authorization token
|
116
|
+
def refresh
|
117
|
+
reply = JSON.parse RestClient.post token_url, {
|
118
|
+
:grant_type => 'refresh_token',
|
119
|
+
:refresh_token => @refresh_token,
|
120
|
+
:client_id => 'ghost-admin',
|
121
|
+
}
|
122
|
+
@access_token = reply['access_token']
|
123
|
+
end
|
124
|
+
|
125
|
+
# ensure we are logged in
|
126
|
+
# refreshes token if we are,
|
127
|
+
# prompts for user/password if we are not
|
128
|
+
def authorize
|
129
|
+
if @refresh_token
|
130
|
+
begin
|
131
|
+
refresh
|
132
|
+
rescue RestClient::Exception
|
133
|
+
STDERR.puts "refresh token expired"
|
134
|
+
@refresh_token = nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
if @refresh_token.nil?
|
139
|
+
puts "Need username, password to generate new token (only used once, not stored)."
|
140
|
+
username = @username = ask("Username: ") do |q|
|
141
|
+
q.default = @username
|
142
|
+
q.validate = /\S+/
|
143
|
+
end
|
144
|
+
password = ask("Password (not stored): ") { |q| q.echo = '*' }
|
145
|
+
new_token(username, password)
|
146
|
+
puts "Login success!"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# backup methods
|
151
|
+
|
152
|
+
# download the JSON dump of the database
|
153
|
+
def download_db
|
154
|
+
puts "Downloading ghost database #{db_url}"
|
155
|
+
if @access_token.nil?
|
156
|
+
authorize
|
157
|
+
end
|
158
|
+
RestClient.get db_url, {:params => {:access_token => @access_token}}
|
159
|
+
end
|
160
|
+
|
161
|
+
# run a real backup
|
162
|
+
def backup
|
163
|
+
now = DateTime.now
|
164
|
+
dest_root = now.strftime(@backup_dir)
|
165
|
+
puts "Backing up '#{@base_url}' to '#{dest_root}'"
|
166
|
+
FileUtils.mkdir_p(dest_root)
|
167
|
+
if File.exists? @ghost_dir
|
168
|
+
from = File.join(@ghost_dir, 'content')
|
169
|
+
to = File.join(dest_root, 'content')
|
170
|
+
puts "Copying #{from} → #{to}"
|
171
|
+
FileUtils.cp_r(from, to)
|
172
|
+
else
|
173
|
+
STDERR.puts "Ghost directory '#{@ghost_dir}' doesn't exist, not backing up assets."
|
174
|
+
end
|
175
|
+
db_dest = File.join(dest_root, "ghost-db.json")
|
176
|
+
db = download_db
|
177
|
+
puts "Backing up database to '#{db_dest}'"
|
178
|
+
File.open(db_dest, 'w') { |f| f.write(db) }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
global_option('-f', '--config-file CONFIG_FILE', 'Specify config file to use')
|
183
|
+
|
184
|
+
|
185
|
+
command :login do |c|
|
186
|
+
c.syntax = 'ghost-backup login'
|
187
|
+
c.summary = 'Login to the ghost server'
|
188
|
+
c.description = 'Login to the ghost server and generate an auth token.
|
189
|
+
On first run, requires a username and password,
|
190
|
+
and records the refresh token in the config file.
|
191
|
+
Must be run at least once a week to keep the token active
|
192
|
+
and avoid re-entering username and password.'
|
193
|
+
c.action do |args, options|
|
194
|
+
gb = GhostBackup.new(:config_file => options.config_file)
|
195
|
+
puts "Logging in to #{gb.token_url}"
|
196
|
+
gb.authorize
|
197
|
+
gb.save_config
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
command :backup do |c|
|
203
|
+
c.syntax = 'ghost-backup backup'
|
204
|
+
c.summary = 'backup a ghost blog instance'
|
205
|
+
c.description = 'Downloads the post database and, if available, copies the local assets directory.'
|
206
|
+
c.action do |args, options|
|
207
|
+
gb = GhostBackup.new(:config_file => options.config_file)
|
208
|
+
gb.backup
|
209
|
+
gb.save_config
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
command :config do |c|
|
215
|
+
c.syntax = 'ghost-backup config'
|
216
|
+
c.summary = 'Generate default ghost-backup.yml'
|
217
|
+
c.description = 'Generate default ghost-backup.yml. Edit this file to configure the backup.'
|
218
|
+
c.action do |args, options|
|
219
|
+
gb = GhostBackup.new(:config_file => options.config_file)
|
220
|
+
gb.save_config
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
default_command :backup
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'ghost-backup'
|
5
|
+
s.version = '0.0.1'
|
6
|
+
s.date = Date.today.to_s
|
7
|
+
s.summary = 'Backup ghost blog data'
|
8
|
+
s.description = 'Dump ghost database and assets to local disk'
|
9
|
+
s.authors = ['Min RK']
|
10
|
+
s.email = 'benjaminrk@gmail.com'
|
11
|
+
s.homepage = 'http://github.com/minrk/ghost-backup'
|
12
|
+
s.license = 'BSD'
|
13
|
+
s.executables = ['ghost-backup']
|
14
|
+
s.files = `git ls-files`.split($/)
|
15
|
+
s.require_paths = ['.']
|
16
|
+
s.add_runtime_dependency 'commander', '~> 4.3'
|
17
|
+
s.add_runtime_dependency 'highline', '~> 1.7'
|
18
|
+
s.add_runtime_dependency 'rest-client', '~> 1.8'
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ghost-backup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Min RK
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: commander
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: highline
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rest-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
description: Dump ghost database and assets to local disk
|
56
|
+
email: benjaminrk@gmail.com
|
57
|
+
executables:
|
58
|
+
- ghost-backup
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- bin/ghost-backup
|
66
|
+
- ghost-backup.gemspec
|
67
|
+
homepage: http://github.com/minrk/ghost-backup
|
68
|
+
licenses:
|
69
|
+
- BSD
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- "."
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 2.4.5.1
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: Backup ghost blog data
|
91
|
+
test_files: []
|
92
|
+
has_rdoc:
|