synclayer 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.
- synclayer/__init__.py +13 -0
- synclayer/__main__.py +4 -0
- synclayer/_version.py +1 -0
- synclayer/autostart.py +175 -0
- synclayer/cli.py +913 -0
- synclayer/client/__init__.py +0 -0
- synclayer/client/api.py +283 -0
- synclayer/client/config.py +224 -0
- synclayer/client/sync.py +566 -0
- synclayer/common/__init__.py +0 -0
- synclayer/common/scan.py +40 -0
- synclayer/config.py +317 -0
- synclayer/deploy.py +370 -0
- synclayer/desktop.py +281 -0
- synclayer/devtools.py +209 -0
- synclayer/gui/__init__.py +23 -0
- synclayer/gui/app.py +1123 -0
- synclayer/gui/icon.py +63 -0
- synclayer/py.typed +0 -0
- synclayer/remote.py +364 -0
- synclayer/server/__init__.py +15 -0
- synclayer/server/app.py +1404 -0
- synclayer/server/library.py +423 -0
- synclayer/server/mfa.py +407 -0
- synclayer/server/quota.py +119 -0
- synclayer/server/security.py +268 -0
- synclayer/server/users.py +163 -0
- synclayer/server/web/app.css +277 -0
- synclayer/server/web/app.js +841 -0
- synclayer/server/web/index.html +307 -0
- synclayer/server/web/share.html +30 -0
- synclayer/server/web/share.js +57 -0
- synclayer/theme.py +127 -0
- synclayer/tui.py +1318 -0
- synclayer/vault.py +364 -0
- synclayer/winservice.py +483 -0
- synclayer-0.1.0.dist-info/METADATA +418 -0
- synclayer-0.1.0.dist-info/RECORD +42 -0
- synclayer-0.1.0.dist-info/WHEEL +5 -0
- synclayer-0.1.0.dist-info/entry_points.txt +6 -0
- synclayer-0.1.0.dist-info/licenses/LICENSE +21 -0
- synclayer-0.1.0.dist-info/top_level.txt +1 -0
synclayer/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
APP_NAME = "Synclayer"
|
|
3
|
+
|
|
4
|
+
_LAZY = ("Remote", "Watcher", "connect", "RemoteError", "ApiError", "SyncResult")
|
|
5
|
+
__all__ = ["__version__", "APP_NAME", *_LAZY]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name): # keep `import synclayer` light; load the client library on first use
|
|
9
|
+
if name in _LAZY:
|
|
10
|
+
from . import remote
|
|
11
|
+
|
|
12
|
+
return getattr(remote, name)
|
|
13
|
+
raise AttributeError(f"module 'synclayer' has no attribute {name!r}")
|
synclayer/__main__.py
ADDED
synclayer/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
synclayer/autostart.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Launch-on-boot / login for the tray app (target 'tray') or the server (target 'server').
|
|
2
|
+
|
|
3
|
+
Windows : tray -> HKCU Run key; server -> Task Scheduler ONSTART (needs an elevated shell)
|
|
4
|
+
Linux : tray -> ~/.config/autostart .desktop; server -> systemd *user* unit
|
|
5
|
+
(for a boot-time system service use scripts/install-service.sh)
|
|
6
|
+
macOS : LaunchAgent plist
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import plistlib
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
NAMES = {"tray": "Synclayer", "server": "SynclayerServer"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _python_exe(target: str) -> str:
|
|
20
|
+
"""pythonw.exe for the tray on Windows so no console window flashes at login."""
|
|
21
|
+
exe = Path(sys.executable)
|
|
22
|
+
if target == "tray" and sys.platform.startswith("win"):
|
|
23
|
+
pyw = exe.with_name("pythonw.exe")
|
|
24
|
+
if pyw.is_file():
|
|
25
|
+
return str(pyw)
|
|
26
|
+
return str(exe)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _command(target: str) -> list[str]:
|
|
30
|
+
base = [sys.executable] if getattr(sys, "frozen", False) else [_python_exe(target), "-m", "synclayer"]
|
|
31
|
+
return base + (["tray", "--minimized"] if target == "tray" else ["server", "serve"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _env_file() -> str | None:
|
|
35
|
+
f = os.environ.get("SL_ENV_FILE")
|
|
36
|
+
p = Path(f) if f else Path(".env")
|
|
37
|
+
return str(p.resolve()) if p.is_file() else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------- Windows
|
|
41
|
+
def _win_enable(target):
|
|
42
|
+
cmd = subprocess.list2cmdline(_command(target))
|
|
43
|
+
if target == "tray":
|
|
44
|
+
import winreg
|
|
45
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
46
|
+
0, winreg.KEY_SET_VALUE) as k:
|
|
47
|
+
winreg.SetValueEx(k, NAMES[target], 0, winreg.REG_SZ, cmd)
|
|
48
|
+
return "Tray app will start at login."
|
|
49
|
+
env = _env_file()
|
|
50
|
+
tr = f'cmd /c "set SL_ENV_FILE={env}&& {cmd}"' if env else cmd
|
|
51
|
+
r = subprocess.run(["schtasks", "/Create", "/TN", NAMES[target], "/TR", tr, "/SC", "ONSTART",
|
|
52
|
+
"/RU", "SYSTEM", "/RL", "HIGHEST", "/F"], capture_output=True, text=True)
|
|
53
|
+
if r.returncode:
|
|
54
|
+
raise SystemExit("Could not create scheduled task (run as Administrator): " + r.stderr.strip())
|
|
55
|
+
return "Server scheduled to start at boot."
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _win_disable(target):
|
|
59
|
+
if target == "tray":
|
|
60
|
+
import winreg
|
|
61
|
+
try:
|
|
62
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
63
|
+
0, winreg.KEY_SET_VALUE) as k:
|
|
64
|
+
winreg.DeleteValue(k, NAMES[target])
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
pass
|
|
67
|
+
else:
|
|
68
|
+
subprocess.run(["schtasks", "/Delete", "/TN", NAMES[target], "/F"], capture_output=True)
|
|
69
|
+
return "Disabled."
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _win_enabled(target):
|
|
73
|
+
if target == "tray":
|
|
74
|
+
import winreg
|
|
75
|
+
try:
|
|
76
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run") as k:
|
|
77
|
+
winreg.QueryValueEx(k, NAMES[target])
|
|
78
|
+
return True
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
return False
|
|
81
|
+
return subprocess.run(["schtasks", "/Query", "/TN", NAMES[target]], capture_output=True).returncode == 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------- Linux
|
|
85
|
+
def _desktop_path() -> Path:
|
|
86
|
+
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "autostart" / "synclayer.desktop"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _unit_path() -> Path:
|
|
90
|
+
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "systemd" / "user" / "synclayer-server.service"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _quote(cmd):
|
|
94
|
+
return " ".join('"' + c.replace('"', '\\"') + '"' if " " in c else c for c in cmd)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _linux_enable(target):
|
|
98
|
+
if target == "tray":
|
|
99
|
+
p = _desktop_path()
|
|
100
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
p.write_text("[Desktop Entry]\nType=Application\nName=Synclayer\n"
|
|
102
|
+
f"Exec={_quote(_command('tray'))}\nX-GNOME-Autostart-enabled=true\nTerminal=false\n"
|
|
103
|
+
"Comment=Synclayer sync and file manager\n")
|
|
104
|
+
return f"Autostart entry written: {p}"
|
|
105
|
+
u = _unit_path()
|
|
106
|
+
u.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
env = _env_file()
|
|
108
|
+
u.write_text("[Unit]\nDescription=Synclayer server\nAfter=network-online.target\n\n[Service]\n"
|
|
109
|
+
f"ExecStart={_quote(_command('server'))}\nRestart=on-failure\n"
|
|
110
|
+
+ (f"Environment=SL_ENV_FILE={env}\n" if env else "") +
|
|
111
|
+
"\n[Install]\nWantedBy=default.target\n")
|
|
112
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"])
|
|
113
|
+
subprocess.run(["systemctl", "--user", "enable", "--now", "synclayer-server.service"])
|
|
114
|
+
return ("User service enabled. To start at BOOT without logging in run: "
|
|
115
|
+
"sudo loginctl enable-linger $USER (or use scripts/install-service.sh)")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _linux_disable(target):
|
|
119
|
+
if target == "tray":
|
|
120
|
+
_desktop_path().unlink(missing_ok=True)
|
|
121
|
+
else:
|
|
122
|
+
subprocess.run(["systemctl", "--user", "disable", "--now", "synclayer-server.service"])
|
|
123
|
+
_unit_path().unlink(missing_ok=True)
|
|
124
|
+
return "Disabled."
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _linux_enabled(target):
|
|
128
|
+
return _desktop_path().exists() if target == "tray" else _unit_path().exists()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------- macOS
|
|
132
|
+
def _plist_path(target):
|
|
133
|
+
return Path.home() / "Library" / "LaunchAgents" / f"com.synclayer.{target}.plist"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _mac_enable(target):
|
|
137
|
+
p = _plist_path(target)
|
|
138
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
data = {"Label": f"com.synclayer.{target}", "ProgramArguments": _command(target), "RunAtLoad": True,
|
|
140
|
+
"KeepAlive": target == "server"}
|
|
141
|
+
if _env_file():
|
|
142
|
+
data["EnvironmentVariables"] = {"SL_ENV_FILE": _env_file()}
|
|
143
|
+
logs = Path.home() / "Library" / "Logs" / "Synclayer"
|
|
144
|
+
logs.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
data["StandardOutPath"] = str(logs / f"{target}.log")
|
|
146
|
+
data["StandardErrorPath"] = str(logs / f"{target}.err.log")
|
|
147
|
+
p.write_bytes(plistlib.dumps(data))
|
|
148
|
+
subprocess.run(["launchctl", "load", "-w", str(p)])
|
|
149
|
+
return f"LaunchAgent installed: {p} (logs: ~/Library/Logs/Synclayer)"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _mac_disable(target):
|
|
153
|
+
p = _plist_path(target)
|
|
154
|
+
subprocess.run(["launchctl", "unload", "-w", str(p)], capture_output=True)
|
|
155
|
+
p.unlink(missing_ok=True)
|
|
156
|
+
return "Disabled."
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------- public API
|
|
160
|
+
def enable(target: str = "tray") -> str:
|
|
161
|
+
if sys.platform.startswith("win"):
|
|
162
|
+
return _win_enable(target)
|
|
163
|
+
return _mac_enable(target) if sys.platform == "darwin" else _linux_enable(target)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def disable(target: str = "tray") -> str:
|
|
167
|
+
if sys.platform.startswith("win"):
|
|
168
|
+
return _win_disable(target)
|
|
169
|
+
return _mac_disable(target) if sys.platform == "darwin" else _linux_disable(target)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def is_enabled(target: str = "tray") -> bool:
|
|
173
|
+
if sys.platform.startswith("win"):
|
|
174
|
+
return _win_enabled(target)
|
|
175
|
+
return _plist_path(target).exists() if sys.platform == "darwin" else _linux_enabled(target)
|