Skip to content

Commit 099cb06

Browse files
move paging functions ro rest_list
Signed-off-by: Laurent Martin <laurent.martin.l@gmail.com>
1 parent 07698b2 commit 099cb06

8 files changed

Lines changed: 79 additions & 83 deletions

File tree

lib/aspera/api/aoc.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ def call_paging(query: {})
157157
Aspera.assert(block_given?)
158158
# set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
159159
query['per_page'] = 1000 unless query.key?('per_page')
160-
max_items = query.delete(Rest::MAX_ITEMS)
161-
max_pages = query.delete(Rest::MAX_PAGES)
160+
max_items = query.delete(RestList::MAX_ITEMS)
161+
max_pages = query.delete(RestList::MAX_PAGES)
162162
item_list = []
163163
total_count = nil
164164
current_page = query['page']

lib/aspera/api/node.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,46 @@ def transfer_spec_gen4(file_id, direction, ts_merge = nil)
521521
return transfer_spec
522522
end
523523

524+
# Executes `GET` call in loop using `iteration_token` (`/ops/transfers`)
525+
# @param iteration [Array] a single element array with the iteration token or nil
526+
# @param call_args [Hash] additional arguments to pass to `Rest.call`
527+
# @return [Array] list of items returned by the API call
528+
def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
529+
Aspera.assert_type(iteration, Array, NilClass){'iteration'}
530+
Aspera.assert_type(query, Hash, NilClass){'query'}
531+
Aspera.assert(!call_args.key?(:query))
532+
query = {} if query.nil?
533+
query[:iteration_token] = iteration[0] unless iteration.nil?
534+
max = query.delete(RestList::MAX_ITEMS)
535+
item_list = []
536+
loop do
537+
data, http = read(subpath, query, **call_args, ret: :both)
538+
Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
539+
# no data
540+
break if data.empty?
541+
# get next iteration token from link
542+
next_iteration_token = nil
543+
link_info = http['Link']
544+
unless link_info.nil?
545+
m = link_info.match(/<([^>]+)>/)
546+
Aspera.assert(m){"Cannot parse iteration in Link: #{link_info}"}
547+
next_iteration_token = Rest.query_to_h(URI.parse(m[1]).query)['iteration_token']
548+
end
549+
# same as last iteration: stop
550+
break if next_iteration_token&.eql?(query[:iteration_token])
551+
query[:iteration_token] = next_iteration_token
552+
item_list.concat(data)
553+
if max&.<=(item_list.length)
554+
item_list = item_list.slice(0, max)
555+
break
556+
end
557+
break if next_iteration_token.nil?
558+
end
559+
# save iteration token if needed
560+
iteration[0] = query[:iteration_token] unless iteration.nil?
561+
item_list
562+
end
563+
524564
private
525565

526566
# Method called in loop for each entry for `resolve_api_fid`

lib/aspera/cli/plugins/base.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ class Base
1414
INSTANCE_OPS = %i[modify delete show].freeze
1515
# All standard operations (create list modify delete show)
1616
ALL_OPS = (GLOBAL_OPS + INSTANCE_OPS).freeze
17-
# Special query parameter: `max`: max number of items for list command
18-
MAX_ITEMS = 'max'
19-
# Special query parameter: `pmax`: max number of pages for list command
20-
MAX_PAGES = 'pmax'
2117

2218
class << self
2319
def declare_options(options)
@@ -190,7 +186,7 @@ def entity_execute(
190186
return Main.result_single_object(api.read(one_res_path), fields: display_fields)
191187
when :list
192188
if tclo
193-
data, total = api.list_entities_limit_offset_total_count(entity:, items_key: items_key, query: query_read_delete(default: list_query))
189+
data, total = api.list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query_read_delete(default: list_query))
194190
return Main.result_object_list(data, total: total, fields: display_fields)
195191
end
196192
data, http = api.read(entity, query_read_delete, ret: :both)

