Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b03d7bb1a3 | |||
| ffc972662e | |||
| 452c29ac95 | |||
| ca40cce9e9 | |||
|
|
9305c636e1 | ||
| 7f39e0f271 | |||
| 9f6c417147 | |||
| 639265f718 | |||
| 6af5eb1b26 | |||
|
|
43237dc1f3 |
33
Dockerfile
33
Dockerfile
@ -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"]
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -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
|
||||||
|
|||||||
76
README.md
76
README.md
@ -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/)
|
||||||
|
|||||||
@ -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:
|
|
||||||
@ -1 +1 @@
|
|||||||
from .client import SeafileClient, start_seaf_daemon
|
from .client import SeafileClient, const
|
||||||
|
|||||||
149
dsc/client.py
149
dsc/client.py
@ -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
12
dsc/const.py
Normal 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",
|
||||||
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
DEFAULT_USERNAME = "seafile"
|
|
||||||
STATUS_POLL_PERIOD = 1
|
|
||||||
29
dsc/misc.py
29
dsc/misc.py
@ -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
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
cached_property==1.5.2
|
cached_property==1.5.2
|
||||||
requests==2.27.1
|
requests==2.31.0
|
||||||
|
|||||||
66
start.py
66
start.py
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user