winrmxfer 0.1.0__py3-none-any.whl
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.
winrmxfer/__init__.py
ADDED
winrmxfer/client.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
|
|
5
|
+
import winrm
|
|
6
|
+
|
|
7
|
+
# pywinrm encodes the PS script as UTF-16LE then base64 for -EncodedCommand,
|
|
8
|
+
# giving ~8/3x expansion. With ~250 bytes of PS overhead per chunk, 2 KB of raw
|
|
9
|
+
# data → ~8000 chars of encoded command, just under the WinRM shell limit.
|
|
10
|
+
DEFAULT_CHUNK_SIZE = 2 * 1024
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WinRMFileClient:
|
|
14
|
+
"""File transfer client over WinRM using base64-encoded PowerShell commands.
|
|
15
|
+
|
|
16
|
+
Can be constructed in two ways:
|
|
17
|
+
|
|
18
|
+
# From credentials — the client creates the session internally.
|
|
19
|
+
client = WinRMFileClient("host", "user", "pass", transport="ntlm")
|
|
20
|
+
|
|
21
|
+
# From an existing winrm.Session.
|
|
22
|
+
session = winrm.Session("http://host:5985/wsman", auth=("user", "pass"))
|
|
23
|
+
client = WinRMFileClient(session=session)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
host: str = "",
|
|
29
|
+
username: str = "",
|
|
30
|
+
password: str = "",
|
|
31
|
+
transport: str = "ntlm",
|
|
32
|
+
port: int = 5985,
|
|
33
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
|
34
|
+
session: Optional[winrm.Session] = None,
|
|
35
|
+
**session_kwargs,
|
|
36
|
+
):
|
|
37
|
+
self.chunk_size = chunk_size
|
|
38
|
+
if session is not None:
|
|
39
|
+
self.session = session
|
|
40
|
+
else:
|
|
41
|
+
if not host:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Provide either 'host'/'username'/'password' or an existing 'session'."
|
|
44
|
+
)
|
|
45
|
+
self.session = winrm.Session(
|
|
46
|
+
target=f"http://{host}:{port}/wsman",
|
|
47
|
+
auth=(username, password),
|
|
48
|
+
transport=transport,
|
|
49
|
+
**session_kwargs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _run_ps(self, script: str) -> str:
|
|
53
|
+
result = self.session.run_ps(script)
|
|
54
|
+
if result.status_code != 0:
|
|
55
|
+
err = result.std_err.decode("utf-8", errors="replace").strip()
|
|
56
|
+
raise RuntimeError(f"PowerShell error (exit {result.status_code}): {err}")
|
|
57
|
+
return result.std_out.decode("utf-8", errors="replace").strip()
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _esc(path: str) -> str:
|
|
61
|
+
"""Escape single quotes for PowerShell single-quoted strings."""
|
|
62
|
+
return path.replace("'", "''")
|
|
63
|
+
|
|
64
|
+
def upload(
|
|
65
|
+
self,
|
|
66
|
+
local_path: str | Path,
|
|
67
|
+
remote_path: str,
|
|
68
|
+
progress: Optional[Callable[[int, int], None]] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Upload a local file to the remote machine.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
local_path: Path to the local source file.
|
|
74
|
+
remote_path: Destination path on the remote Windows machine.
|
|
75
|
+
progress: Optional callback(current_chunk, total_chunks) for progress reporting.
|
|
76
|
+
"""
|
|
77
|
+
with open(local_path, "rb") as f:
|
|
78
|
+
data = f.read()
|
|
79
|
+
|
|
80
|
+
# Encode each raw chunk separately so every piece is valid base64.
|
|
81
|
+
raw_chunks = [
|
|
82
|
+
data[i : i + self.chunk_size] for i in range(0, len(data), self.chunk_size)
|
|
83
|
+
]
|
|
84
|
+
chunks = [base64.b64encode(c).decode("ascii") for c in raw_chunks]
|
|
85
|
+
|
|
86
|
+
remote = self._esc(remote_path)
|
|
87
|
+
total = len(chunks)
|
|
88
|
+
|
|
89
|
+
# Create parent directory, then write first chunk (or empty file).
|
|
90
|
+
mkdir_ps = f"""$dir = Split-Path '{remote}' -Parent
|
|
91
|
+
if ($dir -and !(Test-Path $dir)) {{ New-Item -ItemType Directory -Path $dir -Force | Out-Null }}"""
|
|
92
|
+
|
|
93
|
+
if not data:
|
|
94
|
+
self._run_ps(f"{mkdir_ps}\n[System.IO.File]::Create('{remote}').Close()")
|
|
95
|
+
else:
|
|
96
|
+
self._run_ps(
|
|
97
|
+
f"{mkdir_ps}\n[System.IO.File]::WriteAllBytes('{remote}', [System.Convert]::FromBase64String('{chunks[0]}'))"
|
|
98
|
+
)
|
|
99
|
+
if progress:
|
|
100
|
+
progress(1, total)
|
|
101
|
+
|
|
102
|
+
for i, chunk in enumerate(chunks[1:], start=2):
|
|
103
|
+
self._run_ps(f"""
|
|
104
|
+
$path = '{remote}'
|
|
105
|
+
$bytes = [System.Convert]::FromBase64String('{chunk}')
|
|
106
|
+
$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Append)
|
|
107
|
+
try {{
|
|
108
|
+
$stream.Write($bytes, 0, $bytes.Length)
|
|
109
|
+
}} finally {{
|
|
110
|
+
$stream.Close()
|
|
111
|
+
}}
|
|
112
|
+
""")
|
|
113
|
+
if progress:
|
|
114
|
+
progress(i, total)
|
|
115
|
+
|
|
116
|
+
def download(
|
|
117
|
+
self,
|
|
118
|
+
remote_path: str,
|
|
119
|
+
local_path: str | Path,
|
|
120
|
+
progress: Optional[Callable[[int, int], None]] = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Download a file from the remote machine.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
remote_path: Source path on the remote Windows machine.
|
|
126
|
+
local_path: Destination path on the local machine.
|
|
127
|
+
progress: Optional callback(current_chunk, total_chunks) for progress reporting.
|
|
128
|
+
"""
|
|
129
|
+
remote = self._esc(remote_path)
|
|
130
|
+
local_path = Path(local_path)
|
|
131
|
+
|
|
132
|
+
size_str = self._run_ps(f"(Get-Item '{remote}').Length")
|
|
133
|
+
file_size = int(size_str)
|
|
134
|
+
|
|
135
|
+
if local_path.parent:
|
|
136
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
total = max((file_size + self.chunk_size - 1) // self.chunk_size, 1)
|
|
139
|
+
|
|
140
|
+
with open(local_path, "wb") as f:
|
|
141
|
+
offset = 0
|
|
142
|
+
chunk_num = 0
|
|
143
|
+
while offset < file_size:
|
|
144
|
+
b64 = self._run_ps(f"""
|
|
145
|
+
$path = '{remote}'
|
|
146
|
+
$offset = {offset}
|
|
147
|
+
$count = {self.chunk_size}
|
|
148
|
+
$stream = [System.IO.File]::OpenRead($path)
|
|
149
|
+
try {{
|
|
150
|
+
$buf = New-Object byte[] $count
|
|
151
|
+
[void]$stream.Seek($offset, [System.IO.SeekOrigin]::Begin)
|
|
152
|
+
$read = $stream.Read($buf, 0, $count)
|
|
153
|
+
[System.Convert]::ToBase64String($buf, 0, $read)
|
|
154
|
+
}} finally {{
|
|
155
|
+
$stream.Close()
|
|
156
|
+
}}
|
|
157
|
+
""")
|
|
158
|
+
chunk_data = base64.b64decode(b64)
|
|
159
|
+
f.write(chunk_data)
|
|
160
|
+
offset += len(chunk_data)
|
|
161
|
+
chunk_num += 1
|
|
162
|
+
if progress:
|
|
163
|
+
progress(chunk_num, total)
|
|
164
|
+
|
|
165
|
+
# Handle empty file
|
|
166
|
+
if file_size == 0:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
def mkdir(self, remote_path: str, parents: bool = True) -> None:
|
|
170
|
+
"""Create a directory on the remote machine."""
|
|
171
|
+
remote = self._esc(remote_path)
|
|
172
|
+
force = "-Force" if parents else ""
|
|
173
|
+
self._run_ps(
|
|
174
|
+
f"New-Item -ItemType Directory -Path '{remote}' {force} | Out-Null"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def remove(self, remote_path: str, recursive: bool = False) -> None:
|
|
178
|
+
"""Remove a file or directory on the remote machine."""
|
|
179
|
+
remote = self._esc(remote_path)
|
|
180
|
+
recurse = "-Recurse" if recursive else ""
|
|
181
|
+
self._run_ps(f"Remove-Item -Path '{remote}' -Force {recurse}")
|
|
182
|
+
|
|
183
|
+
def exists(self, remote_path: str) -> bool:
|
|
184
|
+
"""Return True if the remote path exists."""
|
|
185
|
+
remote = self._esc(remote_path)
|
|
186
|
+
return self._run_ps(f"Test-Path '{remote}'").lower() == "true"
|
|
187
|
+
|
|
188
|
+
def listdir(self, remote_path: str) -> list[str]:
|
|
189
|
+
"""List names of files and directories in a remote directory."""
|
|
190
|
+
remote = self._esc(remote_path)
|
|
191
|
+
output = self._run_ps(
|
|
192
|
+
f"Get-ChildItem -Path '{remote}' | Select-Object -ExpandProperty Name"
|
|
193
|
+
)
|
|
194
|
+
return output.splitlines() if output else []
|
|
195
|
+
|
|
196
|
+
def move(self, remote_src: str, remote_dst: str) -> None:
|
|
197
|
+
"""Move a file or directory on the remote machine."""
|
|
198
|
+
src = self._esc(remote_src)
|
|
199
|
+
dst = self._esc(remote_dst)
|
|
200
|
+
self._run_ps(f"Move-Item -Path '{src}' -Destination '{dst}' -Force")
|
|
201
|
+
|
|
202
|
+
def copy(self, remote_src: str, remote_dst: str, recursive: bool = False) -> None:
|
|
203
|
+
"""Copy a file or directory on the remote machine."""
|
|
204
|
+
src = self._esc(remote_src)
|
|
205
|
+
dst = self._esc(remote_dst)
|
|
206
|
+
recurse = "-Recurse" if recursive else ""
|
|
207
|
+
self._run_ps(f"Copy-Item -Path '{src}' -Destination '{dst}' -Force {recurse}")
|
|
208
|
+
|
|
209
|
+
def stat(self, remote_path: str) -> dict:
|
|
210
|
+
"""Return basic metadata for a remote path (size, is_dir, last_write)."""
|
|
211
|
+
remote = self._esc(remote_path)
|
|
212
|
+
output = self._run_ps(f"""
|
|
213
|
+
$item = Get-Item '{remote}'
|
|
214
|
+
"$($item.PSIsContainer)|$($item.Length)|$($item.LastWriteTimeUtc.ToString('o'))"
|
|
215
|
+
""")
|
|
216
|
+
is_dir_str, size_str, mtime_str = output.split("|", 2)
|
|
217
|
+
return {
|
|
218
|
+
"is_dir": is_dir_str.lower() == "true",
|
|
219
|
+
"size": int(size_str) if size_str else 0,
|
|
220
|
+
"last_write_utc": mtime_str.strip(),
|
|
221
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: winrmxfer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: File transfer over WinRM using base64-encoded PowerShell commands
|
|
5
|
+
Author: Nagy Szabolcs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Dungscer/WinRMXfer
|
|
8
|
+
Project-URL: Issues, https://github.com/Dungscer/WinRMXfer/issues
|
|
9
|
+
Keywords: winrm,powershell,file-transfer,windows,pywinrm
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pywinrm>=0.4
|
|
18
|
+
Provides-Extra: kerberos
|
|
19
|
+
Requires-Dist: pywinrm[kerberos]; extra == "kerberos"
|
|
20
|
+
Provides-Extra: credssp
|
|
21
|
+
Requires-Dist: pywinrm[credssp]; extra == "credssp"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# winrmxfer
|
|
25
|
+
|
|
26
|
+
A Python package for transferring files over WinRM (Windows Remote Management).
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install winrmxfer
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.10+
|
|
39
|
+
- [pywinrm](https://github.com/diyan/pywinrm)
|
|
40
|
+
- WinRM enabled and configured on the target Windows machine
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from winrmxfer import WinRMFileClient
|
|
46
|
+
|
|
47
|
+
# Option 1 — pass credentials, the client creates the WinRM session internally
|
|
48
|
+
client = WinRMFileClient(
|
|
49
|
+
host="192.168.1.100",
|
|
50
|
+
username="Administrator",
|
|
51
|
+
password="s3cr3t",
|
|
52
|
+
transport="ntlm", # ntlm (default), basic, kerberos, credssp
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Option 2 — pass an existing winrm.Session
|
|
56
|
+
import winrm
|
|
57
|
+
session = winrm.Session("http://192.168.1.100:5985/wsman", auth=("Administrator", "s3cr3t"))
|
|
58
|
+
client = WinRMFileClient(session=session)
|
|
59
|
+
|
|
60
|
+
# Upload a file
|
|
61
|
+
client.upload("local_file.zip", r"C:\Users\Administrator\Desktop\file.zip")
|
|
62
|
+
|
|
63
|
+
# Download a file
|
|
64
|
+
client.download(r"C:\logs\app.log", "app.log")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### `WinRMFileClient(host="", username="", password="", transport="ntlm", port=5985, chunk_size=2048, session=None)`
|
|
70
|
+
|
|
71
|
+
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).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### File transfer
|
|
76
|
+
|
|
77
|
+
#### `upload(local_path, remote_path, progress=None)`
|
|
78
|
+
|
|
79
|
+
Upload a local file to the remote machine. Large files are automatically split into chunks.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def progress(current_chunk, total_chunks):
|
|
83
|
+
print(f"{current_chunk}/{total_chunks}")
|
|
84
|
+
|
|
85
|
+
client.upload("archive.tar.gz", r"C:\Temp\archive.tar.gz", progress=progress)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `download(remote_path, local_path, progress=None)`
|
|
89
|
+
|
|
90
|
+
Download a file from the remote machine. Parent directories on the local side are created automatically.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client.download(r"C:\Temp\report.pdf", "downloads/report.pdf")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Remote filesystem operations
|
|
99
|
+
|
|
100
|
+
#### `exists(remote_path) → bool`
|
|
101
|
+
|
|
102
|
+
Return `True` if the remote path exists.
|
|
103
|
+
|
|
104
|
+
#### `stat(remote_path) → dict`
|
|
105
|
+
|
|
106
|
+
Return basic metadata: `is_dir`, `size` (bytes), `last_write_utc`.
|
|
107
|
+
|
|
108
|
+
#### `mkdir(remote_path, parents=True)`
|
|
109
|
+
|
|
110
|
+
Create a directory. With `parents=True` (default), intermediate directories are created as needed.
|
|
111
|
+
|
|
112
|
+
#### `listdir(remote_path) → list[str]`
|
|
113
|
+
|
|
114
|
+
List the names of files and directories inside a remote directory.
|
|
115
|
+
|
|
116
|
+
#### `move(remote_src, remote_dst)`
|
|
117
|
+
|
|
118
|
+
Move a file or directory on the remote machine.
|
|
119
|
+
|
|
120
|
+
#### `copy(remote_src, remote_dst, recursive=False)`
|
|
121
|
+
|
|
122
|
+
Copy a file or directory on the remote machine.
|
|
123
|
+
|
|
124
|
+
#### `remove(remote_path, recursive=False)`
|
|
125
|
+
|
|
126
|
+
Delete a file or directory. Use `recursive=True` to remove a directory tree.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
WinRM does not have a built-in file transfer mechanism. `winrmxfer` works around this by:
|
|
133
|
+
|
|
134
|
+
1. Reading the local file and splitting it into chunks (default 2 KB).
|
|
135
|
+
2. Base64-encoding each chunk.
|
|
136
|
+
3. Sending a PowerShell command per chunk that decodes the base64 and writes the bytes to the remote file using `[System.IO.File]` methods.
|
|
137
|
+
4. Downloads work in reverse: PowerShell reads chunks from the remote file, base64-encodes them, and the client decodes and reassembles the result locally.
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
## WinRM setup on Windows
|
|
142
|
+
|
|
143
|
+
```powershell
|
|
144
|
+
# Run as Administrator
|
|
145
|
+
Enable-PSRemoting -Force
|
|
146
|
+
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For basic authentication (lab use only):
|
|
150
|
+
```powershell
|
|
151
|
+
Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true
|
|
152
|
+
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $true
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Running the tests
|
|
156
|
+
|
|
157
|
+
The test suite runs against a real WinRM target:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install pytest
|
|
161
|
+
python -m pytest tests/ -v \
|
|
162
|
+
--host 127.0.0.1 \
|
|
163
|
+
--username Administrator \
|
|
164
|
+
--password s3cr3t \
|
|
165
|
+
--port 5985
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Credentials can also be provided via environment variables: `WINRM_HOST`, `WINRM_PORT`, `WINRM_USER`, `WINRM_PASS`, `WINRM_TRANSPORT`.
|
|
169
|
+
|
|
170
|
+
## Publishing
|
|
171
|
+
|
|
172
|
+
Releases are published to PyPI by the GitHub Actions workflow in `.github/workflows/publish.yml`.
|
|
173
|
+
|
|
174
|
+
- It runs when a tag matching `v*` is pushed (e.g. `git tag v0.1.1 && git push origin v0.1.1`).
|
|
175
|
+
- The build job runs `python -m build` and produces a wheel and an sdist artifact.
|
|
176
|
+
- The publish job uploads them to PyPI using OIDC Trusted Publishing, so no `PYPI_TOKEN` secret is needed.
|
|
177
|
+
|
|
178
|
+
One-time setup on PyPI:
|
|
179
|
+
1. PyPI → your project → Publishing → "Add a new publisher".
|
|
180
|
+
2. Enter the GitHub repository, the workflow name `publish.yml` and the environment `pypi`.
|
|
181
|
+
|
|
182
|
+
One-time setup on GitHub:
|
|
183
|
+
- Repository Settings → Environments → create an environment named `pypi` (optionally restrict it to `v*` tags).
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
winrmxfer/__init__.py,sha256=CzjxxgJv3n__M_7ii6INv_V6uXyVOzbUcZEMhroiClE,89
|
|
2
|
+
winrmxfer/client.py,sha256=fGBsTObms7PmYjlh7DJFpsuR7ZhLmxrMcdYB_LCUvCo,8046
|
|
3
|
+
winrmxfer-0.1.0.dist-info/licenses/LICENSE,sha256=45mCYN3HW4Sxg4usPT3iMheQ1fsbD5vbzw1AykTf25w,1070
|
|
4
|
+
winrmxfer-0.1.0.dist-info/METADATA,sha256=ClcFP32kx44pRcaUg-gN6o-b1vYj2pNU3FVGwnBUs7M,6177
|
|
5
|
+
winrmxfer-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
winrmxfer-0.1.0.dist-info/top_level.txt,sha256=mQ2qOGm_wIqpdWMd2L7hPSxKImpFFtSs26P1ZpvbtVI,10
|
|
7
|
+
winrmxfer-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nagy Szabolcs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
winrmxfer
|