lib/aspera/cli/plugins/faspex.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'aspera/transfer/uri'
1010
require 'aspera/transfer/spec'
1111
require 'aspera/persistency_action_once'
12+
require 'aspera/rest_list'
1213
require 'aspera/environment'
1314
require 'aspera/nagios'
1415
require 'aspera/id_generator'
@@ -31,7 +32,7 @@ class Faspex < BasicAuth
3132
# allowed parameters for inbox.atom
3233
ATOM_PARAMS = %w[page count startIndex].freeze
3334
# with special parameters (from Plugin class) : max and pmax (from Plugin)
34-
ATOM_EXT_PARAMS = [MAX_ITEMS, MAX_PAGES].concat(ATOM_PARAMS).freeze
35+
ATOM_EXT_PARAMS = [RestList::MAX_ITEMS, RestList::MAX_PAGES].concat(ATOM_PARAMS).freeze
3536
# sub path in url for public link delivery
3637
PUB_LINK_EXTERNAL_MATCH = 'external_deliveries/'
3738
STANDARD_PATH = '/aspera/faspex'
@@ -176,10 +177,9 @@ def mailbox_filtered_entries(stop_at_id: nil)
176177
Aspera.assert_type(mailbox_query, Hash){'query'}
177178
Aspera.assert((mailbox_query.keys - ATOM_EXT_PARAMS).empty?){"query: supported params: #{ATOM_EXT_PARAMS}"}
178179
Aspera.assert(!(mailbox_query.key?('startIndex') && mailbox_query.key?('page'))){'query: startIndex and page are exclusive'}
179-
max_items = mailbox_query[MAX_ITEMS]
180-
mailbox_query.delete(MAX_ITEMS)
181-
max_pages = mailbox_query[MAX_PAGES]
182-
mailbox_query.delete(MAX_PAGES)
180+
# Extract pagination control parameters (not part of API query)
181+
max_items = mailbox_query.delete(RestList::MAX_ITEMS)
182+
max_pages = mailbox_query.delete(RestList::MAX_PAGES)
183183
end
184184
loop do
185185
# get a batch of package information

lib/aspera/cli/plugins/faspex5.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def browse_folder(browse_endpoint, base_query = {})
332332
Aspera.assert_type(filters, Hash)
333333
filters['basenames'] ||= []
334334
Aspera.assert_type(filters, Hash){'filters'}
335-
max_items = query.delete(MAX_ITEMS)
335+
max_items = query.delete(RestList::MAX_ITEMS)
336336
recursive = query.delete('recursive')
337337
use_paging = query.delete('paging'){true}
338338
if use_paging

lib/aspera/cli/plugins/node.rb

