rsyncthing 0.1.0__tar.gz
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.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: rsyncthing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Dist: asyncssh>=2.22.0
|
|
6
|
+
Requires-Dist: cyclopts>=4.5.3
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: rich>=14.3.2
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# rsyncthing: A CLI for syncthing that's as easy to use as rsync.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# rsyncthing: A CLI for syncthing that's as easy to use as rsync.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rsyncthing"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"asyncssh>=2.22.0",
|
|
9
|
+
"cyclopts>=4.5.3",
|
|
10
|
+
"httpx>=0.28.1",
|
|
11
|
+
"rich>=14.3.2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
rsyncthing = "rsyncthing.cli:app"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.8.22,<0.9.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run
|
|
2
|
+
# (c) 2026 Kimberly Wilber and the rsyncthing contributors
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import asyncssh
|
|
8
|
+
import cyclopts
|
|
9
|
+
import httpx
|
|
10
|
+
app = cyclopts.App()
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SyncThingConnection:
|
|
14
|
+
my_device_id: str | None = field(default=None, kw_only=True)
|
|
15
|
+
syncthing_binary_path: str = field(default="syncthing", kw_only=True)
|
|
16
|
+
_http_client: httpx.AsyncClient | None = field(init=False, default=None)
|
|
17
|
+
|
|
18
|
+
def is_connected(self) -> bool:
|
|
19
|
+
...
|
|
20
|
+
async def connect(self):
|
|
21
|
+
...
|
|
22
|
+
async def fetch_status(self):
|
|
23
|
+
if not self.is_connected():
|
|
24
|
+
raise RuntimeError("Not connected")
|
|
25
|
+
response = await self._http_client.get("/rest/system/status")
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
status = response.json()
|
|
28
|
+
self.my_device_id = status.get("myID")
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SSHSyncThingConnection(SyncThingConnection):
|
|
32
|
+
connection_string: str
|
|
33
|
+
|
|
34
|
+
_ssh_client: asyncssh.SSHClientConnection|None = field(init=False, default=None)
|
|
35
|
+
_ssh_http_port_listener: asyncssh.SSHListener|None = field(init=False, default=None)
|
|
36
|
+
_is_connected: bool = field(init=False, default=False)
|
|
37
|
+
|
|
38
|
+
def is_connected(self) -> bool:
|
|
39
|
+
return self._is_connected
|
|
40
|
+
|
|
41
|
+
async def connect(self):
|
|
42
|
+
try:
|
|
43
|
+
if self.is_connected():
|
|
44
|
+
raise RuntimeError("Already connected")
|
|
45
|
+
self._ssh_client = await asyncssh.connect(
|
|
46
|
+
self.connection_string,
|
|
47
|
+
)
|
|
48
|
+
config_response = await self._ssh_client.run(f"{self.syncthing_binary_path} cli config dump-json", check=True)
|
|
49
|
+
config_json = json.loads(config_response.stdout) # type: ignore
|
|
50
|
+
if not config_json.get("gui", {}).get("enabled", False):
|
|
51
|
+
raise RuntimeError("Syncthing GUI is not enabled on the remote device")
|
|
52
|
+
api_key = config_json.get("gui", {}).get("apiKey", "")
|
|
53
|
+
address = config_json.get("gui", {}).get("address", "")
|
|
54
|
+
remote_st_host, remote_st_port = address.rsplit(":", 1)
|
|
55
|
+
|
|
56
|
+
self._ssh_http_port_listener = await self._ssh_client.forward_local_port(
|
|
57
|
+
"localhost", 0, remote_st_host, int(remote_st_port)
|
|
58
|
+
)
|
|
59
|
+
self._http_client = httpx.AsyncClient(
|
|
60
|
+
base_url=f"http://localhost:{self._ssh_http_port_listener.get_port()}",
|
|
61
|
+
headers={"X-API-Key": api_key},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self._is_connected = True
|
|
65
|
+
|
|
66
|
+
# Fetch the status to verify the connection and get the device ID
|
|
67
|
+
await self.fetch_status()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
msg = f"{self.connection_string}: Couldn't connect: {e!s}"
|
|
72
|
+
raise RuntimeError(msg) from e
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command
|
|
76
|
+
def watch():
|
|
77
|
+
print("Watching for changes...")
|
|
78
|
+
|
|
79
|
+
@app.command
|
|
80
|
+
def cp():
|
|
81
|
+
print("Hello from Cyclopts")
|