Metadata-Version: 2.4
Name: winrmxfer
Version: 0.1.0
Summary: File transfer over WinRM using base64-encoded PowerShell commands
Author: Nagy Szabolcs
License-Expression: MIT
Project-URL: Homepage, https://github.com/Dungscer/WinRMXfer
Project-URL: Issues, https://github.com/Dungscer/WinRMXfer/issues
Keywords: winrm,powershell,file-transfer,windows,pywinrm
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pywinrm>=0.4
Provides-Extra: kerberos
Requires-Dist: pywinrm[kerberos]; extra == "kerberos"
Provides-Extra: credssp
Requires-Dist: pywinrm[credssp]; extra == "credssp"
Dynamic: license-file

# winrmxfer

A Python package for transferring files over WinRM (Windows Remote Management).

This package originated from a university thesis project where file transfer over WinRM was required as part of automating tasks on remote Windows machines. The solution — encoding file data as base64 and sending it through PowerShell commands — proved useful enough to be packaged as a standalone library.

## Installation

```bash
pip install winrmxfer
```

## Requirements

- Python 3.10+
- [pywinrm](https://github.com/diyan/pywinrm)
- WinRM enabled and configured on the target Windows machine

## Quick start

```python
from winrmxfer import WinRMFileClient

# Option 1 — pass credentials, the client creates the WinRM session internally
client = WinRMFileClient(
    host="192.168.1.100",
    username="Administrator",
    password="s3cr3t",
    transport="ntlm",   # ntlm (default), basic, kerberos, credssp
)

# Option 2 — pass an existing winrm.Session
import winrm
session = winrm.Session("http://192.168.1.100:5985/wsman", auth=("Administrator", "s3cr3t"))
client = WinRMFileClient(session=session)

# Upload a file
client.upload("local_file.zip", r"C:\Users\Administrator\Desktop\file.zip")

# Download a file
client.download(r"C:\logs\app.log", "app.log")
```

## API

### `WinRMFileClient(host="", username="", password="", transport="ntlm", port=5985, chunk_size=2048, session=None)`

Creates a new client. Pass either `host`/`username`/`password` to create a session internally, or an existing `winrm.Session` via the `session` parameter. `chunk_size` controls how many raw bytes are sent per WinRM round-trip (default 2 KB, tuned to stay within PowerShell's `-EncodedCommand` limit).

---

### File transfer

#### `upload(local_path, remote_path, progress=None)`

Upload a local file to the remote machine. Large files are automatically split into chunks.

```python
def progress(current_chunk, total_chunks):
    print(f"{current_chunk}/{total_chunks}")

client.upload("archive.tar.gz", r"C:\Temp\archive.tar.gz", progress=progress)
```

#### `download(remote_path, local_path, progress=None)`

Download a file from the remote machine. Parent directories on the local side are created automatically.

```python
client.download(r"C:\Temp\report.pdf", "downloads/report.pdf")
```

---

### Remote filesystem operations

#### `exists(remote_path) → bool`

Return `True` if the remote path exists.

#### `stat(remote_path) → dict`

Return basic metadata: `is_dir`, `size` (bytes), `last_write_utc`.

#### `mkdir(remote_path, parents=True)`

Create a directory. With `parents=True` (default), intermediate directories are created as needed.

#### `listdir(remote_path) → list[str]`

List the names of files and directories inside a remote directory.

#### `move(remote_src, remote_dst)`

Move a file or directory on the remote machine.

#### `copy(remote_src, remote_dst, recursive=False)`

Copy a file or directory on the remote machine.

#### `remove(remote_path, recursive=False)`

Delete a file or directory. Use `recursive=True` to remove a directory tree.

---

## How it works

WinRM does not have a built-in file transfer mechanism. `winrmxfer` works around this by:

1. Reading the local file and splitting it into chunks (default 2 KB).
2. Base64-encoding each chunk.
3. Sending a PowerShell command per chunk that decodes the base64 and writes the bytes to the remote file using `[System.IO.File]` methods.
4. Downloads work in reverse: PowerShell reads chunks from the remote file, base64-encodes them, and the client decodes and reassembles the result locally.

The 2 KB chunk size is chosen because `pywinrm` wraps every PowerShell script in a UTF-16LE + base64 encoded `-EncodedCommand`, producing roughly 8× the script size as a command-line argument. Larger chunks exceed the WinRM shell's command-line length limit.

## WinRM setup on Windows

```powershell
# Run as Administrator
Enable-PSRemoting -Force
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
```

For basic authentication (lab use only):
```powershell
Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $true
```

## Running the tests

The test suite runs against a real WinRM target:

```bash
pip install pytest
python -m pytest tests/ -v \
    --host 127.0.0.1 \
    --username Administrator \
    --password s3cr3t \
    --port 5985
```

Credentials can also be provided via environment variables: `WINRM_HOST`, `WINRM_PORT`, `WINRM_USER`, `WINRM_PASS`, `WINRM_TRANSPORT`.

## Publishing

Releases are published to PyPI by the GitHub Actions workflow in `.github/workflows/publish.yml`.

- It runs when a tag matching `v*` is pushed (e.g. `git tag v0.1.1 && git push origin v0.1.1`).
- The build job runs `python -m build` and produces a wheel and an sdist artifact.
- The publish job uploads them to PyPI using OIDC Trusted Publishing, so no `PYPI_TOKEN` secret is needed.

One-time setup on PyPI:
1. PyPI → your project → Publishing → "Add a new publisher".
2. Enter the GitHub repository, the workflow name `publish.yml` and the environment `pypi`.

One-time setup on GitHub:
- Repository Settings → Environments → create an environment named `pypi` (optionally restrict it to `v*` tags).

## License

MIT — see [LICENSE](LICENSE).
