skypager 0.0.2
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 +20 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +21 -0
- data/bin/skypager +17 -0
- data/examples/.gitignore +4 -0
- data/examples/blog-site/.gitignore +18 -0
- data/examples/blog-site/.pryrc +4 -0
- data/examples/blog-site/Gemfile +8 -0
- data/examples/blog-site/config.rb +17 -0
- data/examples/blog-site/data/dropbox.json +1 -0
- data/examples/blog-site/source/images/background.png +0 -0
- data/examples/blog-site/source/images/middleman.png +0 -0
- data/examples/blog-site/source/index.html.erb +10 -0
- data/examples/blog-site/source/javascripts/all.js +1 -0
- data/examples/blog-site/source/layouts/layout.erb +19 -0
- data/examples/blog-site/source/posts/introduction-to-skypager.html.md +23 -0
- data/examples/blog-site/source/posts/skypager-and-dnsimple-and-amazon-web-services-combo.html.md +9 -0
- data/examples/blog-site/source/stylesheets/all.css +55 -0
- data/examples/blog-site/source/stylesheets/normalize.css +375 -0
- data/examples/gallery-site/.gitignore +18 -0
- data/examples/gallery-site/.pryrc +4 -0
- data/examples/gallery-site/Gemfile +11 -0
- data/examples/gallery-site/config.rb +38 -0
- data/examples/gallery-site/data/dropbox.json +1 -0
- data/examples/gallery-site/data/galleries.json +1 -0
- data/examples/gallery-site/source/gallery.html.erb +7 -0
- data/examples/gallery-site/source/images/background.png +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/001.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/002.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/003.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/004.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/001.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/002.JPG +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/003.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/004.JPG +0 -0
- data/examples/gallery-site/source/images/middleman.png +0 -0
- data/examples/gallery-site/source/index.html.erb +10 -0
- data/examples/gallery-site/source/javascripts/all.js +1 -0
- data/examples/gallery-site/source/layouts/layout.erb +20 -0
- data/examples/gallery-site/source/stylesheets/all.css +0 -0
- data/examples/gallery-site/source/stylesheets/normalize.css +375 -0
- data/examples/gallery-site/source/tutorial.md +151 -0
- data/lib/skypager.rb +92 -0
- data/lib/skypager/build_server.rb +17 -0
- data/lib/skypager/cli/commands/config.rb +58 -0
- data/lib/skypager/cli/commands/create.rb +98 -0
- data/lib/skypager/cli/commands/deploy.rb +30 -0
- data/lib/skypager/cli/commands/edit.rb +32 -0
- data/lib/skypager/cli/commands/list.rb +12 -0
- data/lib/skypager/cli/commands/setup.rb +124 -0
- data/lib/skypager/cli/commands/sync.rb +18 -0
- data/lib/skypager/configuration.rb +173 -0
- data/lib/skypager/data.rb +8 -0
- data/lib/skypager/data/excel_spreadsheet.rb +8 -0
- data/lib/skypager/data/google_spreadsheet.rb +225 -0
- data/lib/skypager/data/request.rb +12 -0
- data/lib/skypager/data/source.rb +171 -0
- data/lib/skypager/data/source_routes_proxy.rb +30 -0
- data/lib/skypager/dns.rb +65 -0
- data/lib/skypager/extension.rb +203 -0
- data/lib/skypager/middleman/commands/data.rb +0 -0
- data/lib/skypager/middleman/commands/deploy.rb +0 -0
- data/lib/skypager/middleman/commands/sync.rb +0 -0
- data/lib/skypager/site.rb +208 -0
- data/lib/skypager/sync.rb +23 -0
- data/lib/skypager/sync/amazon.rb +171 -0
- data/lib/skypager/sync/dropbox.rb +173 -0
- data/lib/skypager/sync/dropbox/delta.rb +67 -0
- data/lib/skypager/sync/folder.rb +235 -0
- data/lib/skypager/sync/google.rb +143 -0
- data/lib/skypager/tar.rb +77 -0
- data/lib/skypager/version.rb +3 -0
- data/skypager.gemspec +40 -0
- data/spec/lib/skypager/configuration_spec.rb +5 -0
- data/spec/lib/skypager/data_spec.rb +5 -0
- data/spec/lib/skypager/site_spec.rb +5 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/json_helper.rb +7 -0
- metadata +383 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Skypager
|
2
|
+
module Sync
|
3
|
+
class DropboxDelta
|
4
|
+
|
5
|
+
attr_accessor :client,
|
6
|
+
:data,
|
7
|
+
:cursor,
|
8
|
+
:entries,
|
9
|
+
:path_prefix
|
10
|
+
|
11
|
+
def initialize(client, cursor, path_prefix=nil)
|
12
|
+
@client = client
|
13
|
+
@cursor = cursor
|
14
|
+
@path_prefix = path_prefix
|
15
|
+
end
|
16
|
+
|
17
|
+
def processed!
|
18
|
+
# TODO
|
19
|
+
# Should update cursor
|
20
|
+
end
|
21
|
+
|
22
|
+
def entries
|
23
|
+
return @entries if @entries
|
24
|
+
fetch
|
25
|
+
@entries
|
26
|
+
end
|
27
|
+
|
28
|
+
def _dropbox_delta at=nil
|
29
|
+
at ||= cursor
|
30
|
+
response = client.delta(at, path_prefix)
|
31
|
+
self.cursor = response["cursor"]
|
32
|
+
response
|
33
|
+
end
|
34
|
+
|
35
|
+
def data
|
36
|
+
@data ||= fetch
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_reset path_prefix, cursor
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch
|
43
|
+
return @response if @response
|
44
|
+
|
45
|
+
response = _dropbox_delta
|
46
|
+
|
47
|
+
if response["reset"] == true
|
48
|
+
on_reset(path_prefix, cursor)
|
49
|
+
end
|
50
|
+
|
51
|
+
self.entries = Hashie::Mash.new({})
|
52
|
+
|
53
|
+
response["entries"].each do |entry|
|
54
|
+
path, meta = entry
|
55
|
+
self.entries[path] = meta
|
56
|
+
end
|
57
|
+
|
58
|
+
if response["has_more"] == true
|
59
|
+
# TODO Implement
|
60
|
+
end
|
61
|
+
|
62
|
+
@response = response
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module Skypager
|
2
|
+
module Sync
|
3
|
+
class Folder
|
4
|
+
attr_accessor :options,
|
5
|
+
:cursor,
|
6
|
+
:app
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@options = options
|
10
|
+
@cursor = options.fetch(:cursor, nil)
|
11
|
+
@app = options[:app]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Used to generate the config line in the
|
15
|
+
# middleman config.rb
|
16
|
+
def config_line
|
17
|
+
"dropbox_sync('#{ relative_local_path }','#{ remote_path }')"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Gets the reference to the dropbox folder settings for the app
|
21
|
+
# which allows us to persist info about the sync state of the folder
|
22
|
+
def folder_settings
|
23
|
+
data = app.data.dropbox
|
24
|
+
data[:folders][options[:folder]].tap {|h| h && h.symbolize_keys! }
|
25
|
+
end
|
26
|
+
|
27
|
+
def cursor= value
|
28
|
+
folder_settings[:cursor] = value
|
29
|
+
save_folder_settings
|
30
|
+
end
|
31
|
+
|
32
|
+
def save_folder_settings
|
33
|
+
data = app.data.dropbox.dup
|
34
|
+
|
35
|
+
Pathname(app.data_dir).join("dropbox.json").open('w+') do |fh|
|
36
|
+
fh.write(data.to_json)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def auto_prefix
|
41
|
+
options[:site_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def dropbox?
|
45
|
+
options[:type] == "dropbox"
|
46
|
+
end
|
47
|
+
|
48
|
+
def google?
|
49
|
+
options[:type] == "google"
|
50
|
+
end
|
51
|
+
|
52
|
+
def sync
|
53
|
+
return dropbox_pull if dropbox?
|
54
|
+
return google_sync if google?
|
55
|
+
end
|
56
|
+
|
57
|
+
def dropbox_pull
|
58
|
+
delta = dropbox_delta
|
59
|
+
|
60
|
+
delta.entries.each do |entry|
|
61
|
+
local = matching_local_path_for(entry.path)
|
62
|
+
|
63
|
+
if entry.is_deleted
|
64
|
+
puts "Removing #{ local }"
|
65
|
+
local.rmtree
|
66
|
+
elsif entry.is_dir == true
|
67
|
+
unless local.exist?
|
68
|
+
puts "Creating #{ local } directory"
|
69
|
+
local.mkdir
|
70
|
+
end
|
71
|
+
elsif entry.is_dir == false || entry.is_dir == nil
|
72
|
+
puts "Downloading #{ entry.size } bytes to #{ local }"
|
73
|
+
local.open('wb+') do |fh|
|
74
|
+
fh.write(entry.download)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
self.cursor = delta.cursor
|
80
|
+
end
|
81
|
+
|
82
|
+
def google_sync
|
83
|
+
# TODO
|
84
|
+
# Implement google drive folder cloning
|
85
|
+
end
|
86
|
+
|
87
|
+
def newer_local_files
|
88
|
+
remotes = all_remote_files
|
89
|
+
|
90
|
+
if dropbox?
|
91
|
+
all_local_files.select do |e|
|
92
|
+
remote = remotes.find do |x|
|
93
|
+
"#{x[:path]}".match(/#{ e[:path] }$/)
|
94
|
+
end
|
95
|
+
|
96
|
+
remote.nil? || (remote[:mtime] < e[:mtime])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def all_dropbox_files
|
102
|
+
h = lambda do |node|
|
103
|
+
base = [{
|
104
|
+
path: node.path,
|
105
|
+
mtime: DateTime.parse(node.modified).to_i
|
106
|
+
}]
|
107
|
+
|
108
|
+
base += node.ls.map(&h) if node.is_dir
|
109
|
+
|
110
|
+
base
|
111
|
+
end
|
112
|
+
|
113
|
+
dropbox_folder_files.map(&h).flatten
|
114
|
+
end
|
115
|
+
|
116
|
+
def dropbox_folder_files
|
117
|
+
Array((dropbox_folder rescue nil).try(:ls))
|
118
|
+
end
|
119
|
+
|
120
|
+
def matching_local_path_for(remote)
|
121
|
+
if sandboxed?
|
122
|
+
parts = remote.to_s.split("/")
|
123
|
+
parts.shift
|
124
|
+
part = parts.join("/")
|
125
|
+
root.join(part)
|
126
|
+
else
|
127
|
+
parts = remote.to_s.split("/")
|
128
|
+
parts.shift
|
129
|
+
part = parts.join("/")
|
130
|
+
local_path.join(part)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def root
|
135
|
+
Pathname(options[:root] || Dir.pwd)
|
136
|
+
end
|
137
|
+
|
138
|
+
def all_local_files
|
139
|
+
h = lambda do |path|
|
140
|
+
base = [{
|
141
|
+
path: path.relative_path_from(local_path).to_s,
|
142
|
+
realpath: path,
|
143
|
+
mtime: path.mtime.to_i
|
144
|
+
}]
|
145
|
+
|
146
|
+
base += path.children.map(&h) if path.directory?
|
147
|
+
|
148
|
+
base
|
149
|
+
end
|
150
|
+
|
151
|
+
local_path.children.map(&h).flatten
|
152
|
+
end
|
153
|
+
|
154
|
+
def all_remote_files
|
155
|
+
if dropbox?
|
156
|
+
all_dropbox_files
|
157
|
+
else
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def relative_local_path
|
163
|
+
local_path.relative_path_from(root).to_s
|
164
|
+
end
|
165
|
+
|
166
|
+
def local_path
|
167
|
+
root.join(options[:local_path]).tap do |folder|
|
168
|
+
folder.mkdir unless folder.exist?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def synced
|
173
|
+
push_sync
|
174
|
+
self
|
175
|
+
end
|
176
|
+
|
177
|
+
def push_sync
|
178
|
+
return dropbox_push_sync if dropbox?
|
179
|
+
return google_push_sync if google?
|
180
|
+
end
|
181
|
+
|
182
|
+
def dropbox_push_sync
|
183
|
+
api = Skypager.dropbox.api
|
184
|
+
|
185
|
+
newer_local_files.each do |entry|
|
186
|
+
target = "#{ path_prefix }/#{ entry[:path] }"
|
187
|
+
target = target.gsub(/^\//,'')
|
188
|
+
api.upload(target, entry[:realpath].read, overwrite: true)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def google_push_sync
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
def dropbox_folder
|
197
|
+
Skypager.dropbox.find(path_prefix)
|
198
|
+
end
|
199
|
+
|
200
|
+
def remote_path
|
201
|
+
options[:remote_path].to_s
|
202
|
+
end
|
203
|
+
|
204
|
+
def sandboxed?
|
205
|
+
Skypager.config.dropbox_app_type == "sandbox"
|
206
|
+
end
|
207
|
+
|
208
|
+
# Which path do we scope our delta calls to?
|
209
|
+
def path_prefix
|
210
|
+
if sandboxed?
|
211
|
+
if remote_path.match(/^\/#{ options[:site_name] }/)
|
212
|
+
remote_path
|
213
|
+
else
|
214
|
+
"/#{ options[:site_name] }/#{ remote_path.gsub(/^\//,'') }"
|
215
|
+
end
|
216
|
+
else
|
217
|
+
unless remote_path.match(/^\//)
|
218
|
+
return "/#{ remote_path }"
|
219
|
+
end
|
220
|
+
|
221
|
+
remote_path
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def has_remote_changes?
|
226
|
+
dropbox_delta.entries.length > 0
|
227
|
+
end
|
228
|
+
|
229
|
+
def dropbox_delta
|
230
|
+
@dropbox_delta ||= Skypager.dropbox.delta(cursor, path_prefix: path_prefix)
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Skypager
|
2
|
+
module Sync
|
3
|
+
class Google
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def self.method_missing(meth, *args, &block)
|
7
|
+
if client.respond_to?(meth)
|
8
|
+
return client.send(meth, *args, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.client(options={})
|
15
|
+
@client ||= begin
|
16
|
+
instance.with_options(options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def refreshable?
|
21
|
+
has_application_keys? && has_refresh_token?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Runs through an interactive session where we get the
|
25
|
+
# necessary tokens needed to integrate with google drive.
|
26
|
+
def setup(options={})
|
27
|
+
get_application_keys unless has_application_keys?
|
28
|
+
|
29
|
+
if options[:client_id]
|
30
|
+
Skypager.config.set "google_client_id", options[:client_id]
|
31
|
+
end
|
32
|
+
|
33
|
+
if options[:client_secret]
|
34
|
+
Skypager.config.set "google_client_secret", options[:client_secret]
|
35
|
+
end
|
36
|
+
|
37
|
+
if has_refresh_token?
|
38
|
+
refresh_access_token!
|
39
|
+
else
|
40
|
+
Launchy.open(auth_client.authorization_uri)
|
41
|
+
say("\n1. Open this page:\n%s\n\n" % auth_client.authorization_uri)
|
42
|
+
auth_client.code = ask("2. Enter the authorization code shown in the page: ", String)
|
43
|
+
auth_client.fetch_access_token!
|
44
|
+
Skypager.config.set "google_refresh_token", auth_client.refresh_token
|
45
|
+
Skypager.config.set "google_access_token", auth_client.access_token
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def session
|
50
|
+
api
|
51
|
+
end
|
52
|
+
|
53
|
+
def api
|
54
|
+
@api ||= begin
|
55
|
+
refresh_access_token!
|
56
|
+
GoogleDrive.login_with_oauth(Skypager.config.google_access_token)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing meth, *args, &block
|
61
|
+
if api.respond_to?(meth)
|
62
|
+
return api.send(meth, *args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
def authorize(token, secret)
|
69
|
+
@api = nil if @api
|
70
|
+
options[:token] = token
|
71
|
+
options[:secret] = secret
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def options
|
76
|
+
@options ||= {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_options(opts={})
|
80
|
+
options.merge!(opts)
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_application_keys?
|
85
|
+
(Skypager.config.google_client_id.to_s.length > 0 && Skypager.config.google_client_secret.to_s.length > 0)
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_application_keys
|
89
|
+
unless Skypager.config.google_client_id.to_s.length > 0
|
90
|
+
google_client_id = ask("What is the Google Client ID?", String)
|
91
|
+
Skypager.config.set "google_client_id", google_client_id
|
92
|
+
end
|
93
|
+
|
94
|
+
unless Skypager.config.google_client_secret.to_s.length > 0
|
95
|
+
google_client_secret = ask("What is the Google Client Secret?", String)
|
96
|
+
Skypager.config.set "google_client_secret", google_client_secret
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def auth_client
|
101
|
+
return @auth_client if @auth_client
|
102
|
+
|
103
|
+
client = ::Google::APIClient.new(
|
104
|
+
:application_name => "google_drive Ruby library",
|
105
|
+
:application_version => "0.3.11"
|
106
|
+
)
|
107
|
+
|
108
|
+
client_id = "452925651630-egr1f18o96acjjvphpbbd1qlsevkho1d.apps.googleusercontent.com"
|
109
|
+
client_secret = "1U3-Krii5x1oLPrwD5zgn-ry"
|
110
|
+
|
111
|
+
@auth_client = auth = client.authorization
|
112
|
+
auth.client_id = client_id #Skypager.config.google_client_id
|
113
|
+
auth.client_secret = client_secret #Skypager.config.google_client_secret
|
114
|
+
auth.scope =
|
115
|
+
"https://www.googleapis.com/auth/drive " +
|
116
|
+
"https://spreadsheets.google.com/feeds/ " +
|
117
|
+
"https://docs.google.com/feeds/ " +
|
118
|
+
"https://docs.googleusercontent.com/"
|
119
|
+
|
120
|
+
auth.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
|
121
|
+
|
122
|
+
auth
|
123
|
+
end
|
124
|
+
|
125
|
+
def refresh_token
|
126
|
+
Skypager.config.google_refresh_token.to_s
|
127
|
+
end
|
128
|
+
|
129
|
+
def has_refresh_token?
|
130
|
+
refresh_token.length > 0
|
131
|
+
end
|
132
|
+
|
133
|
+
def refresh_access_token!
|
134
|
+
if has_refresh_token?
|
135
|
+
auth_client.refresh_token = refresh_token
|
136
|
+
auth_client.fetch_access_token!
|
137
|
+
Skypager.config.set "google_access_token", auth_client.access_token
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/skypager/tar.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/package'
|
3
|
+
require 'zlib'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Util
|
7
|
+
module Tar
|
8
|
+
# Creates a tar file in memory recursively
|
9
|
+
# from the given path.
|
10
|
+
#
|
11
|
+
# Returns a StringIO whose underlying String
|
12
|
+
# is the contents of the tar file.
|
13
|
+
def tar(path)
|
14
|
+
tarfile = StringIO.new("")
|
15
|
+
Gem::Package::TarWriter.new(tarfile) do |tar|
|
16
|
+
Dir[File.join(path, "**/*")].each do |file|
|
17
|
+
mode = File.stat(file).mode
|
18
|
+
relative_file = file.sub /^#{Regexp::escape path}\/?/, ''
|
19
|
+
|
20
|
+
if File.directory?(file)
|
21
|
+
tar.mkdir relative_file, mode
|
22
|
+
else
|
23
|
+
tar.add_file relative_file, mode do |tf|
|
24
|
+
File.open(file, "rb") { |f| tf.write f.read }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
tarfile.rewind
|
31
|
+
tarfile
|
32
|
+
end
|
33
|
+
|
34
|
+
# gzips the underlying string in the given StringIO,
|
35
|
+
# returning a new StringIO representing the
|
36
|
+
# compressed file.
|
37
|
+
def gzip(tarfile)
|
38
|
+
gz = StringIO.new("")
|
39
|
+
z = Zlib::GzipWriter.new(gz)
|
40
|
+
z.write tarfile.string
|
41
|
+
z.close # this is necessary!
|
42
|
+
|
43
|
+
# z was closed to write the gzip footer, so
|
44
|
+
# now we need a new StringIO
|
45
|
+
StringIO.new gz.string
|
46
|
+
end
|
47
|
+
|
48
|
+
# un-gzips the given IO, returning the
|
49
|
+
# decompressed version as a StringIO
|
50
|
+
def ungzip(tarfile)
|
51
|
+
z = Zlib::GzipReader.new(tarfile)
|
52
|
+
unzipped = StringIO.new(z.read)
|
53
|
+
z.close
|
54
|
+
unzipped
|
55
|
+
end
|
56
|
+
|
57
|
+
# untars the given IO into the specified
|
58
|
+
# directory
|
59
|
+
def untar(io, destination)
|
60
|
+
Gem::Package::TarReader.new io do |tar|
|
61
|
+
tar.each do |tarfile|
|
62
|
+
destination_file = File.join destination, tarfile.full_name
|
63
|
+
|
64
|
+
if tarfile.directory?
|
65
|
+
FileUtils.mkdir_p destination_file
|
66
|
+
else
|
67
|
+
destination_directory = File.dirname(destination_file)
|
68
|
+
FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
|
69
|
+
File.open destination_file, "wb" do |f|
|
70
|
+
f.print tarfile.read
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|