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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Testing the MCP Server with an AI Agent
|
|
2
|
+
|
|
3
|
+
This document is a test procedure for validating the `ascli` MCP server end-to-end using
|
|
4
|
+
an AI assistant (Bob, Claude Desktop, VS Code Copilot, or any MCP-capable client).
|
|
5
|
+
It complements the automated tests in `tests/tests.yml`, which cover transport-layer
|
|
6
|
+
correctness but cannot validate AI-driven discovery and reasoning.
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- `ascli` installed and working (`ascli config gem version` prints a version string).
|
|
11
|
+
- The `mcp` gem installed: `gem install mcp`
|
|
12
|
+
- An AI client that supports MCP — Bob, Claude Desktop, or VS Code with Copilot.
|
|
13
|
+
- Optional: credentials for a live Aspera service (AoC, Faspex 5, Node, or HSTS demo server).
|
|
14
|
+
|
|
15
|
+
## 1. Start the MCP server
|
|
16
|
+
|
|
17
|
+
### Option A — stdio (recommended for Bob and Claude Desktop)
|
|
18
|
+
|
|
19
|
+
Register `ascli` directly in the client's MCP configuration file.
|
|
20
|
+
No manual server start is required — the client launches it automatically.
|
|
21
|
+
|
|
22
|
+
**Bob / Claude Desktop** (`~/.config/bob/mcp_settings.json` or equivalent):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"ascli": {
|
|
28
|
+
"command": "ascli",
|
|
29
|
+
"args": ["mcp", "server"],
|
|
30
|
+
"description": "Aspera CLI — IBM Aspera file transfer and management"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> If running from source (no gem installed), add `"env": {"RUBYLIB": "/path/to/aspera-cli/lib"}`.
|
|
37
|
+
|
|
38
|
+
### Option B — HTTP (useful for manual curl testing or multi-client scenarios)
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
ascli mcp server @: transport=http port=3000
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Verify the server is up:
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
curl -s http://127.0.0.1:3000/ | python3 -m json.tool
|
|
48
|
+
# Expected: {"name":"aspera-cli","version":"...","description":"..."}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2. Verify the tool is visible to the AI
|
|
52
|
+
|
|
53
|
+
Ask the AI:
|
|
54
|
+
|
|
55
|
+
> "What MCP tools do you have available?"
|
|
56
|
+
|
|
57
|
+
**Expected**: the AI lists `execute_ascli_command` and describes it as running `ascli` commands.
|
|
58
|
+
|
|
59
|
+
## 3. Discovery sequence — let the AI drive
|
|
60
|
+
|
|
61
|
+
These prompts test the self-discovery path without providing any hints.
|
|
62
|
+
The AI should be able to answer each one using only MCP tool calls.
|
|
63
|
+
|
|
64
|
+
### 3.1 List all commands
|
|
65
|
+
|
|
66
|
+
> "List all available ascli commands and their syntax."
|
|
67
|
+
|
|
68
|
+
**Expected behaviour**: the AI calls `["config", "commands"]` and returns a structured list
|
|
69
|
+
with `syntax` and `description` columns. It should not ask for documentation or make up
|
|
70
|
+
command names.
|
|
71
|
+
|
|
72
|
+
**Pass criterion**: the response includes at least `aoc packages list`, `server ls`,
|
|
73
|
+
`node info`, `config preset list` with their argument syntax.
|
|
74
|
+
|
|
75
|
+
### 3.2 Inspect a Hash argument schema
|
|
76
|
+
|
|
77
|
+
> "What fields are required to create an AoC user?"
|
|
78
|
+
|
|
79
|
+
**Expected behaviour**: the AI calls `["aoc", "admin", "user", "create", "help"]` and lists
|
|
80
|
+
fields such as `email` (required), `first_name`, `last_name`, `name`.
|
|
81
|
+
|
|
82
|
+
**Pass criterion**: `email` is identified as the only required field for POST.
|
|
83
|
+
|
|
84
|
+
### 3.3 List plugin options
|
|
85
|
+
|
|
86
|
+
> "What authentication options does the `aoc` plugin support?"
|
|
87
|
+
|
|
88
|
+
**Expected behaviour**: the AI calls `["config", "options", "aoc"]` and finds options
|
|
89
|
+
such as `--auth`, `--url`, `--username`, `--password`, `--private-key`, `--preset`.
|
|
90
|
+
|
|
91
|
+
**Pass criterion**: the AI reports the allowed values for `--auth`
|
|
92
|
+
(e.g. `basic`, `oauth2`, `web`, `jwt`).
|
|
93
|
+
|
|
94
|
+
### 3.4 Read a documentation section
|
|
95
|
+
|
|
96
|
+
> "How do I configure a saved preset for AoC?"
|
|
97
|
+
|
|
98
|
+
**Expected behaviour**: the AI calls `["config", "documentation", "toc"]` to find the
|
|
99
|
+
relevant anchor, then `["config", "documentation", "local", "<anchor>"]` to read it.
|
|
100
|
+
It should not call the full README unless a section is insufficient.
|
|
101
|
+
|
|
102
|
+
**Pass criterion**: the AI returns actionable steps for creating a preset
|
|
103
|
+
(e.g. using `config preset update` or the wizard).
|
|
104
|
+
|
|
105
|
+
## 4. End-to-end task — live server required
|
|
106
|
+
|
|
107
|
+
These tests require real credentials. Use the Aspera demo server if no private
|
|
108
|
+
environment is available.
|
|
109
|
+
|
|
110
|
+
### 4.1 Demo server — browse files
|
|
111
|
+
|
|
112
|
+
> "Browse the root folder of the Aspera demo server at ssh://demo.asperademo.com:33001
|
|
113
|
+
> using username `asperaweb` and password `demoaspera`."
|
|
114
|
+
|
|
115
|
+
**Expected MCP calls**:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
["server", "browse", "/",
|
|
119
|
+
"--url=ssh://demo.asperademo.com:33001",
|
|
120
|
+
"--username=asperaweb",
|
|
121
|
+
"--password=demoaspera"]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Pass criterion**: the AI returns a file/folder listing without errors.
|
|
125
|
+
|
|
126
|
+
### 4.2 Demo server — download a file
|
|
127
|
+
|
|
128
|
+
> "Download the file `/aspera-test-dir-small/10KB.1` from the demo server
|
|
129
|
+
> (same credentials as above) to `/tmp`."
|
|
130
|
+
|
|
131
|
+
**Expected MCP calls**:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
["server", "download", "/aspera-test-dir-small/10KB.1",
|
|
135
|
+
"--url=ssh://demo.asperademo.com:33001",
|
|
136
|
+
"--username=asperaweb",
|
|
137
|
+
"--password=demoaspera",
|
|
138
|
+
"--to-folder=/tmp"]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Pass criterion**: the AI reports a successful transfer and the file exists in `/tmp`.
|
|
142
|
+
|
|
143
|
+
### 4.3 AoC — create user and add to workspace (requires AoC credentials)
|
|
144
|
+
|
|
145
|
+
Provide the AI with your AoC URL and admin credentials (or a saved preset), then ask:
|
|
146
|
+
|
|
147
|
+
> "Create an AoC user with email `test-ai@example.com`, first name `Test`, last name `AI`,
|
|
148
|
+
> then add them to the workspace named `Engineering`."
|
|
149
|
+
|
|
150
|
+
**Expected AI workflow** (4 calls minimum):
|
|
151
|
+
|
|
152
|
+
1. `["aoc", "admin", "user", "create", "help"]` — inspect schema
|
|
153
|
+
2. `["aoc", "admin", "workspace", "list", ...]` — resolve workspace name to id
|
|
154
|
+
3. `["aoc", "admin", "user", "create", "@json:{...}", ...]` — create user
|
|
155
|
+
4. `["aoc", "admin", "workspace_membership", "create", "@json:{...}", ...]` — add membership
|
|
156
|
+
|
|
157
|
+
**Pass criterion**: the AI completes all steps without being told which commands to use,
|
|
158
|
+
and the user appears in the AoC admin console.
|
|
159
|
+
|
|
160
|
+
## 5. Truncation warning test
|
|
161
|
+
|
|
162
|
+
> "List all ascli commands and tell me how many there are in total."
|
|
163
|
+
|
|
164
|
+
**Expected behaviour**: the AI receives the truncation WARNING in the second content block
|
|
165
|
+
(`WARNING: result truncated to 100 of NNN items`) and reads `structuredContent` to get
|
|
166
|
+
the full count.
|
|
167
|
+
|
|
168
|
+
**Pass criterion**: the AI reports the correct total (800+), not just 100.
|
|
169
|
+
|
|
170
|
+
## 6. Error handling
|
|
171
|
+
|
|
172
|
+
> "Run the command `ascli config options no_such_plugin_xyz`."
|
|
173
|
+
|
|
174
|
+
**Expected behaviour**: the AI receives an error response and reports the error message
|
|
175
|
+
(`plugin not found: no_such_plugin_xyz`) without crashing or retrying indefinitely.
|
|
176
|
+
|
|
177
|
+
**Pass criterion**: the AI surfaces the error clearly and does not invent a workaround.
|
|
178
|
+
|
|
179
|
+
## 7. Credential safety
|
|
180
|
+
|
|
181
|
+
> "Show me the current value of the `password` option."
|
|
182
|
+
|
|
183
|
+
**Expected behaviour**: the AI either reports that it cannot retrieve sensitive values,
|
|
184
|
+
or returns a masked/redacted value. It must not print a real password in plain text.
|
|
185
|
+
|
|
186
|
+
**Pass criterion**: no real secret appears in the response.
|
|
187
|
+
|
|
188
|
+
## Checklist summary
|
|
189
|
+
|
|
190
|
+
| # | Test | Without live server | With live server |
|
|
191
|
+
|---|------|:-------------------:|:----------------:|
|
|
192
|
+
| 2 | Tool visible to AI | ✓ | ✓ |
|
|
193
|
+
| 3.1 | `config commands` discovery | ✓ | ✓ |
|
|
194
|
+
| 3.2 | Hash schema via `help` | ✓ | ✓ |
|
|
195
|
+
| 3.3 | Options via `config options` | ✓ | ✓ |
|
|
196
|
+
| 3.4 | Doc section via `doc local` | ✓ | ✓ |
|
|
197
|
+
| 4.1 | Browse demo server | | ✓ |
|
|
198
|
+
| 4.2 | Download from demo server | | ✓ |
|
|
199
|
+
| 4.3 | AoC user + workspace | | ✓ (AoC only) |
|
|
200
|
+
| 5 | Truncation warning surfaced | ✓ | ✓ |
|
|
201
|
+
| 6 | Error response handled | ✓ | ✓ |
|
|
202
|
+
| 7 | Credentials not leaked | ✓ | ✓ |
|
data/lib/aspera/agent/base.rb
CHANGED
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/assert'
|
|
4
|
+
require 'aspera/transfer/result'
|
|
4
5
|
module Aspera
|
|
5
6
|
module Agent
|
|
6
7
|
# Base class for transfer agents
|
|
7
8
|
# Transfer agents provide methods:
|
|
8
|
-
# - `start_transfer`
|
|
9
|
-
# - `wait_for_transfers_completion`
|
|
10
|
-
# - `notify_progress`
|
|
9
|
+
# - `start_transfer` : take a transfer spec and start a transfer asynchronously
|
|
10
|
+
# - `wait_for_transfers_completion` : waits for all transfer sessions to finish
|
|
11
|
+
# - `notify_progress` : called back by transfer agent to notify transfer progress
|
|
12
|
+
# - `self.transfer_status` : (optional) re-query a previously started transfer by id
|
|
11
13
|
class Base
|
|
12
|
-
|
|
14
|
+
class << self
|
|
15
|
+
# Re-query the status of a transfer that was started asynchronously.
|
|
16
|
+
# Override in agents that support live status queries (desktop, node, connect, transferd).
|
|
17
|
+
# @param transfer_id [String] opaque id returned by start_transfer
|
|
18
|
+
# @param agent_params [Hash] connection parameters stored at submission time
|
|
19
|
+
# @return [Hash, nil] status hash with at least 'status' key, or nil if not supported
|
|
20
|
+
def transfer_status(_transfer_id, _agent_params)
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Wait for all sessions to terminate and return a typed Transfer::Result.
|
|
26
|
+
# @return [Transfer::Result::Success, Transfer::Result::Error]
|
|
13
27
|
def wait_for_completion
|
|
14
|
-
# list of: :success or "error message string"
|
|
15
28
|
statuses = wait_for_transfers_completion
|
|
16
29
|
@progress&.reset
|
|
17
30
|
Aspera.assert_type(statuses, Array)
|
|
18
|
-
Aspera.assert(statuses.none?{ |i| !i.eql?(:success) && !i.is_a?(StandardError)}){"bad statuses content: #{statuses}"}
|
|
19
|
-
|
|
31
|
+
Aspera.assert(statuses.none? { |i| !i.eql?(:success) && !i.is_a?(StandardError) }) { "bad statuses content: #{statuses}" }
|
|
32
|
+
errors = statuses.reject { |i| i.eql?(:success) }
|
|
33
|
+
return errors.empty? ? Transfer::Result.success : Transfer::Result.error(errors.first)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Return the job_id of the last transfer submitted by this agent.
|
|
37
|
+
# In-process agents (direct, httpgw) override this to expose their internal job id.
|
|
38
|
+
# Remote-daemon agents do not need to override it: TransferAgent generates a UUID itself.
|
|
39
|
+
# @return [String, nil]
|
|
40
|
+
def last_job_id
|
|
41
|
+
nil
|
|
20
42
|
end
|
|
21
43
|
|
|
22
44
|
private
|
data/lib/aspera/agent/connect.rb
CHANGED
|
@@ -15,18 +15,63 @@ module Aspera
|
|
|
15
15
|
# delay between each try to start connect
|
|
16
16
|
SLEEP_SEC_BETWEEN_RETRY = 5
|
|
17
17
|
private_constant :CONNECT_START_URIS, :SLEEP_SEC_BETWEEN_RETRY
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Re-query a previously started transfer via the Connect REST API.
|
|
21
|
+
# agent_params must contain 'app_id'; the Connect URL is auto-discovered.
|
|
22
|
+
# @return [Hash] normalized status hash
|
|
23
|
+
def transfer_status(transfer_id, agent_params)
|
|
24
|
+
connect_api = Rest.new(
|
|
25
|
+
base_url: "#{connect_api_url}/v5/connect",
|
|
26
|
+
headers: {'Origin' => RestParameters.instance.user_agent}
|
|
27
|
+
)
|
|
28
|
+
tr_info = connect_api.create("transfers/info/#{transfer_id}", {'aspera_connect_settings' => {'app_id' => agent_params['app_id']}})
|
|
29
|
+
transfer = tr_info['transfer_info']
|
|
30
|
+
return {'status' => 'running', 'bytes_transferred' => 0} unless transfer.is_a?(Hash)
|
|
31
|
+
normalize_status(transfer)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Normalize a Connect transfer_info hash into the standard async-transfer fields.
|
|
35
|
+
def normalize_status(transfer)
|
|
36
|
+
result = {'bytes_transferred' => transfer['bytes_written'].to_i}
|
|
37
|
+
case transfer['status']
|
|
38
|
+
when 'completed'
|
|
39
|
+
result['status'] = 'completed'
|
|
40
|
+
result['ended_at'] = Time.now.utc.iso8601
|
|
41
|
+
when 'failed'
|
|
42
|
+
result.merge!('status' => 'failed', 'ended_at' => Time.now.utc.iso8601, 'error' => transfer['error_desc'].to_s)
|
|
43
|
+
when 'cancelled'
|
|
44
|
+
result['status'] = 'cancelled'
|
|
45
|
+
result['ended_at'] = Time.now.utc.iso8601
|
|
46
|
+
else
|
|
47
|
+
result['status'] = 'running'
|
|
48
|
+
end
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Auto-discover the Connect API base URL from its local URI file.
|
|
53
|
+
def connect_api_url
|
|
54
|
+
folder = File.join(Products::Other.find(Products::Connect.locations).first[:run_root], 'var', 'run')
|
|
55
|
+
['', 's'].each do |ext|
|
|
56
|
+
uri_file = File.join(folder, "http#{ext}.uri")
|
|
57
|
+
return File.open(uri_file, &:gets).strip if File.exist?(uri_file)
|
|
58
|
+
end
|
|
59
|
+
raise "no connect uri file found in #{folder}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
18
63
|
def initialize(**base_options)
|
|
19
64
|
super
|
|
20
65
|
@transfer_id = nil
|
|
21
66
|
@connect_settings = {
|
|
22
67
|
'app_id' => SecureRandom.uuid
|
|
23
68
|
}
|
|
24
|
-
|
|
69
|
+
Aspera.assert(Environment.instance.graphical?, type: Error) { 'Using connect requires a graphical environment' }
|
|
25
70
|
method_index = 0
|
|
26
71
|
begin
|
|
27
72
|
# raise exception if connect not started and file does not exist
|
|
28
|
-
connect_url = connect_api_url
|
|
29
|
-
Log.log.debug{"found: #{connect_url}"}
|
|
73
|
+
connect_url = self.class.connect_api_url
|
|
74
|
+
Log.log.debug { "found: #{connect_url}" }
|
|
30
75
|
@connect_api = Rest.new(
|
|
31
76
|
base_url: "#{connect_url}/v5/connect", # could use v6 also now
|
|
32
77
|
headers: {'Origin' => RestParameters.instance.user_agent}
|
|
@@ -35,21 +80,26 @@ module Aspera
|
|
|
35
80
|
Log.log.debug('Connect was reached') if method_index > 0
|
|
36
81
|
Log.dump(:connect_version, connect_info)
|
|
37
82
|
rescue StandardError => e # Errno::ECONNREFUSED
|
|
38
|
-
Log.log.debug{"Exception: #{e}"}
|
|
83
|
+
Log.log.debug { "Exception: #{e}" }
|
|
39
84
|
start_url = CONNECT_START_URIS[method_index]
|
|
40
85
|
method_index += 1
|
|
41
|
-
|
|
42
|
-
Log.log.warn{"Aspera Connect is not started (#{e}). Trying to start it ##{method_index}..."}
|
|
86
|
+
Aspera.assert(!start_url.nil?) { "Unable to start connect #{method_index} times" }
|
|
87
|
+
Log.log.warn { "Aspera Connect is not started (#{e}). Trying to start it ##{method_index}..." }
|
|
43
88
|
Environment.instance.open_uri_graphical(start_url)
|
|
44
89
|
sleep(SLEEP_SEC_BETWEEN_RETRY)
|
|
45
90
|
retry
|
|
46
91
|
end
|
|
47
92
|
end
|
|
48
93
|
|
|
94
|
+
# Returns the app_id so callers can persist it in agent_params.
|
|
95
|
+
def app_id
|
|
96
|
+
@connect_settings['app_id']
|
|
97
|
+
end
|
|
98
|
+
|
|
49
99
|
# :reek:UnusedParameters token_regenerator
|
|
50
100
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
|
51
101
|
if transfer_spec['direction'] == 'send'
|
|
52
|
-
Log.log.warn{"Connect requires upload selection using GUI, ignoring #{transfer_spec['paths']}".red}
|
|
102
|
+
Log.log.warn { "Connect requires upload selection using GUI, ignoring #{transfer_spec['paths']}".red }
|
|
53
103
|
transfer_spec.delete('paths')
|
|
54
104
|
selection = @connect_api.create('windows/select-open-file-dialog/', {
|
|
55
105
|
'aspera_connect_settings' => @connect_settings,
|
|
@@ -58,7 +108,7 @@ module Aspera
|
|
|
58
108
|
'allowMultipleSelection' => true,
|
|
59
109
|
'allowedFileTypes' => ''
|
|
60
110
|
})
|
|
61
|
-
transfer_spec['paths'] = selection['dataTransfer']['files'].map{ |i| {'source' => i['name']}}
|
|
111
|
+
transfer_spec['paths'] = selection['dataTransfer']['files'].map { |i| {'source' => i['name']} }
|
|
62
112
|
end
|
|
63
113
|
# if there is a token, we ask connect client to use well known ssh private keys
|
|
64
114
|
# instead of asking password
|
|
@@ -131,19 +181,6 @@ module Aspera
|
|
|
131
181
|
end
|
|
132
182
|
return [:success]
|
|
133
183
|
end
|
|
134
|
-
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
# @return the file path of local connect where API's URI can be read
|
|
138
|
-
def connect_api_url
|
|
139
|
-
folder = File.join(Products::Other.find(Products::Connect.locations).first[:run_root], 'var', 'run')
|
|
140
|
-
['', 's'].each do |ext|
|
|
141
|
-
uri_file = File.join(folder, "http#{ext}.uri")
|
|
142
|
-
Log.log.debug{"checking connect port file: #{uri_file}"}
|
|
143
|
-
return File.open(uri_file, &:gets).strip if File.exist?(uri_file)
|
|
144
|
-
end
|
|
145
|
-
raise "no connect uri file found in #{folder}"
|
|
146
|
-
end
|
|
147
184
|
end
|
|
148
185
|
end
|
|
149
186
|
end
|
data/lib/aspera/agent/desktop.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'aspera/agent/base'
|
|
4
4
|
require 'aspera/rest'
|
|
5
5
|
require 'aspera/environment'
|
|
6
|
-
require 'aspera/json_rpc'
|
|
6
|
+
require 'aspera/json_rpc/client'
|
|
7
7
|
require 'aspera/products/desktop'
|
|
8
8
|
require 'aspera/transfer/spec'
|
|
9
9
|
require 'securerandom'
|
|
@@ -18,30 +18,76 @@ module Aspera
|
|
|
18
18
|
SLEEP_SEC_BETWEEN_RETRY = 5
|
|
19
19
|
private_constant :START_URIS, :SLEEP_SEC_BETWEEN_RETRY
|
|
20
20
|
|
|
21
|
+
class << self
|
|
22
|
+
# Re-query a previously started transfer from the Desktop JSON-RPC daemon.
|
|
23
|
+
# agent_params must contain 'application_id'; the JSON-RPC URL is auto-discovered.
|
|
24
|
+
# @return [Hash] normalized status hash
|
|
25
|
+
def transfer_status(transfer_id, agent_params)
|
|
26
|
+
app_id = agent_params['application_id']
|
|
27
|
+
client = Aspera::JsonRpc::Client.new(Aspera::Rest.new(base_url: desktop_api_url))
|
|
28
|
+
raw = client.get_transfer(app_id: app_id, transfer_id: transfer_id)
|
|
29
|
+
normalize_status(raw['status'], bytes: raw['bytes_written'].to_i, error: raw['error_desc'])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Normalize a raw status string into the standard async-transfer hash fields.
|
|
33
|
+
def normalize_status(raw_status, bytes: 0, error: nil)
|
|
34
|
+
result = {'bytes_transferred' => bytes}
|
|
35
|
+
case raw_status
|
|
36
|
+
when 'completed'
|
|
37
|
+
result['status'] = 'completed'
|
|
38
|
+
result['ended_at'] = Time.now.utc.iso8601
|
|
39
|
+
when 'failed'
|
|
40
|
+
result.merge!('status' => 'failed', 'ended_at' => Time.now.utc.iso8601, 'error' => error.to_s)
|
|
41
|
+
when 'cancelled'
|
|
42
|
+
result['status'] = 'cancelled'
|
|
43
|
+
result['ended_at'] = Time.now.utc.iso8601
|
|
44
|
+
else
|
|
45
|
+
result['status'] = 'running'
|
|
46
|
+
end
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Auto-discover the JSON-RPC URL from the Desktop log file (same logic as instance method).
|
|
51
|
+
def desktop_api_url
|
|
52
|
+
log_file = File.join(Products::Other.find(Products::Desktop.locations).first[:log_root], Products::Desktop::LOG_FILENAME)
|
|
53
|
+
url = 'http://127.0.0.1:33024'
|
|
54
|
+
File.open(log_file, 'r') do |file|
|
|
55
|
+
file.each_line do |line|
|
|
56
|
+
line = line.chomp
|
|
57
|
+
url = "http://#{Regexp.last_match(1)}" if line =~ /JSON-RPC server listening on (.*)/
|
|
58
|
+
end
|
|
59
|
+
end if File.exist?(log_file)
|
|
60
|
+
url
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
21
64
|
def initialize(**base_options)
|
|
22
65
|
@application_id = SecureRandom.uuid
|
|
23
66
|
@transfer_id = nil
|
|
24
67
|
super
|
|
25
|
-
|
|
68
|
+
Aspera.assert(Environment.instance.graphical?, type: Error) { 'Using client requires a graphical environment' }
|
|
26
69
|
method_index = 0
|
|
27
70
|
begin
|
|
28
71
|
# curl 'http://127.0.0.1:33024/' -X POST -H 'content-type: application/json' --data-raw '{"jsonrpc":"2.0","params":[],"id":999999,"method":"rpc.discover"}'
|
|
29
72
|
# https://playground.open-rpc.org/?schemaUrl=http://127.0.0.1:33024
|
|
30
|
-
@client_app_api = Aspera::
|
|
73
|
+
@client_app_api = Aspera::JsonRpc::Client.new(Aspera::Rest.new(base_url: aspera_client_api_url))
|
|
31
74
|
client_info = @client_app_api.get_info
|
|
32
75
|
Log.dump(:client_version, client_info)
|
|
33
76
|
Log.log.debug('Client was reached') if method_index > 0
|
|
34
77
|
rescue Errno::ECONNREFUSED => e
|
|
35
78
|
start_url = START_URIS[method_index]
|
|
36
79
|
method_index += 1
|
|
37
|
-
|
|
38
|
-
Log.log.warn{"#{Products::Desktop::APP_NAME} is not started (#{e}). Trying to start it ##{method_index}..."}
|
|
80
|
+
Aspera.assert(!start_url.nil?) { "Unable to start #{Products::Desktop::APP_NAME} #{method_index} times" }
|
|
81
|
+
Log.log.warn { "#{Products::Desktop::APP_NAME} is not started (#{e}). Trying to start it ##{method_index}..." }
|
|
39
82
|
Environment.instance.open_uri_graphical(start_url)
|
|
40
83
|
sleep(SLEEP_SEC_BETWEEN_RETRY)
|
|
41
84
|
retry
|
|
42
85
|
end
|
|
43
86
|
end
|
|
44
87
|
|
|
88
|
+
# Returns the application_id so callers can persist it in agent_params.
|
|
89
|
+
attr_reader :application_id
|
|
90
|
+
|
|
45
91
|
# :reek:UnusedParameters token_regenerator
|
|
46
92
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
|
47
93
|
Transfer::Spec.fix_transferd_resume_policy(transfer_spec)
|
|
@@ -100,20 +146,9 @@ module Aspera
|
|
|
100
146
|
|
|
101
147
|
private
|
|
102
148
|
|
|
103
|
-
#
|
|
149
|
+
# Delegate to the class method (avoids duplication).
|
|
104
150
|
def aspera_client_api_url
|
|
105
|
-
|
|
106
|
-
url = 'http://127.0.0.1:33024'
|
|
107
|
-
File.open(log_file, 'r') do |file|
|
|
108
|
-
file.each_line do |line|
|
|
109
|
-
line = line.chomp
|
|
110
|
-
if (m = line.match(/JSON-RPC server listening on (.*)/))
|
|
111
|
-
url = "http://#{m[1]}"
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end if File.exist?(log_file)
|
|
115
|
-
# raise StandardError, "Unable to find the JSON-RPC server URL in #{log_file}" if url.nil?
|
|
116
|
-
return url
|
|
151
|
+
self.class.desktop_api_url
|
|
117
152
|
end
|
|
118
153
|
end
|
|
119
154
|
end
|