Lines changed: 24 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require 'aspera/api/node'
1212
require 'aspera/oauth'
1313
require 'aspera/node_simulator'
14+
require 'aspera/rest_list'
1415
require 'aspera/assert'
1516
require 'base64'
1617
require 'zlib'
@@ -166,7 +167,7 @@ def wizard(wizard, app_url)
166167
# @param api [Rest] an existing API object for the Node API
167168
# @param prefix_path [String,nil] for Faspex 4, allows browsing a package without full path in node (removes storage prefix)
168169
def initialize(context:, api: nil, prefix_path: nil)
169-
@prefixer = prefix_path ? NodePathPrefix.new(prefix_path) : nil
170+
@node_path_prefix = prefix_path ? NodePathPrefix.new(prefix_path) : nil
170171
super(context: context, basic_options: api.nil?)
171172
Node.declare_options(options)
172173
return if context.only_manual?
@@ -196,11 +197,11 @@ def initialize(context:, api: nil, prefix_path: nil)
196197
# Gen3 API
197198
def browse_gen3
198199
folders_to_process = options.get_next_argument('path', validation: String)
199-
folders_to_process = @prefixer.add_to_path(folders_to_process) unless @prefixer.nil?
200+
folders_to_process = @node_path_prefix.add_to_path(folders_to_process) unless @node_path_prefix.nil?
200201
folders_to_process = [folders_to_process]
201202
query = options.get_option(:query) || {}
202203
# special parameter: max number of entries in result
203-
max_items = query.delete(MAX_ITEMS)
204+
max_items = query.delete(RestList::MAX_ITEMS)
204205
# special parameter: recursive browsing
205206
recursive = query.delete('recursive')
206207
# special parameter: only return one entry for the path, even if folder
@@ -221,7 +222,7 @@ def browse_gen3
221222
response = @api_node.create('files/browse', query)
222223
# 'file','symbolic_link'
223224
if !Node.gen3_entry_folder?(response['self']) || only_path
224-
@prefixer&.remove_in_object_list!([response['self']])
225+
@node_path_prefix&.remove_in_object_list!([response['self']])
225226
return Main.result_single_object(response['self'])
226227
end
227228
items = response['items']
@@ -243,7 +244,7 @@ def browse_gen3
243244
end
244245
query.delete('skip')
245246
end
246-
@prefixer&.remove_in_object_list!(all_items)
247+
@node_path_prefix&.remove_in_object_list!(all_items)
247248
return Main.result_object_list(all_items)
248249
ensure
249250
RestParameters.instance.spinner_cb.call(action: :success)
@@ -286,12 +287,12 @@ def execute_command_gen3(command)
286287
when :delete
287288
# TODO: add query for recursive
288289
paths_to_delete = options.get_next_argument('file list', multiple: true)
289-
@prefixer&.add_to_paths!(paths_to_delete)
290+
@node_path_prefix&.add_to_paths!(paths_to_delete)
290291
resp = @api_node.create('files/delete', {paths: paths_to_delete.map{ |i| {'path' => i.start_with?('/') ? i : "/#{i}"}}})
291292
return cli_result_from_paths_response(resp, 'file deleted')
292293
when :search
293294
search_root = options.get_next_argument('search root', validation: String)
294-
search_root = @prefixer.add_to_path(search_root) unless @prefixer.nil?
295+
search_root = @node_path_prefix.add_to_path(search_root) unless @node_path_prefix.nil?
295296
parameters = {'path' => search_root}
296297
other_options = options.get_option(:query)
297298
parameters.merge!(other_options) unless other_options.nil?
@@ -300,40 +301,40 @@ def execute_command_gen3(command)
300301
fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
301302
formatter.display_item_count(resp['item_count'], resp['total_count'])
302303
formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
303-
@prefixer&.remove_in_object_list!(resp['items'])
304+
@node_path_prefix&.remove_in_object_list!(resp['items'])
304305
return Main.result_object_list(resp['items'], fields: fields)
305306
when :space
306307
path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
307-
@prefixer&.add_to_paths!(path_list)
308+
@node_path_prefix&.add_to_paths!(path_list)
308309
resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
309-
@prefixer&.remove_in_object_list!(resp['paths'])
310+
@node_path_prefix&.remove_in_object_list!(resp['paths'])
310311
return Main.result_object_list(resp['paths'])
311312
when :mkdir
312313
path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
313-
@prefixer&.add_to_paths!(path_list)
314+
@node_path_prefix&.add_to_paths!(path_list)
314315
resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
315316
return cli_result_from_paths_response(resp, 'folder created')
316317
when :mklink
317318
target = options.get_next_argument('target', validation: String)
318-
target = @prefixer.add_to_path(target) unless @prefixer.nil?
319+
target = @node_path_prefix.add_to_path(target) unless @node_path_prefix.nil?
319320
one_path = options.get_next_argument('link path', validation: String)
320-
one_path = @prefixer.add_to_path(one_path) unless @prefixer.nil?
321+
one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
321322
resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
322323
return cli_result_from_paths_response(resp, 'link created')
323324
when :mkfile
324325
one_path = options.get_next_argument('file path', validation: String)
325-
one_path = @prefixer.add_to_path(one_path) unless @prefixer.nil?
326+
one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
326327
contents64 = Base64.strict_encode64(options.get_next_argument('contents'))
327328
resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
328329
return cli_result_from_paths_response(resp, 'file created')
329330
when :rename
330331
# TODO: multiple ?
331332
path_base = options.get_next_argument('path_base', validation: String)
332-
path_base = @prefixer.add_to_path(path_base) unless @prefixer.nil?
333+
path_base = @node_path_prefix.add_to_path(path_base) unless @node_path_prefix.nil?
333334
path_src = options.get_next_argument('path_src', validation: String)
334-
path_src = @prefixer.add_to_path(path_src) unless @prefixer.nil?
335+
path_src = @node_path_prefix.add_to_path(path_src) unless @node_path_prefix.nil?
335336
path_dst = options.get_next_argument('path_dst', validation: String)
336-
path_dst = @prefixer.add_to_path(path_dst) unless @prefixer.nil?
337+
path_dst = @node_path_prefix.add_to_path(path_dst) unless @node_path_prefix.nil?
337338
resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
338339
return cli_result_from_paths_response(resp, 'entry moved')
339340
when :browse
@@ -378,7 +379,7 @@ def execute_command_gen3(command)
378379
return Main.result_transfer(transfer.start(transfer_spec))
379380
when :cat
380381
remote_path = options.get_next_argument('remote path', validation: String)
381-
remote_path = @prefixer.add_to_path(remote_path) unless @prefixer.nil?
382+
remote_path = @node_path_prefix.add_to_path(remote_path) unless @node_path_prefix.nil?
382383
File.basename(remote_path)
383384
http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
384385
return Main.result_text(http.body)
@@ -882,10 +883,10 @@ def execute_action(command = nil)
882883
iteration_persistency.save
883884
return Main.result_status('Persistency reset')
884885
end
886+
else
887+
Aspera.assert(!transfer_filter.key?('reset'), type: Cli::BadArgument){'reset only with once_only'}
885888
end
886-
raise Cli::BadArgument, 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
887-
max_items = transfer_filter.delete(MAX_ITEMS)
888-
transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', max: max_items, query: transfer_filter, iteration: iteration_persistency&.data)
889+
transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
889890
iteration_persistency&.save
890891
return Main.result_object_list(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
891892
when :sessions
@@ -1092,7 +1093,7 @@ def execute_action(command = nil)
10921093
}
10931094
loop do
10941095
timestamp = Time.now
1095-
transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', query: {active_only: true})
1096+
transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
10961097
datapoint[:asInt] = transfers_data.length
10971098
datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
10981099
Log.log.info("#{datapoint[:asInt]} active transfers")
@@ -1130,50 +1131,9 @@ def response_to_result(response, success_msg)
11301131
# Translates paths results into CLI result, and removes prefix
11311132
def cli_result_from_paths_response(response, success_msg)
11321133
obj_list = response_to_result(response, success_msg)
1133-
@prefixer&.remove_in_object_list!(obj_list)
1134+
@node_path_prefix&.remove_in_object_list!(obj_list)
11341135
return Main.result_object_list(obj_list, fields: %w[path result])
11351136
end
1136-
1137-
# Executes the provided API call in loop
1138-
# @param api [Rest] the API to call
1139-
# @param iteration [Array] a single element array with the iteration token or nil
1140-
# @param max [Integer] maximum number of items to return, or nil for no limit
1141-
# @param query [Hash] query parameters to use for the API call
1142-
# @param call_args [Hash] additional arguments to pass to the API call
1143-
# @return [Array] list of items returned by the API call
1144-
def call_with_iteration(api:, iteration: nil, max: nil, query: nil, **call_args)
1145-
Aspera.assert_type(iteration, Array, NilClass){'iteration'}
1146-
Aspera.assert_type(query, Hash, NilClass){'query'}
1147-
query_token = query&.dup || {}
1148-
item_list = []
1149-
query_token[:iteration_token] = iteration[0] unless iteration.nil?
1150-
loop do
1151-
data, http = api.call(**call_args, query: query_token, ret: :both)
1152-
Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
1153-
# no data
1154-
break if data.empty?
1155-
# get next iteration token from link
1156-
next_iteration_token = nil
1157-
link_info = http['Link']
1158-
unless link_info.nil?
1159-
m = link_info.match(/<([^>]+)>/)
1160-
Aspera.assert(m){"Cannot parse iteration in Link: #{link_info}"}
1161-
next_iteration_token = Rest.query_to_h(URI.parse(m[1]).query)['iteration_token']
1162-
end
1163-
# same as last iteration: stop
1164-
break if next_iteration_token&.eql?(query_token[:iteration_token])
1165-
query_token[:iteration_token] = next_iteration_token
1166-
item_list.concat(data)
1167-
if max&.<=(item_list.length)
1168-
item_list = item_list.slice(0, max)
1169-
break
1170-
end
1171-
break if next_iteration_token.nil?
1172-
end
1173-
# save iteration token if needed
1174-
iteration[0] = query_token[:iteration_token] unless iteration.nil?
1175-
item_list
1176-
end
11771137
end
11781138
end
11791139
end

lib/aspera/rest.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ def json?(mime)
7272
# rest call errors are raised as exception RestCallError
7373
# and error are analyzed in RestErrorAnalyzer
7474
class Rest
75-
# Special query parameter: max number of items for list command
76-
MAX_ITEMS = 'max'
77-
# Special query parameter: max number of pages for list command
78-
MAX_PAGES = 'pmax'
79-
8075
class << self
8176
# @return [String] Basic auth token
8277
def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end

lib/aspera/rest_list.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
module Aspera
44
# List and lookup methods for Rest
5-
# include in classes inheriting Rest
5+
# To be included in classes inheriting Rest that require those methods.
66
module RestList
7+
# `max`: special query parameter: max number of items for list command
8+
MAX_ITEMS = 'max'
9+
# `pmax`: special query parameter: max number of pages for list command
10+
MAX_PAGES = 'pmax'
11+
712
# Query entity by general search (read with parameter `q`)
813
#
914
# @param subpath [String] Path of entity in API

0 commit comments

Comments
 (0)