diff --git a/README.md b/README.md index 5356661..2251650 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # docker-seafile-client -Docker image for Seafile terminal client. +Docker image for [Seafile CLI client](https://help.seafile.com/syncing_client/linux-cli/). ### Docker-compose example: ```yaml @@ -25,10 +25,20 @@ volumes: ``` ### Environment variables: - - `LIBRARY_ID` - library to sync, ID or name. Multiple libraries could be separated by colon `:`. - - `SERVER_HOST` - hostname of your Seafile server, eg: `seafile.example.com`. If you're using non-standard port, you can specify it here, eg: `seafile.example.com:8080`. - - `USERNAME`/ `PASSWORD` - credentials to access Seafile server. - - `SEAFILE_UID` / `SEAFILE_GID` - UID/GID of user inside container. You can use it to set permissions on synced files. Default values are `1000`/`1000`. + - `LIBRARY_ID` - library to sync, ID or name. Multiple libraries could be + separated by colon `:`. + - `SERVER_HOST` - hostname of your Seafile server, eg: _seafile.example.com_. + If you're using non-standard port, you can specify it here, + eg: _seafile.example.com:8080_. + - `USERNAME` / `PASSWORD` - credentials to access Seafile server. + - `SEAFILE_UID` / `SEAFILE_GID` - UID/GID of user inside container. You can + use it to set permissions on synced files. Default values are _1000_ / _1000_. + - `DELETE_CONFIRM_THRESHOLD` - represents the number of files that require + confirmation before being deleted simultaneously. Default value is _500_. + - `DISABLE_VERIFY_CERTIFICATE` - set to _true_ to disable server's certificate + verification. Default value is _false_. + - `UPLOAD_LIMIT` / `DOWNLOAD_LIMIT` - upload/download speed limit in B/s + (bytes per second). Default values are _0_ (unlimited). ### Volumes: - `/dsc/seafile-data` Seafile client data directory (sync status, etc). @@ -36,11 +46,23 @@ volumes: ### Some notes -`LIBRARY_ID` could be library ID or library name. Library ID is a 36-character string, which is a part of URI when you open library in webUI. Library name is a name you gave to library when you created it. +`LIBRARY_ID` could be library ID or library name. Library ID is a 36-character + string, which is a part of URI when you open library in webUI. Library name is + a name you gave to library when you created it. -Libraries will be synced in subdirectories of `/dsc/seafile` directory inside container. You can mount it to host directory to access files. +Libraries will be synced in subdirectories of `/dsc/seafile` directory inside + container. You can mount it to host directory to access files. -`hostname` parameter is optional, but it's recommended to set it to some unique value, it will be shown in Seafile webUI as client name (`terminal-dsc` in given example). +`hostname` parameter is optional, but it's recommended to set it to some unique + value, it will be shown in Seafile webUI as client name (`terminal-dsc` in + given example). -`sync-data` volume is optional too, but it's recommended to use it. Otherwise, sync status will be lost when container is recreated. +`sync-data` volume is optional too, but it's recommended to use it. Otherwise, + sync status will be lost when container is recreated. +At the moment there is no suitable way to confirm deletion of large number of + files. So, if you're going to delete a lot of files, you should set + `DELETE_CONFIRM_THRESHOLD` to some larger value. + +### Links +- [Official Seafile CLI client documentation](https://help.seafile.com/syncing_client/linux-cli/) diff --git a/dsc/client.py b/dsc/client.py index d90b08a..625f82b 100644 --- a/dsc/client.py +++ b/dsc/client.py @@ -1,3 +1,4 @@ +import argparse import logging import os import subprocess @@ -90,6 +91,19 @@ class SeafileClient: _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): @@ -152,3 +166,37 @@ class SeafileClient: 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() diff --git a/dsc/const.py b/dsc/const.py index 66ffdef..15b800f 100644 --- a/dsc/const.py +++ b/dsc/const.py @@ -3,3 +3,10 @@ DEFAULT_LIBS_DIR = "/dsc/seafile" DEPRECATED_LIBS_DIR = "/data" DEFAULT_USERNAME = "seafile" STATUS_POLL_PERIOD = 1 + +AVAILABLE_SEAFCLI_OPTIONS = { + "delete_confirm_threshold", + "disable_verify_certificate", + "upload_limit", + "download_limit", +} diff --git a/start.py b/start.py index 6dddc16..8990974 100755 --- a/start.py +++ b/start.py @@ -16,28 +16,54 @@ def main(): logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO) parser = argparse.ArgumentParser() - parser.add_argument("--uid", default=os.getenv("SEAFILE_UID", default=1000), type=int) - parser.add_argument("--gid", default=os.getenv("SEAFILE_GID", default=1000), type=int) - parser.add_argument("--host", default=os.getenv("SERVER_HOST")) - parser.add_argument("--username", default=os.getenv("USERNAME")) - parser.add_argument("--password", default=os.getenv("PASSWORD")) - parser.add_argument("--libs", default=os.getenv("LIBRARY_ID")) + parser.add_argument("-s", "--server") + parser.add_argument("-u", "--username") + parser.add_argument("-p", "--password") + parser.add_argument("-l", "--libraries") + parser.add_argument("--uid", type=int) + parser.add_argument("--gid", type=int) + parser.add_argument("--upload-limit", type=int, default=0) + parser.add_argument("--download-limit", type=int, default=0) + parser.add_argument("--disable-verify-certificate", action="store_true") + parser.add_argument("--delete-confirm-threshold", type=int, default=500) + + parser.set_defaults( + server=os.getenv("SERVER_HOST"), + username=os.getenv("USERNAME"), + password=os.getenv("PASSWORD"), + libraries=os.getenv("LIBRARY_ID"), + uid=os.getenv("SEAFILE_UID", default=1000), + gid=os.getenv("SEAFILE_GID", default=1000), + upload_limit=os.getenv("UPLOAD_LIMIT"), + download_limit=os.getenv("DOWNLOAD_LIMIT"), + disable_verify_certificate=os.getenv("DISABLE_VERIFY_CERTIFICATE") in ("true", "1", "True"), + delete_confirm_threshold=os.getenv("DELETE_CONFIRM_THRESHOLD"), + ) args = parser.parse_args() + if not args.server: + parser.error("Seafile server is not specified") + if not args.username: + parser.error("username is not specified") + if not args.password: + parser.error("password is not specified") + if not args.libraries: + parser.error("library is not specified") setup_uid(args.uid, args.gid) create_dir(const.DEFAULT_APP_DIR) - client = SeafileClient(args.host, args.username, args.password, const.DEFAULT_APP_DIR) + client = SeafileClient(args.server, args.username, args.password, const.DEFAULT_APP_DIR) client.init_config() client.start_daemon() + client.configure(args, check_for_daemon=False) libs_to_sync = set() - for arg_lib in args.libs.split(sep=":"): + for arg_lib in args.libraries.split(sep=":"): lib_id = client.get_library_id(arg_lib) if lib_id: libs_to_sync.add(lib_id) else: - _lg.warning("Library %s is not found on server %s", arg_lib, args.host) + _lg.warning("Library %s is not found on server %s", arg_lib, args.server) # don't start to sync libraries already in sync libs_to_sync -= client.get_local_libraries() @@ -55,6 +81,7 @@ def main(): client.sync_lib(lib_id, libs_dir) client.watch_status() + client.stop_daemon() return 0