Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 639265f718 | |||
| 6af5eb1b26 | |||
|
|
43237dc1f3 | ||
| 724166b4a4 |
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3-slim
|
FROM python:3.8.12-slim-buster
|
||||||
|
|
||||||
RUN apt-get update && apt-get install gnupg curl -y && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install gnupg curl -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 - && \
|
||||||
@@ -11,7 +11,7 @@ WORKDIR /seafile-client
|
|||||||
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 seafile_client ./seafile_client/
|
COPY dsc ./dsc/
|
||||||
COPY start.py ./start.py
|
COPY start.py ./start.py
|
||||||
|
|
||||||
RUN chmod +x /seafile-client/start.py && \
|
RUN chmod +x /seafile-client/start.py && \
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Inside container libraries' content will be put in `/data` directory, so map you
|
|||||||
Also you could check [docker-compose example](docker-compose.example.yml).
|
Also you could check [docker-compose example](docker-compose.example.yml).
|
||||||
|
|
||||||
## Environment variables:
|
## Environment variables:
|
||||||
- `LIBRARY_ID=<your-library-id-here>` ID of library to sync; multiple libraries could be separated by colon `:`.
|
- `LIBRARY_ID=<your-library-id-here>` Library to sync, ID or name; multiple libraries could be separated by colon `:`.
|
||||||
- `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`.
|
- `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.
|
- `USERNAME=<username>` Seafile account username.
|
||||||
- `PASSWORD=<password>` Seafile account password.
|
- `PASSWORD=<password>` Seafile account password.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
image: snegov/seafile-client
|
image: snegov/seafile-client
|
||||||
environment:
|
environment:
|
||||||
- LIBRARY_ID="79867cbf-2944-488d-9105-859463ecdf9e:8078e3ff-b2a0-450a-b4dd-c1ed9ef18294"
|
- LIBRARY_ID="79867cbf-2944-488d-9105-859463ecdf9e:test_library"
|
||||||
- SERVER_HOST=seafile.example.com
|
- SERVER_HOST=seafile.example.com
|
||||||
- USERNAME=user
|
- USERNAME=user
|
||||||
- PASSWORD=password
|
- PASSWORD=password
|
||||||
@@ -15,7 +15,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- seafile-data:/seafile-client/seafile-data
|
- seafile-data:/seafile-client/seafile-data
|
||||||
- /home/johndow/seafile:/data
|
- /home/johndow/seafile:/data
|
||||||
|
container_name: seafile-client
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
seafile-data:
|
seafile-data:
|
||||||
|
|||||||
131
dsc/client.py
Normal file
131
dsc/client.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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 consts
|
||||||
|
from dsc.misc import create_dir, hide_password
|
||||||
|
|
||||||
|
_lg = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SeafileClient:
|
||||||
|
def __init__(self, host: str, user: str, passwd: str):
|
||||||
|
up = urlparse(requests.get(f"http://{host}").url)
|
||||||
|
self.url = f"{up.scheme}://{up.netloc}"
|
||||||
|
self.user = user
|
||||||
|
self.password = passwd
|
||||||
|
self.__token = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"SeafileClient({self.user}@{self.url})"
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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, data_dir: str):
|
||||||
|
lib_name = self.remote_libraries[lib_id]
|
||||||
|
lib_dir = os.path.join(data_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(['su', '-', consts.DEFAULT_USERNAME, '-c', ' '.join(cmd)])
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
cmd = 'seaf-cli status'
|
||||||
|
_lg.debug("Fetching seafile client status: %s", cmd)
|
||||||
|
out = subprocess.check_output(['su', '-', consts.DEFAULT_USERNAME, '-c', 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(consts.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(['su', '-', consts.DEFAULT_USERNAME, '-c', 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 check_seaf_daemon_is_ready() -> bool:
|
||||||
|
cmd = 'seaf-cli status'
|
||||||
|
_lg.info("Checking seafile daemon status: %s", cmd)
|
||||||
|
proc = subprocess.run(
|
||||||
|
['su', '-', consts.DEFAULT_USERNAME, '-c', cmd],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
return proc.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def start_seaf_daemon():
|
||||||
|
cmd = 'seaf-cli start'
|
||||||
|
_lg.info("Starting seafile daemon: %s", cmd)
|
||||||
|
subprocess.run(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd])
|
||||||
|
_lg.info("Waiting for seafile daemon to start")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if check_seaf_daemon_is_ready():
|
||||||
|
break
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
_lg.info("Seafile daemon is ready")
|
||||||
@@ -1 +1,2 @@
|
|||||||
DEFAULT_USERNAME = "seafile"
|
DEFAULT_USERNAME = "seafile"
|
||||||
|
STATUS_POLL_PERIOD = 1
|
||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import pwd
|
import pwd
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .consts import DEFAULT_USERNAME
|
from dsc.consts import DEFAULT_USERNAME
|
||||||
|
|
||||||
|
|
||||||
def setup_uid(uid: int, gid: int):
|
def setup_uid(uid: int, gid: int):
|
||||||
@@ -21,5 +21,9 @@ def create_dir(dir_path: str):
|
|||||||
raise RuntimeError(f"Data dir {dir_path} is not a directory")
|
raise RuntimeError(f"Data dir {dir_path} is not a directory")
|
||||||
|
|
||||||
|
|
||||||
def tail_f(fpath):
|
def hide_password(cmd: list, password: str) -> list:
|
||||||
os.system(f"tail -f {fpath}")
|
cmd = cmd.copy()
|
||||||
|
for i, arg in enumerate(cmd):
|
||||||
|
if arg == password:
|
||||||
|
cmd[i] = '********'
|
||||||
|
return cmd
|
||||||
@@ -1 +1,2 @@
|
|||||||
requests
|
cached_property==1.5.2
|
||||||
|
requests==2.31.0
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from .consts import DEFAULT_USERNAME
|
|
||||||
from .misc import create_dir
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(format="%(asctime)s %(message)s",
|
|
||||||
level=logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
class SeafileClient:
|
|
||||||
def __init__(self, host: str, user: str, passwd: str):
|
|
||||||
up = urlparse(requests.get(f"http://{host}").url)
|
|
||||||
self.url = f"{up.scheme}://{up.netloc}"
|
|
||||||
self.user = user
|
|
||||||
self.password = passwd
|
|
||||||
self.__token = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"SeafileClient({self.user}@{self.url})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def token(self):
|
|
||||||
if self.__token is None:
|
|
||||||
url = f"{self.url}/api2/auth-token/"
|
|
||||||
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
|
|
||||||
|
|
||||||
def get_lib_name(self, lib_id: str) -> str:
|
|
||||||
url = f"{self.url}/api2/repos/{lib_id}"
|
|
||||||
auth_header = {"Authorization": f"Token {self.token}"}
|
|
||||||
r = requests.get(url, headers=auth_header)
|
|
||||||
if r.status_code != 200:
|
|
||||||
raise RuntimeError(r.text)
|
|
||||||
return r.json()['name']
|
|
||||||
|
|
||||||
def sync_lib(self, lib_id: str, data_dir: str):
|
|
||||||
lib_name = self.get_lib_name(lib_id)
|
|
||||||
lib_dir = os.path.join(data_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]
|
|
||||||
cmd = ' '.join(cmd)
|
|
||||||
subprocess.run(['su', '-', DEFAULT_USERNAME, '-c', cmd])
|
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
cmd = 'seaf-cli status'
|
|
||||||
out = subprocess.check_output(['su', '-', DEFAULT_USERNAME, '-c', 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(5)
|
|
||||||
cur_status = self.get_status()
|
|
||||||
for folder, state in cur_status.items():
|
|
||||||
if state != prev_status.get(folder):
|
|
||||||
logging.info(f"Library {folder}:\t{state}")
|
|
||||||
prev_status[folder] = cur_status[folder]
|
|
||||||
|
|
||||||
|
|
||||||
def start_seaf_daemon():
|
|
||||||
cmd = 'seaf-cli start'
|
|
||||||
subprocess.run(['su', '-', DEFAULT_USERNAME, '-c', cmd])
|
|
||||||
time.sleep(5)
|
|
||||||
26
start.py
26
start.py
@@ -1,15 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from seafile_client import SeafileClient, start_seaf_daemon
|
from dsc import SeafileClient, start_seaf_daemon
|
||||||
from seafile_client.misc import setup_uid, create_dir
|
from dsc.misc import setup_uid, create_dir
|
||||||
|
|
||||||
|
_lg = logging.getLogger('dsc')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
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("--uid", default=os.getenv("SEAFILE_UID", default=1000), type=int)
|
||||||
parser.add_argument("--gid", default=os.getenv("SEAFILE_GID", default=100), type=int)
|
parser.add_argument("--gid", default=os.getenv("SEAFILE_GID", default=100), type=int)
|
||||||
@@ -21,10 +26,23 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
setup_uid(args.uid, args.gid)
|
setup_uid(args.uid, args.gid)
|
||||||
start_seaf_daemon()
|
|
||||||
create_dir(args.data_dir)
|
create_dir(args.data_dir)
|
||||||
|
start_seaf_daemon()
|
||||||
|
|
||||||
|
libs_to_sync = set()
|
||||||
|
|
||||||
client = SeafileClient(args.host, args.username, args.password)
|
client = SeafileClient(args.host, args.username, args.password)
|
||||||
for lib_id in args.libs.split(sep=":"):
|
for arg_lib in args.libs.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)
|
||||||
|
|
||||||
|
# don't start to sync libraries already in sync
|
||||||
|
libs_to_sync -= client.get_local_libraries()
|
||||||
|
|
||||||
|
for lib_id in libs_to_sync:
|
||||||
client.sync_lib(lib_id, args.data_dir)
|
client.sync_lib(lib_id, args.data_dir)
|
||||||
client.watch_status()
|
client.watch_status()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user