Skip to content

Commit 2641819

Browse files
committed
CLI: implement server-server transfers
1 parent 8274e53 commit 2641819

4 files changed

Lines changed: 77 additions & 6 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Changelog for PyUNICORE
33

44
Issue tracker: https://github.com/HumanBrainProject/pyunicore
55

6+
Version 1.4.0 (MMM dd, 2025)
7+
----------------------------
8+
- CLI: implement server-to-server transfers
9+
610
Version 1.3.3 (June 2, 2025)
711
----------------------------
812
- fix: UFTPFS: add workaround for bug in base class FTPFS

pyunicore/cli/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import argparse
44
import getpass
55
import json
6-
import os.path
6+
import os
77
from base64 import b64decode
88

99
import pyunicore.client
@@ -275,7 +275,6 @@ def run(self, args):
275275
self._print_response(response)
276276

277277
def _print_response(self, response, print_body=True):
278-
print(f"HTTP/1.1 {response.status_code} {response.reason}")
279278
if self.args.include:
280279
self._print_headers(response)
281280
if response.headers.get("Location"):
@@ -288,6 +287,7 @@ def _print_response(self, response, print_body=True):
288287
print(response.content)
289288

290289
def _print_headers(self, response):
290+
print(f"HTTP/1.1 {response.status_code} {response.reason}")
291291
for h in response.headers:
292292
print(f"{h}: {response.headers[h]}")
293293

pyunicore/cli/info.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ def show_endpoint_details(self, ep: Resource):
5454
print(ep.resource_url)
5555
if ep.resource_url.endswith("/rest/core"):
5656
self._show_details_core(ep)
57-
elif "/rest/core/storages/" in ep.resource_url:
57+
elif re.match(".*/rest/core/storages/.+", ep.resource_url):
5858
self._show_details_storage(ep)
5959
else:
6060
print(" * no further details available.")
6161

6262
def _show_details_core(self, ep: Resource):
6363
props = ep.properties
64+
print(" * type: UNICORE/X base")
6465
print(f" * server v{props['server']['version']}")
6566
xlogin = props["client"]["xlogin"]
6667
role = props["client"]["role"]["selected"]
@@ -78,5 +79,9 @@ def _show_details_core(self, ep: Resource):
7879

7980
def _show_details_storage(self, ep: Resource):
8081
props = ep.properties
82+
t = "storage"
83+
if ep.resource_url.endswith("-uspace"):
84+
t = t + " (job directory)"
85+
print(f" * type: {t}")
8186
print(f" * mount point: {props['mountPoint']}")
8287
print(f" * free space : {int(props['freeSpace']/1024/1024)} MB")

pyunicore/cli/io.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import re
77
import sys
88
from os.path import basename
9+
from urllib.parse import urlparse
910

1011
from pyunicore.cli.base import Base
1112
from pyunicore.client import PathFile
1213
from pyunicore.client import Storage
14+
from pyunicore.client import Transfer
1315

1416

1517
class IOBase(Base):
@@ -90,6 +92,20 @@ def add_command_args(self):
9092
self.parser.description = self.get_synopsis()
9193
self.parser.add_argument("source", nargs="+", help="Source(s)")
9294
self.parser.add_argument("target", help="Target")
95+
self.parser.add_argument(
96+
"-E",
97+
"--extra-parameters",
98+
required=False,
99+
type=str,
100+
help="Additional settings for the transfer (key1=val1,key2=val2)",
101+
)
102+
self.parser.add_argument(
103+
"-a",
104+
"--asynchronous",
105+
required=False,
106+
action="store_true",
107+
help="(server-server only) Asynchronous mode, don't wait for transfer to finish",
108+
)
93109

94110
def get_synopsis(self):
95111
return """Copy files from/to local or UNICORE storages"""
@@ -124,15 +140,61 @@ def _upload(self, source_path, target_endpoint, target_path):
124140
self.verbose(f"... {source_path} -> {target_endpoint}/files{target}")
125141
storage.upload(source_path, destination=target)
126142

143+
def _stage_in(self, source_url, target_endpoint, target_path, params={}):
144+
storage = Storage(self.credential, storage_url=target_endpoint)
145+
if target_path.endswith("/"):
146+
source_path = urlparse(source_url).path
147+
target = normalized(target_path + os.path.basename(source_path))
148+
else:
149+
target = normalized(target_path)
150+
self.verbose(f"... {source_url} -> {target_endpoint}: {target}")
151+
return storage.receive_file(
152+
remote_url=source_url, file_name=target, additional_parameters=params
153+
)
154+
155+
def _stage_out(self, source_endpoint, source_path, target_url, params={}):
156+
storage = Storage(self.credential, storage_url=source_endpoint)
157+
self.verbose(f"... {source_endpoint}: {source_path} -> {target_url}")
158+
return storage.send_file(
159+
remote_url=target_url, file_name=source_path, additional_parameters=params
160+
)
161+
162+
def _is_remote(self, location):
163+
return re.match(r"([-a-z0-9]*:)?(http[s]?)?://(.*)", location.lower()) is not None
164+
165+
def _parse_extra_params(self, param_spec: str):
166+
res = {}
167+
if param_spec:
168+
for kv in param_spec.split(","):
169+
k, v = kv.split("=", 1)
170+
res[k] = v
171+
return res
172+
127173
def run(self, args):
128174
super().setup(args)
175+
params = self._parse_extra_params(self.args.extra_parameters)
129176
target_endpoint, target_path = self.parse_location(self.args.target)
177+
controller: Transfer = None
130178
for s in self.args.source:
131179
source_endpoint, source_path = self.parse_location(s)
132180
if source_endpoint is not None:
133-
self._download(source_endpoint, source_path, target_path)
181+
if self._is_remote(self.args.target):
182+
controller = self._stage_out(
183+
source_endpoint, source_path, self.args.target, params
184+
)
185+
else:
186+
self._download(source_endpoint, source_path, target_path)
187+
elif target_endpoint is not None:
188+
if self._is_remote(s):
189+
controller = self._stage_in(s, target_endpoint, target_path, params)
190+
else:
191+
print(f"Cannot process: {s}->{self.args.target}")
192+
if controller:
193+
if self.args.asynchronous:
194+
print(controller.resource_url)
134195
else:
135-
self._upload(source_path, target_endpoint, target_path)
196+
self.verbose(f"Waiting for transfer {controller.resource_url} to finish...")
197+
controller.poll()
136198

137199

138200
class Cat(IOBase):
@@ -166,7 +228,7 @@ def run(self, args):
166228
if source_endpoint is not None:
167229
self._cat(source_endpoint, source_path)
168230
else:
169-
raise ValueError("Not a remote file: %s" % s)
231+
raise ValueError("Not a remote UNICORE file: %s" % s)
170232

171233

172234
def normalized(path: str):

0 commit comments

Comments
 (0)