sync: auto-sync from HOWARD-HOME at 2026-07-05 19:59:52

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 19:59:52
This commit is contained in:
2026-07-05 20:00:20 -07:00
parent b0ebd44c9d
commit 976bfe7957
12 changed files with 1712 additions and 14 deletions

View File

@@ -174,6 +174,111 @@ class SeafileClient:
devices = [d for d in devices if d.get("user") == user_email]
return devices
# -- write plumbing --------------------------------------------------------
def _write(self, method: str, path: str, form: Optional[dict] = None,
retry_auth: bool = True) -> Any:
"""POST/PUT/DELETE against the admin API using form-encoding.
The Seafile admin endpoints read from request.data; the share endpoints
use QueryDict.getlist('share_to'), which only works for form-encoded
bodies (not JSON) -- so all writes go out as
application/x-www-form-urlencoded. `form` values that are lists are
expanded (doseq) into repeated fields.
"""
url = f"{self.base}{path}"
data = urllib.parse.urlencode(form or {}, doseq=True).encode()
req = bc.urllib.request.Request(
url, data=data, method=method,
headers={"Authorization": f"Token {self._tok()}",
"Content-Type": "application/x-www-form-urlencoded"})
try:
with bc.urllib.request.urlopen(req, timeout=bc.HTTP_TIMEOUT) as resp:
status = resp.getcode()
raw = resp.read().decode("utf-8", errors="replace")
except bc.urllib.error.HTTPError as exc:
status = exc.code
raw = exc.read().decode("utf-8", errors="replace")
except bc.urllib.error.URLError as exc:
raise bc.BackupError(f"Seafile {method} {path} failed: {exc}")
if status in (401, 403) and retry_auth:
self._token = self._authorize()
return self._write(method, path, form, retry_auth=False)
try:
parsed = json.loads(raw) if raw.strip() else {}
except json.JSONDecodeError:
parsed = {"_raw": raw[:600]}
if status not in (200, 201):
msg = parsed.get("error_msg") or parsed.get("detail") or raw[:400]
raise bc.BackupError(f"Seafile {method} {path} failed (HTTP {status}): {msg}",
status=status)
return parsed
@staticmethod
def _uenc(email: str) -> str:
return urllib.parse.quote(email, safe="")
# -- user writes -----------------------------------------------------------
def create_user(self, email: str, password: str, name: Optional[str] = None,
quota_mb: Optional[int] = None, role: Optional[str] = None,
is_active: bool = True) -> dict:
form: dict = {"email": email, "password": password,
"is_active": "true" if is_active else "false"}
if name:
form["name"] = name
if quota_mb is not None:
form["quota_total"] = str(int(quota_mb))
if role:
form["role"] = role
return self._write("POST", "/api/v2.1/admin/users/", form)
def update_user(self, email: str, is_active: Optional[bool] = None,
password: Optional[str] = None, name: Optional[str] = None,
quota_mb: Optional[int] = None, role: Optional[str] = None) -> dict:
form: dict = {}
if is_active is not None:
form["is_active"] = "true" if is_active else "false"
if password is not None:
form["password"] = password
if name is not None:
form["name"] = name
if quota_mb is not None:
form["quota_total"] = str(int(quota_mb))
if role is not None:
form["role"] = role
if not form:
raise bc.BackupError("update_user: nothing to change")
return self._write("PUT", f"/api/v2.1/admin/users/{self._uenc(email)}/", form)
def delete_user(self, email: str) -> dict:
return self._write("DELETE", f"/api/v2.1/admin/users/{self._uenc(email)}/")
# -- library writes --------------------------------------------------------
def create_library(self, name: str, owner: Optional[str] = None) -> dict:
form: dict = {"name": name}
if owner:
form["owner"] = owner
return self._write("POST", "/api/v2.1/admin/libraries/", form)
def transfer_library(self, repo_id: str, new_owner: str) -> dict:
return self._write("PUT", f"/api/v2.1/admin/libraries/{repo_id}/",
{"owner": new_owner})
def delete_library(self, repo_id: str) -> dict:
return self._write("DELETE", f"/api/v2.1/admin/libraries/{repo_id}/")
# -- sharing writes --------------------------------------------------------
def share_library(self, repo_id: str, share_to, permission: str = "rw",
share_type: str = "user") -> dict:
recipients = share_to if isinstance(share_to, list) else [share_to]
form = {"share_type": share_type, "permission": permission,
"share_to": recipients}
return self._write("POST", f"/api/v2.1/admin/libraries/{repo_id}/shares/", form)
def unshare_library(self, repo_id: str, share_to: str,
share_type: str = "user") -> dict:
return self._write("DELETE", f"/api/v2.1/admin/libraries/{repo_id}/shares/",
{"share_type": share_type, "share_to": share_to})
def main() -> int:
try: