aspera-cli 4.26.2 → 4.27.1
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# cspell:ignore snid fnid bidi ssync asyncs rund asnodeadmin mkfile mklink asperabrowser asperabrowserurl watchfolders watchfolderd entsrv
|
|
4
|
+
require 'aspera/schema/registry'
|
|
4
5
|
require 'aspera/cli/plugins/basic_auth'
|
|
5
6
|
require 'aspera/cli/sync_actions'
|
|
6
7
|
require 'aspera/cli/special_values'
|
|
@@ -22,39 +23,16 @@ module Aspera
|
|
|
22
23
|
class Node < BasicAuth
|
|
23
24
|
include SyncActions
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
# Used only by Faspex4 to browse packages
|
|
27
|
-
class NodePathPrefix
|
|
28
|
-
def initialize(path)
|
|
29
|
-
@root = path
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# get next path argument from command line, and add prefix
|
|
33
|
-
def add_to_path(path_arg)
|
|
34
|
-
File.join(@root, path_arg)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# get remaining path arguments from command line, and add prefix
|
|
38
|
-
def add_to_paths!(path_args)
|
|
39
|
-
path_args.map!{ |p| add_to_path(p)}
|
|
40
|
-
end
|
|
26
|
+
application_name 'HSTS Node API'
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
item['path'] = item['path'].delete_prefix(@root) if item['path'].start_with?(@root)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
28
|
+
SESSION_TIME_FIELDS = %i[start end].freeze
|
|
29
|
+
private_constant :SESSION_TIME_FIELDS
|
|
48
30
|
|
|
49
31
|
class << self
|
|
50
32
|
# directory: node, container: shares
|
|
51
33
|
FOLDER_TYPES = %w[directory container].freeze
|
|
52
34
|
private_constant :FOLDER_TYPES
|
|
53
35
|
|
|
54
|
-
def application_name
|
|
55
|
-
'HSTS Node API'
|
|
56
|
-
end
|
|
57
|
-
|
|
58
36
|
# @return [Hash,NilClass]
|
|
59
37
|
def detect(address_or_url)
|
|
60
38
|
urls = if address_or_url.match?(%r{^[a-z]{1,6}://})
|
|
@@ -68,7 +46,7 @@ module Aspera
|
|
|
68
46
|
end
|
|
69
47
|
error = nil
|
|
70
48
|
urls.each do |base_url|
|
|
71
|
-
next unless base_url.match?(
|
|
49
|
+
next unless base_url.match?(%r{^https?://})
|
|
72
50
|
api = Rest.new(base_url: base_url)
|
|
73
51
|
test_endpoint = 'ping'
|
|
74
52
|
http = api.read(test_endpoint, ret: :resp)
|
|
@@ -81,37 +59,29 @@ module Aspera
|
|
|
81
59
|
}
|
|
82
60
|
rescue StandardError => e
|
|
83
61
|
error = e
|
|
84
|
-
Log.log.debug{"detect error: #{e}"}
|
|
62
|
+
Log.log.debug { "detect error: #{e}" }
|
|
85
63
|
end
|
|
86
64
|
raise error if error
|
|
87
65
|
return
|
|
88
66
|
end
|
|
89
67
|
|
|
90
|
-
def declare_options(options)
|
|
91
|
-
return if @options_declared
|
|
92
|
-
@options_declared = true
|
|
93
|
-
@dynamic_key = nil
|
|
94
|
-
options.declare(:validator, 'Identifier of validator (optional for central)')
|
|
95
|
-
options.declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
|
|
96
|
-
options.declare(
|
|
97
|
-
:node_api, 'Gen4: standard_ports: Use standard FASP ports (true) or get from node API (false). cache: Set to false to force actual file system read',
|
|
98
|
-
allowed: Hash,
|
|
99
|
-
handler: {o: Api::Node, m: :api_options}
|
|
100
|
-
)
|
|
101
|
-
options.declare(:root_id, 'Gen4: File id of top folder when using access key (override AK root id)')
|
|
102
|
-
options.declare(:dynamic_key, 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key})
|
|
103
|
-
SyncActions.declare_options(options)
|
|
104
|
-
options.parse_options!
|
|
105
|
-
end
|
|
106
|
-
|
|
107
68
|
# Using /files/browse: is it a folder (node and shares)
|
|
108
69
|
def gen3_entry_folder?(entry)
|
|
109
70
|
FOLDER_TYPES.include?(entry['type'])
|
|
110
71
|
end
|
|
111
72
|
end
|
|
112
73
|
|
|
74
|
+
# DSL option declarations - at class level, picked up by Base#initialize via ancestor chain.
|
|
75
|
+
# Included by other plugins (Ats, Cos, Aoc) via `use_options Node`.
|
|
76
|
+
option :validator, description: 'Identifier of validator (optional for central)'
|
|
77
|
+
option :asperabrowserurl, description: 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net'
|
|
78
|
+
option :node_api, description: 'Gen4: standard_ports: Use standard FASP ports (true) or get from node API (false). cache: Set to false to force actual file system read',
|
|
79
|
+
allowed: Hash, handler: {o: Api::Node, m: :api_options}
|
|
80
|
+
option :root_id, description: 'Gen4: File id of top folder when using access key (override AK root id)'
|
|
81
|
+
option :dynamic_key, description: 'Private key PEM to use for dynamic key auth', handler: {o: Api::Node, m: :use_dynamic_key}
|
|
82
|
+
|
|
113
83
|
# @param wizard [Wizard] The wizard object
|
|
114
|
-
# @param app_url [
|
|
84
|
+
# @param app_url [String] Tested URL
|
|
115
85
|
# @return [Hash] :preset_value, :test_args
|
|
116
86
|
def wizard(wizard, app_url)
|
|
117
87
|
return {
|
|
@@ -137,40 +107,83 @@ module Aspera
|
|
|
137
107
|
SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
|
|
138
108
|
|
|
139
109
|
# Actions in execute_command_gen3
|
|
140
|
-
COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete
|
|
110
|
+
COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete ls upload download cat sync transport spec]
|
|
141
111
|
|
|
142
|
-
|
|
112
|
+
# Shared DSL metadata for Gen3 commands (description:, arguments:, transfer_paths:, aliases:).
|
|
113
|
+
# Consumed by commands_under(:files) in shares.rb and commands_under %i[source node]) in faspex.rb.
|
|
114
|
+
# :sync is excluded (intermediate node with sub-commands, handled separately).
|
|
115
|
+
# action: entries are node-specific and intentionally omitted.
|
|
116
|
+
COMMANDS_GEN3_SPEC = {
|
|
117
|
+
search: {description: 'Search for files', arguments: [{name: :search_root, type: String}]},
|
|
118
|
+
space: {description: 'Show space information', arguments: [{name: :path_list, multiple: true}]},
|
|
119
|
+
mkdir: {description: 'Create a folder (Gen3)', arguments: [{name: :path_list, multiple: true}]},
|
|
120
|
+
mklink: {description: 'Create a symbolic link (Gen3)', arguments: [{name: :target, type: String}, {name: :link_path, type: String}]},
|
|
121
|
+
mkfile: {description: 'Create a file (Gen3)', arguments: [{name: :file_path, type: String}, {name: :contents, mandatory: false, default: nil}]},
|
|
122
|
+
rename: {description: 'Rename a file or folder (Gen3)', arguments: [{name: :path_base, type: String}, {name: :path_src, type: String}, {name: :path_dst, type: String}]},
|
|
123
|
+
delete: {description: 'Delete files or folders (Gen3)', arguments: [{name: :paths, multiple: true}]},
|
|
124
|
+
ls: {description: 'List files (Gen3)', arguments: [{name: :path, type: String}], aliases: [:browse]},
|
|
125
|
+
upload: {description: 'Upload files (Gen3)', transfer_paths: :send},
|
|
126
|
+
download: {description: 'Download files (Gen3)', transfer_paths: :receive},
|
|
127
|
+
cat: {description: 'Show file contents (Gen3)', arguments: [{name: :remote_path, type: String}]},
|
|
128
|
+
transport: {description: 'Show transport parameters'},
|
|
129
|
+
spec: {description: 'Show transfer spec base'},
|
|
130
|
+
api_details: {description: 'Show API details'},
|
|
131
|
+
health: {description: 'Check node health'},
|
|
132
|
+
events: {description: 'List events'},
|
|
133
|
+
info: {description: 'Show node info'},
|
|
134
|
+
slash: {description: 'Show root info'},
|
|
135
|
+
license: {description: 'Show license'},
|
|
136
|
+
access_keys: {description: 'Manage access keys'}
|
|
137
|
+
}.freeze
|
|
138
|
+
|
|
139
|
+
BASE_ACTIONS = (%i[api_details] + COMMANDS_GEN3).freeze
|
|
143
140
|
|
|
144
141
|
SPECIAL_ACTIONS = %i[health events info slash license].freeze
|
|
145
142
|
|
|
146
143
|
# commands for : `execute_simple_common`: actions used commonly when a node is involved
|
|
147
|
-
COMMON_ACTIONS = %i[access_keys]
|
|
148
|
-
|
|
149
|
-
# actions available in v3 in gen4
|
|
150
|
-
V3_IN_V4_ACTIONS = %i[transfer].concat(COMMON_ACTIONS).freeze
|
|
144
|
+
COMMON_ACTIONS = (%i[access_keys] + BASE_ACTIONS + SPECIAL_ACTIONS).freeze
|
|
151
145
|
|
|
152
|
-
private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :
|
|
146
|
+
private_constant :CENTRAL_SOAP_API_TEST, :SEARCH_REMOVE_FIELDS, :BASE_ACTIONS, :SPECIAL_ACTIONS, :COMMON_ACTIONS
|
|
153
147
|
|
|
154
148
|
# used in aoc
|
|
155
|
-
NODE4_READ_ACTIONS = %i[bearer_token_node node_info
|
|
149
|
+
NODE4_READ_ACTIONS = %i[bearer_token_node node_info ls find].freeze
|
|
156
150
|
|
|
157
151
|
# commands for execute_command_gen4
|
|
158
|
-
COMMANDS_GEN4 = %i[mkdir mklink mkfile rename delete upload download sync cat show modify permission thumbnail v3]
|
|
152
|
+
COMMANDS_GEN4 = (%i[mkdir mklink mkfile rename delete upload download sync cat show modify permission thumbnail v3] + NODE4_READ_ACTIONS).freeze
|
|
153
|
+
|
|
154
|
+
# Shared DSL metadata for all Gen4 commands (description:, arguments:, transfer_paths:, aliases:).
|
|
155
|
+
# Consumed by commands_under %i[access_keys do]) in node.rb and by aoc.rb.
|
|
156
|
+
# :sync and :permission are excluded: they are intermediate nodes handled separately.
|
|
157
|
+
SINGLE_PATH_ARG = [{name: :path, type: String}].freeze
|
|
158
|
+
COMMANDS_GEN4_SPEC = {
|
|
159
|
+
mkdir: {description: 'Create folder', arguments: SINGLE_PATH_ARG},
|
|
160
|
+
mklink: {description: 'Create symbolic link', arguments: SINGLE_PATH_ARG},
|
|
161
|
+
mkfile: {description: 'Create file', arguments: [{name: :path, type: String}, {name: :contents, mandatory: false, default: nil}]},
|
|
162
|
+
rename: {description: 'Rename entry', arguments: [{name: :source_path, type: String}, {name: :new_name, type: String}]},
|
|
163
|
+
delete: {description: 'Delete entry', arguments: [{name: :paths, type: String, bulk: true}]},
|
|
164
|
+
upload: {description: 'Upload files', transfer_paths: :send},
|
|
165
|
+
download: {description: 'Download files', transfer_paths: :receive},
|
|
166
|
+
modify: {description: 'Modify file', arguments: [{name: :path, type: String}, {name: :update_value, type: Hash, schema: 'node:components.schemas.files-id-put-request'}]},
|
|
167
|
+
cat: {description: 'Show file contents', arguments: SINGLE_PATH_ARG},
|
|
168
|
+
show: {description: 'Show file info', arguments: SINGLE_PATH_ARG},
|
|
169
|
+
thumbnail: {description: 'Show file thumbnail', arguments: SINGLE_PATH_ARG},
|
|
170
|
+
bearer_token_node: {description: 'Show bearer token for file node', arguments: SINGLE_PATH_ARG},
|
|
171
|
+
node_info: {description: 'Show node info for file', arguments: SINGLE_PATH_ARG},
|
|
172
|
+
ls: {description: 'List files', arguments: SINGLE_PATH_ARG, aliases: [:browse]},
|
|
173
|
+
find: {description: 'Find files', arguments: SINGLE_PATH_ARG + FILTER_ARGS},
|
|
174
|
+
v3: {description: 'Legacy v3 commands on files'}
|
|
175
|
+
}.freeze
|
|
176
|
+
private_constant :SINGLE_PATH_ARG
|
|
159
177
|
|
|
160
178
|
# commands supported in ATS for COS
|
|
161
179
|
COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
|
|
162
180
|
COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
|
|
163
|
-
COMMANDS_FASPEX = COMMON_ACTIONS
|
|
164
|
-
|
|
165
181
|
# `browse` display fields for gen4
|
|
166
182
|
GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
|
|
167
183
|
|
|
168
184
|
# @param api [Rest] an existing API object for the Node API
|
|
169
|
-
|
|
170
|
-
def initialize(context:, api: nil, prefix_path: nil)
|
|
171
|
-
@node_path_prefix = prefix_path ? NodePathPrefix.new(prefix_path) : nil
|
|
185
|
+
def initialize(context:, api: nil)
|
|
172
186
|
super(context: context, basic_options: api.nil?)
|
|
173
|
-
Node.declare_options(options)
|
|
174
187
|
return if context.only_manual?
|
|
175
188
|
@api_node =
|
|
176
189
|
if !api.nil?
|
|
@@ -196,9 +209,9 @@ module Aspera
|
|
|
196
209
|
end
|
|
197
210
|
|
|
198
211
|
# Gen3 API
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
folders_to_process =
|
|
212
|
+
# @param path [String] starting path
|
|
213
|
+
def browse_gen3(path)
|
|
214
|
+
folders_to_process = path
|
|
202
215
|
folders_to_process = [folders_to_process]
|
|
203
216
|
query = options.get_option(:query) || {}
|
|
204
217
|
# special parameter: max number of entries in result
|
|
@@ -211,7 +224,7 @@ module Aspera
|
|
|
211
224
|
single_call = query.key?('skip')
|
|
212
225
|
# API default is 100, so use 1000 for default
|
|
213
226
|
query['count'] ||= 1000
|
|
214
|
-
|
|
227
|
+
Aspera.assert(!(recursive && single_call), type: Cli::BadArgument) { 'options `recursive` and `skip` cannot be used together' }
|
|
215
228
|
all_items = []
|
|
216
229
|
until folders_to_process.empty?
|
|
217
230
|
path = folders_to_process.shift
|
|
@@ -222,10 +235,7 @@ module Aspera
|
|
|
222
235
|
# example: send_result={'items'=>[{'file'=>"filename1","permissions"=>[{'name'=>'read'},{'name'=>'write'}]}]}
|
|
223
236
|
response = @api_node.create('files/browse', query)
|
|
224
237
|
# 'file','symbolic_link'
|
|
225
|
-
if !Node.gen3_entry_folder?(response['self']) || only_path
|
|
226
|
-
@node_path_prefix&.remove_in_object_list!([response['self']])
|
|
227
|
-
return Result::SingleObject.new(response['self'])
|
|
228
|
-
end
|
|
238
|
+
return Result::SingleObject.new(response['self']) if !Node.gen3_entry_folder?(response['self']) || only_path
|
|
229
239
|
items = response['items']
|
|
230
240
|
total_count ||= response['total_count']
|
|
231
241
|
all_items.concat(items)
|
|
@@ -233,7 +243,7 @@ module Aspera
|
|
|
233
243
|
formatter.display_item_count(response['item_count'], total_count)
|
|
234
244
|
break
|
|
235
245
|
end
|
|
236
|
-
folders_to_process.concat(items.select{ |i| Node.gen3_entry_folder?(i)}.map{ |i| i['path']}) if recursive
|
|
246
|
+
folders_to_process.concat(items.select { |i| Node.gen3_entry_folder?(i) }.map { |i| i['path'] }) if recursive
|
|
237
247
|
if !max_items.nil? && (all_items.count >= max_items)
|
|
238
248
|
all_items = all_items.slice(0, max_items) if all_items.count > max_items
|
|
239
249
|
break
|
|
@@ -245,16 +255,15 @@ module Aspera
|
|
|
245
255
|
end
|
|
246
256
|
query.delete('skip')
|
|
247
257
|
end
|
|
248
|
-
@node_path_prefix&.remove_in_object_list!(all_items)
|
|
249
258
|
return Result::ObjectList.new(all_items)
|
|
250
259
|
ensure
|
|
251
260
|
RestParameters.instance.spinner_cb.call(action: :success)
|
|
252
261
|
end
|
|
253
262
|
|
|
254
263
|
# Create async transfer spec request from direction and folders
|
|
255
|
-
# @param sync_direction one of push pull bidi
|
|
256
|
-
# @param local_path local folder to sync
|
|
257
|
-
# @param remote_path remote folder to sync
|
|
264
|
+
# @param sync_direction [Symbol] one of push pull bidi
|
|
265
|
+
# @param local_path [String] local folder to sync
|
|
266
|
+
# @param remote_path [String] remote folder to sync
|
|
258
267
|
def sync_spec_request(sync_direction, local_path, remote_path)
|
|
259
268
|
case sync_direction
|
|
260
269
|
when :push then {
|
|
@@ -282,382 +291,28 @@ module Aspera
|
|
|
282
291
|
end
|
|
283
292
|
end
|
|
284
293
|
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return
|
|
294
|
-
when :search
|
|
295
|
-
search_root = options.get_next_argument('search root', validation: String)
|
|
296
|
-
search_root = @node_path_prefix.add_to_path(search_root) unless @node_path_prefix.nil?
|
|
297
|
-
parameters = {'path' => search_root}
|
|
298
|
-
other_options = options.get_option(:query)
|
|
299
|
-
parameters.merge!(other_options) unless other_options.nil?
|
|
300
|
-
resp = @api_node.create('files/search', parameters)
|
|
301
|
-
return Result::Empty.new if resp['items'].empty?
|
|
302
|
-
fields = resp['items'].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
|
|
303
|
-
formatter.display_item_count(resp['item_count'], resp['total_count'])
|
|
304
|
-
formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
|
|
305
|
-
@node_path_prefix&.remove_in_object_list!(resp['items'])
|
|
306
|
-
return Result::ObjectList.new(resp['items'], fields: fields)
|
|
307
|
-
when :space
|
|
308
|
-
path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
|
|
309
|
-
@node_path_prefix&.add_to_paths!(path_list)
|
|
310
|
-
resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
|
|
311
|
-
@node_path_prefix&.remove_in_object_list!(resp['paths'])
|
|
312
|
-
return Result::ObjectList.new(resp['paths'])
|
|
313
|
-
when :mkdir
|
|
314
|
-
path_list = options.get_next_argument('folder path or ext.val. list', multiple: true)
|
|
315
|
-
@node_path_prefix&.add_to_paths!(path_list)
|
|
316
|
-
resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
|
|
317
|
-
return cli_result_from_paths_response(resp, 'folder created')
|
|
318
|
-
when :mklink
|
|
319
|
-
target = options.get_next_argument('target', validation: String)
|
|
320
|
-
target = @node_path_prefix.add_to_path(target) unless @node_path_prefix.nil?
|
|
321
|
-
one_path = options.get_next_argument('link path', validation: String)
|
|
322
|
-
one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
|
|
323
|
-
resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
|
|
324
|
-
return cli_result_from_paths_response(resp, 'link created')
|
|
325
|
-
when :mkfile
|
|
326
|
-
one_path = options.get_next_argument('file path', validation: String)
|
|
327
|
-
one_path = @node_path_prefix.add_to_path(one_path) unless @node_path_prefix.nil?
|
|
328
|
-
contents64 = Base64.strict_encode64(options.get_next_argument('contents'))
|
|
329
|
-
resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
|
|
330
|
-
return cli_result_from_paths_response(resp, 'file created')
|
|
331
|
-
when :rename
|
|
332
|
-
# TODO: multiple ?
|
|
333
|
-
path_base = options.get_next_argument('path_base', validation: String)
|
|
334
|
-
path_base = @node_path_prefix.add_to_path(path_base) unless @node_path_prefix.nil?
|
|
335
|
-
path_src = options.get_next_argument('path_src', validation: String)
|
|
336
|
-
path_src = @node_path_prefix.add_to_path(path_src) unless @node_path_prefix.nil?
|
|
337
|
-
path_dst = options.get_next_argument('path_dst', validation: String)
|
|
338
|
-
path_dst = @node_path_prefix.add_to_path(path_dst) unless @node_path_prefix.nil?
|
|
339
|
-
resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
|
|
340
|
-
return cli_result_from_paths_response(resp, 'entry moved')
|
|
341
|
-
when :browse
|
|
342
|
-
return browse_gen3
|
|
343
|
-
when :sync
|
|
344
|
-
return execute_sync_action do |sync_direction, local_path, remote_path|
|
|
345
|
-
# Gen3 API
|
|
346
|
-
# empty transfer spec for authorization request
|
|
347
|
-
request_transfer_spec = sync_spec_request(sync_direction, local_path, remote_path)
|
|
348
|
-
# add fixed parameters if any (for COS)
|
|
349
|
-
@api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
|
|
350
|
-
# prepare payload for single request
|
|
351
|
-
setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
|
|
352
|
-
# only one request, so only one answer
|
|
353
|
-
transfer_spec = @api_node.create('files/sync_setup', setup_payload)['transfer_specs'].first['transfer_spec']
|
|
354
|
-
# API returns null tag... but async does not like it
|
|
355
|
-
transfer_spec.delete_if{ |_k, v| v.nil?}
|
|
356
|
-
# delete this part, as the returned value contains only destination, and not sources
|
|
357
|
-
# transfer_spec.delete('paths') if command.eql?(:upload)
|
|
358
|
-
Log.dump(:ts, transfer_spec)
|
|
359
|
-
transfer_spec
|
|
360
|
-
end
|
|
361
|
-
when :upload, :download
|
|
362
|
-
# empty transfer spec for authorization request
|
|
363
|
-
request_transfer_spec = {}
|
|
364
|
-
# set requested paths depending on direction
|
|
365
|
-
request_transfer_spec[:paths] = if command.eql?(:download)
|
|
366
|
-
transfer.ts_source_paths
|
|
367
|
-
else
|
|
368
|
-
[{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
|
|
369
|
-
end
|
|
370
|
-
# add fixed parameters if any (for COS)
|
|
371
|
-
@api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
|
|
372
|
-
Api::Node.add_public_key(request_transfer_spec)
|
|
373
|
-
# prepare payload for single request
|
|
374
|
-
setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
|
|
375
|
-
# only one request, so only one answer
|
|
376
|
-
transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)['transfer_specs'].first['transfer_spec']
|
|
377
|
-
Api::Node.add_private_key(transfer_spec)
|
|
378
|
-
# delete this part, as the returned value contains only destination, and not sources
|
|
379
|
-
transfer_spec.delete('paths') if command.eql?(:upload)
|
|
380
|
-
return Runner.result_transfer(transfer.start(transfer_spec))
|
|
381
|
-
when :cat
|
|
382
|
-
remote_path = options.get_next_argument('remote path', validation: String)
|
|
383
|
-
remote_path = @node_path_prefix.add_to_path(remote_path) unless @node_path_prefix.nil?
|
|
384
|
-
File.basename(remote_path)
|
|
385
|
-
http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
|
|
386
|
-
return Result::Text.new(http.body)
|
|
387
|
-
when :transport
|
|
388
|
-
return Result::SingleObject.new(@api_node.transport_params)
|
|
389
|
-
when :spec
|
|
390
|
-
return Result::SingleObject.new(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC))
|
|
391
|
-
end
|
|
392
|
-
Aspera.error_unreachable_line
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# common API to node and Shares
|
|
396
|
-
def execute_simple_common(command)
|
|
397
|
-
case command
|
|
398
|
-
when *COMMANDS_GEN3
|
|
399
|
-
execute_command_gen3(command)
|
|
400
|
-
when :access_keys
|
|
401
|
-
ak_command = options.get_next_command(%i[do set_bearer_key].concat(Operations::ALL))
|
|
402
|
-
case ak_command
|
|
403
|
-
when *Operations::ALL
|
|
404
|
-
return entity_execute(
|
|
405
|
-
api: @api_node,
|
|
406
|
-
entity: 'access_keys',
|
|
407
|
-
command: ak_command
|
|
408
|
-
) do |field, value|
|
|
409
|
-
raise BadArgument, 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
|
|
410
|
-
@api_node.read('access_keys/self')['id']
|
|
411
|
-
end
|
|
412
|
-
when :do
|
|
413
|
-
access_key_id = options.get_next_argument('access key id')
|
|
414
|
-
root_file_id = options.get_option(:root_id)
|
|
415
|
-
if root_file_id.nil?
|
|
416
|
-
ak_info = @api_node.read("access_keys/#{access_key_id}")
|
|
417
|
-
ak_secret = config.lookup_secret(url: @api_node.base_url, username: ak_info['id'])
|
|
418
|
-
# change API credentials if different access key
|
|
419
|
-
if !access_key_id.eql?('self')
|
|
420
|
-
Aspera.assert(ak_secret, type: Cli::MissingArgument){"Please provide secret for #{ak_info['id']} using option: secret or by setting a preset for #{ak_info['id']}@#{@api_node.base_url}."}
|
|
421
|
-
@api_node.auth_params[:username] = ak_info['id']
|
|
422
|
-
@api_node.auth_params[:password] = ak_secret
|
|
423
|
-
end
|
|
424
|
-
root_file_id = ak_info['root_file_id']
|
|
425
|
-
end
|
|
426
|
-
command_repo = options.get_next_command(COMMANDS_GEN4)
|
|
427
|
-
return execute_command_gen4(command_repo, root_file_id)
|
|
428
|
-
when :set_bearer_key
|
|
429
|
-
access_key_id = options.get_next_argument('access key id')
|
|
430
|
-
access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
|
|
431
|
-
bearer_key_pem = options.get_next_argument('public or private RSA key PEM value', validation: String)
|
|
432
|
-
key = OpenSSL::PKey.read(bearer_key_pem)
|
|
433
|
-
key = key.public_key if key.private?
|
|
434
|
-
bearer_key_pem = key.to_pem
|
|
435
|
-
@api_node.update("access_keys/#{access_key_id}", {token_verification_key: bearer_key_pem})
|
|
436
|
-
return Result::Status.new('public key updated')
|
|
437
|
-
end
|
|
438
|
-
when :health
|
|
439
|
-
nagios = Nagios.new
|
|
440
|
-
begin
|
|
441
|
-
info = @api_node.read('info')
|
|
442
|
-
nagios.add_ok('node api', 'accessible')
|
|
443
|
-
nagios.check_time_offset(info['current_time'], 'node api')
|
|
444
|
-
nagios.check_product_version('node api', 'entsrv', info['version'])
|
|
445
|
-
rescue StandardError => e
|
|
446
|
-
nagios.add_critical('node api', e.to_s)
|
|
447
|
-
end
|
|
448
|
-
begin
|
|
449
|
-
@api_node.call(
|
|
450
|
-
operation: 'POST',
|
|
451
|
-
subpath: 'services/soap/Transfer-201210',
|
|
452
|
-
content_type: Mime::TEXT,
|
|
453
|
-
body: CENTRAL_SOAP_API_TEST,
|
|
454
|
-
headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'},
|
|
455
|
-
ret: :resp
|
|
456
|
-
).body
|
|
457
|
-
nagios.add_ok('central', 'accessible by node')
|
|
458
|
-
rescue StandardError => e
|
|
459
|
-
nagios.add_critical('central', e.to_s)
|
|
460
|
-
end
|
|
461
|
-
Result::ObjectList.new(nagios.status_list)
|
|
462
|
-
when :events
|
|
463
|
-
events = @api_node.read('events', query_read_delete)
|
|
464
|
-
return Result::ObjectList.new(events, fields: ->(f){!f.start_with?('data')})
|
|
465
|
-
when :info
|
|
466
|
-
nd_info = @api_node.read('info')
|
|
467
|
-
return Result::SingleObject.new(nd_info)
|
|
468
|
-
when :slash
|
|
469
|
-
nd_info = @api_node.read('')
|
|
470
|
-
return Result::SingleObject.new(nd_info)
|
|
471
|
-
when :license
|
|
472
|
-
# requires: asnodeadmin -mu <node user> --acl-add=internal --internal
|
|
473
|
-
return Result::SingleObject.new(@api_node.read('license'))
|
|
474
|
-
when :api_details
|
|
475
|
-
return Result::SingleObject.new({base_url: @api_node.base_url}.merge(@api_node.params))
|
|
294
|
+
# Resolve a NodeFileId from a path argument.
|
|
295
|
+
# Supports %id:<file_id> syntax (returns NodeFileId for that id) or a plain path.
|
|
296
|
+
# @param top_file_id [String] root file id for path resolution
|
|
297
|
+
# @param path [String] plain path or %id:<file_id> selector
|
|
298
|
+
def apifid_from_path(top_file_id, path)
|
|
299
|
+
if (m = Parser.percent_selector(path))
|
|
300
|
+
Aspera.assert_values(m[:field], ['id'], type: BadArgument) { 'file id' }
|
|
301
|
+
val = m[:value]
|
|
302
|
+
return Api::NodeFileId.new(@api_node, val.nil? || val.empty? ? top_file_id : val)
|
|
476
303
|
end
|
|
304
|
+
@api_node.resolve_api_fid(top_file_id, path)
|
|
477
305
|
end
|
|
478
306
|
|
|
479
|
-
#
|
|
480
|
-
#
|
|
307
|
+
# Legacy helper: resolve a NodeFileId by reading path from the CLI argument stream.
|
|
308
|
+
# Used only by setup_access_key_do_permission and action_access_keys_do_permission
|
|
309
|
+
# (intermediate nodes whose path cannot yet be injected via execute_leaf).
|
|
481
310
|
def apifid_from_next_arg(top_file_id)
|
|
482
311
|
file_path = options.instance_identifier(description: 'path or %id:<id> or %id:') do |attribute, value|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return Api::NodeFileId.new(@api_node, value)
|
|
486
|
-
end
|
|
487
|
-
# There was no selector, so it is a path
|
|
488
|
-
return @api_node.resolve_api_fid(top_file_id, file_path)
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def execute_command_gen4(command_repo, top_file_id)
|
|
492
|
-
override_file_id = options.get_option(:root_id)
|
|
493
|
-
top_file_id = override_file_id unless override_file_id.nil?
|
|
494
|
-
raise Cli::Error, 'Specify root file id with option root_id' if top_file_id.nil?
|
|
495
|
-
case command_repo
|
|
496
|
-
when :v3
|
|
497
|
-
# NOTE: other common actions are unauthorized with user scope
|
|
498
|
-
command_legacy = options.get_next_command(V3_IN_V4_ACTIONS)
|
|
499
|
-
# TODO: shall we support all methods here ? what if there is a link ?
|
|
500
|
-
apifid = @api_node.resolve_api_fid(top_file_id, '')
|
|
501
|
-
return Node.new(context: context, api: apifid.node_api).execute_action(command_legacy)
|
|
502
|
-
when :node_info, :bearer_token_node
|
|
503
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
504
|
-
result = {
|
|
505
|
-
url: apifid.node_api.base_url,
|
|
506
|
-
root_id: apifid.file_id
|
|
507
|
-
}
|
|
508
|
-
case apifid.node_api.auth_params[:type]
|
|
509
|
-
when :basic
|
|
510
|
-
result[:username] = apifid.node_api.auth_params[:username]
|
|
511
|
-
result[:password] = apifid.node_api.auth_params[:password]
|
|
512
|
-
when :oauth2
|
|
513
|
-
result[:username] = apifid.node_api.params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
|
|
514
|
-
result[:password] = apifid.node_api.oauth.authorization
|
|
515
|
-
else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]){'Node API Auth type'}
|
|
516
|
-
end
|
|
517
|
-
return Result::SingleObject.new(result) if command_repo.eql?(:node_info)
|
|
518
|
-
Log.dump(:result, result)
|
|
519
|
-
raise BadArgument, "Cannot get bearer token if authenticating with secret (#{apifid.node_api.auth_params[:type]})" unless apifid.node_api.auth_params[:type].eql?(:oauth2)
|
|
520
|
-
Aspera.assert(OAuth::Factory.bearer_auth?(result[:password]), 'Not using bearer token auth')
|
|
521
|
-
return Result::Text.new(result[:password])
|
|
522
|
-
when :browse
|
|
523
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
524
|
-
file_info = apifid.node_api.read("files/#{apifid.file_id}", headers: Api::Node.add_cache_control)
|
|
525
|
-
# a single file
|
|
526
|
-
return Result::ObjectList.new([file_info], fields: GEN4_LS_FIELDS) unless file_info['type'].eql?('folder')
|
|
527
|
-
return Result::ObjectList.new(apifid.node_api.list_files(apifid.file_id, query: query_read_delete), fields: GEN4_LS_FIELDS)
|
|
528
|
-
when :find
|
|
529
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
530
|
-
find_lambda = Api::Node.file_matcher_from_argument(options)
|
|
531
|
-
return Result::ObjectList.new(@api_node.find_files(apifid.file_id, find_lambda), fields: ['path'])
|
|
532
|
-
when :mkdir, :mklink, :mkfile
|
|
533
|
-
containing_folder_path, new_item = Api::Node.split_folder(options.get_next_argument('path'))
|
|
534
|
-
apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
|
|
535
|
-
query = options.get_option(:query)
|
|
536
|
-
check_exists = true
|
|
537
|
-
payload = {name: new_item}
|
|
538
|
-
if query
|
|
539
|
-
check_exists = !query.delete('check').eql?(false)
|
|
540
|
-
target = query.delete('target')
|
|
541
|
-
if target
|
|
542
|
-
target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
|
|
543
|
-
payload[:target_id] = target_apifid.file_id
|
|
544
|
-
end
|
|
545
|
-
payload.merge!(query.symbolize_keys)
|
|
546
|
-
end
|
|
547
|
-
if check_exists
|
|
548
|
-
folder_content = apifid.node_api.read("files/#{apifid.file_id}/files")
|
|
549
|
-
link_name = ".#{new_item}.asp-lnk"
|
|
550
|
-
found = folder_content.find{ |i| i['name'].eql?(new_item) || i['name'].eql?(link_name)}
|
|
551
|
-
raise "A #{found['type']} already exists with name #{new_item}" if found
|
|
552
|
-
end
|
|
553
|
-
case command_repo
|
|
554
|
-
when :mkdir
|
|
555
|
-
payload[:type] = :folder
|
|
556
|
-
when :mklink
|
|
557
|
-
payload[:type] = :link
|
|
558
|
-
Aspera.assert(payload[:target_id], 'Missing target_id')
|
|
559
|
-
Aspera.assert(payload[:target_node_id], 'Missing target_node_id')
|
|
560
|
-
when :mkfile
|
|
561
|
-
payload[:type] = :file
|
|
562
|
-
payload[:contents] = Base64.strict_encode64(options.get_next_argument('contents'))
|
|
563
|
-
end
|
|
564
|
-
result = apifid.node_api.create("files/#{apifid.file_id}/files", payload)
|
|
565
|
-
return Result::SingleObject.new(result)
|
|
566
|
-
when :rename
|
|
567
|
-
file_path = options.get_next_argument('source path')
|
|
568
|
-
apifid = @api_node.resolve_api_fid(top_file_id, file_path)
|
|
569
|
-
newname = options.get_next_argument('new name')
|
|
570
|
-
result = apifid.node_api.update("files/#{apifid.file_id}", {name: newname})
|
|
571
|
-
return Result::Status.new("renamed to #{newname}")
|
|
572
|
-
when :delete
|
|
573
|
-
return do_bulk_operation(command: command_repo, descr: 'path', values: String, id_result: 'path') do |l_path|
|
|
574
|
-
apifid = if (m = Manager.percent_selector(l_path))
|
|
575
|
-
Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
|
|
576
|
-
Api::NodeFileId.new(@api_node, m[:value])
|
|
577
|
-
else
|
|
578
|
-
@api_node.resolve_api_fid(top_file_id, l_path)
|
|
579
|
-
end
|
|
580
|
-
result = apifid.node_api.delete("files/#{apifid.file_id}")
|
|
581
|
-
{'path' => l_path}
|
|
582
|
-
end
|
|
583
|
-
when :sync
|
|
584
|
-
return execute_sync_action do |sync_direction, _local_path, remote_path|
|
|
585
|
-
# Gen4 API
|
|
586
|
-
Aspera.assert_values(sync_direction, %i[push pull bidi])
|
|
587
|
-
ts_direction = case sync_direction
|
|
588
|
-
when :push, :bidi then Transfer::Spec::DIRECTION_SEND
|
|
589
|
-
when :pull then Transfer::Spec::DIRECTION_RECEIVE
|
|
590
|
-
else Aspera.error_unreachable_line
|
|
591
|
-
end
|
|
592
|
-
# remote is specified by option: `to_folder`
|
|
593
|
-
apifid = @api_node.resolve_api_fid(top_file_id, remote_path)
|
|
594
|
-
apifid.node_api.transfer_spec_gen4(apifid.file_id, ts_direction)
|
|
595
|
-
end
|
|
596
|
-
when :upload
|
|
597
|
-
apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
|
|
598
|
-
return Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_SEND)))
|
|
599
|
-
when :download
|
|
600
|
-
apifid, source_paths = @api_node.resolve_api_fid_paths(top_file_id, transfer.ts_source_paths)
|
|
601
|
-
return Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
|
|
602
|
-
when :cat
|
|
603
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
604
|
-
http = apifid.node_api.read("files/#{apifid.file_id}/content", ret: :resp)
|
|
605
|
-
return Result::Text.new(http.body)
|
|
606
|
-
when :show
|
|
607
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
608
|
-
items = apifid.node_api.read("files/#{apifid.file_id}")
|
|
609
|
-
return Result::SingleObject.new(items)
|
|
610
|
-
when :modify
|
|
611
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
612
|
-
update_param = options.get_next_argument('update data', validation: Hash)
|
|
613
|
-
apifid.node_api.update("files/#{apifid.file_id}", update_param)
|
|
614
|
-
return Result::Status.new('Done')
|
|
615
|
-
when :thumbnail
|
|
616
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
617
|
-
http = apifid.node_api.read("files/#{apifid.file_id}/preview", headers: {'Accept' => 'image/png'}, ret: :resp)
|
|
618
|
-
return Result::Image.new(http.body)
|
|
619
|
-
when :permission
|
|
620
|
-
apifid = apifid_from_next_arg(top_file_id)
|
|
621
|
-
command_perm = options.get_next_command(%i[list show create delete modify])
|
|
622
|
-
case command_perm
|
|
623
|
-
when :modify
|
|
624
|
-
apifid.node_api.update("permissions/#{options.instance_identifier}", value_create_modify(command: 'permission modify'))
|
|
625
|
-
return Result::Status.new('Updated')
|
|
626
|
-
when :list
|
|
627
|
-
list_query = query_read_delete(default: Rest.php_style({'include' => %w[access_level permission_count]}))
|
|
628
|
-
# Specify file to get permissions for unless not specified (then, get all permissions)
|
|
629
|
-
list_query['file_id'] = apifid.file_id unless apifid.file_id.to_s.empty?
|
|
630
|
-
list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
|
|
631
|
-
items = apifid.node_api.read_with_pages('permissions', list_query)
|
|
632
|
-
return Result::ObjectList.new(items)
|
|
633
|
-
when :show
|
|
634
|
-
return Result::SingleObject.new(apifid.node_api.read("permissions/#{options.instance_identifier}"))
|
|
635
|
-
when :delete
|
|
636
|
-
return do_bulk_operation(command: command_perm, values: :identifier) do |one_id|
|
|
637
|
-
apifid.node_api.delete("permissions/#{one_id}")
|
|
638
|
-
# notify application of deletion
|
|
639
|
-
the_app = apifid.node_api.app_info
|
|
640
|
-
the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
|
|
641
|
-
{'id' => one_id}
|
|
642
|
-
end
|
|
643
|
-
when :create
|
|
644
|
-
create_param = options.get_next_argument('creation data', validation: Hash)
|
|
645
|
-
raise Cli::BadArgument, 'no file_id' if create_param.key?('file_id')
|
|
646
|
-
create_param['file_id'] = apifid.file_id
|
|
647
|
-
create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
|
|
648
|
-
# add application specific tags (AoC)
|
|
649
|
-
the_app = apifid.node_api.app_info
|
|
650
|
-
the_app&.api&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
|
|
651
|
-
# create permission
|
|
652
|
-
created_data = apifid.node_api.create('permissions', create_param)
|
|
653
|
-
# notify application of creation
|
|
654
|
-
the_app&.api&.permissions_send_event(event_data: created_data, app_info: the_app)
|
|
655
|
-
return Result::SingleObject.new(created_data)
|
|
656
|
-
else Aspera.error_unreachable_line
|
|
657
|
-
end
|
|
658
|
-
else Aspera.error_unreachable_line
|
|
312
|
+
Aspera.assert_values(attribute, ['id'], type: BadArgument) { 'file id' }
|
|
313
|
+
return Api::NodeFileId.new(@api_node, value.nil? || value.empty? ? top_file_id : value)
|
|
659
314
|
end
|
|
660
|
-
|
|
315
|
+
@api_node.resolve_api_fid(top_file_id, file_path)
|
|
661
316
|
end
|
|
662
317
|
|
|
663
318
|
# Search /async by name
|
|
@@ -666,81 +321,21 @@ module Aspera
|
|
|
666
321
|
# @return [Integer] id of the sync
|
|
667
322
|
# @raise [Cli::BadArgument] if no such sync, or not by name
|
|
668
323
|
def async_lookup(field, value)
|
|
669
|
-
|
|
324
|
+
Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
|
|
670
325
|
async_ids = @api_node.read('async/list')['sync_ids']
|
|
671
326
|
summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
|
|
672
|
-
selected = summaries.find{ |s| s['name'].eql?(value)}
|
|
327
|
+
selected = summaries.find { |s| s['name'].eql?(value) }
|
|
673
328
|
raise Cli::BadIdentifier.new('sync', value, field: field) if selected.nil?
|
|
674
329
|
return selected['snid']
|
|
675
330
|
end
|
|
676
331
|
|
|
677
|
-
#
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
async_ids = @api_node.read('async/list')['sync_ids']
|
|
685
|
-
else
|
|
686
|
-
Integer(async_id) # must be integer
|
|
687
|
-
async_ids = [async_id]
|
|
688
|
-
end
|
|
689
|
-
post_data = {'syncs' => async_ids}
|
|
690
|
-
end
|
|
691
|
-
case command
|
|
692
|
-
when :list
|
|
693
|
-
resp = @api_node.read('async/list')['sync_ids']
|
|
694
|
-
return Result::ValueList.new(resp)
|
|
695
|
-
when :show
|
|
696
|
-
resp = @api_node.create('async/summary', post_data)['sync_summaries']
|
|
697
|
-
return Result::Empty.new if resp.empty?
|
|
698
|
-
return Result::ObjectList.new(resp, fields: %w[snid name local_dir remote_dir]) if async_id.eql?(SpecialValues::ALL)
|
|
699
|
-
return Result::SingleObject.new(resp.first)
|
|
700
|
-
when :delete
|
|
701
|
-
resp = @api_node.create('async/delete', post_data)
|
|
702
|
-
return Result::SingleObject.new(resp)
|
|
703
|
-
when :bandwidth
|
|
704
|
-
post_data['seconds'] = 100 # TODO: as parameter with --query
|
|
705
|
-
resp = @api_node.create('async/bandwidth', post_data)
|
|
706
|
-
data = resp['bandwidth_data']
|
|
707
|
-
return Result::Empty.new if data.empty?
|
|
708
|
-
data = data.first[async_id]['data']
|
|
709
|
-
return Result::ObjectList.new(data)
|
|
710
|
-
when :files
|
|
711
|
-
# count int
|
|
712
|
-
# filename str
|
|
713
|
-
# skip int
|
|
714
|
-
# status int
|
|
715
|
-
filter = options.get_option(:query)
|
|
716
|
-
post_data.merge!(filter) unless filter.nil?
|
|
717
|
-
resp = @api_node.create('async/files', post_data)
|
|
718
|
-
data = resp['sync_files']
|
|
719
|
-
data = data.first[async_id] unless data.empty?
|
|
720
|
-
iteration_data = []
|
|
721
|
-
skip_ids_persistency = nil
|
|
722
|
-
if options.get_option(:once_only, mandatory: true)
|
|
723
|
-
skip_ids_persistency = PersistencyActionOnce.new(
|
|
724
|
-
manager: persistency,
|
|
725
|
-
data: iteration_data,
|
|
726
|
-
id: IdGenerator.from_list(
|
|
727
|
-
'sync_files',
|
|
728
|
-
options.get_option(:url, mandatory: true),
|
|
729
|
-
options.get_option(:username, mandatory: true),
|
|
730
|
-
async_id
|
|
731
|
-
)
|
|
732
|
-
)
|
|
733
|
-
data.select!{ |l| l['fnid'].to_i > iteration_data.first} unless iteration_data.first.nil?
|
|
734
|
-
iteration_data[0] = data.last['fnid'].to_i unless data.empty?
|
|
735
|
-
end
|
|
736
|
-
return Result::Empty.new if data.empty?
|
|
737
|
-
skip_ids_persistency&.save
|
|
738
|
-
return Result::ObjectList.new(data)
|
|
739
|
-
when :counters
|
|
740
|
-
resp = @api_node.create('async/counters', post_data)['sync_counters'].first[async_id].last
|
|
741
|
-
return Result::Empty.new if resp.nil?
|
|
742
|
-
return Result::SingleObject.new(resp)
|
|
743
|
-
end
|
|
332
|
+
# Lookup for access_keys: only supports %id:self selector.
|
|
333
|
+
# @param field [String] must be 'id'
|
|
334
|
+
# @param value [String] must be 'self'
|
|
335
|
+
# @return [String] the resolved access key id
|
|
336
|
+
def lookup_access_key_self_id(field, value, **)
|
|
337
|
+
Aspera.assert(field.eql?('id') && value.eql?('self'), type: BadArgument) { 'only selector: %id:self' }
|
|
338
|
+
@api_node.read('access_keys/self')['id']
|
|
744
339
|
end
|
|
745
340
|
|
|
746
341
|
# Search /asyncs by name
|
|
@@ -749,7 +344,7 @@ module Aspera
|
|
|
749
344
|
# @return [Integer] id of the sync
|
|
750
345
|
# @raise [Cli::BadArgument] if no such sync, or not by name
|
|
751
346
|
def ssync_lookup(field, value)
|
|
752
|
-
|
|
347
|
+
Aspera.assert_values(field, ['name'], type: Cli::BadArgument) { 'search field' }
|
|
753
348
|
@api_node.read('asyncs')['ids'].each do |id|
|
|
754
349
|
sync_info = @api_node.read("asyncs/#{id}")['configuration']
|
|
755
350
|
# name is unique, so we can return
|
|
@@ -758,376 +353,929 @@ module Aspera
|
|
|
758
353
|
raise Cli::BadIdentifier.new('ssync', value, field: field)
|
|
759
354
|
end
|
|
760
355
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
356
|
+
# --- DSL command declarations ---
|
|
357
|
+
|
|
358
|
+
# Gen3 leaf commands — metadata from COMMANDS_GEN3_SPEC; action: added where node-specific.
|
|
359
|
+
# :sync is declared separately (intermediate node with sub-commands).
|
|
360
|
+
GEN3_NODE_ACTIONS = {
|
|
361
|
+
ls: ->(path:, **) { browse_gen3(path) },
|
|
362
|
+
transport: ->(**) { Result::SingleObject.new(@api_node.transport_params) },
|
|
363
|
+
spec: ->(**) { Result::SingleObject.new(@api_node.base_spec, fields: Formatter.all_but(Transfer::Spec::SPECIFIC)) },
|
|
364
|
+
api_details: ->(**) { Result::SingleObject.new({base_url: @api_node.base_url}.merge(@api_node.params)) },
|
|
365
|
+
events: ->(**) { Result::ObjectList.new(@api_node.read('events', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'events'))), fields: ->(f) { !f.start_with?('data') }) },
|
|
366
|
+
info: ->(**) { Result::SingleObject.new(@api_node.read('info')) },
|
|
367
|
+
slash: ->(**) { Result::SingleObject.new(@api_node.read('')) },
|
|
368
|
+
license: ->(**) { Result::SingleObject.new(@api_node.read('license')) }
|
|
369
|
+
}.freeze
|
|
370
|
+
private_constant :GEN3_NODE_ACTIONS
|
|
371
|
+
COMMANDS_GEN3_SPEC.each do |cmd, spec|
|
|
372
|
+
next if cmd.eql?(:sync) # intermediate node with sub-commands
|
|
373
|
+
next if cmd.eql?(:access_keys) # intermediate node declared separately below
|
|
374
|
+
action = GEN3_NODE_ACTIONS[cmd]
|
|
375
|
+
command cmd, **spec, **(action ? {action: action} : {})
|
|
376
|
+
end
|
|
377
|
+
command :sync, description: 'Synchronize folders (Gen3)'
|
|
378
|
+
commands_under :sync do
|
|
379
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
380
|
+
command dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
|
|
381
|
+
end
|
|
382
|
+
command :admin, description: 'Manage sync database (admin operations)'
|
|
383
|
+
SyncActions.register_sync_admin_commands(self, :admin)
|
|
384
|
+
end
|
|
385
|
+
# access_keys sub-tree
|
|
386
|
+
command :access_keys, description: 'Manage access keys'
|
|
387
|
+
commands_under :access_keys do
|
|
388
|
+
command :do, description: 'Execute Gen4 command via access key',
|
|
389
|
+
arguments: [{name: :access_key_id, type: :identifier}],
|
|
390
|
+
setup: :setup_access_key_do
|
|
391
|
+
command :set_bearer_key, description: 'Set bearer key on access key',
|
|
392
|
+
arguments: [{name: :access_key_id}, {name: :bearer_key_pem, type: String}]
|
|
393
|
+
crud_commands entity: 'access_keys',
|
|
394
|
+
api: :@api_node,
|
|
395
|
+
|
|
396
|
+
body_component: Schema::Registry::NODE,
|
|
397
|
+
lookup: :lookup_access_key_self_id
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
commands_under %i[access_keys do] do
|
|
401
|
+
COMMANDS_GEN4_SPEC.each do |cmd, spec|
|
|
402
|
+
command cmd, **spec
|
|
403
|
+
end
|
|
404
|
+
command :permission, description: 'Manage permissions', setup: :setup_access_key_do_permission
|
|
405
|
+
command :sync, description: 'Synchronize folders'
|
|
406
|
+
commands_under :sync do
|
|
407
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
408
|
+
command dir, description: "#{dir.capitalize}-sync", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
|
|
409
|
+
end
|
|
410
|
+
command :admin, description: 'Manage sync database (admin operations)'
|
|
411
|
+
SyncActions.register_sync_admin_commands(self, :admin)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
commands_under %i[access_keys do permission] do
|
|
415
|
+
command :list, description: 'List permissions on a file'
|
|
416
|
+
command :show, description: 'Show a permission',
|
|
417
|
+
arguments: [{name: :perm_id, type: :identifier}],
|
|
418
|
+
action: ->(apifid:, perm_id:, **) { Result::SingleObject.new(apifid.node_api.read("permissions/#{perm_id}")) }
|
|
419
|
+
command :create, description: 'Create a permission',
|
|
420
|
+
arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.permissions-post-request'}]
|
|
421
|
+
command :modify, description: 'Modify a permission',
|
|
422
|
+
arguments: [{name: :perm_id, type: :identifier}, {name: :data, type: Hash, schema: 'node:components.schemas.permissions-id-put-request'}]
|
|
423
|
+
command :delete, description: 'Delete permissions',
|
|
424
|
+
arguments: [{name: :perm_id, bulk: true}]
|
|
425
|
+
end
|
|
426
|
+
# async (legacy /async)
|
|
427
|
+
commands_under :async, description: 'synchronization (legacy /async)' do
|
|
428
|
+
command :list, description: 'List async sync IDs', action: -> { Result::ValueList.new(@api_node.read('async/list')['sync_ids']) }
|
|
429
|
+
command :show, description: 'Show async summary',
|
|
430
|
+
arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
|
|
431
|
+
command :delete, description: 'Delete async',
|
|
432
|
+
arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
|
|
433
|
+
command :bandwidth, description: 'Show async bandwidth',
|
|
434
|
+
arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
|
|
435
|
+
command :files, description: 'List async files',
|
|
436
|
+
arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
|
|
437
|
+
command :counters, description: 'Show async counters',
|
|
438
|
+
arguments: [{name: :async_id, type: :identifier, lookup: :async_lookup}]
|
|
439
|
+
end
|
|
440
|
+
# ssync (/asyncs)
|
|
441
|
+
# command :ssync, description: 'Manage sync operations (/asyncs)'
|
|
442
|
+
commands_under :ssync, description: 'synchronization (/asyncs)' do
|
|
443
|
+
crud_commands entity: 'asyncs',
|
|
444
|
+
api: :@api_node,
|
|
445
|
+
operations: %i[create list show delete],
|
|
446
|
+
items_key: 'ids',
|
|
447
|
+
lookup: :ssync_lookup
|
|
448
|
+
command :start, description: 'Start a sync',
|
|
449
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
450
|
+
action: :action_ssync_start
|
|
451
|
+
command :stop, description: 'Stop a sync',
|
|
452
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
453
|
+
action: :action_ssync_stop
|
|
454
|
+
command :bandwidth, description: 'Show sync bandwidth',
|
|
455
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
456
|
+
action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/bandwidth", options.get_option(:query) || {})) }
|
|
457
|
+
command :counters, description: 'Show sync counters',
|
|
458
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
459
|
+
action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/counters", options.get_option(:query) || {})) }
|
|
460
|
+
command :files, description: 'List sync files',
|
|
461
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
462
|
+
action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/files", options.get_option(:query) || {})) }
|
|
463
|
+
command :state, description: 'Show sync state',
|
|
464
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
465
|
+
action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/state")) }
|
|
466
|
+
command :summary, description: 'Show sync summary',
|
|
467
|
+
arguments: [{name: :ssync_id, type: :identifier, lookup: :ssync_lookup}],
|
|
468
|
+
action: ->(ssync_id:, **) { Result::SingleObject.new(@api_node.read("asyncs/#{ssync_id}/summary")) }
|
|
469
|
+
end
|
|
470
|
+
# stream
|
|
471
|
+
command :stream, description: 'Manage stream operations'
|
|
472
|
+
commands_under :stream do
|
|
473
|
+
command :list, description: 'List streams', action: -> { Result::ObjectList.new(@api_node.read('ops/transfers', query_read_delete), fields: %w[id status]) }
|
|
474
|
+
command :create, description: 'Create a stream',
|
|
475
|
+
arguments: [{name: :data, type: Hash, schema: 'node:components.schemas.transferPostRequest'}],
|
|
476
|
+
action: ->(data:, **) { Result::SingleObject.new(@api_node.create('streams', data)) }
|
|
477
|
+
command :show, description: 'Show a stream',
|
|
478
|
+
arguments: [{name: :transfer_id, type: :identifier}],
|
|
479
|
+
action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
|
|
480
|
+
command :modify, description: 'Modify a stream',
|
|
481
|
+
arguments: [{name: :transfer_id, type: :identifier}, {name: :data, type: Hash, schema: 'node:components.schemas.transferPutRequest'}],
|
|
482
|
+
action: ->(data:, transfer_id:, **) { Result::SingleObject.new(@api_node.update("streams/#{transfer_id}", data)) }
|
|
483
|
+
command :cancel, description: 'Cancel a stream',
|
|
484
|
+
arguments: [{name: :transfer_id, type: :identifier}],
|
|
485
|
+
action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.cancel("streams/#{transfer_id}")) }
|
|
486
|
+
end
|
|
487
|
+
# transfer
|
|
488
|
+
command :transfer, description: 'Manage transfer operations'
|
|
489
|
+
commands_under :transfer do
|
|
490
|
+
command :list, description: 'List transfers'
|
|
491
|
+
command :cancel, description: 'Cancel a transfer',
|
|
492
|
+
arguments: [{name: :transfer_id, type: :identifier}]
|
|
493
|
+
command :show, description: 'Show a transfer',
|
|
494
|
+
arguments: [{name: :transfer_id, type: :identifier}],
|
|
495
|
+
action: ->(transfer_id:, **) { Result::SingleObject.new(@api_node.read("ops/transfers/#{transfer_id}")) }
|
|
496
|
+
command :modify, description: 'Modify a transfer',
|
|
497
|
+
arguments: [{name: :transfer_id, type: :identifier}, {name: :update_value, type: Hash, schema: 'node:components.schemas.transferPutRequest'}]
|
|
498
|
+
command :bandwidth_average, description: 'Show average bandwidth per period'
|
|
499
|
+
command :sessions, description: 'List transfer sessions'
|
|
500
|
+
end
|
|
501
|
+
# service
|
|
502
|
+
command :service, description: 'Manage services'
|
|
503
|
+
commands_under :service do
|
|
504
|
+
command :list, description: 'List services', action: -> { Result::ObjectList.new(@api_node.read('rund/services')['services']) }
|
|
505
|
+
command :create, description: 'Create a service',
|
|
506
|
+
arguments: [{name: :creation_data, type: Hash}]
|
|
507
|
+
command :delete, description: 'Delete a service',
|
|
508
|
+
arguments: [{name: :service_id, type: :identifier}]
|
|
509
|
+
end
|
|
510
|
+
# watch_folder
|
|
511
|
+
command :watch_folder, description: 'Manage watch folders', setup: :setup_watch_folder
|
|
512
|
+
commands_under :watch_folder do
|
|
513
|
+
command :create, description: 'Create a watch folder',
|
|
514
|
+
arguments: [{name: :data, type: Hash}],
|
|
515
|
+
action: ->(data:, **) { Result::Status.new("#{@api_node.create('v3/watchfolders', data)['id']} created") }
|
|
516
|
+
command :list, description: 'List watch folders',
|
|
517
|
+
action: -> { Result::ValueList.new(@api_node.read('v3/watchfolders', query_read_delete)['ids']) }
|
|
518
|
+
command :show, description: 'Show a watch folder',
|
|
519
|
+
arguments: [{name: :watch_folder_id, type: :identifier}],
|
|
520
|
+
action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}")) }
|
|
521
|
+
command :modify, description: 'Modify a watch folder',
|
|
522
|
+
arguments: [{name: :watch_folder_id, type: :identifier}, {name: :data, type: Hash}]
|
|
523
|
+
command :delete, description: 'Delete a watch folder',
|
|
524
|
+
arguments: [{name: :watch_folder_id, type: :identifier}]
|
|
525
|
+
command :state, description: 'Show watch folder state',
|
|
526
|
+
arguments: [{name: :watch_folder_id, type: :identifier}],
|
|
527
|
+
action: ->(watch_folder_id:, **) { Result::SingleObject.new(@api_node.read("v3/watchfolders/#{watch_folder_id}/state")) }
|
|
528
|
+
end
|
|
529
|
+
# central
|
|
530
|
+
command :central, description: 'Query Central service'
|
|
531
|
+
commands_under :central do
|
|
532
|
+
command :session, description: 'Query sessions'
|
|
533
|
+
command :file, description: 'Query files'
|
|
534
|
+
end
|
|
535
|
+
commands_under %i[central session] do
|
|
536
|
+
command :list, description: 'List sessions',
|
|
537
|
+
arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
|
|
538
|
+
end
|
|
539
|
+
commands_under %i[central file] do
|
|
540
|
+
command :list, description: 'List file transfers',
|
|
541
|
+
arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
|
|
542
|
+
command :modify, description: 'Modify file transfer validation',
|
|
543
|
+
arguments: [{name: :request_data, type: Hash, mandatory: false, default: nil}]
|
|
544
|
+
end
|
|
545
|
+
# Standalone leaf commands
|
|
546
|
+
command :asperabrowser, description: 'Open Aspera browser'
|
|
547
|
+
command :basic_token, description: 'Generate basic auth token', action: -> { Result::Text.new(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true))) }
|
|
548
|
+
command :bearer_token, description: 'Generate bearer token',
|
|
549
|
+
arguments: [{name: :private_key_pem, type: String}, {name: :user_group_id, type: Hash, schema: 'opts:components.schemas.NodeBearerTokenOptions'}]
|
|
550
|
+
command :simulator, description: 'Start node simulator',
|
|
551
|
+
arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeSimulatorOptions'}]
|
|
552
|
+
command :telemetry, description: 'Report telemetry to external system',
|
|
553
|
+
arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.NodeTelemetryOptions'}]
|
|
554
|
+
|
|
555
|
+
# --- Handler methods (Gen3) ---
|
|
556
|
+
|
|
557
|
+
def action_delete(paths:, **)
|
|
558
|
+
# TODO: add query for recursive
|
|
559
|
+
paths_to_delete = Array(paths)
|
|
560
|
+
resp = @api_node.create('files/delete', {paths: paths_to_delete.map { |i| {'path' => i.start_with?('/') ? i : "/#{i}"} }})
|
|
561
|
+
cli_result_from_paths_response(resp, 'file deleted')
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def action_search(search_root:, **)
|
|
565
|
+
parameters = {'path' => search_root}
|
|
566
|
+
other_options = options.get_option(:query)
|
|
567
|
+
parameters.merge!(other_options) unless other_options.nil?
|
|
568
|
+
resp = @api_node.create('files/search', parameters)
|
|
569
|
+
return Result::Empty.new if resp['items'].empty?
|
|
570
|
+
fields = resp['items'].first.keys.reject { |i| SEARCH_REMOVE_FIELDS.include?(i) }
|
|
571
|
+
formatter.display_item_count(resp['item_count'], resp['total_count'])
|
|
572
|
+
formatter.display_status("params: #{resp['parameters'].keys.map { |k| "#{k}:#{resp['parameters'][k]}" }.join(',')}")
|
|
573
|
+
Result::ObjectList.new(resp['items'], fields: fields)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def action_space(path_list:, **)
|
|
577
|
+
path_list = Array(path_list)
|
|
578
|
+
resp = @api_node.create('space', {'paths' => path_list.map { |i| {path: i} }})
|
|
579
|
+
Result::ObjectList.new(resp['paths'])
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def action_mkdir(path_list:, **)
|
|
583
|
+
path_list = Array(path_list)
|
|
584
|
+
resp = @api_node.create('files/create', {'paths' => path_list.map { |i| {type: :directory, path: i} }})
|
|
585
|
+
cli_result_from_paths_response(resp, 'folder created')
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def action_mklink(target:, link_path:, **)
|
|
589
|
+
resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: link_path, target: {path: target}}]})
|
|
590
|
+
cli_result_from_paths_response(resp, 'link created')
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def action_mkfile(file_path:, contents:, **)
|
|
594
|
+
contents64 = contents.nil? ? '' : Base64.strict_encode64(contents)
|
|
595
|
+
resp = @api_node.create('files/create', {'paths' => [{type: :file, path: file_path, contents: contents64}]})
|
|
596
|
+
cli_result_from_paths_response(resp, 'file created')
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def action_rename(path_base:, path_src:, path_dst:, **)
|
|
600
|
+
# TODO: multiple ?
|
|
601
|
+
resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
|
|
602
|
+
cli_result_from_paths_response(resp, 'entry moved')
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Obtains a transfer spec via the Node API for the given direction/folders.
|
|
606
|
+
def sync_gen3_block
|
|
607
|
+
lambda do |direction, local_path, remote_path|
|
|
608
|
+
request_transfer_spec = sync_spec_request(direction, local_path, remote_path)
|
|
609
|
+
@api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
|
|
610
|
+
transfer_spec = @api_node.create(
|
|
611
|
+
'files/sync_setup',
|
|
612
|
+
{transfer_requests: [{transfer_request: request_transfer_spec}]}
|
|
613
|
+
)['transfer_specs'].first['transfer_spec']
|
|
614
|
+
transfer_spec.delete_if { |_k, v| v.nil? }
|
|
615
|
+
Log.dump(:ts, transfer_spec)
|
|
616
|
+
transfer_spec
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
621
|
+
define_method(:"action_sync_#{dir}") { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen3_block) }
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def action_upload
|
|
625
|
+
# empty transfer spec for authorization request
|
|
626
|
+
request_transfer_spec = {}
|
|
627
|
+
request_transfer_spec[:paths] = [{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
|
|
628
|
+
# add fixed parameters if any (for COS)
|
|
629
|
+
@api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
|
|
630
|
+
Api::Node.add_public_key(request_transfer_spec)
|
|
631
|
+
setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
|
|
632
|
+
transfer_spec = @api_node.create('files/upload_setup', setup_payload)['transfer_specs'].first['transfer_spec']
|
|
633
|
+
Api::Node.add_private_key(transfer_spec)
|
|
634
|
+
transfer_spec.delete('paths')
|
|
635
|
+
Runner.result_transfer(transfer.start(transfer_spec))
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def action_download
|
|
639
|
+
# empty transfer spec for authorization request
|
|
640
|
+
request_transfer_spec = {}
|
|
641
|
+
request_transfer_spec[:paths] = transfer.ts_source_paths
|
|
642
|
+
# add fixed parameters if any (for COS)
|
|
643
|
+
@api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
|
|
644
|
+
Api::Node.add_public_key(request_transfer_spec)
|
|
645
|
+
setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
|
|
646
|
+
transfer_spec = @api_node.create('files/download_setup', setup_payload)['transfer_specs'].first['transfer_spec']
|
|
647
|
+
Api::Node.add_private_key(transfer_spec)
|
|
648
|
+
Runner.result_transfer(transfer.start(transfer_spec))
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def action_cat(remote_path:, **)
|
|
652
|
+
http = @api_node.read("files/#{URI.encode_www_form_component(remote_path)}/contents", ret: :resp)
|
|
653
|
+
Result::Text.new(http.body)
|
|
654
|
+
end
|
|
764
655
|
|
|
765
|
-
def
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
656
|
+
def action_health
|
|
657
|
+
nagios = Nagios.new
|
|
658
|
+
begin
|
|
659
|
+
info = @api_node.read('info')
|
|
660
|
+
nagios.add_ok('node api', 'accessible')
|
|
661
|
+
nagios.check_time_offset(info['current_time'], 'node api')
|
|
662
|
+
nagios.check_product_version('node api', 'entsrv', info['version'])
|
|
663
|
+
rescue StandardError => e
|
|
664
|
+
nagios.add_critical('node api', e.to_s)
|
|
771
665
|
end
|
|
772
|
-
|
|
666
|
+
begin
|
|
667
|
+
@api_node.call(
|
|
668
|
+
operation: 'POST',
|
|
669
|
+
subpath: 'services/soap/Transfer-201210',
|
|
670
|
+
content_type: Mime::TEXT,
|
|
671
|
+
body: CENTRAL_SOAP_API_TEST,
|
|
672
|
+
headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'},
|
|
673
|
+
ret: :resp
|
|
674
|
+
).body
|
|
675
|
+
nagios.add_ok('central', 'accessible by node')
|
|
676
|
+
rescue StandardError => e
|
|
677
|
+
nagios.add_critical('central', e.to_s)
|
|
678
|
+
end
|
|
679
|
+
Result::ObjectList.new(nagios.status_list)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# watch_folder setup: inject required API header (avoids "Unable to convert 2016_09_14 configuration")
|
|
683
|
+
def setup_watch_folder(**)
|
|
773
684
|
@api_node.params[:headers] ||= {}
|
|
774
685
|
@api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
686
|
+
{}
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def action_access_keys_do_permission_modify(data:, apifid:, perm_id:, **)
|
|
690
|
+
apifid.node_api.update("permissions/#{perm_id}", data)
|
|
691
|
+
Result::Status.new('Updated')
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def action_watch_folder_modify(data:, watch_folder_id:, **)
|
|
695
|
+
@api_node.update("v3/watchfolders/#{watch_folder_id}", data)
|
|
696
|
+
Result::Status.new("#{watch_folder_id} updated")
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def action_watch_folder_delete(watch_folder_id:, **)
|
|
700
|
+
@api_node.delete("v3/watchfolders/#{watch_folder_id}")
|
|
701
|
+
Result::Status.new("#{watch_folder_id} deleted")
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# access_keys > do - setup: resolve access key and root file id
|
|
705
|
+
# access_key_id is resolved by execute_leaf (via instance_arg in CommandSpec)
|
|
706
|
+
# @return [Hash] context hash containing :do_root_file_id
|
|
707
|
+
def setup_access_key_do(access_key_id:, **)
|
|
708
|
+
@do_root_file_id = options.get_option(:root_id)
|
|
709
|
+
if @do_root_file_id.nil?
|
|
710
|
+
ak_info = @api_node.read("access_keys/#{access_key_id}")
|
|
711
|
+
ak_secret = context.secret_finder.lookup(url: @api_node.base_url, username: ak_info['id'])
|
|
712
|
+
if !access_key_id.eql?('self')
|
|
713
|
+
Aspera.assert(ak_secret, type: Cli::MissingArgument) { "Please provide secret for #{ak_info['id']} using option: secret or by setting a preset for #{ak_info['id']}@#{@api_node.base_url}." }
|
|
714
|
+
@api_node.auth_params[:username] = ak_info['id']
|
|
715
|
+
@api_node.auth_params[:password] = ak_secret
|
|
716
|
+
end
|
|
717
|
+
@do_root_file_id = ak_info['root_file_id']
|
|
792
718
|
end
|
|
719
|
+
{do_root_file_id: @do_root_file_id}
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# access_keys > do > permission - setup: resolve apifid from CLI path argument
|
|
723
|
+
# (path is read from CLI here as 'permission' is an intermediate node)
|
|
724
|
+
# @return [Hash] context hash containing :apifid
|
|
725
|
+
def setup_access_key_do_permission(**)
|
|
726
|
+
{apifid: apifid_from_next_arg(@do_root_file_id)}
|
|
793
727
|
end
|
|
794
728
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
def
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
729
|
+
# access_keys > do > ls
|
|
730
|
+
def action_access_keys_do_ls(path:, do_root_file_id:, **)
|
|
731
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
732
|
+
file_info = apifid.node_api.read("files/#{apifid.file_id}", headers: Api::Node.add_cache_control)
|
|
733
|
+
return Result::ObjectList.new([file_info], fields: GEN4_LS_FIELDS) unless file_info['type'].eql?('folder')
|
|
734
|
+
Result::ObjectList.new(apifid.node_api.list_files(apifid.file_id, query: query_read_delete), fields: GEN4_LS_FIELDS)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# access_keys > do > find
|
|
738
|
+
def action_access_keys_do_find(path:, filter: nil, do_root_file_id:, **)
|
|
739
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
740
|
+
Result::ObjectList.new(@api_node.find_files(apifid.file_id, Base.file_matcher(filter)), fields: ['path'])
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# access_keys > do > cat
|
|
744
|
+
def action_access_keys_do_cat(path:, do_root_file_id:, **)
|
|
745
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
746
|
+
Result::Text.new(apifid.node_api.read("files/#{apifid.file_id}/content", ret: :resp).body)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# access_keys > do > show
|
|
750
|
+
def action_access_keys_do_show(path:, do_root_file_id:, **)
|
|
751
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
752
|
+
Result::SingleObject.new(apifid.node_api.read("files/#{apifid.file_id}"))
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# access_keys > do > modify
|
|
756
|
+
def action_access_keys_do_modify(path:, update_value:, do_root_file_id:, **)
|
|
757
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
758
|
+
apifid.node_api.update("files/#{apifid.file_id}", update_value)
|
|
759
|
+
Result::Status.new('Done')
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# access_keys > do > thumbnail
|
|
763
|
+
def action_access_keys_do_thumbnail(path:, do_root_file_id:, **)
|
|
764
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
765
|
+
Result::Image.new(apifid.node_api.read("files/#{apifid.file_id}/preview", headers: {'Accept' => 'image/png'}, ret: :resp).body)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# access_keys > do > rename
|
|
769
|
+
def action_access_keys_do_rename(source_path:, new_name:, do_root_file_id:, **)
|
|
770
|
+
apifid = @api_node.resolve_api_fid(do_root_file_id, source_path)
|
|
771
|
+
apifid.node_api.update("files/#{apifid.file_id}", {name: new_name})
|
|
772
|
+
Result::Status.new("renamed to #{new_name}")
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# access_keys > do > delete
|
|
776
|
+
def action_access_keys_do_delete(paths:, do_root_file_id:, **)
|
|
777
|
+
bulk_result(paths, command: :delete, id_result: 'path') do |l_path|
|
|
778
|
+
apifid = if (m = Parser.percent_selector(l_path))
|
|
779
|
+
Aspera.assert_values(m[:field], ['id'], type: BadIdentifier)
|
|
780
|
+
Api::NodeFileId.new(@api_node, m[:value])
|
|
826
781
|
else
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
782
|
+
@api_node.resolve_api_fid(do_root_file_id, l_path)
|
|
783
|
+
end
|
|
784
|
+
apifid.node_api.delete("files/#{apifid.file_id}")
|
|
785
|
+
{'path' => l_path}
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Shared Gen4 sync block: obtains a transfer spec via the Gen4 API for the given direction/remote_path.
|
|
790
|
+
def sync_gen4_block(do_root_file_id)
|
|
791
|
+
lambda do |direction, _local_path, remote_path|
|
|
792
|
+
ts_direction = direction.eql?(:pull) ? Transfer::Spec::DIRECTION_RECEIVE : Transfer::Spec::DIRECTION_SEND
|
|
793
|
+
apifid = @api_node.resolve_api_fid(do_root_file_id, remote_path)
|
|
794
|
+
apifid.node_api.transfer_spec_gen4(apifid.file_id, ts_direction)
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
799
|
+
define_method(:"action_access_keys_do_sync_#{dir}") do |path:, sync_info: {}, do_root_file_id:, **|
|
|
800
|
+
run_sync_transfer(dir, path: path, sync_info: sync_info, &sync_gen4_block(do_root_file_id))
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# access_keys > do > upload
|
|
805
|
+
def action_access_keys_do_upload(do_root_file_id:, **)
|
|
806
|
+
apifid = @api_node.resolve_api_fid(do_root_file_id, transfer.destination_folder(Transfer::Spec::DIRECTION_SEND), true)
|
|
807
|
+
Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_SEND)))
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
# access_keys > do > download
|
|
811
|
+
def action_access_keys_do_download(do_root_file_id:, **)
|
|
812
|
+
apifid, source_paths = @api_node.resolve_api_fid_paths(do_root_file_id, transfer.ts_source_paths)
|
|
813
|
+
Runner.result_transfer(transfer.start(apifid.node_api.transfer_spec_gen4(apifid.file_id, Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# access_keys > do > permission (intermediate node: re-enter DSL at permission sub-tree)
|
|
817
|
+
def action_access_keys_do_permission(do_root_file_id:, **)
|
|
818
|
+
apifid = apifid_from_next_arg(do_root_file_id)
|
|
819
|
+
dispatch_from_registry(%i[access_keys do permission], {apifid: apifid}, skip_setup: true)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# access_keys > do > v3
|
|
823
|
+
def action_access_keys_do_v3(do_root_file_id:, **)
|
|
824
|
+
# Build a fresh Node for the v3 root, then let the DSL dispatcher consume the next command.
|
|
825
|
+
v3_node = Node.new(context: context, api: @api_node.resolve_api_fid(do_root_file_id, '').node_api)
|
|
826
|
+
v3_node.execute_action
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# access_keys > do > node_info / bearer_token_node — shared helper builds the result hash
|
|
830
|
+
def gen4_apifid_info(do_root_file_id, path)
|
|
831
|
+
apifid = apifid_from_path(do_root_file_id, path)
|
|
832
|
+
result = {url: apifid.node_api.base_url, root_id: apifid.file_id}
|
|
833
|
+
case apifid.node_api.auth_params[:type]
|
|
834
|
+
when :basic
|
|
835
|
+
result[:username] = apifid.node_api.auth_params[:username]
|
|
836
|
+
result[:password] = apifid.node_api.auth_params[:password]
|
|
837
|
+
when :oauth2
|
|
838
|
+
result[:username] = apifid.node_api.params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
|
|
839
|
+
result[:password] = apifid.node_api.oauth.authorization
|
|
840
|
+
else Aspera.error_unexpected_value(apifid.node_api.auth_params[:type]) { 'Node API Auth type' }
|
|
841
|
+
end
|
|
842
|
+
[apifid, result]
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def action_access_keys_do_node_info(path:, do_root_file_id:, **)
|
|
846
|
+
_apifid, result = gen4_apifid_info(do_root_file_id, path)
|
|
847
|
+
Result::SingleObject.new(result)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def action_access_keys_do_bearer_token_node(path:, do_root_file_id:, **)
|
|
851
|
+
apifid, result = gen4_apifid_info(do_root_file_id, path)
|
|
852
|
+
Log.dump(:result, result)
|
|
853
|
+
Aspera.assert(apifid.node_api.auth_params[:type].eql?(:oauth2), type: BadArgument) { "Cannot get bearer token if authenticating with secret (#{apifid.node_api.auth_params[:type]})" }
|
|
854
|
+
Aspera.assert(OAuth::Factory.bearer_auth?(result[:password]), 'Not using bearer token auth')
|
|
855
|
+
Result::Text.new(result[:password])
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# access_keys > do > mkdir / mklink / mkfile — shared helper: resolve parent folder,
|
|
859
|
+
# build payload from query option, optionally check for name collision.
|
|
860
|
+
# @return [Array(NodeFileId, Hash)] parent apifid and payload with :name set
|
|
861
|
+
# @param path [String] full path (folder/name)
|
|
862
|
+
def gen4_mk_resolve(top_file_id, path)
|
|
863
|
+
containing_folder_path, new_item = Api::Node.split_folder(path)
|
|
864
|
+
apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path, true)
|
|
865
|
+
query = options.get_option(:query)
|
|
866
|
+
check_exists = true
|
|
867
|
+
payload = {name: new_item}
|
|
868
|
+
if query
|
|
869
|
+
check_exists = !query.delete('check').eql?(false)
|
|
870
|
+
target = query.delete('target')
|
|
871
|
+
if target
|
|
872
|
+
target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
|
|
873
|
+
payload[:target_id] = target_apifid.file_id
|
|
840
874
|
end
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
875
|
+
payload.merge!(query.symbolize_keys)
|
|
876
|
+
end
|
|
877
|
+
if check_exists
|
|
878
|
+
folder_content = apifid.node_api.read("files/#{apifid.file_id}/files")
|
|
879
|
+
link_name = ".#{new_item}.asp-lnk"
|
|
880
|
+
found = folder_content.find { |i| i['name'].eql?(new_item) || i['name'].eql?(link_name) }
|
|
881
|
+
Aspera.assert(!found, type: Cli::Error) { "A #{found['type']} already exists with name #{new_item}" }
|
|
882
|
+
end
|
|
883
|
+
[apifid, payload]
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def action_access_keys_do_mkdir(path:, do_root_file_id:, **)
|
|
887
|
+
apifid, payload = gen4_mk_resolve(do_root_file_id, path)
|
|
888
|
+
payload[:type] = :folder
|
|
889
|
+
Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def action_access_keys_do_mklink(path:, do_root_file_id:, **)
|
|
893
|
+
apifid, payload = gen4_mk_resolve(do_root_file_id, path)
|
|
894
|
+
payload[:type] = :link
|
|
895
|
+
Aspera.assert(payload[:target_id], 'Missing target_id')
|
|
896
|
+
Aspera.assert(payload[:target_node_id], 'Missing target_node_id')
|
|
897
|
+
Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def action_access_keys_do_mkfile(path:, contents:, do_root_file_id:, **)
|
|
901
|
+
apifid, payload = gen4_mk_resolve(do_root_file_id, path)
|
|
902
|
+
payload[:type] = :file
|
|
903
|
+
payload[:contents] = contents.nil? ? '' : Base64.strict_encode64(contents)
|
|
904
|
+
Result::SingleObject.new(apifid.node_api.create("files/#{apifid.file_id}/files", payload))
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# access_keys > do > permission > list/show/create/modify/delete
|
|
908
|
+
def action_access_keys_do_permission_list(apifid:, **)
|
|
909
|
+
list_query = query_read_delete(default: Rest.php_style({'include' => %w[access_level permission_count]}))
|
|
910
|
+
# Specify file to get permissions for unless not specified (then, get all permissions)
|
|
911
|
+
list_query['file_id'] = apifid.file_id unless apifid.file_id.to_s.empty?
|
|
912
|
+
list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
|
|
913
|
+
Result::ObjectList.new(apifid.node_api.read_with_pages('permissions', list_query))
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def action_access_keys_do_permission_delete(perm_id:, apifid:, **)
|
|
917
|
+
bulk_result(perm_id, command: :delete) do |one_id|
|
|
918
|
+
apifid.node_api.delete("permissions/#{one_id}")
|
|
919
|
+
the_app = apifid.node_api.app_info
|
|
920
|
+
the_app&.api&.permissions_send_event(event_data: {}, app_info: the_app, types: ['permission.deleted'])
|
|
921
|
+
{'id' => one_id}
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def action_access_keys_do_permission_create(data:, apifid:, **)
|
|
926
|
+
create_param = data
|
|
927
|
+
Aspera.assert(!create_param.key?('file_id'), type: Cli::BadArgument) { 'no file_id' }
|
|
928
|
+
create_param['file_id'] = apifid.file_id
|
|
929
|
+
create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
|
|
930
|
+
the_app = apifid.node_api.app_info
|
|
931
|
+
the_app&.api&.permissions_set_create_params(perm_data: create_param, app_info: the_app)
|
|
932
|
+
created_data = apifid.node_api.create('permissions', create_param)
|
|
933
|
+
the_app&.api&.permissions_send_event(event_data: created_data, app_info: the_app)
|
|
934
|
+
Result::SingleObject.new(created_data)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# access_keys > set_bearer_key
|
|
938
|
+
def action_access_keys_set_bearer_key(access_key_id:, bearer_key_pem:, **)
|
|
939
|
+
access_key_id = @api_node.read('access_keys/self')['id'] if access_key_id.eql?('self')
|
|
940
|
+
key = OpenSSL::PKey.read(bearer_key_pem)
|
|
941
|
+
key = key.public_key if key.private?
|
|
942
|
+
@api_node.update("access_keys/#{access_key_id}", {token_verification_key: key.to_pem})
|
|
943
|
+
Result::Status.new('public key updated')
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# async sub-commands: individual handlers
|
|
947
|
+
def action_async_show(async_id:, **)
|
|
948
|
+
async_ids = @api_node.read('async/list')['sync_ids']
|
|
949
|
+
if async_id.eql?(SpecialValues::ALL)
|
|
950
|
+
resp = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
|
|
951
|
+
return Result::Empty.new if resp.empty?
|
|
952
|
+
return Result::ObjectList.new(resp, fields: %w[snid name local_dir remote_dir])
|
|
953
|
+
end
|
|
954
|
+
Integer(async_id)
|
|
955
|
+
resp = @api_node.create('async/summary', {'syncs' => [async_id]})['sync_summaries']
|
|
956
|
+
return Result::Empty.new if resp.empty?
|
|
957
|
+
Result::SingleObject.new(resp.first)
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def action_async_delete(async_id:, **)
|
|
961
|
+
async_ids = async_id.eql?(SpecialValues::ALL) ? @api_node.read('async/list')['sync_ids'] : [async_id]
|
|
962
|
+
Result::SingleObject.new(@api_node.create('async/delete', {'syncs' => async_ids}))
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def action_async_bandwidth(async_id:, **)
|
|
966
|
+
Integer(async_id)
|
|
967
|
+
post_data = {'syncs' => [async_id], 'seconds' => 100}
|
|
968
|
+
resp = @api_node.create('async/bandwidth', post_data)
|
|
969
|
+
data = resp['bandwidth_data']
|
|
970
|
+
return Result::Empty.new if data.empty?
|
|
971
|
+
Result::ObjectList.new(data.first[async_id]['data'])
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def action_async_files(async_id:, **)
|
|
975
|
+
Integer(async_id)
|
|
976
|
+
post_data = {'syncs' => [async_id]}
|
|
977
|
+
filter = options.get_option(:query)
|
|
978
|
+
post_data.merge!(filter) unless filter.nil?
|
|
979
|
+
resp = @api_node.create('async/files', post_data)
|
|
980
|
+
data = resp['sync_files']
|
|
981
|
+
data = data.first[async_id] unless data.empty?
|
|
982
|
+
iteration_data = []
|
|
983
|
+
skip_ids_persistency = nil
|
|
984
|
+
if options.get_option(:once_only, mandatory: true)
|
|
985
|
+
skip_ids_persistency = PersistencyActionOnce.new(
|
|
986
|
+
manager: persistency,
|
|
987
|
+
data: iteration_data,
|
|
988
|
+
id: IdGenerator.from_list('sync_files', options.get_option(:url, mandatory: true), options.get_option(:username, mandatory: true), async_id)
|
|
989
|
+
)
|
|
990
|
+
data.select! { |l| l['fnid'].to_i > iteration_data.first } unless iteration_data.first.nil?
|
|
991
|
+
iteration_data[0] = data.last['fnid'].to_i unless data.empty?
|
|
992
|
+
end
|
|
993
|
+
return Result::Empty.new if data.empty?
|
|
994
|
+
skip_ids_persistency&.save
|
|
995
|
+
Result::ObjectList.new(data)
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
def action_async_counters(async_id:, **)
|
|
999
|
+
Integer(async_id)
|
|
1000
|
+
resp = @api_node.create('async/counters', {'syncs' => [async_id]})['sync_counters'].first[async_id].last
|
|
1001
|
+
return Result::Empty.new if resp.nil?
|
|
1002
|
+
Result::SingleObject.new(resp)
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def action_ssync_start(ssync_id:, **)
|
|
1006
|
+
@api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/start", content_type: Mime::TEXT, body: '', ret: :resp).body
|
|
1007
|
+
Result::Status.new('Done')
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
def action_ssync_stop(ssync_id:, **)
|
|
1011
|
+
@api_node.call(operation: 'POST', subpath: "asyncs/#{ssync_id}/stop", content_type: Mime::TEXT, body: '', ret: :resp).body
|
|
1012
|
+
Result::Status.new('Done')
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
# transfer sub-commands
|
|
1016
|
+
def action_transfer_list
|
|
1017
|
+
transfer_filter = query_read_delete(default: {}, schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers'))
|
|
1018
|
+
iteration_persistency = nil
|
|
1019
|
+
if options.get_option(:once_only, mandatory: true)
|
|
1020
|
+
iteration_persistency = PersistencyActionOnce.new(
|
|
1021
|
+
manager: persistency,
|
|
1022
|
+
data: [],
|
|
1023
|
+
id: IdGenerator.from_list('node_transfers', options.get_option(:url, mandatory: true), options.get_option(:username, mandatory: true))
|
|
1024
|
+
)
|
|
1025
|
+
if transfer_filter.delete('reset')
|
|
1026
|
+
iteration_persistency.data.clear
|
|
1027
|
+
iteration_persistency.save
|
|
1028
|
+
return Result::Status.new('Persistency reset')
|
|
860
1029
|
end
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
876
|
-
)
|
|
877
|
-
if transfer_filter.delete('reset')
|
|
878
|
-
iteration_persistency.data.clear
|
|
879
|
-
iteration_persistency.save
|
|
880
|
-
return Result::Status.new('Persistency reset')
|
|
881
|
-
end
|
|
882
|
-
else
|
|
883
|
-
Aspera.assert(!transfer_filter.key?('reset'), 'reset only with once_only', type: Cli::BadArgument)
|
|
884
|
-
end
|
|
885
|
-
transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
|
|
886
|
-
iteration_persistency&.save
|
|
887
|
-
return Result::ObjectList.new(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
|
|
888
|
-
when :sessions
|
|
889
|
-
transfers_data = @api_node.read('ops/transfers', query_read_delete)
|
|
890
|
-
sessions = transfers_data.flat_map{ |t| t['sessions']}
|
|
891
|
-
start_end = %i[start end].freeze
|
|
892
|
-
sessions.each do |session|
|
|
893
|
-
start_end.each do |what|
|
|
894
|
-
session["#{what}_time"] = session["#{what}_time_usec"] ? Time.at(session["#{what}_time_usec"] / 1_000_000.0).utc.iso8601(0) : nil
|
|
895
|
-
end
|
|
896
|
-
end
|
|
897
|
-
return Result::ObjectList.new(sessions, fields: %w[id status start_time end_time target_rate_kbps])
|
|
898
|
-
when :cancel
|
|
899
|
-
@api_node.cancel("ops/transfers/#{options.instance_identifier}")
|
|
900
|
-
return Result::Status.new('Cancelled')
|
|
901
|
-
when :show
|
|
902
|
-
resp = @api_node.read("ops/transfers/#{options.instance_identifier}")
|
|
903
|
-
return Result::SingleObject.new(resp)
|
|
904
|
-
when :modify
|
|
905
|
-
@api_node.update("ops/transfers/#{options.instance_identifier}", options.get_next_argument('update value', validation: Hash))
|
|
906
|
-
return Result::Status.new('Modified')
|
|
907
|
-
when :bandwidth_average
|
|
908
|
-
transfers_data = @api_node.read('ops/transfers', query_read_delete)
|
|
909
|
-
# collect all key dates
|
|
910
|
-
bandwidth_period = {}
|
|
911
|
-
dir_info = %i[avg_kbps sessions].freeze
|
|
912
|
-
transfers_data.each do |transfer|
|
|
913
|
-
session = transfer
|
|
914
|
-
# transfer['sessions'].each do |session|
|
|
915
|
-
next if session['avg_rate_kbps'].zero?
|
|
916
|
-
bandwidth_period[session['start_time_usec']] = 0
|
|
917
|
-
bandwidth_period[session['end_time_usec']] = 0
|
|
918
|
-
# end
|
|
919
|
-
end
|
|
920
|
-
result = []
|
|
921
|
-
# all dates sorted numerically
|
|
922
|
-
all_dates = bandwidth_period.keys.sort
|
|
923
|
-
all_dates.each_with_index do |start_date, index|
|
|
924
|
-
end_date = all_dates[index + 1]
|
|
925
|
-
# do not process last one
|
|
926
|
-
break if end_date.nil?
|
|
927
|
-
# init data for this period
|
|
928
|
-
period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |direction|
|
|
929
|
-
[direction, dir_info.to_h{ |k2| [k2, 0]}]
|
|
930
|
-
end
|
|
931
|
-
# find all transfers that were active at this time
|
|
932
|
-
transfers_data.each do |transfer|
|
|
933
|
-
session = transfer
|
|
934
|
-
# transfer['sessions'].each do |session|
|
|
935
|
-
# skip if not information for this period
|
|
936
|
-
next if session['avg_rate_kbps'].zero?
|
|
937
|
-
# skip if not in this period
|
|
938
|
-
next if session['start_time_usec'] >= end_date || session['end_time_usec'] <= start_date
|
|
939
|
-
info = period_bandwidth[transfer['start_spec']['direction'].to_sym]
|
|
940
|
-
info[:avg_kbps] += session['avg_rate_kbps']
|
|
941
|
-
info[:sessions] += 1
|
|
942
|
-
# end
|
|
943
|
-
end
|
|
944
|
-
next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? do |dir|
|
|
945
|
-
period_bandwidth[dir][:sessions].zero?
|
|
946
|
-
end
|
|
947
|
-
result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
|
|
948
|
-
end
|
|
949
|
-
return Result::ObjectList.new(result)
|
|
950
|
-
else Aspera.error_unexpected_value(command)
|
|
1030
|
+
else
|
|
1031
|
+
Aspera.assert(!transfer_filter.key?('reset'), 'reset only with once_only', type: Cli::BadArgument)
|
|
1032
|
+
end
|
|
1033
|
+
transfers_data = @api_node.read_with_paging('ops/transfers', transfer_filter, iteration: iteration_persistency&.data)
|
|
1034
|
+
iteration_persistency&.save
|
|
1035
|
+
Result::ObjectList.new(transfers_data, fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path])
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def action_transfer_sessions
|
|
1039
|
+
transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
|
|
1040
|
+
sessions = transfers_data.flat_map { |t| t['sessions'] }
|
|
1041
|
+
sessions.each do |session|
|
|
1042
|
+
SESSION_TIME_FIELDS.each do |what|
|
|
1043
|
+
session["#{what}_time"] = session["#{what}_time_usec"] ? Time.at(session["#{what}_time_usec"] / 1_000_000.0).utc.iso8601(0) : nil
|
|
951
1044
|
end
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1045
|
+
end
|
|
1046
|
+
Result::ObjectList.new(sessions, fields: %w[id status start_time end_time target_rate_kbps])
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def action_transfer_cancel(transfer_id:, **)
|
|
1050
|
+
@api_node.cancel("ops/transfers/#{transfer_id}")
|
|
1051
|
+
Result::Status.new('Cancelled')
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
def action_transfer_modify(update_value:, transfer_id:, **)
|
|
1055
|
+
@api_node.update("ops/transfers/#{transfer_id}", update_value)
|
|
1056
|
+
Result::Status.new('Modified')
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
def action_transfer_bandwidth_average
|
|
1060
|
+
transfers_data = @api_node.read('ops/transfers', query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::NODE, 'ops/transfers')))
|
|
1061
|
+
bandwidth_period = {}
|
|
1062
|
+
dir_info = %i[avg_kbps sessions].freeze
|
|
1063
|
+
transfers_data.each do |t|
|
|
1064
|
+
next if t['avg_rate_kbps'].zero?
|
|
1065
|
+
bandwidth_period[t['start_time_usec']] = 0
|
|
1066
|
+
bandwidth_period[t['end_time_usec']] = 0
|
|
1067
|
+
end
|
|
1068
|
+
result = []
|
|
1069
|
+
all_dates = bandwidth_period.keys.sort
|
|
1070
|
+
all_dates.each_with_index do |start_date, index|
|
|
1071
|
+
end_date = all_dates[index + 1]
|
|
1072
|
+
break if end_date.nil?
|
|
1073
|
+
period_bandwidth = Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).to_h do |dir|
|
|
1074
|
+
[dir, dir_info.to_h { |k2| [k2, 0] }]
|
|
967
1075
|
end
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
request_data = options.get_option(:query) || {}
|
|
975
|
-
case command
|
|
976
|
-
when :session
|
|
977
|
-
command = options.get_next_command([:list])
|
|
978
|
-
case command
|
|
979
|
-
when :list
|
|
980
|
-
request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
|
|
981
|
-
request_data.deep_merge!({'validation' => validation}) unless validation.nil?
|
|
982
|
-
resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
|
|
983
|
-
return Result::ObjectList.new(resp['session_info_result']['session_info'], fields: %w[session_uuid status transport direction bytes_transferred])
|
|
984
|
-
end
|
|
985
|
-
when :file
|
|
986
|
-
command = options.get_next_command(%i[list modify])
|
|
987
|
-
case command
|
|
988
|
-
when :list
|
|
989
|
-
request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
|
|
990
|
-
request_data.deep_merge!({'validation' => validation}) unless validation.nil?
|
|
991
|
-
resp = @api_node.create('services/rest/transfers/v1/files', request_data)
|
|
992
|
-
resp = JSON.parse(resp) if resp.is_a?(String)
|
|
993
|
-
Log.dump(:resp, resp)
|
|
994
|
-
return Result::ObjectList.new(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
|
|
995
|
-
when :modify
|
|
996
|
-
request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
|
|
997
|
-
request_data.deep_merge!(validation) unless validation.nil?
|
|
998
|
-
@api_node.update('services/rest/transfers/v1/files', request_data)
|
|
999
|
-
return Result::Status.new('updated')
|
|
1000
|
-
end
|
|
1076
|
+
transfers_data.each do |t|
|
|
1077
|
+
next if t['avg_rate_kbps'].zero?
|
|
1078
|
+
next if t['start_time_usec'] >= end_date || t['end_time_usec'] <= start_date
|
|
1079
|
+
info = period_bandwidth[t['start_spec']['direction'].to_sym]
|
|
1080
|
+
info[:avg_kbps] += t['avg_rate_kbps']
|
|
1081
|
+
info[:sessions] += 1
|
|
1001
1082
|
end
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1083
|
+
next if Transfer::Spec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? { |dir| period_bandwidth[dir][:sessions].zero? }
|
|
1084
|
+
result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
|
|
1085
|
+
end
|
|
1086
|
+
Result::ObjectList.new(result)
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# service sub-commands
|
|
1090
|
+
def action_service_create(creation_data:, **)
|
|
1091
|
+
resp = @api_node.create('rund/services', creation_data)
|
|
1092
|
+
Result::Status.new("#{resp['id']} created")
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
def action_service_delete(service_id:, **)
|
|
1096
|
+
@api_node.delete("rund/services/#{service_id}")
|
|
1097
|
+
Result::Status.new("#{service_id} deleted")
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# central: shared helper
|
|
1101
|
+
def central_validation
|
|
1102
|
+
validator_id = options.get_option(:validator)
|
|
1103
|
+
validator_id ? {'validator_id' => validator_id} : nil
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
# central > session > list
|
|
1107
|
+
def action_central_session_list(request_data:, **)
|
|
1108
|
+
request_data ||= {}
|
|
1109
|
+
validation = central_validation
|
|
1110
|
+
request_data.deep_merge!({'validation' => validation}) unless validation.nil?
|
|
1111
|
+
resp = @api_node.create('services/rest/transfers/v1/sessions', request_data)
|
|
1112
|
+
Result::ObjectList.new(resp['session_info_result']['session_info'], fields: %w[session_uuid status transport direction bytes_transferred])
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
# central > file > list
|
|
1116
|
+
def action_central_file_list(request_data:, **)
|
|
1117
|
+
request_data ||= {}
|
|
1118
|
+
validation = central_validation
|
|
1119
|
+
request_data.deep_merge!({'validation' => validation}) unless validation.nil?
|
|
1120
|
+
resp = @api_node.create('services/rest/transfers/v1/files', request_data)
|
|
1121
|
+
resp = JSON.parse(resp) if resp.is_a?(String)
|
|
1122
|
+
Log.dump(:resp, resp)
|
|
1123
|
+
Result::ObjectList.new(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# central > file > modify
|
|
1127
|
+
def action_central_file_modify(request_data:, **)
|
|
1128
|
+
request_data ||= {}
|
|
1129
|
+
validation = central_validation
|
|
1130
|
+
request_data.deep_merge!(validation) unless validation.nil?
|
|
1131
|
+
@api_node.update('services/rest/transfers/v1/files', request_data)
|
|
1132
|
+
Result::Status.new('updated')
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
def action_asperabrowser
|
|
1136
|
+
browse_params = {
|
|
1137
|
+
'nodeUser' => options.get_option(:username, mandatory: true),
|
|
1138
|
+
'nodePW' => options.get_option(:password, mandatory: true),
|
|
1139
|
+
'nodeURL' => options.get_option(:url, mandatory: true)
|
|
1140
|
+
}
|
|
1141
|
+
# encode parameters so that it looks good in url
|
|
1142
|
+
encoded_params = Base64.strict_encode64(Zlib::Deflate.deflate(JSON.generate(browse_params))).gsub(/=+$/, '').tr('+/', '-_').reverse
|
|
1143
|
+
Environment.instance.open_uri("#{options.get_option(:asperabrowserurl)}?goto=#{encoded_params}")
|
|
1144
|
+
return Result::Status.new('done')
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
def action_bearer_token(private_key_pem:, user_group_id:, **)
|
|
1148
|
+
private_key = OpenSSL::PKey::RSA.new(private_key_pem)
|
|
1149
|
+
access_key = options.get_option(:username, mandatory: true)
|
|
1150
|
+
Result::Text.new(Api::Node.bearer_token(payload: user_group_id, access_key: access_key, private_key: private_key))
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
def action_simulator(parameters: {}, **)
|
|
1154
|
+
require 'aspera/node_simulator'
|
|
1155
|
+
parameters = parameters.symbolize_keys
|
|
1156
|
+
uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
|
|
1157
|
+
server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
|
|
1158
|
+
server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
|
|
1159
|
+
server.start
|
|
1160
|
+
return Result::Status.new('Simulator terminated')
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
def action_telemetry(parameters: {}, **)
|
|
1164
|
+
parameters = parameters.symbolize_keys
|
|
1165
|
+
%i[url key].each do |psym|
|
|
1166
|
+
Aspera.assert(parameters.key?(psym), type: Cli::BadArgument) { "Missing parameter: #{psym}" }
|
|
1167
|
+
end
|
|
1168
|
+
require 'socket'
|
|
1169
|
+
parameters[:interval] = 10 unless parameters.key?(:interval)
|
|
1170
|
+
parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
|
|
1171
|
+
interval = parameters[:interval].to_f
|
|
1172
|
+
Aspera.assert(interval > 0, type: Cli::BadArgument) { 'Interval must be a positive number in seconds' }
|
|
1173
|
+
otel_api = Rest.new(
|
|
1174
|
+
base_url: "#{parameters[:url]}/v1",
|
|
1175
|
+
headers: {
|
|
1176
|
+
# 'Authorization' => "apiToken #{parameters[:key]}",
|
|
1177
|
+
'x-instana-key' => parameters[:key],
|
|
1178
|
+
'x-instana-host' => parameters[:hostname]
|
|
1007
1179
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
private_key = OpenSSL::PKey::RSA.new(options.get_next_argument('private RSA key PEM value', validation: String))
|
|
1016
|
-
token_info = options.get_next_argument('user and group identification', validation: Hash)
|
|
1017
|
-
access_key = options.get_option(:username, mandatory: true)
|
|
1018
|
-
return Result::Text.new(Api::Node.bearer_token(payload: token_info, access_key: access_key, private_key: private_key))
|
|
1019
|
-
when :simulator
|
|
1020
|
-
require 'aspera/node_simulator'
|
|
1021
|
-
parameters = value_create_modify(command: command, default: {}).symbolize_keys
|
|
1022
|
-
uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
|
|
1023
|
-
server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
|
|
1024
|
-
server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
|
|
1025
|
-
server.start
|
|
1026
|
-
return Result::Status.new('Simulator terminated')
|
|
1027
|
-
when :telemetry
|
|
1028
|
-
parameters = value_create_modify(command: command, default: {}).symbolize_keys
|
|
1029
|
-
%i[url key].each do |psym|
|
|
1030
|
-
raise Cli::BadArgument, "Missing parameter: #{psym}" unless parameters.key?(psym)
|
|
1031
|
-
end
|
|
1032
|
-
require 'socket'
|
|
1033
|
-
parameters[:interval] = 10 unless parameters.key?(:interval)
|
|
1034
|
-
parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
|
|
1035
|
-
interval = parameters[:interval].to_f
|
|
1036
|
-
raise Cli::BadArgument, 'Interval must be a positive number in seconds' if interval <= 0
|
|
1037
|
-
otel_api = Rest.new(
|
|
1038
|
-
base_url: "#{parameters[:url]}/v1",
|
|
1039
|
-
headers: {
|
|
1040
|
-
# 'Authorization' => "apiToken #{parameters[:key]}",
|
|
1041
|
-
'x-instana-key' => parameters[:key],
|
|
1042
|
-
'x-instana-host' => parameters[:hostname]
|
|
1043
|
-
}
|
|
1044
|
-
)
|
|
1045
|
-
datapoint = {
|
|
1046
|
-
attributes: [
|
|
1047
|
-
{
|
|
1048
|
-
key: 'server.name',
|
|
1049
|
-
value: {
|
|
1050
|
-
stringValue: 'HSTS1'
|
|
1051
|
-
}
|
|
1180
|
+
)
|
|
1181
|
+
datapoint = {
|
|
1182
|
+
attributes: [
|
|
1183
|
+
{
|
|
1184
|
+
key: 'server.name',
|
|
1185
|
+
value: {
|
|
1186
|
+
stringValue: 'HSTS1'
|
|
1052
1187
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1188
|
+
}
|
|
1189
|
+
],
|
|
1190
|
+
asInt: nil,
|
|
1191
|
+
timeUnixNano: nil
|
|
1192
|
+
}
|
|
1193
|
+
# https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge
|
|
1194
|
+
metrics = {
|
|
1195
|
+
resourceMetrics: [
|
|
1196
|
+
{
|
|
1197
|
+
resource: {
|
|
1198
|
+
attributes: [
|
|
1199
|
+
{
|
|
1200
|
+
key: 'service.name',
|
|
1201
|
+
value: {
|
|
1202
|
+
stringValue: 'IBMAspera'
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
]
|
|
1206
|
+
},
|
|
1207
|
+
scopeMetrics: [
|
|
1208
|
+
{
|
|
1209
|
+
metrics: [
|
|
1063
1210
|
{
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1211
|
+
name: 'active.transfers',
|
|
1212
|
+
description: 'Number of active transfers',
|
|
1213
|
+
unit: '1',
|
|
1214
|
+
gauge: {
|
|
1215
|
+
dataPoints: [
|
|
1216
|
+
datapoint
|
|
1217
|
+
]
|
|
1067
1218
|
}
|
|
1068
1219
|
}
|
|
1069
1220
|
]
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
]
|
|
1087
|
-
}
|
|
1088
|
-
]
|
|
1089
|
-
}
|
|
1090
|
-
loop do
|
|
1091
|
-
timestamp = Time.now
|
|
1092
|
-
transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
|
|
1093
|
-
datapoint[:asInt] = transfers_data.length
|
|
1094
|
-
datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
|
|
1095
|
-
Log.log.info("#{datapoint[:asInt]} active transfers")
|
|
1096
|
-
# https://www.ibm.com/docs/en/instana-observability/current?topic=instana-backend
|
|
1097
|
-
otel_api.create('metrics', metrics)
|
|
1098
|
-
break if interval.eql?(0.0)
|
|
1099
|
-
sleep([0.0, interval - (Time.now - timestamp)].max)
|
|
1100
|
-
end
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
}
|
|
1226
|
+
loop do
|
|
1227
|
+
timestamp = Time.now
|
|
1228
|
+
transfers_data = @api_node.read_with_paging('ops/transfers', {active_only: true})
|
|
1229
|
+
datapoint[:asInt] = transfers_data.length
|
|
1230
|
+
datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
|
|
1231
|
+
Log.log.info("#{datapoint[:asInt]} active transfers")
|
|
1232
|
+
# https://www.ibm.com/docs/en/instana-observability/current?topic=instana-backend
|
|
1233
|
+
otel_api.create('metrics', metrics)
|
|
1234
|
+
break if interval.eql?(0.0)
|
|
1235
|
+
sleep([0.0, interval - (Time.now - timestamp)].max)
|
|
1101
1236
|
end
|
|
1102
|
-
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# Dispatch a Gen3 command that has already been read from the argument stream.
|
|
1240
|
+
# Used when a new Node instance is created by an external plugin (shares, faspex, cos, :v3)
|
|
1241
|
+
# and the command symbol was already consumed before the Node was instantiated.
|
|
1242
|
+
# Re-enters the DSL registry so Proc actions and intermediate nodes (sync, access_keys)
|
|
1243
|
+
# are resolved normally.
|
|
1244
|
+
# Pre-resolved CLI arguments (e.g. path:) can be forwarded via +ctx+ to avoid
|
|
1245
|
+
# re-consuming tokens that were already resolved by the calling plugin's DSL.
|
|
1246
|
+
# @param command [Symbol] command already consumed from the argument stream
|
|
1247
|
+
# @param ctx [Hash] pre-resolved context (keyword args forwarded to dispatch)
|
|
1248
|
+
# @return [Object] CLI result
|
|
1249
|
+
def dispatch_v3_command(command, **ctx)
|
|
1250
|
+
Aspera.assert_values(command, COMMANDS_GEN3 + %i[health events info slash license api_details access_keys transfer]) { 'v3 command' }
|
|
1251
|
+
dispatch_from_registry([command], ctx)
|
|
1103
1252
|
end
|
|
1104
1253
|
|
|
1105
1254
|
private
|
|
1106
1255
|
|
|
1107
1256
|
# Response has key `paths`.
|
|
1108
1257
|
# From those, check if there is an error
|
|
1109
|
-
# @return [Array] of
|
|
1258
|
+
# @return [Array<Hash>] list of hashes with 2 keys: `path` and `result`
|
|
1110
1259
|
def response_to_result(response, success_msg)
|
|
1111
1260
|
errors = []
|
|
1112
1261
|
obj_list = []
|
|
1113
1262
|
response['paths'].each do |p|
|
|
1114
1263
|
result = success_msg
|
|
1115
1264
|
if p.key?('error')
|
|
1116
|
-
Log.log.error{"#{p['error']['user_message']} : #{p['path']}"}
|
|
1265
|
+
Log.log.error { "#{p['error']['user_message']} : #{p['path']}" }
|
|
1117
1266
|
result = p['error']['user_message']
|
|
1118
1267
|
errors.push([p['path'], p['error']['user_message']])
|
|
1119
1268
|
end
|
|
1120
1269
|
obj_list.push({'path' => p['path'], 'result' => result})
|
|
1121
1270
|
end
|
|
1122
1271
|
# one error make all fail
|
|
1123
|
-
raise errors.map{ |i| "#{i.first}: #{i.last}"}.join(', ') unless errors.empty?
|
|
1272
|
+
raise errors.map { |i| "#{i.first}: #{i.last}" }.join(', ') unless errors.empty?
|
|
1124
1273
|
obj_list
|
|
1125
1274
|
end
|
|
1126
1275
|
|
|
1127
1276
|
# Translates paths results into CLI result, and removes prefix
|
|
1128
1277
|
def cli_result_from_paths_response(response, success_msg)
|
|
1129
1278
|
obj_list = response_to_result(response, success_msg)
|
|
1130
|
-
@node_path_prefix&.remove_in_object_list!(obj_list)
|
|
1131
1279
|
return Result::ObjectList.new(obj_list, fields: %w[path result])
|
|
1132
1280
|
end
|
|
1133
1281
|
end
|