ghost-backup 0.0.1
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 +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:
|