Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 724166b4a4 | |||
| 681b11bbc1 | |||
| 310eb9028c | |||
| 4dcac70520 | |||
| 6eadca4041 | |||
| 8a0b1abfe7 | |||
| a7ef878490 | |||
| d8438e13f5 | |||
| e73c62d034 | |||
| 6048280983 | |||
| e35cf0efa5 | |||
| ebf7574bde | |||
| e4268c0b21 | |||
| 91758903e9 | |||
| e1d84f2d75 | |||
| 10f6a5ec32 |
53
.github/workflows/docker-publish.yml
vendored
Normal file
53
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -f docker-compose.test.yml ]; then
|
||||
docker-compose --file docker-compose.test.yml build
|
||||
docker-compose --file docker-compose.test.yml run sut
|
||||
else
|
||||
docker build . --file Dockerfile
|
||||
fi
|
||||
|
||||
push:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: snegov/seafile-client
|
||||
tag_with_ref: true
|
||||
add_git_labels: true
|
||||
push: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
- uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: snegov/seafile-client
|
||||
tags: latest
|
||||
add_git_labels: true
|
||||
push: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,17 +1,17 @@
|
||||
FROM python:3-slim
|
||||
FROM python:3.8.12-slim-buster
|
||||
|
||||
RUN apt-get update && apt-get install gnupg -y && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 8756C4F765C9AC3CB6B85D62379CE192D401AB61 && \
|
||||
echo deb http://deb.seadrive.org buster main | tee /etc/apt/sources.list.d/seafile.list && \
|
||||
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 - && \
|
||||
echo 'deb [arch=amd64] https://linux-clients.seafile.com/seafile-deb/buster/ stable main' > /etc/apt/sources.list.d/seafile.list && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y seafile-cli procps curl grep && \
|
||||
apt-get install -y seafile-cli procps grep && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /seafile-client
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY seafile_client ./seafile_client/
|
||||
COPY dsc ./dsc/
|
||||
COPY start.py ./start.py
|
||||
|
||||
RUN chmod +x /seafile-client/start.py && \
|
||||
@@ -20,4 +20,6 @@ RUN chmod +x /seafile-client/start.py && \
|
||||
chown seafile:seafile -R /seafile-client && \
|
||||
su - seafile -c "seaf-cli init -d /seafile-client"
|
||||
|
||||
VOLUME /seafile-client/seafile-data
|
||||
|
||||
CMD ["./start.py"]
|
||||
|
||||
37
README.md
37
README.md
@@ -1,35 +1,42 @@
|
||||
# docker-seafile-client
|
||||
Run a seafile client inside docker witch can sync files from seafile repositories
|
||||
|
||||
See docker-compose how to use.
|
||||
Runs a seafile client in docker with possibility to sync seafile repositories.
|
||||
|
||||
## Docker-compose example:
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
seafile-client:
|
||||
restart: always
|
||||
image: snegov/seafile-client
|
||||
container_name: seafile-client
|
||||
environment:
|
||||
- LIBRARY_ID=<your-library-id-here>
|
||||
- SERVER_HOST=<server-host>
|
||||
- SERVER_PORT=<server-port>
|
||||
- USERNAME=<username>
|
||||
- PASSWORD=<password>
|
||||
- DATA_DIR=<directory-path-to-sync>
|
||||
- SEAFILE_UID=<your_uid>
|
||||
- SEAFILE_GID=<your_gid>
|
||||
hostname: docker-seafile-client
|
||||
volumes:
|
||||
- <host-volume-path>:<directory-path-to-sync>
|
||||
- seafile-data:/seafile-client/seafile-data
|
||||
- <host-volume-path>:/data
|
||||
|
||||
volumes:
|
||||
seafile-data:
|
||||
```
|
||||
|
||||
Library id could be found from "My Libraries" page in Seafile webUI - link to each library contains library ID in it.
|
||||
|
||||
Inside container libraries' content will be put in `/data` directory, so map your host directory to it.
|
||||
|
||||
`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).
|
||||
|
||||
## Environment variables:
|
||||
- `LIBRARY_ID=<your-library-id-here>` ID of library to sync; multiple libraries could be separated by colon `:`
|
||||
- `SERVER_HOST=<server-host>` Hostname of your seafile server, eg: seafile.example.com
|
||||
- `SERVER_PORT=<server-port>` Which port the server is hosted on: usually 443 (https) or 80 (http)
|
||||
- `USERNAME=<username>` Seafile account username
|
||||
- `PASSWORD=<password>` Seafile account password
|
||||
- `DATA_DIR=<directory-path-to-sync>` The path where to put the files
|
||||
- `SEAFILE_UID=<uid>` Downloaded files will have this uid
|
||||
- `SEAFILE_GID=<gid>` Downloaded files will have this gid
|
||||
- `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`.
|
||||
- `USERNAME=<username>` Seafile account username.
|
||||
- `PASSWORD=<password>` Seafile account password.
|
||||
- `SEAFILE_UID=<uid>` Downloaded files will have this uid.
|
||||
- `SEAFILE_GID=<gid>` Downloaded files will have this gid.
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
seafile-client:
|
||||
restart: always
|
||||
image: snegov/seafile-client
|
||||
container_name: seafile-client
|
||||
environment:
|
||||
- LIBRARY_ID=<your-library-id-here>
|
||||
- SERVER_HOST=<server-host>
|
||||
- SERVER_PORT=<server-port>
|
||||
- USERNAME=<username>
|
||||
- PASSWORD=<password>
|
||||
- DATA_DIR=<directory-path-to-sync>
|
||||
- SEAFILE_UID=<your_uid>
|
||||
- SEAFILE_GID=<your_gid>
|
||||
- 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:
|
||||
- <host-volume-path>:<directory-path-to-sync>
|
||||
- seafile-data:/seafile-client/seafile-data
|
||||
- /home/johndow/seafile:/data
|
||||
container_name: seafile-client
|
||||
|
||||
volumes:
|
||||
seafile-data:
|
||||
|
||||
@@ -2,20 +2,22 @@ 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 .consts import DEFAULT_USERNAME
|
||||
from .misc import create_dir
|
||||
from dsc import consts
|
||||
from dsc.misc import create_dir
|
||||
|
||||
|
||||
logging.basicConfig(format="%(asctime)s %(message)s",
|
||||
level=logging.INFO)
|
||||
_lg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SeafileClient:
|
||||
def __init__(self, host: str, port: int, user: str, passwd: str):
|
||||
self.url = f"https://{host}:{port}"
|
||||
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
|
||||
@@ -34,16 +36,24 @@ class SeafileClient:
|
||||
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}"
|
||||
@cached_property_with_ttl(ttl=60)
|
||||
def remote_libraries(self) -> dict:
|
||||
url = f"{self.url}/api2/repos/"
|
||||
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']
|
||||
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.get_lib_name(lib_id)
|
||||
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',
|
||||
@@ -53,11 +63,11 @@ class SeafileClient:
|
||||
'-u', self.user,
|
||||
'-p', self.password]
|
||||
cmd = ' '.join(cmd)
|
||||
subprocess.run(['su', '-', DEFAULT_USERNAME, '-c', cmd])
|
||||
subprocess.run(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd])
|
||||
|
||||
def get_status(self):
|
||||
cmd = 'seaf-cli status'
|
||||
out = subprocess.check_output(['su', '-', DEFAULT_USERNAME, '-c', cmd])
|
||||
out = subprocess.check_output(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd])
|
||||
out = out.decode().splitlines()
|
||||
|
||||
statuses = dict()
|
||||
@@ -73,15 +83,25 @@ class SeafileClient:
|
||||
def watch_status(self):
|
||||
prev_status = dict()
|
||||
while True:
|
||||
time.sleep(5)
|
||||
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(f"Library {folder}:\t{state}")
|
||||
logging.info("Library %s:\t%s", folder, state)
|
||||
prev_status[folder] = cur_status[folder]
|
||||
|
||||
def get_local_libraries(self) -> set:
|
||||
cmd = 'seaf-cli list'
|
||||
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 start_seaf_daemon():
|
||||
cmd = 'seaf-cli start'
|
||||
subprocess.run(['su', '-', DEFAULT_USERNAME, '-c', cmd])
|
||||
time.sleep(5)
|
||||
subprocess.run(['su', '-', consts.DEFAULT_USERNAME, '-c', cmd])
|
||||
@@ -1 +1,2 @@
|
||||
DEFAULT_USERNAME = "seafile"
|
||||
STATUS_POLL_PERIOD = 1
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import pwd
|
||||
import subprocess
|
||||
|
||||
from .consts import DEFAULT_USERNAME
|
||||
from dsc.consts import DEFAULT_USERNAME
|
||||
|
||||
|
||||
def setup_uid(uid: int, gid: int):
|
||||
@@ -19,7 +19,3 @@ def create_dir(dir_path: str):
|
||||
else:
|
||||
if not os.path.isdir(dir_path):
|
||||
raise RuntimeError(f"Data dir {dir_path} is not a directory")
|
||||
|
||||
|
||||
def tail_f(fpath):
|
||||
os.system(f"tail -f {fpath}")
|
||||
@@ -1 +1,2 @@
|
||||
requests
|
||||
cached_property==1.5.2
|
||||
requests==2.27.1
|
||||
|
||||
35
start.py
35
start.py
@@ -1,31 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from seafile_client import SeafileClient, start_seaf_daemon
|
||||
from seafile_client.misc import setup_uid, create_dir
|
||||
from dsc import SeafileClient, start_seaf_daemon
|
||||
from dsc.misc import setup_uid, create_dir
|
||||
|
||||
_lg = logging.getLogger('dsc')
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--uid", default=os.getenv("SEAFILE_UID"), type=int)
|
||||
parser.add_argument("--gid", default=os.getenv("SEAFILE_GID"), type=int)
|
||||
parser.add_argument("--data-dir", default=os.getenv("DATA_DIR"))
|
||||
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("--data-dir", default=os.getenv("DATA_DIR", default="/data"))
|
||||
parser.add_argument("--host", default=os.getenv("SERVER_HOST"))
|
||||
parser.add_argument("--port", default=os.getenv("SERVER_PORT"), type=int)
|
||||
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"))
|
||||
args = parser.parse_args()
|
||||
|
||||
setup_uid(args.uid, args.gid)
|
||||
start_seaf_daemon()
|
||||
create_dir(args.data_dir)
|
||||
client = SeafileClient(args.host, args.port, args.username, args.password)
|
||||
for lib_id in args.libs.split(sep=":"):
|
||||
start_seaf_daemon()
|
||||
|
||||
libs_to_sync = set()
|
||||
|
||||
client = SeafileClient(args.host, args.username, args.password)
|
||||
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.watch_status()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user