# Copyright (c) 2025 Kim Jarvis TPF Software Services S.A. kim.jarvis@tpfsystems.com
# This software is licensed under the MIT License. See the LICENSE file for details.
#
import os
import asyncssh
from reemote.command import Command
from typing import Optional, Callable, Union
[docs]
class Mput_files:
"""
A class to encapsulate the functionality of multiple builtin uploads using SFTP.
It allows users to upload multiple local builtin to remote hosts with full parameter support.
Attributes:
localpaths (str): The local builtin or directory path(s) to upload.
remotepath (str): The remote path where builtin will be uploaded.
preserve (bool): Preserve builtin attributes (permissions, timestamps).
recurse (bool): Recursively upload directories.
follow_symlinks (bool): Follow symbolic links during upload.
sparse (bool): Create sparse builtin on the remote system.
block_size (int): Block size for builtin transfers.
max_requests (int): Maximum number of concurrent transfer requests.
progress_handler (Callable): Callback for transfer progress.
error_handler (Callable): Callback for handling errors.
**Examples:**
.. code:: python
from reemote.operations.filesystem.mput_files import Mput_files
dir='/home/user/dir'
r = yield Mkdir(path=dir, attrs=SFTPAttrs(permissions=0o755))
r = yield Mput_files(
localpaths='~/reemote/development/hfs/*',
remotepath=dir,
preserve=True,
recurse=True,
progress_handler=my_progress_callback
)
r = yield Shell(f"tree {dir}")
print(r.cp.stdout)
Usage:
This class is designed to be used in a generator-based workflow where commands are yielded for execution.
"""
def __init__(self,
localpaths: str,
remotepath: str,
preserve: bool = False,
recurse: bool = False,
follow_symlinks: bool = False,
sparse: bool = True,
block_size: int = -1,
max_requests: int = -1,
progress_handler: Optional[Callable] = None,
error_handler: Optional[Callable] = None):
self.localpaths = localpaths
self.remotepath = remotepath
self.preserve = preserve
self.recurse = recurse
self.follow_symlinks = follow_symlinks
self.sparse = sparse
self.block_size = block_size
self.max_requests = max_requests
self.progress_handler = progress_handler
self.error_handler = error_handler
def __repr__(self):
return (f"Mput_files(localpaths={self.localpaths!r}, "
f"remotepath={self.remotepath!r}, "
f"preserve={self.preserve!r}, "
f"recurse={self.recurse!r}, "
f"follow_symlinks={self.follow_symlinks!r}, "
f"sparse={self.sparse!r}, "
f"block_size={self.block_size!r}, "
f"max_requests={self.max_requests!r})")
[docs]
@staticmethod
def get_absolute_path(path):
"""
Expands a given path to its absolute form, resolving '~' to the user's home directory,
while preserving any wildcard (*) in the path.
Args:
path (str): The input builtin path, which may include '~' and/or wildcards.
Returns:
str: The absolute path with wildcards preserved.
"""
# Step 1: Expand ~ to the user's home directory
expanded_path = os.path.expanduser(path)
# Step 2: Resolve the absolute path (while keeping the wildcard)
absolute_path_with_glob = os.path.abspath(expanded_path)
return absolute_path_with_glob
@staticmethod
async def _mput_files_callback(host_info, global_info, command, cp, caller):
"""Static callback method for multiple builtin upload with full parameter support"""
# Validate host_info (matching Mget_files error handling)
required_keys = ['host', 'username', 'password']
for key in required_keys:
if key not in host_info or host_info[key] is None:
raise ValueError(f"Missing or invalid value for '{key}' in host_info.")
# Validate caller attributes (matching Mget_files error handling)
if caller.localpaths is None:
raise ValueError("The 'localpaths' attribute of the caller cannot be None.")
if caller.remotepath is None:
raise ValueError("The 'remotepath' attribute of the caller cannot be None.")
try:
# Connect to the SSH server
async with asyncssh.connect(**host_info) as conn:
# Start an SFTP session
async with conn.start_sftp_client() as sftp:
# Use the mput method for builtin upload
await sftp.mput(
localpaths=Mput_files.get_absolute_path(caller.localpaths),
remotepath=caller.remotepath,
preserve=caller.preserve,
recurse=caller.recurse,
follow_symlinks=caller.follow_symlinks,
sparse=caller.sparse,
block_size=caller.block_size,
max_requests=caller.max_requests,
progress_handler=caller.progress_handler,
error_handler=caller.error_handler
)
return f"Successfully uploaded builtin to {host_info['host']}"
except (OSError, asyncssh.Error) as exc:
raise # Re-raise the exception to handle it in the caller
def execute(self):
r = yield Command(f"{self}", local=True, callback=self._mput_files_callback, caller=self)
r.executed = True
r.changed = True
return r