Compare commits

...

10 Commits

Author SHA1 Message Date
b03d7bb1a3 Fix concealing a password in logs 2023-10-20 18:07:54 -07:00
ffc972662e
Merge pull request #6 from snegov/5-add-delete-confirm-threshold-option
Add DELETE_CONFIRM_THRESHOLD option
2023-10-20 17:39:03 -07:00
452c29ac95 Add DELETE_CONFIRM_THRESHOLD option 2023-10-20 17:32:34 -07:00
ca40cce9e9
Merge pull request #4 from subvillion/patch-1
fix: client.py sync_lib password escaping
2023-10-18 09:21:00 -07:00
subvillion
9305c636e1
fix: client.py sync_lib password escaping
Please add some escaping in password env, I have password similar "xxxx&zzzz", and "&" broke syncing:

```
2023-10-16 12:50:04,655 Listing local libraries: seaf-cli list
2023-10-16 12:50:05,108 Syncing library btsync: seaf-cli sync -l 3XX2-XXX-2eXXea -s https://xxxxxxxx.xx -d /dsc/seafile/btsync -u xxx@xx.xx -p ********
-bash: line 1: zzzz: command not found
Traceback (most recent call last):
  File "/usr/bin/seaf-cli", line 1023, in <module>
    main()
  File "/usr/bin/seaf-cli", line 1019, in main
    args.func(args)
  File "/usr/bin/seaf-cli", line 675, in seaf_sync
    token = get_token(url, username, password, tfa, conf_dir)
```
2023-10-16 13:11:26 +03:00
7f39e0f271 Initialize seafile client during container start 2023-09-16 01:27:46 -07:00
9f6c417147 Use debian as base docker image 2023-09-15 20:00:04 -07:00
639265f718
Merge pull request #3 from snegov/dependabot/pip/requests-2.31.0
Bump requests from 2.27.1 to 2.31.0
2023-09-14 16:24:59 -07:00
6af5eb1b26 Wait for seafile daemon to start 2023-09-14 16:22:02 -07:00
dependabot[bot]
43237dc1f3
Bump requests from 2.27.1 to 2.31.0
Bumps [requests](https://github.com/psf/requests) from 2.27.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.27.1...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 03:15:40 +00:00
11 changed files with 289 additions and 105 deletions

View File

@ -1,25 +1,36 @@
FROM python:3.8.12-slim-buster FROM debian:bookworm-slim
RUN apt-get update && apt-get install gnupg curl -y && rm -rf /var/lib/apt/lists/* # Install seafile client
RUN apt-get update && \
apt-get install gnupg curl python3.11-venv -y && \
rm -rf /var/lib/apt/lists/*
RUN curl https://linux-clients.seafile.com/seafile.asc | apt-key add - && \ RUN curl https://linux-clients.seafile.com/seafile.asc | apt-key add - && \
echo 'deb [arch=amd64] https://linux-clients.seafile.com/seafile-deb/buster/ stable main' > /etc/apt/sources.list.d/seafile.list && \ echo 'deb [arch=amd64] https://linux-clients.seafile.com/seafile-deb/bookworm/ stable main' \
> /etc/apt/sources.list.d/seafile.list && \
apt-get update -y && \ apt-get update -y && \
apt-get install -y seafile-cli procps grep && \ apt-get install -y seafile-cli && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
WORKDIR /seafile-client # Use virtual environment
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install app requirements
WORKDIR /dsc
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy app
COPY dsc ./dsc/ COPY dsc ./dsc/
COPY start.py ./start.py COPY start.py ./start.py
RUN chmod +x /seafile-client/start.py && \ # Create seafile user and init seafile client
useradd -U -d /seafile-client -s /bin/bash seafile && \ RUN chmod +x /dsc/start.py && \
useradd -U -d /dsc -s /bin/bash seafile && \
usermod -G users seafile && \ usermod -G users seafile && \
chown seafile:seafile -R /seafile-client && \ mkdir -p /dsc/seafile-data && \
su - seafile -c "seaf-cli init -d /seafile-client" chown seafile:seafile -R /dsc
VOLUME /seafile-client/seafile-data
VOLUME /dsc/seafile-data
CMD ["./start.py"] CMD ["./start.py"]

View File

@ -1,7 +1,7 @@
MIT License MIT License
Copyright (c) 2018 Robin Grönberg Copyright (c) 2018 Robin Grönberg
Copyright (c) 2019 Maks Snegov Copyright (c) 2019-2023 Maks Snegov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,42 +1,68 @@
# docker-seafile-client # docker-seafile-client
Runs a seafile client in docker with possibility to sync seafile repositories. Docker image for [Seafile CLI client](https://help.seafile.com/syncing_client/linux-cli/).
## Docker-compose example: ### Docker-compose example:
```yaml ```yaml
version: '3'
services: services:
seafile-client: seafile-client:
restart: always restart: always
image: snegov/seafile-client image: snegov/seafile-client
environment: environment:
- LIBRARY_ID=<your-library-id-here> - LIBRARY_ID="79867cbf-2944-488d-9105-852463ecdf9e:my_library"
- SERVER_HOST=<server-host> - SERVER_HOST=seafile.example.com
- USERNAME=<username> - USERNAME=user
- PASSWORD=<password> - PASSWORD=password
- SEAFILE_UID=<your_uid> - SEAFILE_UID=1000
- SEAFILE_GID=<your_gid> - SEAFILE_GID=100
hostname: docker-seafile-client hostname: dsc
volumes: volumes:
- seafile-data:/seafile-client/seafile-data - /home/johndow/seafile:/dsc/seafile
- <host-volume-path>:/data - sync-data:/dsc/seafile-data
container_name: seafile-client
volumes: volumes:
seafile-data: sync-data:
``` ```
Library id could be found from "My Libraries" page in Seafile webUI - link to each library contains library ID in it. ### 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_.
- `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).
Inside container libraries' content will be put in `/data` directory, so map your host directory to it. ### Volumes:
- `/dsc/seafile-data` Seafile client data directory (sync status, etc).
- `/dsc/seafile` Seafile libraries content.
`hostname` parameter in docker-compose will set client name in Seafile's "Linked devices" admin page. Resulting name will be prefixed by "terminal-".
Also you could check [docker-compose example](docker-compose.example.yml). ### 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.
## Environment variables: Libraries will be synced in subdirectories of `/dsc/seafile` directory inside
- `LIBRARY_ID=<your-library-id-here>` Library to sync, ID or name; multiple libraries could be separated by colon `:`. container. You can mount it to host directory to access files.
- `SERVER_HOST=<server-host>` Hostname of your seafile server, eg: `seafile.example.com`. If you're using non-standart port, specify it here, eg: `seafile.example.com:8080`.
- `USERNAME=<username>` Seafile account username. `hostname` parameter is optional, but it's recommended to set it to some unique
- `PASSWORD=<password>` Seafile account password. value, it will be shown in Seafile webUI as client name (`terminal-dsc` in
- `SEAFILE_UID=<uid>` Downloaded files will have this uid. given example).
- `SEAFILE_GID=<gid>` Downloaded files will have this gid.
`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/)

View File

@ -1,21 +0,0 @@
version: '3'
services:
seafile-client:
restart: always
image: snegov/seafile-client
environment:
- LIBRARY_ID="79867cbf-2944-488d-9105-859463ecdf9e:test_library"
- SERVER_HOST=seafile.example.com
- USERNAME=user
- PASSWORD=password
- SEAFILE_UID=1000
- SEAFILE_GID=100
hostname: docker-seafcli
volumes:
- seafile-data:/seafile-client/seafile-data
- /home/johndow/seafile:/data
container_name: seafile-client
volumes:
seafile-data:

View File

@ -1 +1 @@
from .client import SeafileClient, start_seaf_daemon from .client import SeafileClient, const

View File

@ -1,3 +1,4 @@
import argparse
import logging import logging
import os import os
import subprocess import subprocess
@ -8,37 +9,46 @@ from urllib.parse import urlparse
from cached_property import cached_property_with_ttl from cached_property import cached_property_with_ttl
import requests import requests
from dsc import consts from dsc import const
from dsc.misc import create_dir from dsc.misc import create_dir, hide_password
_lg = logging.getLogger(__name__) _lg = logging.getLogger(__name__)
class SeafileClient: class SeafileClient:
def __init__(self, host: str, user: str, passwd: str): def __init__(self,
host: str,
user: str,
passwd: str,
app_dir: str = const.DEFAULT_APP_DIR):
up = urlparse(requests.get(f"http://{host}").url) up = urlparse(requests.get(f"http://{host}").url)
self.url = f"{up.scheme}://{up.netloc}" self.url = f"{up.scheme}://{up.netloc}"
self.user = user self.user = user
self.password = passwd self.password = passwd
self.app_dir = os.path.abspath(app_dir)
self.__token = None self.__token = None
def __str__(self): def __str__(self):
return f"SeafileClient({self.user}@{self.url})" return f"SeafileClient({self.user}@{self.url})"
def __gen_cmd(self, cmd: str) -> list:
return ["su", "-", const.DEFAULT_USERNAME, "-c", cmd]
@property @property
def token(self): def token(self):
if self.__token is None: if self.__token is None:
url = f"{self.url}/api2/auth-token/" url = f"{self.url}/api2/auth-token/"
r = requests.post(url, data={"username": self.user, _lg.info("Fetching token: %s", url)
"password": self.password}) r = requests.post(url, data={"username": self.user, "password": self.password})
if r.status_code != 200: if r.status_code != 200:
raise RuntimeError(f"Can't get token: {r.text}") raise RuntimeError(f"Can't get token: {r.text}")
self.__token = r.json()['token'] self.__token = r.json()["token"]
return self.__token return self.__token
@cached_property_with_ttl(ttl=60) @cached_property_with_ttl(ttl=60)
def remote_libraries(self) -> dict: def remote_libraries(self) -> dict:
url = f"{self.url}/api2/repos/" url = f"{self.url}/api2/repos/"
_lg.info("Fetching remote libraries: %s", url)
auth_header = {"Authorization": f"Token {self.token}"} auth_header = {"Authorization": f"Token {self.token}"}
r = requests.get(url, headers=auth_header) r = requests.get(url, headers=auth_header)
if r.status_code != 200: if r.status_code != 200:
@ -46,35 +56,90 @@ class SeafileClient:
r_libs = {lib["id"]: lib["name"] for lib in r.json()} r_libs = {lib["id"]: lib["name"] for lib in r.json()}
return r_libs 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]: def get_library_id(self, library) -> Optional[str]:
for lib_id, lib_name in self.remote_libraries.items(): for lib_id, lib_name in self.remote_libraries.items():
if library in (lib_id, lib_name): if library in (lib_id, lib_name):
return lib_id return lib_id
return None return None
def sync_lib(self, lib_id: str, data_dir: str): def sync_lib(self, lib_id: str, parent_dir: str = const.DEFAULT_LIBS_DIR):
lib_name = self.remote_libraries[lib_id] lib_name = self.remote_libraries[lib_id]
lib_dir = os.path.join(data_dir, lib_name.replace(' ', '_')) lib_dir = os.path.join(parent_dir, lib_name.replace(" ", "_"))
create_dir(lib_dir) create_dir(lib_dir)
cmd = ['seaf-cli', 'sync', cmd = [
'-l', lib_id, "seaf-cli",
'-s', self.url, "sync",
'-d', lib_dir, "-l", lib_id,
'-u', self.user, "-s", self.url,
'-p', self.password] "-d", lib_dir,
cmd = ' '.join(cmd) "-u", self.user,
subprocess.run(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd]) "-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): def get_status(self):
cmd = 'seaf-cli status' cmd = "seaf-cli status"
out = subprocess.check_output(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd]) _lg.debug("Fetching seafile client status: %s", cmd)
out = subprocess.check_output(self.__gen_cmd(cmd))
out = out.decode().splitlines() out = out.decode().splitlines()
statuses = dict() statuses = dict()
for line in out: for line in out:
if line.startswith('#') or not line.strip(): if line.startswith("#") or not line.strip():
continue continue
lib, status = line.split(sep='\t', maxsplit=1) lib, status = line.split(sep="\t", maxsplit=1)
lib = lib.strip() lib = lib.strip()
status = " ".join(status.split()) status = " ".join(status.split())
statuses[lib] = status statuses[lib] = status
@ -83,7 +148,7 @@ class SeafileClient:
def watch_status(self): def watch_status(self):
prev_status = dict() prev_status = dict()
while True: while True:
time.sleep(consts.STATUS_POLL_PERIOD) time.sleep(const.STATUS_POLL_PERIOD)
cur_status = self.get_status() cur_status = self.get_status()
for folder, state in cur_status.items(): for folder, state in cur_status.items():
if state != prev_status.get(folder): if state != prev_status.get(folder):
@ -91,9 +156,10 @@ class SeafileClient:
prev_status[folder] = cur_status[folder] prev_status[folder] = cur_status[folder]
def get_local_libraries(self) -> set: def get_local_libraries(self) -> set:
cmd = 'seaf-cli list' cmd = "seaf-cli list"
out = subprocess.check_output(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd]) _lg.info("Listing local libraries: %s", cmd)
out = out.decode().splitlines()[1:] # first line is a header out = subprocess.check_output(self.__gen_cmd(cmd))
out = out.decode().splitlines()[1:] # first line is a header
local_libs = set() local_libs = set()
for line in out: for line in out:
@ -101,7 +167,36 @@ class SeafileClient:
local_libs.add(lib_id) local_libs.add(lib_id)
return local_libs 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()
def start_seaf_daemon(): for key, value in args.__dict__.items():
cmd = 'seaf-cli start' if key not in const.AVAILABLE_SEAFCLI_OPTIONS:
subprocess.run(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd]) 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()

12
dsc/const.py Normal file
View File

@ -0,0 +1,12 @@
DEFAULT_APP_DIR = "/dsc"
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",
}

View File

@ -1,2 +0,0 @@
DEFAULT_USERNAME = "seafile"
STATUS_POLL_PERIOD = 1

View File

@ -2,15 +2,34 @@ import os
import pwd import pwd
import subprocess import subprocess
from dsc.consts import DEFAULT_USERNAME from dsc.const import DEFAULT_USERNAME
def setup_uid(uid: int, gid: int): def setup_uid(uid: int, gid: int):
"""
Set GID and UID of default user so that seafile client creates files with
correct permissions.
If GID does not match, create a new group with the given GID.
Then update UID and GID of default user to match the given ones.
"""
user_pwinfo = pwd.getpwnam(DEFAULT_USERNAME) user_pwinfo = pwd.getpwnam(DEFAULT_USERNAME)
create_group(gid)
if user_pwinfo.pw_uid != uid or user_pwinfo.pw_gid != gid: if user_pwinfo.pw_uid != uid or user_pwinfo.pw_gid != gid:
subprocess.call(["usermod", "-o", "-u", str(uid), "-g", str(gid), DEFAULT_USERNAME]) subprocess.call(["usermod", "-o", "-u", str(uid), "-g", str(gid), DEFAULT_USERNAME])
def create_group(gid: int):
"""Search for a group with the given GID. If not found, create a new one."""
if not os.path.exists(f"/etc/group"):
raise RuntimeError(f"File /etc/group does not exist")
with open("/etc/group", "r") as f:
for line in f.readlines():
cur_gid = line.split(sep=":", maxsplit=3)[2]
if int(cur_gid) == gid:
return
subprocess.call(["groupadd", "-g", str(gid), DEFAULT_USERNAME + "-data"])
def create_dir(dir_path: str): def create_dir(dir_path: str):
if not os.path.exists(dir_path): if not os.path.exists(dir_path):
os.mkdir(dir_path) os.mkdir(dir_path)
@ -19,3 +38,11 @@ def create_dir(dir_path: str):
else: else:
if not os.path.isdir(dir_path): if not os.path.isdir(dir_path):
raise RuntimeError(f"Data dir {dir_path} is not a directory") raise RuntimeError(f"Data dir {dir_path} is not a directory")
def hide_password(cmd: list, password: str) -> list:
cmd = cmd.copy()
for i, arg in enumerate(cmd):
if password in arg:
cmd[i] = arg.replace(password, "********")
return cmd

View File

@ -1,2 +1,2 @@
cached_property==1.5.2 cached_property==1.5.2
requests==2.27.1 requests==2.31.0

View File

@ -6,7 +6,7 @@ import os
import os.path import os.path
import sys import sys
from dsc import SeafileClient, start_seaf_daemon from dsc import SeafileClient, const
from dsc.misc import setup_uid, create_dir from dsc.misc import setup_uid, create_dir
_lg = logging.getLogger('dsc') _lg = logging.getLogger('dsc')
@ -16,36 +16,72 @@ def main():
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO) logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--uid", default=os.getenv("SEAFILE_UID", default=1000), type=int) parser.add_argument("-s", "--server")
parser.add_argument("--gid", default=os.getenv("SEAFILE_GID", default=100), type=int) parser.add_argument("-u", "--username")
parser.add_argument("--data-dir", default=os.getenv("DATA_DIR", default="/data")) parser.add_argument("-p", "--password")
parser.add_argument("--host", default=os.getenv("SERVER_HOST")) parser.add_argument("-l", "--libraries")
parser.add_argument("--username", default=os.getenv("USERNAME")) parser.add_argument("--uid", type=int)
parser.add_argument("--password", default=os.getenv("PASSWORD")) parser.add_argument("--gid", type=int)
parser.add_argument("--libs", default=os.getenv("LIBRARY_ID")) 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() 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) setup_uid(args.uid, args.gid)
create_dir(args.data_dir) create_dir(const.DEFAULT_APP_DIR)
start_seaf_daemon()
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() libs_to_sync = set()
for arg_lib in args.libraries.split(sep=":"):
client = SeafileClient(args.host, args.username, args.password)
for arg_lib in args.libs.split(sep=":"):
lib_id = client.get_library_id(arg_lib) lib_id = client.get_library_id(arg_lib)
if lib_id: if lib_id:
libs_to_sync.add(lib_id) libs_to_sync.add(lib_id)
else: 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 # don't start to sync libraries already in sync
libs_to_sync -= client.get_local_libraries() libs_to_sync -= client.get_local_libraries()
# check for deprecated /data directory
if os.path.isdir(const.DEPRECATED_LIBS_DIR):
_lg.warning("*** DEPRECATED DIRECTORY FOUND ***")
_lg.warning("Deprecated directory %s is found, please mount your host directory with"
" libraries to %s instead", const.DEPRECATED_LIBS_DIR, const.DEFAULT_LIBS_DIR)
libs_dir = const.DEPRECATED_LIBS_DIR
else:
libs_dir = const.DEFAULT_LIBS_DIR
for lib_id in libs_to_sync: for lib_id in libs_to_sync:
client.sync_lib(lib_id, args.data_dir) client.sync_lib(lib_id, libs_dir)
client.watch_status() client.watch_status()
client.stop_daemon()
return 0 return 0