skypager 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|