docker-seafile-client/dsc/client.py

203 lines
6.6 KiB
Python

import argparse
import logging
import os
import subprocess
import time
from typing import Optional
from urllib.parse import urlparse
from cached_property import cached_property_with_ttl
import requests
from dsc import const
from dsc.misc import create_dir, hide_password
_lg = logging.getLogger(__name__)
class SeafileClient:
def __init__(self,
host: str,
user: str,
passwd: str,
app_dir: str = const.DEFAULT_APP_DIR):
up = urlparse(requests.get(f"http://{host}").url)
self.url = f"{up.scheme}://{up.netloc}"
self.user = user
self.password = passwd
self.app_dir = os.path.abspath(app_dir)
self.__token = None
def __str__(self):
return f"SeafileClient({self.user}@{self.url})"
def __gen_cmd(self, cmd: str) -> list:
return ["su", "-", const.DEFAULT_USERNAME, "-c", cmd]
@property
def token(self):
if self.__token is None:
url = f"{self.url}/api2/auth-token/"
_lg.info("Fetching token: %s", url)
r = requests.post(url, data={"username": self.user, "password": self.password})
if r.status_code != 200:
raise RuntimeError(f"Can't get token: {r.text}")
self.__token = r.json()["token"]
return self.__token
@cached_property_with_ttl(ttl=60)
def remote_libraries(self) -> dict:
url = f"{self.url}/api2/repos/"
_lg.info("Fetching remote libraries: %s", url)
auth_header = {"Authorization": f"Token {self.token}"}
r = requests.get(url, headers=auth_header)
if r.status_code != 200:
raise RuntimeError(r.text)
r_libs = {lib["id"]: lib["name"] for lib in r.json()}
return r_libs
@property
def config_initialized(self) -> bool:
return os.path.isdir(os.path.join(self.app_dir, ".ccnet"))
@property
def daemon_ready(self) -> bool:
cmd = "seaf-cli status"
_lg.info("Checking seafile daemon status: %s", cmd)
proc = subprocess.run(
self.__gen_cmd(cmd),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return proc.returncode == 0
def init_config(self):
if self.config_initialized:
return
cmd = "seaf-cli init -d %s" % self.app_dir
_lg.info("Initializing seafile config: %s", cmd)
subprocess.run(self.__gen_cmd(cmd))
def start_daemon(self):
cmd = "seaf-cli start"
_lg.info("Starting seafile daemon: %s", cmd)
subprocess.run(self.__gen_cmd(cmd))
_lg.info("Waiting for seafile daemon to start")
while True:
if self.daemon_ready:
break
time.sleep(5)
_lg.info("Seafile daemon is ready")
def stop_daemon(self):
cmd = "seaf-cli stop"
_lg.info("Stopping seafile daemon: %s", cmd)
subprocess.run(self.__gen_cmd(cmd))
_lg.info("Waiting for seafile daemon to stop")
while True:
if not self.daemon_ready:
break
time.sleep(5)
_lg.info("Seafile daemon is stopped")
def get_library_id(self, library) -> Optional[str]:
for lib_id, lib_name in self.remote_libraries.items():
if library in (lib_id, lib_name):
return lib_id
return None
def sync_lib(self, lib_id: str, parent_dir: str = const.DEFAULT_LIBS_DIR):
lib_name = self.remote_libraries[lib_id]
lib_dir = os.path.join(parent_dir, lib_name.replace(" ", "_"))
create_dir(lib_dir)
cmd = [
"seaf-cli",
"sync",
"-l", lib_id,
"-s", self.url,
"-d", lib_dir,
"-u", self.user,
"-p", '"' + self.password + '"',
]
_lg.info(
"Syncing library %s: %s", lib_name,
" ".join(hide_password(cmd, self.password)),
)
subprocess.run(self.__gen_cmd(" ".join(cmd)))
def get_status(self):
cmd = "seaf-cli status"
_lg.debug("Fetching seafile client status: %s", cmd)
out = subprocess.check_output(self.__gen_cmd(cmd))
out = out.decode().splitlines()
statuses = dict()
for line in out:
if line.startswith("#") or not line.strip():
continue
lib, status = line.split(sep="\t", maxsplit=1)
lib = lib.strip()
status = " ".join(status.split())
statuses[lib] = status
return statuses
def watch_status(self):
prev_status = dict()
while True:
time.sleep(const.STATUS_POLL_PERIOD)
cur_status = self.get_status()
for folder, state in cur_status.items():
if state != prev_status.get(folder):
logging.info("Library %s:\t%s", folder, state)
prev_status[folder] = cur_status[folder]
def get_local_libraries(self) -> set:
cmd = "seaf-cli list"
_lg.info("Listing local libraries: %s", cmd)
out = subprocess.check_output(self.__gen_cmd(cmd))
out = out.decode().splitlines()[1:] # first line is a header
local_libs = set()
for line in out:
lib_name, lib_id, lib_path = line.rsplit(maxsplit=3)
local_libs.add(lib_id)
return local_libs
def configure(self, args: argparse.Namespace, check_for_daemon: bool = True):
need_restart = False
# Options can be fetched or set only when daemon is running
if check_for_daemon and not self.daemon_ready:
self.start_daemon()
for key, value in args.__dict__.items():
if key not in const.AVAILABLE_SEAFCLI_OPTIONS:
continue
# check current value
cmd = f"seaf-cli config -k {key}"
_lg.info("Checking seafile client option: %s", cmd)
proc = subprocess.run(self.__gen_cmd(cmd), stdout=subprocess.PIPE)
# stdout looks like "option = value"
cur_value = proc.stdout.decode().strip()
try:
cur_value = cur_value.split(sep="=")[1].strip()
except IndexError:
cur_value = None
if cur_value == str(value):
continue
# set new value
cmd = f"seaf-cli config -k {key} -v {value}"
_lg.info("Setting seafile client option: %s", cmd)
subprocess.run(self.__gen_cmd(cmd))
need_restart = True
if need_restart:
_lg.info("Restarting seafile daemon")
self.stop_daemon()
self.start_daemon()