aspera-cli 4.2.1 → 4.5.0
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 +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
|