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:
@@ -227,6 +227,210 @@ class OwncloudClient:
|
||||
u["used_source"] = "du" if du is not None else None
|
||||
return users
|
||||
|
||||
# -- occ helpers for management ops ----------------------------------------
|
||||
def occ_env(self, env: dict, args: str,
|
||||
timeout: int = EXEC_TIMEOUT) -> tuple[int, str, str]:
|
||||
"""Run occ with extra environment (e.g. OC_PASS) exported for the apache
|
||||
user. `sudo -u apache env KEY=val php occ ...` — the explicit `env` sets
|
||||
the var inside apache's context regardless of sudo's env_reset policy.
|
||||
|
||||
Caveat: the value is briefly visible in `ps` on the server to root; this
|
||||
is occ's own documented mechanism for passwords and the box is root-only.
|
||||
"""
|
||||
prefix = " ".join(f"{k}={shlex.quote(str(v))}" for k, v in env.items())
|
||||
cmd = f"sudo -u apache env {prefix} php {OCC_INSTALL_DIR}/occ {args}"
|
||||
return self.run(cmd, timeout=timeout)
|
||||
|
||||
def _occ_ok(self, args: str, timeout: int = EXEC_TIMEOUT) -> str:
|
||||
"""Run occ, raise BackupError on non-zero, return stdout."""
|
||||
rc, out, err = self.occ(args, timeout=timeout)
|
||||
if rc != 0:
|
||||
label = args.split()[0] if args else "occ"
|
||||
raise bc.BackupError(f"occ {label} failed (rc={rc}): "
|
||||
f"{(err or out).strip()[:300]}")
|
||||
return out
|
||||
|
||||
# -- user lifecycle --------------------------------------------------------
|
||||
def create_user(self, uid: str, password: str,
|
||||
display_name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
groups: Optional[list[str]] = None) -> dict:
|
||||
parts = [f"user:add {shlex.quote(uid)} --password-from-env -n"]
|
||||
if display_name:
|
||||
parts.append(f"--display-name={shlex.quote(display_name)}")
|
||||
if email:
|
||||
parts.append(f"--email={shlex.quote(email)}")
|
||||
for g in (groups or []):
|
||||
parts.append(f"-g {shlex.quote(g)}")
|
||||
rc, out, err = self.occ_env({"OC_PASS": password}, " ".join(parts))
|
||||
if rc != 0:
|
||||
raise bc.BackupError(f"user:add {uid} failed (rc={rc}): "
|
||||
f"{(err or out).strip()[:300]}")
|
||||
return {"uid": uid, "output": out.strip()}
|
||||
|
||||
def delete_user(self, uid: str, force: bool = False) -> str:
|
||||
return self._occ_ok(f"user:delete {shlex.quote(uid)}"
|
||||
+ (" -f" if force else ""), timeout=DU_TIMEOUT).strip()
|
||||
|
||||
def set_user_enabled(self, uid: str, enabled: bool) -> str:
|
||||
verb = "user:enable" if enabled else "user:disable"
|
||||
return self._occ_ok(f"{verb} {shlex.quote(uid)}").strip()
|
||||
|
||||
def reset_password(self, uid: str, password: str) -> str:
|
||||
rc, out, err = self.occ_env(
|
||||
{"OC_PASS": password},
|
||||
f"user:resetpassword {shlex.quote(uid)} --password-from-env")
|
||||
if rc != 0:
|
||||
raise bc.BackupError(f"user:resetpassword {uid} failed (rc={rc}): "
|
||||
f"{(err or out).strip()[:300]}")
|
||||
return out.strip()
|
||||
|
||||
def modify_user(self, uid: str, key: str, value: str) -> str:
|
||||
"""key is one of: displayname, email (per occ user:modify)."""
|
||||
return self._occ_ok(
|
||||
f"user:modify {shlex.quote(uid)} {shlex.quote(key)} {shlex.quote(value)}").strip()
|
||||
|
||||
# -- quota (occ user:setting <uid> files quota) ----------------------------
|
||||
def get_quota(self, uid: str) -> Optional[str]:
|
||||
rc, out, err = self.occ(f"user:setting {shlex.quote(uid)} files quota")
|
||||
return out.strip() or None if rc == 0 else None
|
||||
|
||||
def set_quota(self, uid: str, value: str) -> str:
|
||||
return self._occ_ok(
|
||||
f"user:setting {shlex.quote(uid)} files quota --value={shlex.quote(value)}").strip()
|
||||
|
||||
def reset_quota(self, uid: str) -> str:
|
||||
return self._occ_ok(
|
||||
f"user:setting {shlex.quote(uid)} files quota --delete").strip()
|
||||
|
||||
# -- groups ----------------------------------------------------------------
|
||||
def list_groups(self) -> Any:
|
||||
return json.loads(self._occ_ok("group:list --output=json") or "[]")
|
||||
|
||||
def group_members(self, group: str) -> Any:
|
||||
return json.loads(self._occ_ok(
|
||||
f"group:list-members {shlex.quote(group)} --output=json") or "[]")
|
||||
|
||||
def user_groups(self, uid: str) -> Any:
|
||||
return json.loads(self._occ_ok(
|
||||
f"user:list-groups {shlex.quote(uid)} --output=json") or "[]")
|
||||
|
||||
def create_group(self, name: str) -> str:
|
||||
return self._occ_ok(f"group:add {shlex.quote(name)}").strip()
|
||||
|
||||
def delete_group(self, name: str) -> str:
|
||||
return self._occ_ok(f"group:delete {shlex.quote(name)}").strip()
|
||||
|
||||
def add_group_members(self, group: str, members: list[str]) -> str:
|
||||
ms = " ".join(f"-m {shlex.quote(m)}" for m in members)
|
||||
return self._occ_ok(f"group:add-member {shlex.quote(group)} {ms}").strip()
|
||||
|
||||
def remove_group_members(self, group: str, members: list[str]) -> str:
|
||||
ms = " ".join(f"-m {shlex.quote(m)}" for m in members)
|
||||
return self._occ_ok(f"group:remove-member {shlex.quote(group)} {ms}").strip()
|
||||
|
||||
# -- shares (read-only, straight from the DB; occ has no share list) --------
|
||||
SHARE_TYPES = {0: "user", 1: "group", 3: "public_link",
|
||||
4: "email", 6: "federated"}
|
||||
|
||||
def list_shares(self, uid: Optional[str] = None) -> list[dict]:
|
||||
# NB: uid is NOT inlined into SQL. MySQL honours backslash escapes by
|
||||
# default, so ''-doubling would not safely contain a hostile/odd uid
|
||||
# (and occ allows "'" in uids). Fetch all rows, filter in Python.
|
||||
pfx = self._db_config()["dbtableprefix"]
|
||||
sql = (f"SELECT s.id, s.share_type, s.share_with, s.uid_owner, "
|
||||
f"s.file_target, s.permissions, s.stime, fc.path "
|
||||
f"FROM {pfx}share s "
|
||||
f"LEFT JOIN {pfx}filecache fc ON fc.fileid=s.file_source "
|
||||
f"ORDER BY s.stime DESC;")
|
||||
out = self._mysql(sql)
|
||||
shares: list[dict] = []
|
||||
for row in out.splitlines():
|
||||
f = [None if v in ("NULL", "") else v for v in row.split("\t")]
|
||||
if len(f) < 8:
|
||||
continue
|
||||
if uid and uid not in (f[2], f[3]): # share_with / owner
|
||||
continue
|
||||
st = _int(f[1])
|
||||
shares.append({
|
||||
"id": _int(f[0]),
|
||||
"share_type": self.SHARE_TYPES.get(st, str(st)),
|
||||
"share_with": f[2],
|
||||
"owner": f[3],
|
||||
"target": f[4],
|
||||
"permissions": _int(f[5]),
|
||||
"created": _epoch(f[6]),
|
||||
"path": f[7],
|
||||
})
|
||||
return shares
|
||||
|
||||
# -- server / apps / config ------------------------------------------------
|
||||
def maintenance_mode(self, on: bool) -> str:
|
||||
return self._occ_ok("maintenance:mode " + ("--on" if on else "--off")).strip()
|
||||
|
||||
def maintenance_status(self) -> bool:
|
||||
rc, out, err = self.occ("config:system:get maintenance")
|
||||
return rc == 0 and out.strip().lower() in ("true", "1", "yes")
|
||||
|
||||
def list_apps(self) -> Any:
|
||||
return json.loads(self._occ_ok("app:list --output=json") or "{}")
|
||||
|
||||
def enable_app(self, app: str) -> str:
|
||||
return self._occ_ok(f"app:enable {shlex.quote(app)}").strip()
|
||||
|
||||
def disable_app(self, app: str) -> str:
|
||||
return self._occ_ok(f"app:disable {shlex.quote(app)}").strip()
|
||||
|
||||
def config_get(self, name: str) -> Optional[str]:
|
||||
rc, out, err = self.occ(f"config:system:get {shlex.quote(name)}")
|
||||
return out.strip() if rc == 0 else None
|
||||
|
||||
def config_list(self) -> Any:
|
||||
return json.loads(self._occ_ok("config:list system --output=json") or "{}")
|
||||
|
||||
def config_set(self, name: str, value: str, vtype: str = "string") -> str:
|
||||
return self._occ_ok(
|
||||
f"config:system:set {shlex.quote(name)} "
|
||||
f"--value={shlex.quote(value)} --type={shlex.quote(vtype)}").strip()
|
||||
|
||||
# -- files admin -----------------------------------------------------------
|
||||
def files_scan(self, uid: Optional[str] = None, all_users: bool = False,
|
||||
path: Optional[str] = None) -> str:
|
||||
if all_users:
|
||||
args = "files:scan --all"
|
||||
elif uid:
|
||||
args = f"files:scan {shlex.quote(uid)}"
|
||||
else:
|
||||
raise bc.BackupError("files_scan requires a uid or all_users=True")
|
||||
if path:
|
||||
args += f" --path={shlex.quote(path)}"
|
||||
return self._occ_ok(args, timeout=DU_TIMEOUT).strip()
|
||||
|
||||
def transfer_ownership(self, src: str, dst: str,
|
||||
path: Optional[str] = None) -> str:
|
||||
args = (f"files:transfer-ownership {shlex.quote(src)} "
|
||||
f"{shlex.quote(dst)} -s")
|
||||
if path:
|
||||
args += f" --path={shlex.quote(path)}"
|
||||
return self._occ_ok(args, timeout=DU_TIMEOUT).strip()
|
||||
|
||||
def trashbin_cleanup(self, users: list[str]) -> str:
|
||||
us = " ".join(shlex.quote(u) for u in users)
|
||||
return self._occ_ok(f"trashbin:cleanup {us}".strip(),
|
||||
timeout=DU_TIMEOUT).strip()
|
||||
|
||||
def versions_cleanup(self, users: list[str]) -> str:
|
||||
us = " ".join(shlex.quote(u) for u in users)
|
||||
return self._occ_ok(f"versions:cleanup {us}".strip(),
|
||||
timeout=DU_TIMEOUT).strip()
|
||||
|
||||
|
||||
def _int(v: Any) -> Optional[int]:
|
||||
try:
|
||||
return int(str(v).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _epoch(v: Any) -> Optional[str]:
|
||||
"""occ emits lastLogin/creationTime as Unix epoch seconds; 0 = never."""
|
||||
|
||||
Reference in New Issue
Block a user