aspera-cli 4.2.1 → 4.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1580 -946
- data/bin/ascli +1 -1
- data/bin/asession +3 -5
- data/docs/Makefile +8 -11
- data/docs/README.erb.md +1521 -829
- data/docs/doc_tools.rb +58 -0
- data/docs/test_env.conf +3 -1
- data/examples/faspex4.rb +28 -19
- data/examples/transfer.rb +2 -2
- data/lib/aspera/aoc.rb +157 -134
- data/lib/aspera/cli/listener/progress_multi.rb +5 -5
- data/lib/aspera/cli/main.rb +106 -48
- data/lib/aspera/cli/manager.rb +19 -20
- data/lib/aspera/cli/plugin.rb +22 -7
- data/lib/aspera/cli/plugins/aoc.rb +260 -208
- data/lib/aspera/cli/plugins/ats.rb +11 -10
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +360 -189
- data/lib/aspera/cli/plugins/faspex.rb +119 -56
- data/lib/aspera/cli/plugins/faspex5.rb +32 -17
- data/lib/aspera/cli/plugins/node.rb +72 -31
- data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
- data/lib/aspera/cli/plugins/preview.rb +94 -68
- data/lib/aspera/cli/plugins/server.rb +16 -5
- data/lib/aspera/cli/plugins/shares.rb +17 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -82
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +48 -31
- data/lib/aspera/cos_node.rb +4 -3
- data/lib/aspera/environment.rb +4 -4
- data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +42 -38
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +50 -29
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +43 -4
- data/lib/aspera/fasp/agent_trsdk.rb +106 -0
- data/lib/aspera/fasp/default.rb +17 -0
- data/lib/aspera/fasp/installation.rb +64 -48
- data/lib/aspera/fasp/parameters.rb +78 -91
- data/lib/aspera/fasp/parameters.yaml +531 -0
- data/lib/aspera/fasp/uri.rb +1 -1
- data/lib/aspera/faspex_gw.rb +12 -11
- data/lib/aspera/id_generator.rb +22 -0
- data/lib/aspera/keychain/encrypted_hash.rb +120 -0
- data/lib/aspera/keychain/macos_security.rb +94 -0
- data/lib/aspera/log.rb +45 -32
- data/lib/aspera/node.rb +9 -4
- data/lib/aspera/oauth.rb +116 -100
- data/lib/aspera/persistency_action_once.rb +11 -7
- data/lib/aspera/persistency_folder.rb +6 -26
- data/lib/aspera/rest.rb +66 -50
- data/lib/aspera/sync.rb +40 -35
- data/lib/aspera/timer_limiter.rb +22 -0
- metadata +86 -29
- data/docs/transfer_spec.html +0 -99
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/fasp/aoc.rb +0 -24
- data/lib/aspera/secrets.rb +0 -20
data/lib/aspera/oauth.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'aspera/open_application'
|
2
2
|
require 'aspera/web_auth'
|
3
|
+
require 'aspera/id_generator'
|
3
4
|
require 'base64'
|
4
5
|
require 'date'
|
5
6
|
require 'socket'
|
@@ -9,8 +10,14 @@ module Aspera
|
|
9
10
|
# Implement OAuth 2 for the REST client and generate a bearer token
|
10
11
|
# call get_authorization() to get a token.
|
11
12
|
# bearer tokens are kept in memory and also in a file cache for later re-use
|
12
|
-
# if a token is expired (api returns 4xx), call again get_authorization({:
|
13
|
+
# if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
|
13
14
|
class Oauth
|
15
|
+
# used for code exchange
|
16
|
+
DEFAULT_PATH_AUTHORIZE='authorize'
|
17
|
+
# to generate token
|
18
|
+
DEFAULT_PATH_TOKEN='token'
|
19
|
+
# field with token in result
|
20
|
+
DEFAULT_TOKEN_FIELD='access_token'
|
14
21
|
private
|
15
22
|
# remove 5 minutes to account for time offset (TODO: configurable?)
|
16
23
|
JWT_NOTBEFORE_OFFSET_SEC=300
|
@@ -20,7 +27,9 @@ module Aspera
|
|
20
27
|
TOKEN_CACHE_EXPIRY_SEC=1800
|
21
28
|
# a prefix for persistency of tokens (garbage collect)
|
22
29
|
PERSIST_CATEGORY_TOKEN='token'
|
23
|
-
|
30
|
+
ONE_HOUR_AS_DAY_FRACTION=Rational(1,24)
|
31
|
+
|
32
|
+
private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_CACHE_EXPIRY_SEC,:ONE_HOUR_AS_DAY_FRACTION
|
24
33
|
class << self
|
25
34
|
# OAuth methods supported
|
26
35
|
def auth_types
|
@@ -45,8 +54,25 @@ module Aspera
|
|
45
54
|
def flush_tokens
|
46
55
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
|
47
56
|
end
|
57
|
+
|
58
|
+
def register_decoder(method)
|
59
|
+
@decoders||=[]
|
60
|
+
@decoders.push(method)
|
61
|
+
end
|
62
|
+
|
63
|
+
def decode_token(token)
|
64
|
+
Log.log.debug(">>>> #{token} : #{@decoders.length}")
|
65
|
+
@decoders.each do |decoder|
|
66
|
+
result=decoder.call(token) rescue nil
|
67
|
+
return result unless result.nil?
|
68
|
+
end
|
69
|
+
return nil
|
70
|
+
end
|
48
71
|
end
|
49
72
|
|
73
|
+
# seems to be quite standard token encoding (RFC?)
|
74
|
+
self.register_decoder lambda { |token| parts=token.split('.'); raise "not aoc token" unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
|
75
|
+
|
50
76
|
# for supported parameters, look in the code for @params
|
51
77
|
# parameters are provided all with oauth_ prefix :
|
52
78
|
# :base_url
|
@@ -56,8 +82,8 @@ module Aspera
|
|
56
82
|
# :jwt_audience
|
57
83
|
# :jwt_private_key_obj
|
58
84
|
# :jwt_subject
|
59
|
-
# :path_authorize (default:
|
60
|
-
# :path_token (default:
|
85
|
+
# :path_authorize (default: DEFAULT_PATH_AUTHORIZE)
|
86
|
+
# :path_token (default: DEFAULT_PATH_TOKEN)
|
61
87
|
# :scope (optional)
|
62
88
|
# :grant (one of returned by self.auth_types)
|
63
89
|
# :url_token
|
@@ -65,21 +91,21 @@ module Aspera
|
|
65
91
|
# :user_pass
|
66
92
|
# :token_type
|
67
93
|
def initialize(auth_params)
|
68
|
-
Log.log.debug
|
94
|
+
Log.log.debug("auth=#{auth_params}")
|
69
95
|
@params=auth_params.clone
|
70
96
|
# default values
|
71
97
|
# name of field to take as token from result of call to /token
|
72
|
-
@params[:token_field]||=
|
98
|
+
@params[:token_field]||=DEFAULT_TOKEN_FIELD
|
73
99
|
# default endpoint for /token
|
74
|
-
@params[:path_token]||=
|
100
|
+
@params[:path_token]||=DEFAULT_PATH_TOKEN
|
75
101
|
# default endpoint for /authorize
|
76
|
-
@params[:path_authorize]||=
|
77
|
-
rest_params={:
|
102
|
+
@params[:path_authorize]||=DEFAULT_PATH_AUTHORIZE
|
103
|
+
rest_params={base_url: @params[:base_url]}
|
78
104
|
if @params.has_key?(:client_id)
|
79
|
-
rest_params.merge!({:
|
80
|
-
:
|
81
|
-
:
|
82
|
-
:
|
105
|
+
rest_params.merge!({auth: {
|
106
|
+
type: :basic,
|
107
|
+
username: @params[:client_id],
|
108
|
+
password: @params[:client_secret]
|
83
109
|
}})
|
84
110
|
end
|
85
111
|
@token_auth_api=Rest.new(rest_params)
|
@@ -107,28 +133,20 @@ module Aspera
|
|
107
133
|
return code
|
108
134
|
end
|
109
135
|
|
110
|
-
def
|
136
|
+
def create_token(rest_params)
|
111
137
|
return @token_auth_api.call({
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:
|
115
|
-
end
|
116
|
-
|
117
|
-
# shortcut for create_token_advanced
|
118
|
-
def create_token_www_body(creation_params)
|
119
|
-
return create_token_advanced({:www_body_params=>creation_params})
|
138
|
+
operation: 'POST',
|
139
|
+
subpath: @params[:path_token],
|
140
|
+
headers: {'Accept'=>'application/json'}}.merge(rest_params))
|
120
141
|
end
|
121
142
|
|
122
|
-
# @return
|
123
|
-
def
|
143
|
+
# @return unique identifier of token
|
144
|
+
def token_cache_id(api_scope)
|
124
145
|
oauth_uri=URI.parse(@params[:base_url])
|
125
|
-
parts=[PERSIST_CATEGORY_TOKEN,oauth_uri.host
|
126
|
-
|
127
|
-
parts.push(@params[
|
128
|
-
|
129
|
-
parts.push(@params[:url_token]) if @params.has_key?(:url_token)
|
130
|
-
parts.push(@params[:api_key]) if @params.has_key?(:api_key)
|
131
|
-
return parts
|
146
|
+
parts=[PERSIST_CATEGORY_TOKEN,api_scope,oauth_uri.host,oauth_uri.path]
|
147
|
+
# add some of the parameters that uniquely define the token
|
148
|
+
[:grant,:jwt_subject,:user_name,:url_token,:api_key].inject(parts){|p,i|p.push(@params[i])}
|
149
|
+
return IdGenerator.from_list(parts)
|
132
150
|
end
|
133
151
|
|
134
152
|
public
|
@@ -148,22 +166,23 @@ module Aspera
|
|
148
166
|
use_refresh_token=options[:refresh]
|
149
167
|
|
150
168
|
# generate token identifier to use with cache
|
151
|
-
|
169
|
+
token_id=token_cache_id(api_scope)
|
152
170
|
|
153
171
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
154
|
-
token_data=self.class.persist_mgr.get(
|
172
|
+
token_data=self.class.persist_mgr.get(token_id)
|
155
173
|
token_data=JSON.parse(token_data) unless token_data.nil?
|
156
|
-
|
157
174
|
# Optional optimization: check if node token is expired, then force refresh
|
158
175
|
# in case the transfer agent cannot refresh himself
|
159
176
|
# else, anyway, faspmanager is equipped with refresh code
|
160
177
|
if !token_data.nil?
|
161
|
-
|
178
|
+
# TODO: use @params[:token_field] ?
|
179
|
+
decoded_node_token = self.class.decode_token(token_data['access_token'])
|
180
|
+
Log.dump('decoded_node_token',decoded_node_token) unless decoded_node_token.nil?
|
162
181
|
if decoded_node_token.is_a?(Hash) and decoded_node_token['expires_at'].is_a?(String)
|
163
|
-
Log.dump('decoded_node_token',decoded_node_token)
|
164
182
|
expires_at=DateTime.parse(decoded_node_token['expires_at'])
|
165
|
-
|
166
|
-
|
183
|
+
# Time.at(decoded_node_token['exp'])
|
184
|
+
# does it seem expired, with one hour of security
|
185
|
+
use_refresh_token=true if DateTime.now > (expires_at-ONE_HOUR_AS_DAY_FRACTION)
|
167
186
|
end
|
168
187
|
end
|
169
188
|
|
@@ -174,7 +193,7 @@ module Aspera
|
|
174
193
|
refresh_token=token_data['refresh_token']
|
175
194
|
end
|
176
195
|
# delete caches
|
177
|
-
self.class.persist_mgr.delete(
|
196
|
+
self.class.persist_mgr.delete(token_id)
|
178
197
|
token_data=nil
|
179
198
|
# lets try the existing refresh token
|
180
199
|
if !refresh_token.nil?
|
@@ -182,14 +201,14 @@ module Aspera
|
|
182
201
|
# try to refresh
|
183
202
|
# note: admin token has no refresh, and lives by default 1800secs
|
184
203
|
# Note: scope is mandatory in Files, and we can either provide basic auth, or client_Secret in data
|
185
|
-
resp=
|
186
|
-
:
|
187
|
-
:refresh_token
|
204
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
205
|
+
grant_type: 'refresh_token',
|
206
|
+
refresh_token: refresh_token}))
|
188
207
|
if resp[:http].code.start_with?('2') then
|
189
|
-
# save only if success
|
208
|
+
# save only if success
|
190
209
|
json_data=resp[:http].body
|
191
210
|
token_data=JSON.parse(json_data)
|
192
|
-
self.class.persist_mgr.put(
|
211
|
+
self.class.persist_mgr.put(token_id,json_data)
|
193
212
|
else
|
194
213
|
Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
|
195
214
|
end
|
@@ -204,19 +223,19 @@ module Aspera
|
|
204
223
|
# AoC Web based Auth
|
205
224
|
check_code=SecureRandom.uuid
|
206
225
|
auth_params=p_client_id_and_scope.merge({
|
207
|
-
:
|
208
|
-
:
|
209
|
-
:
|
226
|
+
response_type: 'code',
|
227
|
+
redirect_uri: @params[:redirect_uri],
|
228
|
+
state: check_code
|
210
229
|
})
|
211
230
|
auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
|
212
231
|
login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
|
213
232
|
# here, we need a human to authorize on a web page
|
214
233
|
code=goto_page_and_get_code(login_page_url,check_code)
|
215
234
|
# exchange code for token
|
216
|
-
resp=
|
217
|
-
:
|
218
|
-
:
|
219
|
-
:
|
235
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
236
|
+
grant_type: 'authorization_code',
|
237
|
+
code: code,
|
238
|
+
redirect_uri: @params[:redirect_uri]
|
220
239
|
}))
|
221
240
|
when :jwt
|
222
241
|
# https://tools.ietf.org/html/rfc7519
|
@@ -226,84 +245,81 @@ module Aspera
|
|
226
245
|
Log.log.info("seconds=#{seconds_since_epoch}")
|
227
246
|
|
228
247
|
payload = {
|
229
|
-
:
|
230
|
-
:
|
231
|
-
:
|
232
|
-
:
|
233
|
-
:
|
248
|
+
iss: @params[:client_id], # issuer
|
249
|
+
sub: @params[:jwt_subject], # subject
|
250
|
+
aud: @params[:jwt_audience], # audience
|
251
|
+
nbf: seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
|
252
|
+
exp: seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC # expiration
|
234
253
|
}
|
235
254
|
# Hum.. compliant ? TODO: remove when Faspex5 API is clarified
|
236
|
-
if @params
|
237
|
-
payload[:jti] = SecureRandom.uuid
|
238
|
-
payload[:iat] = seconds_since_epoch
|
239
|
-
payload.delete(:nbf)
|
240
|
-
p_scope[:redirect_uri]="https://127.0.0.1:5000/token"
|
255
|
+
if @params.has_key?(:f5_username)
|
256
|
+
payload[:jti] = SecureRandom.uuid # JWT id
|
257
|
+
payload[:iat] = seconds_since_epoch # issued at
|
258
|
+
payload.delete(:nbf) # not used in f5
|
259
|
+
p_scope[:redirect_uri]="https://127.0.0.1:5000/token" # used ?
|
241
260
|
p_scope[:state]=SecureRandom.uuid
|
242
261
|
p_scope[:client_id]=@params[:client_id]
|
243
|
-
@token_auth_api.params[:auth]={:
|
262
|
+
@token_auth_api.params[:auth]={type: :basic,username: @params[:f5_username], password: @params[:f5_password]}
|
244
263
|
end
|
245
264
|
|
246
265
|
# non standard, only for global ids
|
247
266
|
payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
|
267
|
+
Log.log.debug("JWT payload=[#{payload}]")
|
248
268
|
|
249
269
|
rsa_private=@params[:jwt_private_key_obj] # type: OpenSSL::PKey::RSA
|
250
|
-
|
251
270
|
Log.log.debug("private=[#{rsa_private}]")
|
252
271
|
|
253
|
-
|
254
|
-
assertion = JWT.encode(payload, rsa_private, 'RS256',@params[:jwt_headers]||{})
|
255
|
-
|
272
|
+
assertion = JWT.encode(payload, rsa_private, 'RS256', @params[:jwt_headers]||{})
|
256
273
|
Log.log.debug("assertion=[#{assertion}]")
|
257
274
|
|
258
|
-
resp=
|
259
|
-
:
|
260
|
-
:
|
275
|
+
resp=create_token(www_body_params: p_scope.merge({
|
276
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
277
|
+
assertion: assertion
|
261
278
|
}))
|
262
279
|
when :url_token
|
263
280
|
# AoC Public Link
|
264
|
-
params={:
|
281
|
+
params={url_token: @params[:url_token]}
|
265
282
|
params[:password]=@params[:password] if @params.has_key?(:password)
|
266
|
-
resp=
|
267
|
-
:
|
268
|
-
:
|
269
|
-
|
270
|
-
})})
|
283
|
+
resp=create_token({
|
284
|
+
json_params: params,
|
285
|
+
url_params: p_scope.merge({grant_type: 'url_token'})
|
286
|
+
})
|
271
287
|
when :ibm_apikey
|
272
288
|
# ATS
|
273
|
-
resp=
|
274
|
-
|
275
|
-
|
276
|
-
|
289
|
+
resp=create_token(www_body_params: {
|
290
|
+
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
291
|
+
response_type: 'cloud_iam',
|
292
|
+
apikey: @params[:api_key]
|
277
293
|
})
|
278
294
|
when :delegated_refresh
|
279
295
|
# COS
|
280
|
-
resp=
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
296
|
+
resp=create_token(www_body_params: {
|
297
|
+
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
298
|
+
response_type: 'delegated_refresh_token',
|
299
|
+
apikey: @params[:api_key],
|
300
|
+
receiver_client_ids: 'aspera_ats'
|
285
301
|
})
|
286
302
|
when :header_userpass
|
287
303
|
# used in Faspex apiv4 and shares2
|
288
|
-
resp=
|
289
|
-
|
290
|
-
|
291
|
-
:
|
292
|
-
:
|
293
|
-
:
|
294
|
-
|
304
|
+
resp=create_token(
|
305
|
+
json_params: p_client_id_and_scope.merge({grant_type: 'password'}), #:www_body_params also works
|
306
|
+
auth: {
|
307
|
+
type: :basic,
|
308
|
+
username: @params[:user_name],
|
309
|
+
password: @params[:user_pass]}
|
310
|
+
)
|
295
311
|
when :body_userpass
|
296
312
|
# legacy, not used
|
297
|
-
resp=
|
298
|
-
:
|
299
|
-
:
|
300
|
-
:
|
313
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
314
|
+
grant_type: 'password',
|
315
|
+
username: @params[:user_name],
|
316
|
+
password: @params[:user_pass]
|
301
317
|
}))
|
302
318
|
when :body_data
|
303
319
|
# used in Faspex apiv5
|
304
|
-
resp=
|
305
|
-
:
|
306
|
-
:
|
320
|
+
resp=create_token({
|
321
|
+
auth: {type: :none},
|
322
|
+
json_params: @params[:userpass_body],
|
307
323
|
})
|
308
324
|
else
|
309
325
|
raise "auth grant type unknown: #{@params[:grant]}"
|
@@ -311,9 +327,9 @@ module Aspera
|
|
311
327
|
# TODO: test return code ?
|
312
328
|
json_data=resp[:http].body
|
313
329
|
token_data=JSON.parse(json_data)
|
314
|
-
self.class.persist_mgr.put(
|
330
|
+
self.class.persist_mgr.put(token_id,json_data)
|
315
331
|
end # if ! in_cache
|
316
|
-
|
332
|
+
raise "API error: No such field in answer: #{@params[:token_field]}" unless token_data.has_key?(@params[:token_field])
|
317
333
|
# ok we shall have a token here
|
318
334
|
return 'Bearer '+token_data[@params[:token_field]]
|
319
335
|
end
|
@@ -6,7 +6,7 @@ module Aspera
|
|
6
6
|
class PersistencyActionOnce
|
7
7
|
# @param :manager Mandatory Database
|
8
8
|
# @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
|
9
|
-
# @param :
|
9
|
+
# @param :id Mandatory identifiers
|
10
10
|
# @param :delete Optional delete persistency condition
|
11
11
|
# @param :parse Optional parse method (default to JSON)
|
12
12
|
# @param :format Optional dump method (default to JSON)
|
@@ -16,27 +16,31 @@ module Aspera
|
|
16
16
|
raise "options shall be Hash" unless options.is_a?(Hash)
|
17
17
|
raise "mandatory :manager" if options[:manager].nil?
|
18
18
|
raise "mandatory :data" if options[:data].nil?
|
19
|
-
raise "mandatory :
|
20
|
-
raise "mandatory 1 element in :
|
19
|
+
raise "mandatory :id (String)" unless options[:id].is_a?(String)
|
20
|
+
raise "mandatory 1 element in :id" unless options[:id].length >= 1
|
21
21
|
@manager=options[:manager]
|
22
22
|
@persisted_object=options[:data]
|
23
|
-
@
|
23
|
+
@object_id=options[:id]
|
24
24
|
# by default , at save time, file is deleted if data is nil
|
25
25
|
@delete_condition=options[:delete] || lambda{|d|d.empty?}
|
26
26
|
@persist_format=options[:format] || lambda {|h| JSON.generate(h)}
|
27
27
|
persist_parse=options[:parse] || lambda {|t| JSON.parse(t)}
|
28
28
|
persist_merge=options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
|
29
|
-
value=@manager.get(@
|
29
|
+
value=@manager.get(@object_id)
|
30
30
|
persist_merge.call(@persisted_object,persist_parse.call(value)) unless value.nil?
|
31
31
|
end
|
32
32
|
|
33
33
|
def save
|
34
34
|
if @delete_condition.call(@persisted_object)
|
35
|
-
@manager.delete(@
|
35
|
+
@manager.delete(@object_id)
|
36
36
|
else
|
37
|
-
@manager.put(@
|
37
|
+
@manager.put(@object_id,@persist_format.call(@persisted_object))
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def data
|
42
|
+
return @persisted_object
|
43
|
+
end
|
44
|
+
|
41
45
|
end
|
42
46
|
end
|
@@ -6,11 +6,8 @@ require 'aspera/log'
|
|
6
6
|
module Aspera
|
7
7
|
# Persist data on file system
|
8
8
|
class PersistencyFolder
|
9
|
-
WINDOWS_PROTECTED_CHAR=%r{[/:"<>\\\*\?]}
|
10
|
-
PROTECTED_CHAR_REPLACE='_'
|
11
|
-
ID_SEPARATOR='_'
|
12
9
|
FILE_SUFFIX='.txt'
|
13
|
-
private_constant :
|
10
|
+
private_constant :FILE_SUFFIX
|
14
11
|
def initialize(folder)
|
15
12
|
@cache={}
|
16
13
|
set_folder(folder)
|
@@ -23,7 +20,6 @@ module Aspera
|
|
23
20
|
|
24
21
|
# @return String or nil string on existing persist, else nil
|
25
22
|
def get(object_id)
|
26
|
-
object_id=marshalled_id(object_id)
|
27
23
|
Log.log.debug("persistency get: #{object_id}")
|
28
24
|
if @cache.has_key?(object_id)
|
29
25
|
Log.log.debug("got from memory cache")
|
@@ -39,18 +35,16 @@ module Aspera
|
|
39
35
|
end
|
40
36
|
|
41
37
|
def put(object_id,value)
|
42
|
-
raise "only String supported" unless value.is_a?(String)
|
43
|
-
object_id=marshalled_id(object_id)
|
38
|
+
raise "value: only String supported" unless value.is_a?(String)
|
44
39
|
persist_filepath=id_to_filepath(object_id)
|
45
|
-
Log.log.debug("saving: #{persist_filepath}")
|
40
|
+
Log.log.debug("persistency saving: #{persist_filepath}")
|
46
41
|
File.write(persist_filepath,value)
|
47
42
|
@cache[object_id]=value
|
48
43
|
end
|
49
44
|
|
50
45
|
def delete(object_id)
|
51
|
-
object_id=marshalled_id(object_id)
|
52
46
|
persist_filepath=id_to_filepath(object_id)
|
53
|
-
Log.log.debug("
|
47
|
+
Log.log.debug("persistency deleting: #{persist_filepath}")
|
54
48
|
File.delete(persist_filepath) if File.exist?(persist_filepath)
|
55
49
|
@cache.delete(object_id)
|
56
50
|
end
|
@@ -63,7 +57,7 @@ module Aspera
|
|
63
57
|
end
|
64
58
|
garbage_files.each do |filepath|
|
65
59
|
File.delete(filepath)
|
66
|
-
Log.log.debug("
|
60
|
+
Log.log.debug("persistency deleted expired: #{filepath}")
|
67
61
|
end
|
68
62
|
return garbage_files
|
69
63
|
end
|
@@ -72,25 +66,11 @@ module Aspera
|
|
72
66
|
|
73
67
|
# @param object_id String or Array
|
74
68
|
def id_to_filepath(object_id)
|
69
|
+
raise "object_id: only String supported" unless object_id.is_a?(String)
|
75
70
|
FileUtils.mkdir_p(@folder)
|
76
71
|
return File.join(@folder,"#{object_id}#{FILE_SUFFIX}")
|
77
72
|
#.gsub(/[^a-z]+/,FILE_FIELD_SEPARATOR)
|
78
73
|
end
|
79
74
|
|
80
|
-
def marshalled_id(object_id)
|
81
|
-
if object_id.is_a?(Array)
|
82
|
-
# special case, url in second position: TODO: check any position
|
83
|
-
if object_id[1].is_a?(String) and object_id[1] =~ URI::ABS_URI
|
84
|
-
object_id=object_id.clone
|
85
|
-
object_id[1]=URI.parse(object_id[1]).host
|
86
|
-
end
|
87
|
-
object_id=object_id.join(ID_SEPARATOR)
|
88
|
-
end
|
89
|
-
raise "id must be a String" unless object_id.is_a?(String)
|
90
|
-
return object_id.
|
91
|
-
gsub(WINDOWS_PROTECTED_CHAR,PROTECTED_CHAR_REPLACE). # remove windows forbidden chars
|
92
|
-
gsub('.',PROTECTED_CHAR_REPLACE). # keep dot for extension only (nicer)
|
93
|
-
downcase
|
94
|
-
end
|
95
75
|
end # PersistencyFolder
|
96
76
|
end # Aspera
|