├── .dockerignore
├── .editorconfig
├── .flake8
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── devops.yml
│ └── gh-pages.yml
├── .gitignore
├── .hadolint.yaml
├── .markdownlint.yaml
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── SECURITY.md
├── config
├── seedboxsync.yml.docker
└── seedboxsync.yml.example
├── docker
└── etc
│ ├── crontabs
│ └── seedboxsync
│ └── s6-overlay
│ └── s6-rc.d
│ ├── init-config
│ ├── dependencies.d
│ │ └── init-usermod
│ ├── run
│ ├── type
│ └── up
│ ├── init-end
│ ├── dependencies.d
│ │ ├── init-config
│ │ └── init-usermod
│ ├── run
│ ├── type
│ └── up
│ ├── init-usermod
│ ├── dependencies.d
│ │ └── base
│ ├── run
│ ├── type
│ └── up
│ ├── svc-cron
│ ├── dependencies.d
│ │ └── init-end
│ ├── run
│ └── type
│ └── user
│ └── contents.d
│ ├── init-config
│ ├── init-end
│ ├── init-usermod
│ └── svc-cron
├── docs
├── .gitignore
├── Gemfile
├── _config.yml
├── configuration.md
├── developers.md
├── docker.md
├── images
│ ├── rutorrent_1.png
│ ├── rutorrent_2.png
│ ├── seedboxsync.ico
│ ├── seedboxsync.png
│ └── seedboxsync.xcf
├── index.md
├── install.md
└── usage.md
├── requirements-dev.txt
├── requirements.txt
├── seedboxsync
├── __init__.py
├── controllers
│ ├── __init__.py
│ ├── base.py
│ ├── clean.py
│ ├── search.py
│ ├── stats.py
│ └── sync.py
├── core
│ ├── __init__.py
│ ├── dao
│ │ ├── __init__.py
│ │ ├── download.py
│ │ ├── model.py
│ │ ├── seedboxsync.py
│ │ └── torrent.py
│ ├── db.py
│ ├── exc.py
│ ├── init_defaults.py
│ ├── sync
│ │ ├── __init__.py
│ │ ├── abstract_client.py
│ │ ├── sftp_client.py
│ │ └── sync.py
│ └── version.py
├── ext
│ ├── __init__.py
│ ├── ext_bcoding.py
│ ├── ext_healthchecks.py
│ └── ext_lock.py
├── main.py
├── plugins
│ └── __init__.py
└── version.py
├── setup.cfg
├── setup.py
├── sonar-project.properties
└── tests
├── conftest.py
├── controlers
├── test_clean.py
├── test_search.py
└── test_stats.py
├── resources
├── Fedora-Server-dvd-x86_64-32.torrent
├── seedboxsync.db
└── seedboxsync.yml
└── test_main.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | .pytest_cache
4 | .vscode
5 | build
6 | dist
7 | env
8 | seedboxsync.egg-info
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; EditorConfig is awesome: http://EditorConfig.org
2 | ; top-most EditorConfig file
3 | root = true
4 |
5 | ; Unix-style
6 | [*]
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = false
11 |
12 | [*.ini]
13 | indent_style = space
14 | indent_size = 4
15 |
16 | [*.ini.dist]
17 | indent_style = space
18 | indent_size = 4
19 |
20 | [*.html]
21 | indent_style = space
22 | indent_size = 4
23 |
24 | [*.md]
25 | indent_style = space
26 | indent_size = 4
27 | insert_final_newline = true
28 |
29 | [*.py]
30 | indent_style = space
31 | indent_size = 4
32 | insert_final_newline = true
33 |
34 | [*.yml]
35 | indent_style = space
36 | indent_size = 2
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = W504
3 | max-line-length = 160
4 | exclude = *.egg,build/*,docs/*
5 | select = E,W,F
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: llaumgui
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | # Maintain dependencies for GitHub Actions
9 | - package-ecosystem: "github-actions"
10 | directory: "/"
11 | schedule:
12 | interval: "daily"
13 |
14 | # Maintain dependencies for pip
15 | - package-ecosystem: "pip"
16 | directory: "/"
17 | schedule:
18 | interval: "daily"
19 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '17 16 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v3
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v3
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v3
73 |
--------------------------------------------------------------------------------
/.github/workflows/devops.yml:
--------------------------------------------------------------------------------
1 | name: DevOps
2 | on:
3 | [push, pull_request]
4 | env:
5 | pythonLastVersion: '3.13'
6 | GHCR_REGISTRY: ghcr.io
7 | IMAGE_NAME: llaumgui/seedboxsync
8 |
9 | jobs:
10 |
11 | ##############################################################################
12 | # Test python application
13 | #
14 | test_python:
15 | runs-on: ubuntu-latest
16 | name: Test Python
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
21 | steps:
22 | - name: Git checkout
23 | uses: actions/checkout@v4
24 |
25 | - name: Install Dependencies
26 | run: pip install -r requirements-dev.txt
27 | - name: Make all tests with make
28 | run: |
29 | make comply
30 | python -m pytest -v --cov=seedboxsync --cov-report=term --cov-report=xml --capture=sys tests/
31 | - uses: actions/upload-artifact@v4
32 | with:
33 | name: coverage-${{ matrix.python-version }}
34 | path: coverage.xml
35 |
36 |
37 | ##############################################################################
38 | # Markdownlint
39 | #
40 | test_markdownlint:
41 | runs-on: ubuntu-latest
42 | name: MarkdownLint
43 | steps:
44 | - name: Git checkout
45 | uses: actions/checkout@v4
46 | - name: markdownlint-cli
47 | uses: nosborn/github-action-markdown-cli@v3.4.0
48 | with:
49 | files: "*.md docs/*.md"
50 | config_file: ".markdownlint.yaml"
51 |
52 |
53 | ##############################################################################
54 | # Dockerfile tests
55 | #
56 | test_dockerfiles:
57 | if: ${{ github.event.schedule == '' }}
58 | runs-on: ubuntu-latest
59 | name: Linters for Dockerfile
60 | steps:
61 | - name: Git checkout
62 | uses: actions/checkout@v4
63 | - name: hadolint
64 | uses: hadolint/hadolint-action@v3.1.0
65 | with:
66 | recursive: true
67 |
68 |
69 | ##############################################################################
70 | # SonarCloud job
71 | #
72 | test_sonar:
73 | if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }}
74 | needs: [
75 | test_python,
76 | test_markdownlint,
77 | test_dockerfiles
78 | ]
79 | runs-on: ubuntu-latest
80 | name: SonarCloud analyse
81 | steps:
82 | - name: Git checkout
83 | uses: actions/checkout@v4
84 | with:
85 | fetch-depth: 0 # Disabling shallow clones is recommended for improving the relevancy of reporting
86 | # https://docs.sonarsource.com/sonarqube/latest/project-administration/analysis-scope/#sonarqube-respects-ignored-files
87 | - name: Remove .gitignore because SonarQube respects ignored files
88 | run: rm .gitignore
89 | - uses: actions/download-artifact@v4
90 | with:
91 | name: coverage-${{ env.pythonLastVersion }}
92 | - name: Display structure of downloaded files
93 | run: |
94 | pwd
95 | ls -R
96 | cat coverage.xml
97 | - name: SonarQube Scan
98 | uses: SonarSource/sonarqube-scan-action@v5.2.0
99 | env:
100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
101 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
102 | #with:
103 | # args: >
104 | # -Dsonar.verbose=true
105 |
106 |
107 | ##############################################################################
108 | # Package job
109 | #
110 | package:
111 | needs: [
112 | test_sonar
113 | ]
114 | runs-on: ubuntu-latest
115 | name: Build package
116 | steps:
117 | - name: Git checkout
118 | uses: actions/checkout@v4
119 | - name: Set up Python ${{ env.pythonLastVersion }}
120 | uses: actions/setup-python@v5
121 | with:
122 | python-version: ${{ env.pythonLastVersion }}
123 | cache: 'pip'
124 | - name: Install Dependencies
125 | run: pip install -r requirements-dev.txt
126 | - name: Package
127 | run: make dist
128 | - name: Archive package
129 | uses: actions/upload-artifact@v4
130 | with:
131 | name: seedboxsync-${{ github.sha }}.tar.gz
132 | path: dist/*.tar.gz
133 |
134 |
135 | ##############################################################################
136 | # Build and tests Docker image
137 | #
138 | test_docker:
139 | needs: [
140 | test_sonar
141 | ]
142 | runs-on: ubuntu-latest
143 | name: Build and test docker images
144 | steps:
145 | - name: Git checkout
146 | uses: actions/checkout@v4
147 | # Extract metadata (tags, labels) for Docker
148 | # https://github.com/docker/metadata-action
149 | - name: Extract Docker metadata
150 | id: meta
151 | uses: docker/metadata-action@v5
152 | with:
153 | images:
154 | ${{ env.IMAGE_NAME }}
155 | # Build and push Docker image with Buildx (don't push on PR)
156 | # https://github.com/docker/build-push-action
157 | - name: Build Docker image
158 | uses: docker/build-push-action@v6
159 | with:
160 | tags: |
161 | ${{ env.IMAGE_NAME }}
162 | labels: ${{ steps.meta.outputs.labels }}
163 | cache-from: type=registry,ref=${{ env.IMAGE_NAME }}
164 | cache-to: type=inline
165 | # Test with Trivy
166 | # https://github.com/aquasecurity/trivy-action
167 | - name: Run Trivy vulnerability scanner
168 | uses: aquasecurity/trivy-action@master
169 | with:
170 | image-ref: ${{ env.IMAGE_NAME }}
171 | format: 'template'
172 | template: '@/contrib/sarif.tpl'
173 | output: 'trivy-results.sarif'
174 | - name: Upload Trivy scan results to GitHub Security tab
175 | uses: github/codeql-action/upload-sarif@v3
176 | with:
177 | sarif_file: 'trivy-results.sarif'
178 |
179 |
180 | ##############################################################################
181 | # Build and deploy job (only on main)
182 | #
183 | docker_build_deploy:
184 | if: ${{ github.event_name != 'pull_request' && ( github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') ) }}
185 | needs: [
186 | test_docker
187 | ]
188 | runs-on: ubuntu-latest
189 | name: Build and deploy docker images
190 | steps:
191 | - name: Git checkout
192 | uses: actions/checkout@v4
193 | # Login against 2 Docker registries except on PR
194 | # https://github.com/docker/login-action
195 | - name: Log in to Docker Hub
196 | uses: docker/login-action@v3
197 | with:
198 | username: ${{ secrets.DOCKERHUB_USERNAME }}
199 | password: ${{ secrets.DOCKERHUB_TOKEN }}
200 | - name: Log into registry ${{ env.GHCR_REGISTRY }}
201 | uses: docker/login-action@v3
202 | with:
203 | registry: ${{ env.GHCR_REGISTRY }}
204 | username: ${{ github.actor }}
205 | password: ${{ secrets.GITHUB_TOKEN }}
206 | # Extract metadata (tags, labels) for Docker
207 | # https://github.com/docker/metadata-action
208 | - name: Extract Docker metadata
209 | id: meta
210 | uses: docker/metadata-action@v5
211 | with:
212 | images: |
213 | ${{ env.IMAGE_NAME }}
214 | ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
215 | # Build and push Docker image with Buildx (don't push on PR)
216 | # https://github.com/docker/build-push-action
217 | - name: Build and push Docker image
218 | if: ${{ github.ref == 'refs/heads/main' }}
219 | uses: docker/build-push-action@v6
220 | with:
221 | push: ${{ github.event_name != 'pull_request' }}
222 | tags: |
223 | ${{ env.IMAGE_NAME }}:main
224 | ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:main
225 | labels: ${{ steps.meta.outputs.labels }}
226 | cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:main
227 | cache-to: type=inline
228 | - name: Set env
229 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
230 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
231 | - name: Build and push Docker image
232 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
233 | uses: docker/build-push-action@v6
234 | with:
235 | push: ${{ github.event_name != 'pull_request' }}
236 | tags: |
237 | ${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}
238 | ${{ env.IMAGE_NAME }}:latest
239 | ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}
240 | ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
241 | labels: ${{ steps.meta.outputs.labels }}
242 | cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest
243 | cache-to: type=inline
244 |
245 |
246 | ##############################################################################
247 | # Release job
248 | #
249 | release:
250 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
251 | needs: [
252 | package,
253 | ]
254 | runs-on: ubuntu-latest
255 | name: Release on GitHub and PyPi
256 | steps:
257 | - name: Git checkout
258 | uses: actions/checkout@v4
259 | - name: Set env
260 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
261 | - name: Set up Python ${{ env.pythonLastVersion }}
262 | uses: actions/setup-python@v5
263 | with:
264 | python-version: ${{ env.pythonLastVersion }}
265 | cache: 'pip'
266 | - name: Install Dependencies
267 | run: pip install -r requirements-dev.txt
268 | - name: Package
269 | run: make dist
270 | - name: Create GitHub release
271 | id: create_release
272 | uses: actions/create-release@v1
273 | env:
274 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
275 | with:
276 | tag_name: ${{ github.ref }}
277 | release_name: Release ${{ github.ref }}
278 | body: |
279 | Changes in this Release
280 | - First Change
281 | - Second Change
282 | draft: true
283 | prerelease: false
284 | - name: Upload asset in GitHub release
285 | id: upload-release-asset
286 | uses: actions/upload-release-asset@v1
287 | env:
288 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
289 | with:
290 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
291 | asset_path: dist/seedboxsync-${{ env.RELEASE_VERSION }}.tar.gz
292 | asset_name: seedboxsync-${{ env.RELEASE_VERSION }}.tar.gz
293 | asset_content_type: application/tar+gzip
294 | - name: Publish package
295 | uses: pypa/gh-action-pypi-publish@master
296 | with:
297 | user: __token__
298 | password: ${{ secrets.PYPI_PASSWORD }}
299 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Pages
2 |
3 | on:
4 | # Runs on pushes targeting the default branch
5 | push:
6 | tags: ['*']
7 |
8 | # Allows you to run this workflow manually from the Actions tab
9 | workflow_dispatch:
10 |
11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
12 | permissions:
13 | contents: read
14 | pages: write
15 | id-token: write
16 |
17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
19 | concurrency:
20 | group: "pages"
21 | cancel-in-progress: false
22 |
23 | jobs:
24 | # Build job
25 | build:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 | - name: Setup Pages
31 | uses: actions/configure-pages@v5
32 | - name: Build with Jekyll
33 | uses: actions/jekyll-build-pages@v1
34 | with:
35 | source: ./docs
36 | destination: ./_site
37 | - name: Upload artifact
38 | uses: actions/upload-pages-artifact@v3
39 |
40 | # Deployment job
41 | deploy:
42 | environment:
43 | name: github-pages
44 | url: ${{ steps.deployment.outputs.page_url }}
45 | runs-on: ubuntu-latest
46 | needs: build
47 | steps:
48 | - name: Deploy to GitHub Pages
49 | id: deployment
50 | uses: actions/deploy-pages@v4
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python
3 |
4 | ### Python ###
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 | cover/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 | db.sqlite3
66 | db.sqlite3-journal
67 |
68 | # Flask stuff:
69 | instance/
70 | .webassets-cache
71 |
72 | # Scrapy stuff:
73 | .scrapy
74 |
75 | # Sphinx documentation
76 | docs/_build/
77 |
78 | # PyBuilder
79 | .pybuilder/
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | # For a library or package, you might want to ignore these files since the code is
91 | # intended to run in multiple environments; otherwise, check them in:
92 | # .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # poetry
102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103 | # This is especially recommended for binary packages to ensure reproducibility, and is more
104 | # commonly ignored for libraries.
105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106 | #poetry.lock
107 |
108 | # pdm
109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110 | #pdm.lock
111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112 | # in version control.
113 | # https://pdm.fming.dev/#use-with-ide
114 | .pdm.toml
115 |
116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117 | __pypackages__/
118 |
119 | # Celery stuff
120 | celerybeat-schedule
121 | celerybeat.pid
122 |
123 | # SageMath parsed files
124 | *.sage.py
125 |
126 | # Environments
127 | .env
128 | .venv
129 | env/
130 | venv/
131 | ENV/
132 | env.bak/
133 | venv.bak/
134 |
135 | # Spyder project settings
136 | .spyderproject
137 | .spyproject
138 |
139 | # Rope project settings
140 | .ropeproject
141 |
142 | # mkdocs documentation
143 | /site
144 |
145 | # mypy
146 | .mypy_cache/
147 | .dmypy.json
148 | dmypy.json
149 |
150 | # Pyre type checker
151 | .pyre/
152 |
153 | # pytype static type analyzer
154 | .pytype/
155 |
156 | # Cython debug symbols
157 | cython_debug/
158 |
159 | # PyCharm
160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162 | # and can be added to the global gitignore or merged into this file. For a more nuclear
163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164 | #.idea/
165 |
166 | ### Python Patch ###
167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168 | poetry.toml
169 |
170 | # ruff
171 | .ruff_cache/
172 |
173 | # LSP config files
174 | pyrightconfig.json
175 |
176 | ### VisualStudioCode ###
177 | .vscode/*
178 | #!.vscode/settings.json
179 | #!.vscode/tasks.json
180 | #!.vscode/launch.json
181 | #!.vscode/extensions.json
182 | #!.vscode/*.code-snippets
183 |
184 | # Local History for Visual Studio Code
185 | .history/
186 |
187 | # Built Visual Studio Code Extensions
188 | *.vsix
189 |
190 | ### VisualStudioCode Patch ###
191 | # Ignore all local history of files
192 | .history
193 | .ionide
194 |
195 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
196 |
197 | !/docker/var
--------------------------------------------------------------------------------
/.hadolint.yaml:
--------------------------------------------------------------------------------
1 | ignored:
2 | - DL3018
3 | - DL3003
4 | - DL3008
5 | - DL3013
6 | - DL3059
7 | - SC2086
8 |
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | line-length: false
2 | no-inline-html: false
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ChangeLog
2 |
3 | ## Next release
4 |
5 | * ✨ Allow to delete entry by id [#56](https://github.com/llaumgui/seedboxsync/issues/56)
6 | * ✨ Add montly stats [#28](https://github.com/llaumgui/seedboxsync/issues/28)
7 | * ✨ Add yearly stats [#28](https://github.com/llaumgui/seedboxsync/issues/28)
8 |
9 | ## 3.1.0 - May 30, 2025
10 |
11 | > ⚠ **Warning:** Docker is now the recommended method.
12 |
13 | * 👷 Add docker support and provide docker images.
14 | * ⬆️ Update Cement framework.
15 | * 📦 Fix packaging issues.
16 | * 📝 Add Changelog and Contributors files.
17 | * 💚 Fix SonarCloud.
18 | * 👷 Drop Python 3.7 support.
19 | * 👷 Drop Python 3.8 support.
20 | * ⬆️ Now support Python version from 3.9 to 3.13.
21 |
22 | ## 3.0.1 - Feb 14, 2022
23 |
24 | * [Enhancement #36](https://github.com/llaumgui/seedboxsync/issues/36) Update Cement framework.
25 | * [Bug #38](https://github.com/llaumgui/seedboxsync/issues/38) ::set-env is now deprecated.
26 | * [Bug #35](https://github.com/llaumgui/seedboxsync/issues/35) Fix SonarCloud analysis.
27 |
28 | ## 3.0.0 (Cement / peewee full rebuild) - Sep 23, 2020
29 |
30 | *SeedboxSync v1 was the first release on Python 2, SeedboxSync v2 was port from Python 2 code to a compatible Python 3 code and SeedboxSync v3 is a full rewrite on [https://builtoncement.com/](Cement framework) and Peepee ORM.*
31 |
32 | Change since v2 serie:
33 |
34 | * [Enhancement #27](https://github.com/llaumgui/seedboxsync/issues/27) Rebuild on Cement.
35 | * [Enhancement #29](https://github.com/llaumgui/seedboxsync/issues/29) Add ORM support: Use Peepee ORM.
36 | * [Enhancement #26](https://github.com/llaumgui/seedboxsync/issues/26) New CI/CD platform and features part 2: release with GitHub Actions.
37 | * [Enhancement #30](https://github.com/llaumgui/seedboxsync/issues/30) Better list-in-progress: Add percentage and time prediction.
38 | * [Bugfix #14](https://github.com/llaumgui/seedboxsync/issues/14) No timeout on connections.
39 | * [Enhancement #34](https://github.com/llaumgui/seedboxsync/issues/34) Add ping architecture and Healthchecks support.
40 |
41 | ## 3.0.0b3 - Sep 15, 2020
42 |
43 | Bugfix
44 |
45 | ## 3.0.0b2 - Sep 4, 2020
46 |
47 | Second beta.
48 |
49 | * Update documentation.
50 | * Update code.
51 | * [Enhancement #34](https://github.com/llaumgui/seedboxsync/issues/34) Add ping architecture and Healthchecks support.
52 |
53 | ## 3.0.0b1 - Aug 26, 2020
54 |
55 | * [Enhancement #27](https://github.com/llaumgui/seedboxsync/issues/27) Rebuild on Cement.
56 | * [Enhancement #29](https://github.com/llaumgui/seedboxsync/issues/29) Add ORM support: Use Peepee ORM.
57 | * [Enhancement #26](https://github.com/llaumgui/seedboxsync/issues/26) New CI/CD platform and features part 2: release with GitHub Actions.
58 | * [Enhancement #30](https://github.com/llaumgui/seedboxsync/issues/30) Better list-in-progress: Add percentage and time prediction.
59 | * [Bugfix #14](https://github.com/llaumgui/seedboxsync/issues/14) No timeout on connections.
60 |
61 | ## 2.0.1 - Nov 1, 2018
62 |
63 | * [Enhancement #22](https://github.com/llaumgui/seedboxsync/issues/22) Migration from Sphinx to GitHub documentation.
64 | * [Enhancement #23](https://github.com/llaumgui/seedboxsync/issues/23) File size check, don't exist.
65 | * [Enhancement #24](https://github.com/llaumgui/seedboxsync/issues/24) New version system.
66 |
67 | ## 2.0.0 (Python3) - May 29, 2018
68 |
69 | Changes since v1 serie:
70 |
71 | * The big change: #12 Python 3 support:
72 | * [Enhancement] Python 3 enhancements.
73 | * [Enhancement] Replace bencode by bcoding because Python 3.
74 | * [Enhancement] Update Paramiko requirement (>=2.2.1).
75 | * Others enhancements:
76 | * [Enhancement] Better usage of configparser.
77 | * [Enhancement #19](https://github.com/llaumgui/seedboxsync/issues/19) Use exception instead exit.
78 | * New features:
79 | * [Enhancement #20](https://github.com/llaumgui/seedboxsync/issues/20) Sync exclusion.
80 | * [Enhancement] New logo.
81 | * QA:
82 | * [Bug] Doc building from Travis. Doc is updated ! See: .
83 | * Use tox for QA and tests.
84 | * Update Travis config.
85 | * Update Code Climate config.
86 |
87 | ## 2.0.0.beta2 (Last beta ?) - May 15, 2018
88 |
89 | * [Enhancement #19](https://github.com/llaumgui/seedboxsync/issues/19) Use exception instead exit.
90 | * [Enhancement #20](https://github.com/llaumgui/seedboxsync/issues/20) Sync Exclusion.
91 | * [Enhancement] New logo.
92 |
93 | ## 2.0.0.beta1 (Python3 usable !) - Aug 17, 2017
94 |
95 | * [Enhancement #15] Replace BencodePy by bcoding to fix --blackhole issue.
96 | * [Bug] Doc building from Travis. Doc is updated ! See: .
97 |
98 | ## 1.1.2 (The last Python 2 version ?) - Jul 22, 2017
99 |
100 | * Some cleanup before archive.
101 | * Backport "prefixed_path" from Python3 branch.
102 |
103 | ## 2.0.0.alpha1 (First Python3 version !) - Jul 22, 2017
104 |
105 | * [Enhancement #12](https://github.com/llaumgui/seedboxsync/issues/12) Python 3 suppport.
106 | * [Enhancement] Python 3 enhancement.
107 | * [Enhancement] Replace bencode by bencodepy because Py3.
108 | * [Enhancement] Update Paramiko requirement (>=2.2.1).
109 | * [Enhancement] Better use of configparser.
110 | * [Enhancement] More Try / except.
111 |
112 | ## 1.1.1 - Feb 17, 2016
113 |
114 | * [Enhancement #13](https://github.com/llaumgui/seedboxsync/issues/13) Better documenttion.
115 | * [Bugfix #13](https://github.com/llaumgui/seedboxsync/issues/13) Typo fix in seedboxsync.ini.
116 |
117 | Important: update your seedboxsync.ini and replace wath_path by watch_path.
118 |
119 | ## 1.1.0 - Feb 4, 2016
120 |
121 | * [Enhancement #10](https://github.com/llaumgui/seedboxsync/issues/10): Make code documentation: .
122 | * [Enhancement #9](https://github.com/llaumgui/seedboxsync/issues/9): Check if the process inside the PID lock file is still running (thanks @johanndt).
123 | * [Enhancement #3](https://github.com/llaumgui/seedboxsync/issues3): Use a transport interface: you can now make a PR for FTP support ;-).
124 |
125 | ## 1.0.0 - Oct 13, 2015
126 |
127 | First stable version.
128 |
129 | ## 0.9.0 (a.k.a v1.0.0 RC1) - Aug 20, 2015
130 |
131 | First version avalaible from Pypi.
132 |
133 | * [enhancement #1](https://github.com/llaumgui/seedboxsync/issues/1): Install seedboxsync with a setup.py.
134 | * [enhancement #2](https://github.com/llaumgui/seedboxsync/issues/2): Check size after download.
135 | * [enhancement #4](https://github.com/llaumgui/seedboxsync/issues/4): Allow shorts arguments.
136 | * [Bug #5](https://github.com/llaumgui/seedboxsync/issues/5): Download fail: cannot concatenate 'str' and 'int' objects.
137 |
138 | ## 0.5.0 (Full rewrite) - Aug 14, 2015
139 |
140 | Pre-release v0.5.0, first release just after a full rewrite.
141 |
142 | ## 0.1.0 (Pre-release) - Aug 8, 2015
143 |
144 | First quick and not so dirty release before an full rewrite.
145 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | The following people have contributed to Cement, either by way of source code,
4 | documentation, or testing:
5 |
6 | - Guillaume Kulakowski (llaumgui) - Creator, Primary Maintainer
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | # Build
3 | #
4 | FROM python:3.13-alpine as builder
5 |
6 | WORKDIR /src
7 |
8 | ENV \
9 | PYTHONDONTWRITEBYTECODE=1 \
10 | PYTHONUNBUFFERED=1
11 |
12 | COPY . /src
13 | RUN pip install --no-cache-dir cement && \
14 | pip wheel --no-cache-dir --no-deps --wheel-dir /src/wheels -r requirements.txt && \
15 | pip wheel --no-cache-dir --no-deps --wheel-dir /src/wheels .
16 |
17 |
18 |
19 | ################################################################################
20 | # Prod
21 | #
22 | FROM python:3.13-alpine
23 |
24 |
25 | # -------------------------------------------- Set environment and ARG variables
26 | ENV \
27 | # Set default PUID / PGUID \
28 | PUID=1000 \
29 | PGID=1000 \
30 | # Setup s6 overlay
31 | S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
32 | S6_VERBOSITY=1
33 | ARG \
34 | # Set version for s6 overlay \
35 | ARG S6_OVERLAY_VERSION="3.2.1.0" \
36 | ARG S6_OVERLAY_ARCH="x86_64"
37 |
38 |
39 | # ------------------------------------------------------------------- s6 overlay
40 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
41 | RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
42 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz /tmp
43 | RUN tar -C / -Jxpf /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz
44 | # Optional symlinks
45 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
46 | RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
47 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz /tmp
48 | RUN tar -C / -Jxpf /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz
49 |
50 | RUN apk add --update --no-cache shadow
51 |
52 | # ------------------------------------------------------------ SeedboxSync setup
53 | RUN addgroup -g ${PGID} seedboxsync && adduser -D -u ${PUID} -G seedboxsync seedboxsync
54 |
55 | WORKDIR /src
56 |
57 | # System folders
58 | RUN mkdir /config && \
59 | mkdir /downloads && \
60 | mkdir /watch && \
61 | chown -R seedboxsync:seedboxsync /config /downloads /watch
62 |
63 | # Add cement for setup.py before
64 | COPY --from=builder /src/wheels wheels
65 | RUN pip install --no-cache-dir wheels/* && \
66 | rm -rf /src
67 |
68 | # Seedboxsync folders
69 | COPY config/seedboxsync.yml.docker /usr/local/config/seedboxsync.yml.example
70 | RUN mkdir /home/seedboxsync/.config && \
71 | ln -s /config /home/seedboxsync/.config/seedboxsync && \
72 | ln -s /downloads /home/seedboxsync/Downloads && \
73 | ln -s /watch /home/seedboxsync/watch && \
74 | cp /usr/local/config/seedboxsync.yml.example /config/seedboxsync.yml
75 |
76 | # Copy all rootfs files with configuration and others scripts
77 | COPY docker/ /
78 | RUN chmod 755 /etc/s6-overlay/s6-rc.d/*/run && \
79 | chmod 755 /etc/s6-overlay/s6-rc.d/*/up
80 |
81 | WORKDIR /home/seedboxsync
82 |
83 | ENTRYPOINT ["/init"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include *.py
2 | include setup.cfg
3 | include README.md CHANGELOG.md LICENSE CONTRIBUTORS.md
4 | include *.txt
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: dev test test-core comply-fix docs clean dist dist-upload docker docker-push
2 |
3 | dev:
4 | docker-compose up -d
5 | docker-compose exec seedboxsync pip install -r requirements-dev.txt
6 | docker-compose exec seedboxsync python setup.py develop
7 | docker-compose exec seedboxsync /bin/sh
8 |
9 | test: comply
10 | python -m pytest -v --cov=seedboxsync --cov-report=term --cov-report=html:coverage-report --capture=sys tests/
11 |
12 | test-core: comply
13 | python -m pytest -v --cov=seedboxsync.core --cov-report=term --cov-report=html:coverage-report --capture=sys tests/core
14 |
15 | virtualenv:
16 | virtualenv --prompt '|> seedboxsync <| ' env
17 | env/bin/pip install -r requirements-dev.txt
18 | env/bin/python setup.py develop
19 | @echo
20 | @echo "VirtualENV Setup Complete. Now run: source env/bin/activate"
21 | @echo
22 |
23 | virtualenv-windows:
24 | virtualenv --prompt '|> seedboxsync <| ' env-windows
25 | env-windows\\Scripts\\pip.exe install -r requirements-dev-windows.txt
26 | env-windows\\Scripts\\python.exe setup.py develop
27 | @echo
28 | @echo "VirtualENV Setup Complete. Now run: .\env-windows\Scripts\activate.ps1"
29 | @echo
30 |
31 | comply:
32 | flake8 seedboxsync/ tests/
33 |
34 | comply-fix:
35 | autopep8 -ri seedboxsync/ tests/
36 |
37 | comply-typing:
38 | mypy ./seedboxsync
39 |
40 | docs:
41 | python setup.py build_sphinx
42 | @echo
43 | @echo DOC: "file://"$$(echo `pwd`/docs/build/html/index.html)
44 | @echo
45 |
46 | clean:
47 | find . -name '*.py[co]' -delete
48 | rm -rf doc/build
49 |
50 | dist: clean
51 | rm -rf dist/*
52 | python setup.py sdist
53 | python setup.py bdist_wheel
54 |
55 | dist-upload:
56 | twine upload dist/*
57 |
58 | docker:
59 | docker build -t llaumgui/seedboxsync:latest .
60 |
61 | docker-push:
62 | docker push llaumgui/seedboxsync:latest
63 |
64 | remove-merged-branches:
65 | git branch --merged | grep -v -e 'main\|stable/*\|dev/*' | xargs git branch -d
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SeedboxSync
2 |
3 | [![Author][ico-bluesky]][link-bluesky]
4 | [![Software License][ico-license]](LICENSE)
5 | [![Build Status][ico-ghactions]][link-ghactions]
6 | [![Latest Version][ico-pypi-version]][link-pypi]
7 | [![Docker Pull][ico-docker]][link-docker]
8 | [![Latest Version][ico-version]][link-docker]
9 |
10 | [![Quality Gate Status][ico-sonarcloud-gate]][link-sonarcloud-gate]
11 | [![Coverage][ico-sonarcloud-coverage]][link-sonarcloud-coverage]
12 | [![Maintainability Rating][ico-sonarcloud-maintainability]][link-sonarcloud-maintainability]
13 | [![Reliability Rating][ico-sonarcloud-reliability]][link-sonarcloud-reliability]
14 | [![Security Rating][ico-sonarcloud-security]][link-sonarcloud-security]
15 |
16 | Provides synchronization functions between your NAS and your seedbox:
17 |
18 | * Syncs a local black hole (ie: a NAS folder) with the black hole of your seedbox.
19 | * Downloads files from your seedbox to your NAS. Stores the list of downloaded files in a sqlite database, to prevent to download a second time.
20 | * Also provides queries to know last torrents, last downloads, etc.
21 |
22 | ## Full documentation
23 |
24 | See: [https://llaumgui.github.io/seedboxsync/](https://llaumgui.github.io/seedboxsync/)
25 |
26 | ## License
27 |
28 | Released under the [GPL v2](http://opensource.org/licenses/GPL-2.0).
29 |
30 | [ico-bluesky]: https://img.shields.io/static/v1?label=Author&message=llaumgui&color=208bfe&logo=bluesky&style=flat-square
31 | [link-bluesky]: https://bsky.app/profile/llaumgui.kulakowski.fr
32 | [ico-ghactions]: https://img.shields.io/github/actions/workflow/status/llaumgui/seedboxsync/devops.yml?branch=main&style=flat-square&logo=github&label=DevOps
33 | [link-ghactions]: https://github.com/llaumgui/seedboxsync/actions
34 | [ico-pypi-version]: https://img.shields.io/pypi/v/seedboxsync?include_prereleases&label=Package%20version&style=flat-square&logo=python
35 | [link-pypi]:https://pypi.org/project/seedboxsync/
36 | [ico-license]: https://img.shields.io/github/license/llaumgui/seedboxsync?style=flat-square
37 | [ico-docker]: https://img.shields.io/docker/pulls/llaumgui/seedboxsync?color=%2496ed&logo=docker&style=flat-square
38 | [link-docker]: https://hub.docker.com/r/llaumgui/seedboxsync
39 | [ico-version]: https://img.shields.io/docker/v/llaumgui/seedboxsync?sort=semver&color=%2496ed&logo=docker&style=flat-square
40 | [ico-sonarcloud-gate]: https://sonarcloud.io/api/project_badges/measure?branch=main&project=llaumgui_seedboxsync&metric=alert_status
41 | [link-sonarcloud-gate]: https://sonarcloud.io/dashboard?id=llaumgui_seedboxsync&branch=main
42 | [ico-sonarcloud-coverage]: https://sonarcloud.io/api/project_badges/measure?project=llaumgui_seedboxsync&metric=coverage
43 | [link-sonarcloud-coverage]: https://sonarcloud.io/dashboard?id=llaumgui_seedboxsync
44 | [ico-sonarcloud-maintainability]: https://sonarcloud.io/api/project_badges/measure?project=llaumgui_seedboxsync&metric=sqale_rating
45 | [link-sonarcloud-maintainability]: https://sonarcloud.io/dashboard?id=llaumgui_seedboxsync
46 | [ico-sonarcloud-reliability]: https://sonarcloud.io/api/project_badges/measure?project=llaumgui_seedboxsync&metric=reliability_rating
47 | [link-sonarcloud-reliability]: https://sonarcloud.io/dashboard?id=llaumgui_seedboxsync
48 | [ico-sonarcloud-security]: https://sonarcloud.io/api/project_badges/measure?project=llaumgui_seedboxsync&metric=security_rating
49 | [link-sonarcloud-security]: https://sonarcloud.io/dashboard?id=llaumgui_seedboxsync
50 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | We release patches for security vulnerabilities. Which versions are eligible receiving such patches depend on the CVSS v3.0 Rating:
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 3.1.x | :white_check_mark: |
10 | | 3.0.x | :white_check_mark: |
11 | | 2.0.x | :x: |
12 | | 1.1.x | :x: |
13 | | 1.0.x | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Please report (suspected) security vulnerabilities to . You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
18 |
--------------------------------------------------------------------------------
/config/seedboxsync.yml.docker:
--------------------------------------------------------------------------------
1 | ### SeedboxSync Configuration Settings
2 | ---
3 |
4 | #
5 | # Information about your seedbox
6 | #
7 | seedbox:
8 |
9 | ### Information about your seedbox connection
10 | # host: my-seedbox.ltd
11 | # port: 22
12 | # login: me
13 | # password: p4sw0rd
14 | # timeout: false
15 |
16 | ### For the moment, only sftp
17 | # protocol: sftp
18 |
19 | ### Chmod torrent after upload (false : disable)
20 | ### Use octal notation like https://docs.python.org/3.4/library/os.html#os.chmod
21 | # chmod: 0o777
22 | # chmod: false
23 |
24 | ### Use a tempory directory (you must create it !)
25 | # tmp_path: ./tmp
26 |
27 | ### Your "watch" folder you must create it!)
28 | # watch_path: ./watch
29 |
30 | ### Your finished folder you must create it!)
31 | # finished_path: ./files
32 |
33 | ### Exclude part files
34 | # part_suffix: .part
35 |
36 | ### Exclude pattern from sync
37 | ### Use re syntaxe: https://docs.python.org/3/library/re.html
38 | ### Example: .*missing$|^\..*\.swap$
39 | # exclude_syncing:
40 |
41 |
42 | #
43 | # Information about local environment (NAS ?)
44 | #
45 | local:
46 |
47 | ### Your local "watch" folder
48 | watch_path: /watch
49 |
50 | ### Path where download files
51 | download_path: /downloads/
52 |
53 | ### Use local sqlite database for store downloaded files
54 | db_file: /config/seedboxsync.db
55 |
56 |
57 | #
58 | # PID and lock management to prevent several launches
59 | #
60 | pid:
61 |
62 | ### PID for blackhole sync
63 | blackhole_path: /config/lock/blackhole.pid
64 |
65 | ### PID for seedbox downloaded sync
66 | download_path: /config/lock/download.pid
67 |
68 |
69 | #
70 | # Healthchecks ping service
71 | #
72 | healthchecks:
73 |
74 | ### sync seedbox part
75 | sync_seedbox:
76 | ## Enable or disable service
77 | enabled: false
78 |
79 | ## Ping URL
80 | # ping_url:
81 |
82 | ### sync blackhole part
83 | sync_blackhole:
84 | ## Enable or disable service
85 | enabled: false
86 |
87 | ## Ping URL
88 | # ping_url:
89 |
90 |
91 | #
92 | # SeedboxSync tuning
93 | #
94 | seedboxsync:
95 |
96 | ### Toggle application level debug (does not toggle framework debugging)
97 | # debug: false
98 |
99 | ### Where external (third-party) plugins are loaded from
100 | # plugin_dir: /var/lib/seedboxsync/plugins/
101 |
102 | ### Where all plugin configurations are loaded from
103 | # plugin_config_dir: /etc/seedboxsync/plugins.d/
104 |
105 | ### The log handler label
106 | # log_handler: colorlog
107 |
108 |
109 | log.colorlog:
110 |
111 | ### Where the log file lives (no log file by default)
112 | # file: null
113 |
114 | ### The level for which to log. One of: info, warning, error, fatal, debug
115 | # level: info
116 |
117 | ### Whether or not to log to console
118 | # to_console: true
119 |
120 | ### Whether or not to rotate the log file when it reaches `max_bytes`
121 | # rotate: false
122 |
123 | ### Max size in bytes that a log file can grow until it is rotated.
124 | # max_bytes: 512000
125 |
126 | ### The maximum number of log files to maintain when rotating
127 | # max_files: 4
--------------------------------------------------------------------------------
/config/seedboxsync.yml.example:
--------------------------------------------------------------------------------
1 | ### SeedboxSync Configuration Settings
2 | ---
3 |
4 | #
5 | # Information about your seedbox
6 | #
7 | seedbox:
8 |
9 | ### Connection information
10 | # host: my-seedbox.ltd
11 | # port: 22
12 | # login: me
13 | # password: p4sw0rd
14 | # timeout: false
15 |
16 | ### Only 'sftp' is supported for now
17 | # protocol: sftp
18 |
19 | ### Chmod torrent after upload (set to false to disable)
20 | ### Use octal notation, e.g. 0o644
21 | # chmod: false
22 |
23 | ### Use a temporary directory for incomplete transfers (must be created manually)
24 | # tmp_path: /tmp
25 |
26 | ### Your BitTorrent client's "watch" folder (must be created manually)
27 | watch_path: /watch
28 |
29 | ### The folder where your BitTorrent client puts finished files
30 | # finished_path: /files
31 |
32 | ### Remove a prefix from the synced path (usually the same as "finished_path")
33 | prefixed_path: /files
34 |
35 | ### Exclude files with this suffix (e.g. incomplete downloads)
36 | # part_suffix: .part
37 |
38 | ### Exclude files from sync using a regular expression (Python re syntax)
39 | ### Example: .*missing$|^\..*\.sw
40 | # exclude_syncing: .*missing$|^\..*\.sw
41 |
42 |
43 | #
44 | # Information about the local environment (NAS, etc.)
45 | #
46 | local:
47 |
48 | ### Your local "watch" folder
49 | # watch_path: ~/watch
50 |
51 | ### Path where files are downloaded
52 | # download_path: ~/Downloads/
53 |
54 | ### Path to the local SQLite database for tracking downloaded files
55 | # db_file: ~/.config/seedboxsync/seedboxsync.db
56 |
57 |
58 | #
59 | # PID and lock management to prevent multiple instances
60 | #
61 | pid:
62 |
63 | ### PID file for blackhole sync
64 | # blackhole_path: ~/.config/seedboxsync/lock/blackhole.pid
65 |
66 | ### PID file for seedbox download sync
67 | # download_path: ~/.config/seedboxsync/lock/download.pid
68 |
69 |
70 | #
71 | # Healthchecks ping service
72 | #
73 | healthchecks:
74 |
75 | ### Sync seedbox part
76 | sync_seedbox:
77 | ### Enable or disable the service
78 | # enabled: true
79 |
80 | # Ping URL
81 | # ping_url: https://hc-ping.com/ca5e1159-9acf-410c-9202-f76a7bb856e0
82 |
83 | ### Sync blackhole part
84 | sync_blackhole:
85 | ## Enable or disable the service
86 | # enabled: true
87 |
88 | ## Ping URL
89 | # ping_url: https://hc-ping.com/ca5e1159-9acf-410c-9202-f76a7bb856e0
90 |
91 |
92 | #
93 | # SeedboxSync tuning
94 | #
95 | seedboxsync:
96 |
97 | ### Toggle application level debug (does not toggle framework debugging)
98 | # debug: false
99 |
100 | ### Where external (third-party) plugins are loaded from
101 | # plugin_dir: /var/lib/seedboxsync/plugins/
102 |
103 | ### Where all plugin configurations are loaded from
104 | # plugin_config_dir: /etc/seedboxsync/plugins.d/
105 |
106 | ### The log handler label
107 | # log_handler: colorlog
108 |
109 |
110 | log.colorlog:
111 |
112 | ### Where the log file lives (no log file by default)
113 | # file: null
114 |
115 | ### The level for which to log. One of: info, warning, error, fatal, debug
116 | # level: info
117 |
118 | ### Whether or not to log to console
119 | # to_console: true
120 |
121 | ### Whether or not to rotate the log file when it reaches `max_bytes`
122 | # rotate: false
123 |
124 | ### Max size in bytes that a log file can grow until it is rotated.
125 | # max_bytes: 512000
126 |
127 | ### The maximum number of log files to maintain when rotating
128 | # max_files: 4
--------------------------------------------------------------------------------
/docker/etc/crontabs/seedboxsync:
--------------------------------------------------------------------------------
1 | # min hour day month weekday command
2 | * * * * * seedboxsync sync blackhole --ping
3 | */15 * * * * seedboxsync sync seedbox --ping
4 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-config/dependencies.d/init-usermod:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/init-config/dependencies.d/init-usermod
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-config/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv sh
2 | # shellcheck shell=sh
3 |
4 | CONFIG_PATH="/config/seedboxsync.yml"
5 | EXAMPLE_PATH="/usr/local/config/seedboxsync.yml.example"
6 | OWNER="seedboxsync"
7 | GROUP="seedboxsync"
8 |
9 | echo '
10 | ───────────────────────────────────────
11 | Auto config
12 | ───────────────────────────────────────'
13 |
14 | # Check if the config file already exists
15 | if [ -f "$CONFIG_PATH" ]; then
16 | echo "The file $CONFIG_PATH already exists."
17 | else
18 | echo "The file $CONFIG_PATH is missing, copying the example..."
19 | cp "$EXAMPLE_PATH" "$CONFIG_PATH"
20 |
21 | # Set ownership to user and group 'seedboxsync'
22 | chown "$OWNER:$GROUP" "$CONFIG_PATH"
23 |
24 | echo "File copied and ownership set to $OWNER:$GROUP."
25 | fi
26 |
27 | echo '───────────────────────────────────────'
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-config/type:
--------------------------------------------------------------------------------
1 | oneshot
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-config/up:
--------------------------------------------------------------------------------
1 | /etc/s6-overlay/s6-rc.d/init-config/run
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-end/dependencies.d/init-config:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/init-end/dependencies.d/init-config
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-end/dependencies.d/init-usermod:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/init-end/dependencies.d/init-usermod
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-end/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv sh
2 | # shellcheck shell=sh
3 |
4 | echo '
5 | ───────────────────────────────────────
6 | To get support, please go to:
7 | https://github.com/llaumgui/seedboxsync
8 |
9 | ───────────────────────────────────────
10 | GID/UID
11 | ───────────────────────────────────────'
12 | echo "
13 | User UID: $(id -u seedboxsync)
14 | User GID: $(id -g seedboxsync)
15 | ───────────────────────────────────────"
16 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-end/type:
--------------------------------------------------------------------------------
1 | oneshot
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-end/up:
--------------------------------------------------------------------------------
1 | /etc/s6-overlay/s6-rc.d/init-end/run
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-usermod/dependencies.d/base:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/init-usermod/dependencies.d/base
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-usermod/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv sh
2 | # shellcheck shell=sh
3 |
4 | DEFAULT_UID=1000
5 | DEFAULT_GID="${DEFAULT_UID}"
6 |
7 | # Change UID and GID of seedboxsync to match the host
8 | if [ "${PUID}" -ne "${DEFAULT_UID}" ]; then
9 | usermod -o -u ${PUID} seedboxsync
10 | chown seedboxsync /home/seedboxsync/ /home/seedboxsync/.config
11 | fi
12 | if [ "${PGID}" -ne "${DEFAULT_GID}" ]; then
13 | groupmod -o -g ${PGID} seedboxsync
14 | chgrp seedboxsync /home/seedboxsync/ /home/seedboxsync/.config
15 | fi
16 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-usermod/type:
--------------------------------------------------------------------------------
1 | oneshot
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/init-usermod/up:
--------------------------------------------------------------------------------
1 | /etc/s6-overlay/s6-rc.d/init-usermod/run
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/svc-cron/dependencies.d/init-end:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/svc-cron/dependencies.d/init-end
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/svc-cron/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv sh
2 | # shellcheck shell=sh
3 |
4 | exec busybox crond -f -l 0 -L /dev/stdout
5 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/svc-cron/type:
--------------------------------------------------------------------------------
1 | longrun
2 |
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-config:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-config
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-end:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-end
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-usermod:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/user/contents.d/init-usermod
--------------------------------------------------------------------------------
/docker/etc/s6-overlay/s6-rc.d/user/contents.d/svc-cron:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docker/etc/s6-overlay/s6-rc.d/user/contents.d/svc-cron
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | .bundle
3 | vendor
4 | _site
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gem 'github-pages', group: :jekyll_plugins
3 | gem "jekyll-remote-theme"
4 | gem "jekyll-docs-theme"
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: SeedboxSync Documentation
3 | description: "Script for sync operations between your NAS and your seedbox."
4 | author: "llaumgui"
5 | markdown: kramdown
6 | kramdown:
7 | input: GFM
8 | hard_wrap: false
9 | plugins:
10 | - jekyll-remote-theme
11 | exclude:
12 | - Gemfile
13 | - Gemfile.lock
14 | - vendor
15 | remote_theme: allejo/jekyll-docs-theme
16 |
17 | # jekyll doc plugin settings
18 | project:
19 | version: 3.2.0
20 | download_url: https://github.com/llaumgui/seedboxsync/releases
21 | download_text: Download
22 | license:
23 | software: GPLv2 License
24 | software_url: http://opensource.org/licenses/GPL-2.0
25 | docs: GPLv2 License
26 | docs_url: http://opensource.org/licenses/GPL-2.0
27 |
28 | links:
29 | header:
30 | - title: GitHub
31 | icon: github
32 | brand: true
33 | url: https://github.com/llaumgui/seedboxsync
34 | - title: LinkedIn
35 | icon: linkedin
36 | brand: true
37 | url: https://www.linkedin.com/in/guillaumekulakowski/
38 | - title: Blog
39 | icon: blogger
40 | brand: true
41 | url: https://blog.kulakowski.fr
42 | homepage:
43 | - title: View on GitHub
44 | icon: github
45 | brand: true
46 | url: https://github.com/llaumgui/seedboxsync
47 | footer:
48 | - title: GitHub
49 | icon: github
50 | brand: true
51 | url: https://github.com/llaumgui/seedboxsync
52 | - title: Issues
53 | icon: bug
54 | url: https://github.com/llaumgui/seedboxsync/issues?state=open
55 |
56 | ui:
57 | mode: 'light' # 'auto', 'dark', 'light'
58 | brand:
59 | dark: "#00ceff"
60 | light: "#5599ff"
61 | border_color:
62 | dark: "#5f5f5f"
63 | light: "#01579b"
64 | header:
65 | trianglify: true
66 | dark:
67 | color1: "#062a48"
68 | color2: "#304e67"
69 | light:
70 | color1: "#5599ff"
71 | color2: "#01579b"
72 | masthead:
73 | color:
74 | dark: "#fff"
75 | light: "#fff"
76 | align:
77 | homepage: 'center'
78 | page: 'left'
79 |
80 | social:
81 | github:
82 | user: llaumgui
83 | repo: seedboxsync
84 | twitter:
85 | enabled: true
86 | via:
87 | hash:
88 | account:
89 | facebook:
90 | enabled: false
91 | profileUrl:
92 |
93 | analytics:
94 | google: UA-63534466-3
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Configuration
4 | description: ~
5 | order: 2
6 | ---
7 |
8 | ## Configuration file
9 |
10 | ### Docker way
11 |
12 | > ⚠ **Warning:** This is the recommended way.
13 |
14 | The configuration file should be placed in `/conf`.
15 |
16 | ### Other ways
17 |
18 | > ⚠ **Warning:** Docker is the recommended method.
19 |
20 | You can use [the example configuration file](https://github.com/llaumgui/seedboxsync/blob/main/config/seedboxsync.yml.example).
21 | This example file can be located in:
22 |
23 | * `/usr/local/config/` (pip install)
24 | * `~/.local/config/` (pip install with user privileges)
25 |
26 | To create your configuration directory and copy the example file:
27 |
28 | ```bash
29 | mkdir -p ~/.config/seedboxsync
30 | cp ~/.local/config/seedboxsync.yml.example ~/.config/seedboxsync/seedboxsync.yml
31 | ```
32 |
33 | Or for a global configuration:
34 |
35 | ```bash
36 | sudo mkdir -p /etc/seedboxsync
37 | sudo cp /usr/local/config/seedboxsync.yml.example /etc/seedboxsync/seedboxsync.yml
38 | ```
39 |
40 | Supported configuration file locations:
41 |
42 | * `/etc/seedboxsync/seedboxsync.yml`
43 | * `~/.config/seedboxsync/seedboxsync.yml`
44 | * `~/.seedboxsync/config/seedboxsync.yml`
45 | * `~/.seedboxsync.yml`
46 |
47 | ## Settings
48 |
49 | ### Seedbox and BitTorrent client configuration
50 |
51 | First, set the connection information for your seedbox.
52 | Currently, only SFTP is supported.
53 |
54 | ```yml
55 | #
56 | # Information about your seedbox
57 | #
58 | seedbox:
59 |
60 | ### Connection information
61 | host: my-seedbox.ltd
62 | port: 22
63 | login: me
64 | password: p4sw0rd
65 | timeout: false
66 |
67 | ### Only 'sftp' is supported for now
68 | protocol: sftp
69 |
70 | ### Chmod torrent after upload (set to false to disable)
71 | ### Use octal notation, e.g. 0o644
72 | chmod: false
73 |
74 | ### Use a temporary directory for incomplete transfers (must be created manually)
75 | tmp_path: /tmp
76 |
77 | ### Your BitTorrent client's "watch" folder (must be created manually)
78 | watch_path: /watch
79 |
80 | ### The folder where your BitTorrent client puts finished files
81 | finished_path: /files
82 |
83 | ### Remove a prefix from the synced path (usually the same as "finished_path")
84 | prefixed_path: /files
85 |
86 | ### Exclude files with this suffix (e.g. incomplete downloads)
87 | part_suffix: .part
88 |
89 | ### Exclude files from sync using a regular expression (Python re syntax)
90 | ### Example: .*missing$|^\..*\.sw
91 | exclude_syncing: .*missing$|^\..*\.sw
92 | ```
93 |
94 | **Notes:**
95 |
96 | * To avoid permission issues between your transfer account and your BitTorrent client account, SeedboxSync can chmod the torrent file after upload.
97 | * To prevent your BitTorrent client from watching (and using) an incomplete torrent file, SeedboxSync first transfers the torrent file to a temporary directory (`tmp_path`). Once the transfer and chmod are complete, the file is moved to the watch folder.
98 | The temporary folder must also be configured in your BitTorrent client for unfinished torrents.
99 |
100 | 
101 |
102 | * The `watch_path` is your BitTorrent client's "blackhole" or "watch" folder, used for blackhole synchronization.
103 | * The `finished_path` is the folder where your BitTorrent client moves completed downloads. You can configure your client to use a specific folder for finished files.
104 |
105 | 
106 |
107 | ### NAS / Local configuration
108 |
109 | Your NAS configuration is defined in the `local` and `pid` sections:
110 |
111 | ```yml
112 | #
113 | # Information about the local environment (NAS, etc.)
114 | #
115 | local:
116 |
117 | ### Your local "watch" folder
118 | watch_path: ~/watch
119 |
120 | ### Path where files are downloaded
121 | download_path: ~/Downloads/
122 |
123 | ### Path to the local SQLite database for tracking downloaded files
124 | db_file: ~/.config/seedboxsync/seedboxsync.db
125 |
126 |
127 | #
128 | # PID and lock management to prevent multiple instances
129 | #
130 | pid:
131 |
132 | ### PID file for blackhole sync
133 | blackhole_path: ~/.config/seedboxsync/lock/blackhole.pid
134 |
135 | ### PID file for seedbox download sync
136 | download_path: ~/.config/seedboxsync/lock/download.pid
137 | ```
138 |
139 | ### Ping service configuration
140 |
141 | The ping service is triggered by the `--ping` argument.
142 | Currently, only [Healthchecks](https://github.com/healthchecks/healthchecks) is supported.
143 |
144 | #### Healthchecks
145 |
146 | Add a Healthchecks configuration for each sync command:
147 |
148 | ```yml
149 | #
150 | # Healthchecks ping service
151 | #
152 | healthchecks:
153 |
154 | ### Sync seedbox part
155 | sync_seedbox:
156 | ### Enable or disable the service
157 | enabled: true
158 |
159 | # Ping URL
160 | ping_url: https://hc-ping.com/ca5e1159-9acf-410c-9202-f76a7bb856e0
161 |
162 | ### Sync blackhole part
163 | sync_blackhole:
164 | ## Enable or disable the service
165 | enabled: true
166 |
167 | ## Ping URL
168 | ping_url: https://hc-ping.com/ca5e1159-9acf-410c-9202-f76a7bb856e0
169 | ```
170 |
--------------------------------------------------------------------------------
/docs/developers.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Development
4 | description: Usage for developers
5 | order: 4
6 | ---
7 |
8 | SeedboxSync is build with [Cement](https://builtoncement.com/) and [Peewee](http://docs.peewee-orm.com/en/latest/) from v3.
9 |
10 | ## Installation
11 |
12 | ```bash
13 | pip install -r requirements.txt
14 |
15 | pip install setup.py
16 | ```
17 |
18 | ## Development
19 |
20 | This project includes a number of helpers in the `Makefile` to streamline common development tasks.
21 |
22 | ### Environment Setup
23 |
24 | The following demonstrates setting up and working with a development environment:
25 |
26 | ```bash
27 | ### create a virtualenv for development
28 | make virtualenv
29 | source env/bin/activate
30 |
31 | ### run seedboxsync cli application
32 | seedboxsync --help
33 |
34 | ### run pytest / coverage
35 | make test
36 |
37 | ### Build package
38 | make dist
39 |
40 | ### Build docker image
41 | make docker
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/docker.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Docker
4 | description: How to install SeedboxSync with Docker
5 | order: 1
6 | ---
7 |
8 | > ⚠ **Warning:**
9 | >
10 | > * Docker is the recommended installation method.
11 | > * This image includes [s6-overlay](https://github.com/just-containers/s6-overlay) and provides extra features.
12 |
13 | ## Extra Features
14 |
15 | ### s6-overlay Integration
16 |
17 | This image uses [s6-overlay](https://github.com/just-containers/s6-overlay) for:
18 |
19 | * Multi-process container management and customization.
20 | * Support for changing the UID/GID running the main process.
21 |
22 | ### Cron Jobs
23 |
24 | * Sync blackhole every minute.
25 | * Sync seedbox every 15 minutes.
26 |
27 | ## Customization
28 |
29 | ### Custom UID/GID
30 |
31 | You can use `PUID` / `PGID` environment variables to run SeedboxSync as a specific non-root user instead of the default UID 1000.
32 | Just set the environment variables as follows:
33 |
34 | ```yaml
35 | environment:
36 | PUID: 1000
37 | PGID: 100
38 | ```
39 |
40 | ## Usage
41 |
42 | ### Running with Docker CLI
43 |
44 | To run the container using the Docker CLI:
45 |
46 | ```bash
47 | docker run -d \
48 | --name seedboxsync \
49 | --volume /data/seedboxsync/config:/config \
50 | --volume /data/seedboxsync/watch:/watch \
51 | --volume /data/seedboxsync/downloads:/downloads \
52 | -e TZ=Europe/Paris \
53 | -e PUID=1000 \
54 | -e PGID=100 \
55 | ghcr.io/llaumgui/seedboxsync:latest
56 | ```
57 |
58 | | Docker tags | Description | Stable |
59 | | ------------ | ----------------------------------------------- | ------ |
60 | | `latest` | Docker image based on latest release version | ✅ |
61 | | `main` | Docker image based on git main branch | ❌ |
62 |
63 | ### Running with Docker Compose
64 |
65 | ```yaml
66 | services:
67 | seedboxsync:
68 | container_name: seedboxsync
69 | hostname: seedboxsync
70 | image: ghcr.io/llaumgui/seedboxsync:latest
71 | restart: unless-stopped
72 | environment:
73 | TZ: 'Europe/Paris'
74 | PUID: 1000
75 | PGID: 100
76 | volumes:
77 | - /data/seedboxsync/config:/config
78 | - /data/seedboxsync/watch:/watch
79 | - /data/seedboxsync/downloads:/downloads
80 | ```
81 |
82 | ### All Environment Variables
83 |
84 | | Variable | Description | Default Value |
85 | |------------|-----------------------------------------------|---------------|
86 | | `TZ` | Timezone configuration | |
87 | | `PUID` | User ID for the main process | `1000` |
88 | | `PGID` | Group ID for the main process | `1000` |
89 |
90 | ## Using the Command Line from the Docker Host
91 |
92 | You can use a script to easily run SeedboxSync commands inside the container:
93 |
94 | ```bash
95 | #!/bin/bash
96 |
97 | CONTAINER_NAME="seedboxsync"
98 | UUID=1000
99 | COMMAND="$@"
100 |
101 | docker exec -it -u ${UUID} ${CONTAINER_NAME} seedboxsync ${COMMAND}
102 | ```
103 |
104 | > **Tip:** Replace `UUID` with the user ID you want to use inside the container.
105 |
--------------------------------------------------------------------------------
/docs/images/rutorrent_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docs/images/rutorrent_1.png
--------------------------------------------------------------------------------
/docs/images/rutorrent_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docs/images/rutorrent_2.png
--------------------------------------------------------------------------------
/docs/images/seedboxsync.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docs/images/seedboxsync.ico
--------------------------------------------------------------------------------
/docs/images/seedboxsync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docs/images/seedboxsync.png
--------------------------------------------------------------------------------
/docs/images/seedboxsync.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/docs/images/seedboxsync.xcf
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: full
3 | homepage: true
4 | disable_anchors: true
5 | description: Script for synchronizing operations between your NAS and your seedbox.
6 | ---
7 |
8 | # SeedboxSync Documentation
9 |
10 | 
11 |
12 | **SeedboxSync** provides powerful synchronization features between your NAS and your seedbox:
13 |
14 | * Synchronizes a local blackhole (e.g., a NAS folder) with the blackhole directory on your seedbox.
15 | * Downloads files from your seedbox to your NAS, maintaining a record of downloaded files in a SQLite database to prevent duplicate downloads.
16 | * Offers queries to retrieve information such as recent torrents, latest downloads, and more.
17 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Installation
4 | description: How to install SeedboxSync
5 | order: 1
6 | ---
7 |
8 | ## With docker
9 |
10 | > ⚠ **Warning:** This is the recommended method.
11 |
12 | See [documentation](https://llaumgui.github.io/seedboxsync/docker.html).
13 |
14 | ## With pip
15 |
16 | > ⚠ **Warning:** Docker is the recommended method.
17 |
18 | With simple user privileges:
19 |
20 | ```bash
21 | pip install --user seedboxsync
22 | ```
23 |
24 | Or in root:
25 |
26 | ```bash
27 | pip install seedboxsync
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Usage
4 | description: SeedboxSync usage
5 | order: 3
6 | ---
7 |
8 | ## Use in command line
9 |
10 | ```bash
11 | usage: seedboxsync [-h] [-d] [-q] [-v] {sync,list,clean} ...
12 |
13 | Script for sync operations between your NAS and your seedbox
14 |
15 | options:
16 | -h, --help show this help message and exit
17 | -d, --debug full application debug mode
18 | -q, --quiet suppress all console output
19 | -v, --version show program's version number and exit
20 |
21 | sub-commands:
22 | {sync,list,clean}
23 | sync all synchronization operations
24 | list all list operations
25 | clean all cleaning operations
26 |
27 | Usage: seedboxsync sync blackhole --dry-run
28 | ```
29 |
30 | * Sync blackhole:
31 |
32 | ```bash
33 | seedboxsync sync blackhole
34 | ```
35 |
36 | * Sync seedbox:
37 |
38 | ```bash
39 | seedboxsync sync seedbox
40 | ```
41 |
42 | * List 20 last torrents downloaded from seedbox:
43 |
44 | ```bash
45 | seedboxsync search downloaded -n 20
46 | ```
47 |
48 | * List 20 last torrents uploaded to seedbox:
49 |
50 | ```bash
51 | seedboxsync search uploaded -n 20
52 | ```
53 |
54 | * List download in progress:
55 |
56 | ```bash
57 | seedboxsync search progress
58 | ```
59 |
60 | * Clean all download in progress:
61 |
62 | ```bash
63 | seedboxsync clean progress
64 | ```
65 |
66 | * Remove downloaded torrent with id 123 (to enable re-download):
67 |
68 | ```bash
69 | seedboxsync clean downloaded 123
70 | ```
71 |
72 | * Get statistics by month:
73 |
74 | ```bash
75 | seedboxsync stats by-month
76 | ```
77 |
78 | * Get statistics by year:
79 |
80 | ```bash
81 | seedboxsync stats by-year
82 | ```
83 |
84 | ## Use in crontab
85 |
86 | > ⚠ **Warning:** Docker is the recommended method and have a cron feature out-of-the-box.
87 |
88 | ```bash
89 | # Sync blackhole every 2mn
90 | */2 * * * * root seedboxsync -q sync blackhole --ping
91 |
92 | # Download torrents finished every 15mn
93 | */15 * * * * root seedboxsync -q sync seedbox --ping
94 | ```
95 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 |
3 | pytest
4 | pytest-cov
5 | coverage
6 | twine
7 | setuptools
8 | wheel
9 | flake8
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cement==3.0.14
2 | pyyaml
3 | colorlog
4 | paramiko>=2.12
5 | bcoding
6 | tabulate
7 | peewee
--------------------------------------------------------------------------------
/seedboxsync/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement import Controller
10 | from cement.utils.version import get_version_banner
11 | from ..core.version import get_version
12 |
13 | VERSION_BANNER = """
14 | Script for sync operations between your NAS and your seedbox
15 |
16 | SeedboxSync %s
17 | %s
18 | """ % (get_version(), get_version_banner())
19 |
20 |
21 | class Base(Controller):
22 | class Meta:
23 | label = 'base'
24 |
25 | # text displayed at the top of --help output
26 | description = 'Script for sync operations between your NAS and your seedbox'
27 |
28 | # text displayed at the bottom of --help output
29 | epilog = 'Usage: seedboxsync sync blackhole --dry-run'
30 |
31 | # controller level arguments. ex: 'seedboxsync --version'
32 | arguments = [
33 | # add a version banner
34 | (['-v', '--version'],
35 | {'action': 'version',
36 | 'version': VERSION_BANNER}),
37 | ]
38 |
39 | def _default(self):
40 | """Default action if no sub-command is passed."""
41 |
42 | self.app.args.print_help()
43 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/clean.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement import Controller, ex
10 | from ..core.dao.download import Download
11 |
12 |
13 | class Clean(Controller):
14 | """
15 | Controller with clean concern.
16 | """
17 | class Meta:
18 | help = 'all cleaning operations'
19 | label = 'clean'
20 | stacked_on = 'base'
21 | stacked_type = 'nested'
22 |
23 | @ex(help='clean the list of files currently in download from seedbox')
24 | def progress(self):
25 | """
26 | Clean the list of files currently in download from seedbox
27 | """
28 | count = Download.delete().where(Download.finished == 0).execute()
29 | self.app.print('In progress list cleaned. %s line(s) deleted' % count)
30 |
31 | @ex(help='remove a downloaded file by ID to enable re-download',
32 | arguments=[([],
33 | {'help': 'downloaded torrent id to remove',
34 | 'action': 'store',
35 | 'dest': 'id'})])
36 | def downloaded(self):
37 | """
38 | Remove a downloaded file by ID to enable re-download
39 | """
40 | count = Download.delete().where(Download.id == self.app.pargs.id).execute()
41 | if count == 0:
42 | self.app.print('No downloaded file with id %s' % self.app.pargs.id)
43 | else:
44 | self.app.print('Torrent with id %s was removed' % self.app.pargs.id)
45 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/search.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import os
10 | import datetime
11 | from cement import Controller, fs, ex
12 | from ..core.dao.torrent import Torrent
13 | from ..core.dao.download import Download
14 | from peewee import fn
15 |
16 |
17 | class Search(Controller):
18 | """
19 | Controller with search concern.
20 | """
21 | class Meta:
22 | help = 'all search operations'
23 | label = 'search'
24 | stacked_on = 'base'
25 | stacked_type = 'nested'
26 |
27 | @ex(help='search lasts torrents uploaded from blackhole',
28 | arguments=[(['-n', '--number'],
29 | {'help': 'number of torrents to display',
30 | 'action': 'store',
31 | 'dest': 'number',
32 | 'default': 10}),
33 | (['-s', '--search'],
34 | {'help': 'term to search',
35 | 'action': 'store',
36 | 'dest': 'term'})])
37 | def uploaded(self):
38 | """
39 | Search lasts torrents uploaded from blackhole
40 | """
41 | # Build "where" expression
42 | if self.app.pargs.term:
43 | where = Torrent.name.contains(self.app.pargs.term)
44 | else:
45 | where = ~(Torrent.id.contains('not_a_int'))
46 |
47 | # DB query
48 | data = Torrent.select(Torrent.id,
49 | Torrent.name,
50 | Torrent.sent
51 | ).where(where).limit(self.app.pargs.number).order_by(Torrent.sent.desc()).dicts()
52 | self.app.render(reversed(data), headers={'id': 'Id', 'name': 'Name', 'sent': 'Sent datetime'})
53 |
54 | @ex(help='search lasts files downloaded from seedbox',
55 | arguments=[(['-n', '--number'],
56 | {'help': 'number of torrents to display',
57 | 'action': 'store',
58 | 'dest': 'number',
59 | 'default': 10}),
60 | (['-s', '--search'],
61 | {'help': 'term to search',
62 | 'action': 'store',
63 | 'dest': 'term'})])
64 | def downloaded(self):
65 | """
66 | Search lasts torrents downloaded from seedbox
67 | """
68 | # Build "where" expression
69 | if self.app.pargs.term:
70 | where = (Download.finished != 0) & (Download.path.contains(self.app.pargs.term))
71 | else:
72 | where = Download.finished != 0
73 |
74 | # DB query
75 | data = Download.select(Download.id,
76 | fn.SUBSTR(Download.path, -100).alias('path'),
77 | Download.finished,
78 | fn.sizeof(Download.local_size).alias('size')
79 | ).where(where).limit(self.app.pargs.number).order_by(Download.finished.desc()).dicts()
80 | self.app.render(reversed(data), headers={'id': 'Id', 'finished': 'Finished', 'path': 'Path', 'size': 'Size'})
81 |
82 | @ex(help='search files currently in download from seedbox',
83 | arguments=[(['-n', '--number'],
84 | {'help': 'number of torrents to display',
85 | 'action': 'store',
86 | 'dest': 'number',
87 | 'default': 10}),
88 | (['-s', '--search'],
89 | {'help': 'term to search',
90 | 'action': 'store',
91 | 'dest': 'term'})])
92 | def progress(self):
93 | """
94 | Search files currently in download from seedbox
95 | """
96 | # Build "where" expression
97 | if self.app.pargs.term:
98 | where = (Download.finished == 0) & (Download.path.contains(self.app.pargs.term))
99 | else:
100 | where = Download.finished == 0
101 |
102 | # DB suery
103 | data = Download.select(Download.id,
104 | fn.SUBSTR(Download.path, -100).alias('path'),
105 | Download.started,
106 | Download.seedbox_size,
107 | fn.sizeof(Download.seedbox_size).alias('size'),
108 | ).where(where).limit(self.app.pargs.number).order_by(Download.started.desc()).dicts()
109 |
110 | in_progress = []
111 | part_suffix = self.app.config.get('seedbox', 'part_suffix')
112 | download_path = fs.abspath(self.app.config.get('local', 'download_path'))
113 |
114 | for torrent in data:
115 | full_path = fs.join(download_path, torrent.get('path') + part_suffix)
116 | try:
117 | local_size = os.stat(full_path).st_size
118 | progress = round(100 * (1 - ((torrent.get('seedbox_size') - local_size) / torrent.get('seedbox_size'))))
119 | eta = str(round(((datetime.datetime.now() - torrent.get('started')).total_seconds() / (progress / 100)) / 60)) + ' mn'
120 | except FileNotFoundError:
121 | self.app.log.warning('File not found "%s"' % full_path)
122 | progress = 0
123 | eta = "-"
124 |
125 | in_progress.append({
126 | 'id': torrent.get('id'),
127 | 'path': torrent.get('path'),
128 | 'started': torrent.get('started'),
129 | 'size': torrent.get('size'),
130 | 'progress': str(progress) + '%',
131 | 'eta': eta
132 | })
133 | self.app.render(reversed(in_progress), headers={'id': 'Id', 'started': 'Started', 'path': 'Path', 'progress': 'Progress', 'eta': 'ETA', 'size': 'Size'})
134 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/stats.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement import Controller, ex
10 | from ..core.dao.download import Download
11 | from peewee import fn
12 | from ..core.db import sizeof
13 |
14 |
15 | class Stats(Controller):
16 | """
17 | Controller with statistics concern.
18 | """
19 | class Meta:
20 | help = 'all stats operations'
21 | label = 'stats'
22 | stacked_on = 'base'
23 | stacked_type = 'nested'
24 |
25 | def __stats_by_period(self, period, header_label):
26 | """
27 | Generic stats by period (month or year).
28 | :param period: 'month' or 'year'
29 | :param header_label: Header label for rendering
30 | """
31 | strftime_format = "%Y-%m" if period == "month" else "%Y"
32 |
33 | data = Download.select(
34 | Download.id,
35 | Download.finished,
36 | fn.strftime(strftime_format, Download.finished).alias(period),
37 | Download.seedbox_size,
38 | ).where(Download.finished != 0).order_by(Download.finished.desc()).dicts()
39 |
40 | tmp = {}
41 | for download in data:
42 | key = download[period]
43 | size = download['seedbox_size']
44 | if not key or not size:
45 | continue
46 | if key not in tmp:
47 | tmp[key] = {"files": 0, "total_size": 0.0}
48 | tmp[key]["files"] += 1
49 | tmp[key]["total_size"] += size
50 |
51 | stats = [
52 | {
53 | period: key,
54 | "files": tmp[key]["files"],
55 | "total_size": sizeof(tmp[key]["total_size"]),
56 | }
57 | for key in sorted(tmp)
58 | ]
59 |
60 | self.app.render(stats, headers={period: header_label, 'files': 'Nb files', 'total_size': 'Total size'})
61 |
62 | @ex(help='statistics by month')
63 | def by_month(self):
64 | """
65 | Show statistics by month.
66 | """
67 | self.__stats_by_period('month', 'Month')
68 |
69 | @ex(help='statistics by year')
70 | def by_year(self):
71 | """
72 | Show statistics by year.
73 | """
74 | self.__stats_by_period('year', 'Year')
75 |
--------------------------------------------------------------------------------
/seedboxsync/controllers/sync.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import datetime
10 | import glob
11 | import os
12 | import re
13 | from paramiko import SSHException
14 | from cement import Controller, ex, fs
15 | from ..core.dao.torrent import Torrent
16 | from ..core.dao.download import Download
17 | from ..core.exc import SeedboxSyncConfigurationError
18 |
19 |
20 | class Sync(Controller):
21 | class Meta:
22 | help = 'all synchronization operations'
23 | label = 'sync'
24 | stacked_on = 'base'
25 | stacked_type = 'nested'
26 |
27 | @ex(help='sync torrent from blackhole to seedbox',
28 | arguments=[(['--dry-run'],
29 | {'help': 'just list, no upload and persistence',
30 | 'action': 'store_true',
31 | 'dest': 'dry_run'}),
32 | (['-p', '--ping'],
33 | {'help': 'ping a service (ie: Healthchecks) during excecution',
34 | 'action': 'store_true',
35 | 'dest': 'ping'})])
36 | def blackhole(self):
37 | """
38 | Do the blackhole synchronization.
39 | """
40 | self.app.log.debug('sync_blackhole dry-run: "%s"' % self.app.pargs.dry_run)
41 |
42 | # Call ping_start_hook
43 | if self.app.pargs.ping:
44 | for res in self.app.hook.run('ping_start_hook', self.app, 'sync_blackhole'):
45 | pass
46 |
47 | # Create lock file.
48 | lock_file = self.app.config.get('pid', 'blackhole_path')
49 | self.app.lock.lock_or_exit(lock_file)
50 |
51 | # Get all torrents
52 | torrents = glob.glob(fs.join(fs.abspath(self.app.config.get('local', 'watch_path')), '*.torrent'))
53 | if len(torrents) > 0:
54 | # Upload torrents one by one
55 | for torrent_file in torrents:
56 | torrent_name = os.path.basename(torrent_file)
57 | if not self.app.pargs.dry_run:
58 | tmp_path = self.app.config.get('seedbox', 'tmp_path')
59 | watch_path = self.app.config.get('seedbox', 'watch_path')
60 |
61 | self.app.log.info('Upload torrent: "%s"' % torrent_name)
62 | self.app.log.debug('Upload "%s" in "%s" directory' % (torrent_file, tmp_path))
63 |
64 | try:
65 | self.app.sync.put(torrent_file, os.path.join(tmp_path, torrent_name))
66 |
67 | # Chmod
68 | chmod = self.app.config.get('seedbox', 'chmod')
69 | if chmod is not False:
70 | self.app.log.debug('Change mod in %s' % chmod)
71 | self.app.sync.chmod(os.path.join(tmp_path, torrent_name), int(chmod, 8))
72 |
73 | # Move from tmp
74 | self.app.log.debug('Move from "%s" to "%s"' % (tmp_path, watch_path))
75 | self.app.sync.rename(os.path.join(tmp_path, torrent_name), os.path.join(watch_path, torrent_name))
76 |
77 | # Store in DB
78 | torrent_info = self.app.bcoding.get_torrent_infos(torrent_file)
79 | torrent = Torrent.create(name=torrent_name)
80 | if torrent_info is not None:
81 | torrent.announce = torrent_info['announce']
82 | torrent.save()
83 |
84 | # Remove local torent
85 | self.app.log.debug('Remove local torrent "%s"' % torrent_file)
86 | os.remove(torrent_file)
87 | else:
88 | self.app.log.warning('Rename local "%s" to .torrent.fail' % torrent_file)
89 | os.rename(torrent_file, torrent_file + '.fail')
90 | except SSHException as exc:
91 | self.app.log.warning('SSH client exception > %s' % str(exc))
92 |
93 | else:
94 | self.app.log.info('Not upload torrent: "%s"' % torrent_name)
95 | else:
96 | self.app.log.info('No torrent in "%s"' % self.app.config.get('local', 'watch_path'))
97 |
98 | # Remove lock file.
99 | self.app.lock.unlock(lock_file)
100 |
101 | # Call ping_start_hook
102 | if self.app.pargs.ping:
103 | for res in self.app.hook.run('ping_success_hook', self.app, 'sync_blackhole'):
104 | pass
105 |
106 | @ex(help='sync file from seedbox',
107 | arguments=[(['--dry-run'],
108 | {'help': 'just list, no download and persistence',
109 | 'action': 'store_true',
110 | 'dest': 'dry_run'}),
111 | (['-o', '--only-store'],
112 | {'help': 'just store the list, no download. Usefull to sync from an already synchronized seedbox',
113 | 'action': 'store_true',
114 | 'dest': 'only_store'}),
115 | (['-p', '--ping'],
116 | {'help': 'ping a service (ie: Healthchecks) during excecution',
117 | 'action': 'store_true',
118 | 'dest': 'ping'})])
119 | def seedbox(self):
120 | """
121 | Do the synchronization.
122 | """
123 |
124 | # Call ping_start_hook
125 | if self.app.pargs.ping:
126 | for res in self.app.hook.run('ping_start_hook', self.app, 'sync_seedbox'):
127 | pass
128 |
129 | self.app.log.debug('sync_blackhole dry-run: "%s"' % self.app.pargs.dry_run)
130 | self.app.log.debug('sync_blackhole only-store: "%s"' % self.app.pargs.only_store)
131 |
132 | # Create lock file.
133 | lock_file = self.app.config.get('pid', 'download_path')
134 | self.app.lock.lock_or_exit(lock_file)
135 |
136 | finished_path = self.app.config.get('seedbox', 'finished_path')
137 | part_suffix = self.app.config.get('seedbox', 'part_suffix')
138 | self.app.log.debug('Get file list in "%s"' % finished_path)
139 |
140 | # Get all files
141 | try:
142 | self.app.sync.chdir(finished_path)
143 | for walker in self.app.sync.walk(''):
144 | for filename in walker[2]:
145 | filepath = os.path.join(walker[0], filename)
146 | if os.path.splitext(filename)[1] == part_suffix:
147 | self.app.log.debug('Skip part file "%s"' % filename)
148 | elif Download.is_already_download(filepath):
149 | self.app.log.debug('Skip already downloaded "%s"' % filename)
150 | elif self.__exclude_by_pattern(filepath):
151 | self.app.log.debug('Skip excluded by pattern "%s"' % filename)
152 | else:
153 | if not self.app.pargs.dry_run:
154 | self.__get_file(filepath)
155 | else:
156 | self.app.log.info('Not download "%s"' % filepath)
157 | except (IOError, FileNotFoundError) as exc:
158 | self.app.log.error('SeedboxSyncError > "%s"' % exc)
159 |
160 | # Remove lock file.
161 | self.app.lock.unlock(lock_file)
162 |
163 | # Call ping_start_hook
164 | if self.app.pargs.ping:
165 | for res in self.app.hook.run('ping_success_hook', self.app, 'sync_seedbox'):
166 | pass
167 |
168 | def __get_file(self, filepath):
169 | """
170 | Download a single file.
171 |
172 | :param str filepath: the filepath
173 | """
174 | # Local path (without seedbox folder prefix)
175 | local_filepath = fs.join(self.app.config.get('local', 'download_path'), filepath)
176 | part_suffix = self.app.config.get('seedbox', 'part_suffix')
177 | local_filepath_part = local_filepath + part_suffix
178 | local_path = os.path.dirname(fs.abspath(local_filepath))
179 |
180 | # Make folder tree
181 | if not self.app.pargs.only_store:
182 | fs.ensure_dir_exists(local_path)
183 | self.app.log.debug('Download: "%s" in "%s"' % (filepath, local_path))
184 |
185 | try:
186 | # Start timestamp in database
187 | seedbox_size = self.app.sync.stat(filepath).st_size
188 | if seedbox_size == 0:
189 | self.app.log.warning('Empty file: "%s" (%s)' % (filepath, str(seedbox_size)))
190 |
191 | download = Download.create(path=filepath,
192 | seedbox_size=seedbox_size)
193 | download.save()
194 |
195 | # Get file with ".part" suffix
196 | if not self.app.pargs.only_store:
197 | self.app.log.info('Download "%s"' % filepath)
198 | self.app.sync.get(filepath, local_filepath_part)
199 | local_size = os.stat(local_filepath_part).st_size
200 |
201 | # Test size of the downloaded file
202 | if (local_size == 0) or (local_size != seedbox_size):
203 | self.app.log.error('Download fail: "%s" (%s/%s)' % (filepath, str(local_size), str(seedbox_size)))
204 | return False
205 |
206 | # All is good ! Remove ".part" suffix
207 | os.rename(local_filepath_part, local_filepath)
208 | else:
209 | self.app.log.info('Mark as downloaded "%s"' % filepath)
210 | local_size = seedbox_size
211 |
212 | # Store in database
213 | download.local_size = local_size
214 | download.finished = datetime.datetime.now()
215 | download.save()
216 | except SSHException as exc:
217 | self.app.log.error('Download fail: %s' % str(exc))
218 |
219 | def __exclude_by_pattern(self, filepath: str):
220 | """
221 | Allow to exclude sync by pattern
222 |
223 | :param str filepath: the filepath
224 | """
225 | pattern = self.app.config.get('seedbox', 'exclude_syncing')
226 | if pattern == "":
227 | return False
228 |
229 | try:
230 | match = re.search(pattern, filepath)
231 | except re.error:
232 | raise SeedboxSyncConfigurationError('Bad configuration for exclude_syncing ! See the doc at https://docs.python.org/3/library/re.html')
233 |
234 | if match is None:
235 | return False
236 | else:
237 | return True
238 |
--------------------------------------------------------------------------------
/seedboxsync/core/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/core/dao/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/core/dao/download.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import datetime
10 | from peewee import AutoField, DateTimeField, IntegerField, TextField
11 | from .model import SeedboxSyncModel
12 |
13 |
14 | class Download(SeedboxSyncModel):
15 | """
16 | A Data Access Object for Torrent.
17 | """
18 | id = AutoField()
19 | path = TextField()
20 | seedbox_size = IntegerField()
21 | local_size = IntegerField(default=0)
22 | started = DateTimeField(default=datetime.datetime.now)
23 | finished = DateTimeField(default=0)
24 |
25 | def is_already_download(filepath):
26 | """
27 | Get if file was already downloaded.
28 |
29 | :param str filepath: the filepath
30 | """
31 | count = Download.select().where(Download.path == filepath, Download.finished > 0).count()
32 | if count == 0:
33 | return False
34 | else:
35 | return True
36 |
--------------------------------------------------------------------------------
/seedboxsync/core/dao/model.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from peewee import Model, Proxy
10 |
11 | global_database_object = Proxy()
12 |
13 |
14 | class SeedboxSyncModel(Model):
15 | """
16 | Basemodel from which all other peewee models are derived.
17 | """
18 |
19 | class Meta:
20 | database = global_database_object
21 |
--------------------------------------------------------------------------------
/seedboxsync/core/dao/seedboxsync.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from peewee import CharField, TextField
10 | from .model import SeedboxSyncModel
11 |
12 |
13 | class SeedboxSync(SeedboxSyncModel):
14 | """
15 | A Data Access Object for Torrent.
16 | """
17 | key = CharField(unique=True)
18 | value = TextField()
19 |
--------------------------------------------------------------------------------
/seedboxsync/core/dao/torrent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import datetime
10 | from peewee import AutoField, DateTimeField, TextField
11 | from .model import SeedboxSyncModel
12 |
13 |
14 | class Torrent(SeedboxSyncModel):
15 | """
16 | A Data Access Object for Torrent.
17 | """
18 | id = AutoField()
19 | name = TextField()
20 | announce = TextField()
21 | sent = DateTimeField(default=datetime.datetime.now)
22 |
--------------------------------------------------------------------------------
/seedboxsync/core/db.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import os
10 | from peewee import SqliteDatabase
11 | from cement import App
12 | from cement.utils import fs
13 | from .dao.model import global_database_object
14 | from .dao.seedboxsync import SeedboxSync
15 | from .dao.download import Download
16 | from .dao.torrent import Torrent
17 |
18 |
19 | def sizeof(num, suffix='B'):
20 | """
21 | Convert in human readable units.
22 |
23 | From: https://stackoverflow.com/a/1094933
24 | """
25 | for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
26 | if abs(num) < 1024.0:
27 | return "%3.1f%s%s" % (num, unit, suffix)
28 | num /= 1024.0
29 | return "%.1f%s%s" % (num, 'Yi', suffix)
30 |
31 |
32 | def extend_db(app: App):
33 | """
34 | Extends SeedboxSync with Peewee
35 |
36 | :param App app: the Cement App object
37 | """
38 | db_file = fs.abspath(app.config.get('local', 'db_file'))
39 |
40 | app.log.debug('Extending seedboxsync application with Peewee (%s)' % db_file)
41 |
42 | if not os.path.exists(db_file):
43 | app.log.info('DataBase "%s" not exists, need to be create' % db_file)
44 | fs.ensure_dir_exists(os.path.dirname(db_file))
45 | db = SqliteDatabase(db_file)
46 | global_database_object.initialize(db)
47 | db.connect()
48 | db.create_tables([Download, Torrent, SeedboxSync])
49 | db_version = SeedboxSync.create(key='db_version', value='1')
50 | db_version.save()
51 | else:
52 | db = SqliteDatabase(db_file)
53 | global_database_object.initialize(db)
54 |
55 | @db.func('sizeof')
56 | def db_sizeof(num, suffix='B'):
57 | return sizeof(num, suffix)
58 |
59 | app.extend('_db', db)
60 |
61 |
62 | def close_db(app: App):
63 | """
64 | Close database
65 | """
66 |
67 | app.log.debug('Close database')
68 | app._db.close()
69 |
--------------------------------------------------------------------------------
/seedboxsync/core/exc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import sys
10 | from cement import minimal_logger
11 |
12 | LOG = minimal_logger(__name__)
13 |
14 |
15 | class SeedboxSyncError(Exception):
16 | """
17 | Generic errors.
18 | """
19 |
20 | def __init__(self, msg: str):
21 | LOG.error(msg)
22 | sys.exit(self)
23 |
24 |
25 | class SeedboxSyncConfigurationError(SeedboxSyncError):
26 | pass
27 |
--------------------------------------------------------------------------------
/seedboxsync/core/init_defaults.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement.utils.misc import init_defaults
10 |
11 | # setup the nested dicts
12 | CONFIG = init_defaults('seedboxsync', 'seedbox', 'local', 'pid', 'healthchecks', 'healthchecks.sync_seedbox')
13 |
14 |
15 | #
16 | # Informations about your seedbox
17 | #
18 |
19 | # Informations about your seedbox connection
20 | CONFIG['seedbox']['host'] = 'my-seedbox.ltd'
21 | CONFIG['seedbox']['port'] = '22'
22 | CONFIG['seedbox']['login'] = 'me'
23 | CONFIG['seedbox']['password'] = 'p4sw0rd'
24 | CONFIG['seedbox']['timeout'] = False
25 |
26 | # For the moment, only sftp
27 | CONFIG['seedbox']['protocol'] = 'sftp'
28 |
29 | # Chmod torrent after upload (false = disable)
30 | # Use octal notation like https://docs.python.org/3.4/library/os.html#os.chmod
31 | CONFIG['seedbox']['chmod'] = False
32 |
33 | # Use a tempory directory (you must create it !)
34 | CONFIG['seedbox']['tmp_path'] = './tmp'
35 |
36 | # Your "watch" folder you must create it!)
37 | CONFIG['seedbox']['watch_path'] = './watch'
38 |
39 | # Your finished folder you must create it!)
40 | CONFIG['seedbox']['finished_path'] = './files'
41 |
42 | # Exclude part files
43 | CONFIG['seedbox']['part_suffix'] = '.part'
44 |
45 | # Exclude pattern from sync
46 | # Use re syntaxe: https://docs.python.org/3/library/re.html
47 | # Example: .*missing$|^\..*\.swap$
48 | CONFIG['seedbox']['exclude_syncing'] = ''
49 |
50 |
51 | #
52 | # Informations about local environment (NAS ?)
53 | #
54 |
55 | # Your local "watch" folder
56 | CONFIG['local']['watch_path'] = '~/watch'
57 |
58 | # Path where download files
59 | CONFIG['local']['download_path'] = '~/Download/'
60 |
61 | # Use local sqlite database for store downloaded files
62 | CONFIG['local']['db_file'] = '~/.config/seedboxsync/seedboxsync.db'
63 |
64 |
65 | #
66 | # PID and lock management to prevent several launch
67 | #
68 |
69 | # PID for blackhole sync
70 | CONFIG['pid']['blackhole_path'] = '~/.config/seedboxsync/lock/blackhole.pid'
71 |
72 | # PID for seedbox downloaded sync
73 | CONFIG['pid']['download_path'] = '~/.config/seedboxsync/lock/download.pid'
74 |
75 |
76 | #
77 | # Healthchecks ping service
78 | #
79 | # Enable or disable service
80 | CONFIG['healthchecks'] = init_defaults('sync_seedbox', 'sync_blackhole')
81 | CONFIG['healthchecks']['sync_seedbox']['enabled'] = False
82 | CONFIG['healthchecks']['sync_blackhole']['enabled'] = False
83 |
84 | # Ping URL
85 | CONFIG['healthchecks']['sync_seedbox']['ping_url'] = ''
86 | CONFIG['healthchecks']['sync_blackhole']['ping_url'] = ''
87 |
--------------------------------------------------------------------------------
/seedboxsync/core/sync/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/core/sync/abstract_client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | """
10 | Transport client abstract class based on paramiko syntaxe.
11 | """
12 |
13 | from abc import ABCMeta, abstractmethod
14 | from cement.core.log import LogInterface
15 |
16 |
17 | class AbstractClient():
18 | __metaclass__ = ABCMeta
19 |
20 | @abstractmethod
21 | def __init__(self, log: LogInterface, host: str, login: str, password: str, port: str, timeout: str = False):
22 | """Init client.
23 |
24 | :param str log: the log interface
25 | :param str host: the host of the server
26 | :param str login: the login to connect on the the server
27 | :param str password: the password to connect on the the server
28 | :param str port: the port of the server
29 | :param str timeout: the timeout for socket connection
30 | """
31 | pass
32 |
33 | @abstractmethod
34 | def put(self, local_path: str, remote_path: str):
35 | """
36 | Copy a local file (``local_path``) to the server as ``remote_path``.
37 |
38 | :param str local_path: the local file to copy
39 | :param str remote_path: the destination path on the server. Note
40 | that the filename should be included. Only specifying a directory
41 | must result in an error.
42 | """
43 | pass
44 |
45 | @abstractmethod
46 | def get(self, remotep_path: str, local_path: str):
47 | """
48 | Copy a remote file (``remote_path``) from the server to the local
49 | host as ``local_path``.
50 |
51 | :param str remote_path: the remote file to copy
52 | :param str local_path: the destination path on the local host
53 | """
54 | pass
55 |
56 | @abstractmethod
57 | def stat(self, filepath: str):
58 | """
59 | Retrieve size about a file on the remote system. The return
60 | value is an object whose attributes correspond to the attributes of
61 | Python's ``stat`` structure as returned by ``os.stat``, except that it
62 | contains fewer fields. An SFTP server may return as much or as little
63 | info as it wants, so the results may vary from server to server.
64 |
65 | Unlike a Python `python:stat` object, the result may not be accessed as
66 | a tuple. This is mostly due to the author's slack factor.
67 | The fields supported are: ``st_mode``, ``st_size``, ``st_uid``,
68 | ``st_gid``, ``st_atime``, and ``st_mtime``.
69 |
70 | :param str filepath: the filename to stat
71 | """
72 | pass
73 |
74 | @abstractmethod
75 | def chdir(self, path: str = None):
76 | """
77 | Change the "current directory" of this session.
78 |
79 | :param str path: new current working directory
80 | """
81 | pass
82 |
83 | @abstractmethod
84 | def chmod(self, path: str, mode: str):
85 | """
86 | Change the mode (permissions) of a file. The permissions are unix-style
87 | and identical to those used by Python’s os.chmod function.
88 |
89 | :param str path: path of the file to change the permissions of
90 | :param int mode: new permissions
91 | """
92 | pass
93 |
94 | @abstractmethod
95 | def rename(self, old_path: str, new_path: str):
96 | """
97 | Rename a file or folder from ``old_path`` to ``new_path``.
98 |
99 | :param str old_path: existing name of the file or folder
100 | :param str new_path: new name for the file or folder
101 | """
102 | pass
103 |
104 | @abstractmethod
105 | def close(self):
106 | """
107 | Close transport client.
108 | """
109 | pass
110 |
--------------------------------------------------------------------------------
/seedboxsync/core/sync/sftp_client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | """
10 | Transport client using sFTP protocol.
11 | """
12 | import os
13 | from .abstract_client import AbstractClient
14 | from .sync import ConnectionError
15 | from stat import S_ISDIR
16 | from cement.core.log import LogInterface
17 | import paramiko
18 |
19 |
20 | class SftpClient(AbstractClient):
21 | """
22 | Transport from NAS to seedbox using sFTP paramiko library.
23 | """
24 |
25 | def __init__(self, log: LogInterface, host: str, login: str, password: str, port: str = "22", timeout: str = False):
26 | """
27 | Init transport and client.
28 |
29 | :param str log: the log interface
30 | :param str host: the host of the server
31 | :param str login: the login to connect on the the server
32 | :param str password: the password to connect on the the server
33 | :param str port: the port of the server
34 | :param str timeout: the timeout for socket connection
35 | """
36 | self.__log = log
37 | self.__host = host
38 | self.__login = login
39 | self.__password = password
40 | self.__port = port
41 | self.__timeout = timeout
42 | self.__transport = None
43 | self.__client = None
44 |
45 | def __connect_before(self):
46 | """
47 | Init connection if not initialized.
48 | """
49 | if self.__transport is None:
50 | self.__log.debug('Init paramiko.Transport')
51 | self.__transport = paramiko.Transport((self.__host, int(self.__port)))
52 | try:
53 | self.__transport.connect(username=self.__login, password=self.__password)
54 | except paramiko.ssh_exception.AuthenticationException as exc:
55 | raise ConnectionError('Connection fail: %s' % str(exc))
56 |
57 | self.__client = paramiko.SFTPClient.from_transport(self.__transport)
58 |
59 | # Setup timeout
60 | if self.__timeout:
61 | channel = self.__client.get_channel()
62 | channel.settimeout(self.__timeout)
63 | self.__log.debug('Timeout is set to %s' % channel.gettimeout())
64 |
65 | def put(self, local_path: str, remote_path: str):
66 | """
67 | Copy a local file (``local_path``) to the SFTP server as ``remote_path``.
68 |
69 | :param str local_path: the local file to copy
70 | :param str remote_path: the destination path on the server. Note
71 | that the filename should be included. Only specifying a directory
72 | must result in an error.
73 | """
74 | self.__connect_before()
75 | return self.__client.put(local_path, remote_path)
76 |
77 | def get(self, remote_path: str, local_path: str):
78 | """
79 | Copy a remote file (``remote_path``) from the SFTP server to the local
80 | host as ``local_path``.
81 |
82 | :param str remote_path: the remote file to copy
83 | :param str local_path: the destination path on the local host
84 | """
85 | self.__connect_before()
86 | return self.__client.get(remote_path, local_path)
87 |
88 | def stat(self, filepath: str):
89 | """
90 | Retrieve informations about a file on the remote system. The return
91 | value is an object whose attributes correspond to the attributes of
92 | Python's ``stat`` structure as returned by ``os.stat``, except that it
93 | contains fewer fields. An SFTP server may return as much or as little
94 | info as it wants, so the results may vary from server to server.
95 |
96 | Unlike a Python `python:stat` object, the result may not be accessed as
97 | a tuple. This is mostly due to the author's slack factor.
98 | The fields supported are: ``st_mode``, ``st_size``, ``st_uid``,
99 | ``st_gid``, ``st_atime``, and ``st_mtime``.
100 |
101 | :param str filepath: the filename to stat
102 | """
103 | self.__connect_before()
104 | return self.__client.stat(filepath)
105 |
106 | def chdir(self, path: str = None):
107 | """
108 | Change the "current directory" of this session.
109 |
110 | :param str path: new current working directory
111 | """
112 | self.__connect_before()
113 | return self.__client.chdir(path)
114 |
115 | def chmod(self, path: str, mode: str):
116 | """
117 | Change the mode (permissions) of a file. The permissions are unix-style
118 | and identical to those used by Python’s os.chmod function.
119 |
120 | :param str path: path of the file to change the permissions of
121 | :param int mode: new permissions
122 | """
123 | self.__connect_before()
124 | return self.__client.chmod(path, mode)
125 |
126 | def rename(self, old_path: str, new_path: str):
127 | """
128 | Rename a file or folder from ``old_path`` to ``new_path``.
129 |
130 | :param str old_path: existing name of the file or folder
131 | :param str new_path: new name for the file or folder
132 | """
133 | return self.__client.posix_rename(old_path, new_path)
134 |
135 | # Code from https://gist.github.com/johnfink8/2190472
136 | def walk(self, remote_path: str):
137 | """
138 | Kindof a stripped down version of os.walk, implemented for
139 | sftp. Tried running it flat without the yields, but it really
140 | chokes on big directories.
141 |
142 | :param str remote_path: the remote path to list
143 | """
144 | self.__connect_before()
145 | path = remote_path
146 | files = []
147 | folders = []
148 | for f in self.__client.listdir_attr(remote_path):
149 | if S_ISDIR(f.st_mode):
150 | folders.append(f.filename)
151 | else:
152 | files.append(f.filename)
153 | yield path, folders, files
154 |
155 | for folder in folders:
156 | new_path = os.path.join(remote_path, folder)
157 | for x in self.walk(new_path):
158 | yield x
159 |
160 | def close(self):
161 | """
162 | Close transport client.
163 | """
164 | if self.__transport is not None:
165 | self.__log.debug('Close paramiko.Transport client')
166 | return self.__transport.close()
167 |
--------------------------------------------------------------------------------
/seedboxsync/core/sync/sync.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from importlib import import_module
10 | from cement import App
11 | from ..exc import SeedboxSyncError
12 |
13 |
14 | class SyncProtocoleError(SeedboxSyncError):
15 | pass
16 |
17 |
18 | class ConnectionError(SeedboxSyncError):
19 | pass
20 |
21 |
22 | def extend_sync(app: App):
23 | """
24 | Extends SeedboxSync with Sync
25 |
26 | :param App app: the Cement App object
27 | """
28 | protocol = app.config.get('seedbox', 'protocol')
29 | client_class = protocol.title() + 'Client'
30 |
31 | app.log.debug('Extending seedboxsync application with sync (%s/%s)' % (protocol, client_class))
32 |
33 | try:
34 | client_module = import_module('..core.sync.' + protocol + '_client', 'seedboxsync.ext')
35 | except ImportError as exc:
36 | raise SyncProtocoleError('Unsupported protocole: %s: %s' % (protocol, str(exc)))
37 |
38 | try:
39 | transfer_client = getattr(client_module, client_class)
40 | except AttributeError:
41 | raise SyncProtocoleError(
42 | 'Unsupported protocole module! No class "%s" in module "seedboxsync.core.sync.%s_client"'
43 | % (client_class, protocol))
44 |
45 | try:
46 | sync = transfer_client(log=app.log,
47 | host=app.config.get('seedbox', 'host'),
48 | port=int(app.config.get('seedbox', 'port')),
49 | login=app.config.get('seedbox', 'login'),
50 | password=app.config.get('seedbox', 'password'),
51 | timeout=app.config.get('seedbox', 'timeout'))
52 | except Exception as exc:
53 | raise ConnectionError('Connection fail: %s' % str(exc))
54 |
55 | app.extend('sync', sync)
56 |
57 |
58 | def close_sync(app: App):
59 | """
60 | Extends SeedboxSync with TinyDB
61 |
62 | :param App app: the Cement App object
63 | """
64 | app.log.debug('Closing sync')
65 | app.sync.close()
66 |
--------------------------------------------------------------------------------
/seedboxsync/core/version.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement.utils.version import get_version as cement_get_version
10 |
11 | VERSION = (3, 2, 0, 'beta', 1) # Change on seedboxsync/version.py, seedboxsync/core/version.py, and doc
12 |
13 |
14 | def get_version(version=VERSION):
15 | return cement_get_version(version)
16 |
--------------------------------------------------------------------------------
/seedboxsync/ext/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/ext/ext_bcoding.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from bcoding import bdecode
10 | from cement import App
11 |
12 |
13 | class Bcoding(object):
14 | """
15 | Extends SeedboxSync with bcoding
16 |
17 | :param App app: the Cement App object
18 | """
19 |
20 | def __init__(self, app: App):
21 | self.app = app
22 |
23 | def get_torrent_infos(self, torrent_path: str):
24 | """
25 | Get information about a torrent file.
26 |
27 | :param str torrent_path: the path to the torrent file
28 | """
29 | with open(torrent_path, 'rb') as torrent:
30 | torrent_info = None
31 |
32 | try:
33 | torrent_info = bdecode(torrent.read())
34 | except Exception as exc:
35 | self.app.log.error('Not valid torrent: "%s"' + str(exc))
36 | finally:
37 | torrent.close()
38 |
39 | return torrent_info
40 |
41 |
42 | def bcoding_post_setup_hook(app: App):
43 | """
44 | Extends SeedboxSync with bcoding
45 |
46 | :param App app: the Cement App object
47 | """
48 | app.log.debug('Extending seedboxsync application with bcoding')
49 | app.extend('bcoding', Bcoding(app))
50 |
51 |
52 | def load(app: App):
53 | """Extension loader"""
54 | app.hook.register('post_setup', bcoding_post_setup_hook)
55 |
--------------------------------------------------------------------------------
/seedboxsync/ext/ext_healthchecks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | from cement import App
10 | import socket
11 | import urllib.request
12 |
13 |
14 | def healthchecks_ping_start_hook(app: App, sub_command: str):
15 | """
16 | Send Healthchecks start
17 |
18 | :param App app: the Cement App object
19 | :param String sub_command: the seedboxsync subcommand
20 | """
21 |
22 | sub_command_config = app.config.get('healthchecks', sub_command)
23 | if sub_command_config['enabled'] is False:
24 | app.log.warning("Healthchecks for \"%s\" disabled by configuration" % sub_command)
25 | else:
26 | ping_url = sub_command_config['ping_url'] + '/start'
27 | app.log.debug('Ping url: %s' % ping_url)
28 |
29 | try:
30 | urllib.request.urlopen(ping_url, timeout=10)
31 | except socket.error as e:
32 | app.log.error("Healthchecks, ping failed: %s" % e)
33 | pass
34 |
35 |
36 | def healthchecks_ping_success_hook(app: App, sub_command: str):
37 | """
38 | Send Healthchecks success
39 |
40 | :param App app: the Cement App object
41 | :param String sub_command: the seedboxsync subcommand
42 | """
43 |
44 | sub_command_config = app.config.get('healthchecks', sub_command)
45 | if sub_command_config['enabled'] is False:
46 | app.log.warning("Healthchecks for \"%s\" disabled by configuration" % sub_command)
47 | else:
48 | ping_url = sub_command_config['ping_url']
49 | app.log.debug('Ping url: %s' % ping_url)
50 |
51 | try:
52 | urllib.request.urlopen(ping_url, timeout=10)
53 | except socket.error as e:
54 | app.log.error("Healthchecks, ping failed: %s" % e)
55 | pass
56 |
57 |
58 | def healthchecks_post_setup_hook(app: App):
59 | """
60 | Extends SeedboxSync with Healthchecks. Use custom SeedboxSync's hooks.
61 |
62 | :param App app: the Cement App object
63 | """
64 | if app.config.has_section('healthchecks'):
65 | app.log.debug('Extending seedboxsync application with Healthchecks')
66 | app.hook.register('ping_start_hook', healthchecks_ping_start_hook)
67 | app.hook.register('ping_success_hook', healthchecks_ping_success_hook)
68 | else:
69 | app.log.debug('Not extending seedboxsync application with Healthchecks')
70 |
71 |
72 | def load(app: App):
73 | """Extension loader"""
74 | app.hook.register('post_setup', healthchecks_post_setup_hook)
75 |
--------------------------------------------------------------------------------
/seedboxsync/ext/ext_lock.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import os
10 | from cement import App, fs
11 | from ..core.exc import SeedboxSyncError
12 |
13 |
14 | class Lock(object):
15 | """
16 | Class which manage PID file a.k.a lock file.
17 | """
18 |
19 | def __init__(self, app: App):
20 | """
21 | Constructor
22 |
23 | :param App app: the Cement App object
24 | """
25 | self.app = app
26 |
27 | def lock(self, lock_file: str):
28 | """
29 | Lock task by a pid file to prevent launch two time.
30 |
31 | :param str lock_file: the lock file path
32 | """
33 | lock_file = fs.abspath(lock_file)
34 | self.app.log.debug('Lock task by %s' % lock_file)
35 | try:
36 | fs.ensure_dir_exists(os.path.dirname(lock_file))
37 | lock = open(lock_file, 'w+')
38 | lock.write(str(os.getpid()))
39 | lock.close()
40 | except Exception as exc:
41 | raise LockError('Lock error: %s' % str(exc))
42 |
43 | def unlock(self, lock_file: str):
44 | """
45 | Unlock task, remove pid file.
46 |
47 | :param str lock_file: the lock file path
48 | """
49 | lock_file = fs.abspath(lock_file)
50 | self.app.log.debug('Unlock task by %s' % lock_file)
51 | try:
52 | os.remove(lock_file)
53 | except Exception as exc:
54 | raise LockError('Lock error: %s' % str(exc))
55 |
56 | def is_locked(self, lock_file: str):
57 | """
58 | Test if task is locked by a pid file to prevent launch two time.
59 |
60 | :param str lock_file: the lock file path
61 | """
62 | lock_file = fs.abspath(lock_file)
63 | if os.path.isfile(lock_file):
64 | pid = int(open(lock_file, 'r').readlines()[0])
65 | if self._check_pid(pid):
66 | self.app.log.info('Already running (pid=%s)' % str(pid))
67 | return True
68 | else:
69 | self.app.log.info('Restored from a previous crash (pid=%s)' % str(pid))
70 |
71 | return False
72 |
73 | def lock_or_exit(self, lock_file: str):
74 | """
75 | Lock task or exit if already running.
76 |
77 | :param str lock_file: the lock file path
78 | """
79 | if self.is_locked(lock_file):
80 | self.app.exit_code = 0
81 | self.app.close()
82 | else:
83 | self.lock(lock_file)
84 |
85 | def _check_pid(self, pid: int):
86 | """
87 | Check for the existence of a unix pid.
88 |
89 | :param int pid: the pid of the process
90 | """
91 | try:
92 | os.kill(pid, 0)
93 | except OSError:
94 | return False
95 | else:
96 | return True
97 |
98 |
99 | class LockError(SeedboxSyncError):
100 | pass
101 |
102 |
103 | def lock_pre_run_hook(app: App):
104 | """
105 | Extends SeedboxSync with Lock
106 |
107 | :param App app: the Cement App object
108 | """
109 | app.log.debug('Extending seedboxsync application with Lock')
110 | app.extend('lock', Lock(app))
111 |
112 |
113 | def load(app: App):
114 | """Extension loader"""
115 | app.hook.register('pre_run', lock_pre_run_hook)
116 |
--------------------------------------------------------------------------------
/seedboxsync/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | import signal
10 | from cement import App, TestApp
11 | from cement.core.exc import CaughtSignal
12 | from .core.db import extend_db, close_db
13 | from .core.exc import SeedboxSyncError
14 | from .core.sync.sync import extend_sync, close_sync
15 | from .core.init_defaults import CONFIG
16 | from .controllers.base import Base
17 | from .controllers.clean import Clean
18 | from .controllers.search import Search
19 | from .controllers.stats import Stats
20 | from .controllers.sync import Sync
21 |
22 |
23 | class SeedboxSync(App):
24 | """SeedboxSync application."""
25 |
26 | class Meta:
27 | label = 'seedboxsync'
28 |
29 | # configuration defaults
30 | config_defaults = CONFIG
31 |
32 | # call sys.exit() on close
33 | exit_on_close = True
34 |
35 | # load additional framework extensions
36 | extensions = [
37 | 'yaml',
38 | 'colorlog',
39 | 'tabulate',
40 | 'print',
41 | 'seedboxsync.ext.ext_bcoding',
42 | 'seedboxsync.ext.ext_lock',
43 | 'seedboxsync.ext.ext_healthchecks'
44 | ]
45 |
46 | # configuration handler
47 | config_handler = 'yaml'
48 |
49 | # configuration file suffix
50 | config_file_suffix = '.yml'
51 |
52 | # set the log handler
53 | framework_logging = False
54 | log_handler = 'colorlog'
55 |
56 | # set the output handler
57 | output_handler = 'tabulate'
58 |
59 | # register handlers
60 | handlers = [
61 | Base,
62 | Clean,
63 | Search,
64 | Stats,
65 | Sync
66 | ]
67 |
68 | # register hook
69 | hooks = [
70 | ('pre_run', extend_sync),
71 | ('pre_close', close_sync),
72 | ('post_setup', extend_db),
73 | ('post_run', close_db)
74 | ]
75 |
76 | # define customs hooks
77 | define_hooks = [
78 | 'ping_start_hook',
79 | 'ping_success_hook'
80 | ]
81 |
82 | # catch signal
83 | catch_signals = [
84 | signal.SIGTERM,
85 | signal.SIGINT,
86 | signal.SIGHUP,
87 | ]
88 |
89 |
90 | class SeedboxSyncTest(TestApp, SeedboxSync):
91 | """A sub-class of SeedboxSync that is better suited for testing."""
92 |
93 | class Meta:
94 | label = 'seedboxsync'
95 |
96 |
97 | def main():
98 | with SeedboxSync() as app:
99 | try:
100 | app.run()
101 |
102 | except AssertionError as e:
103 | print('AssertionError > %s' % e.args[0])
104 | app.exit_code = 1
105 |
106 | if app.debug is True:
107 | import traceback
108 | traceback.print_exc()
109 |
110 | except SeedboxSyncError as e:
111 | print('SeedboxSyncError > %s' % e.args[0])
112 | app.exit_code = 1
113 |
114 | if app.debug is True:
115 | import traceback
116 | traceback.print_exc()
117 |
118 | except (CaughtSignal, KeyboardInterrupt) as e:
119 | # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error)
120 | app.exit_code = 0
121 | if e.signum == signal.SIGTERM:
122 | app.log.warning('Caught SIGTERM')
123 | elif e.signum == signal.SIGINT:
124 | app.log.warning('Caught SIGINT')
125 | else:
126 | app.log.warning('Stopped')
127 | app.close()
128 |
129 |
130 | if __name__ == '__main__':
131 | main()
132 |
--------------------------------------------------------------------------------
/seedboxsync/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
--------------------------------------------------------------------------------
/seedboxsync/version.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015-2025 Guillaume Kulakowski
4 | #
5 | # For the full copyright and license information, please view the LICENSE
6 | # file that was distributed with this source code.
7 | #
8 |
9 | VERSION = (3, 2, 0, 'beta', 1) # Change on seedboxsync/version.py, seedboxsync/core/version.py, and doc
10 |
11 |
12 | def get_version(version: tuple = VERSION) -> str:
13 | "Returns a PEP 386-compliant version number from VERSION."
14 | assert len(version) == 5
15 | assert version[3] in ('alpha', 'beta', 'rc', 'final')
16 |
17 | parts = 3
18 | main = '.'.join(str(x) for x in version[:parts])
19 |
20 | sub = ''
21 | if version[3] != 'final':
22 | mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
23 | sub = mapping[version[3]] + str(version[4])
24 |
25 | return main + sub
26 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | # This includes the license file in the wheel.
3 | license_file = LICENSE
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright (C) 2015-2025 Guillaume Kulakowski
5 | #
6 | # For the full copyright and license information, please view the LICENSE
7 | # file that was distributed with this source code.
8 | #
9 |
10 | from setuptools import setup, find_packages
11 | from seedboxsync.version import get_version
12 |
13 | f = open('README.md', 'r')
14 | LONG_DESCRIPTION = f.read()
15 | f.close()
16 |
17 | setup(
18 | name='seedboxsync',
19 | version=get_version(),
20 | python_requires='>=3.9',
21 | description='Script for sync operations between your NAS and your seedbox',
22 | long_description=LONG_DESCRIPTION,
23 | long_description_content_type='text/markdown',
24 | author='Guillaume Kulakowski',
25 | author_email='guillaume@kulakowski.fr',
26 | url='https://llaumgui.github.io/seedboxsync/',
27 | license='GPL-2.0',
28 |
29 | project_urls={
30 | 'Documentation': 'https://llaumgui.github.io/seedboxsync/',
31 | 'Bug Reports': 'https://github.com/llaumgui/seedboxsync/issues',
32 | 'Source': 'https://github.com/llaumgui/seedboxsync/'
33 | },
34 |
35 | classifiers=[
36 | 'Development Status :: 5 - Production/Stable',
37 | 'Intended Audience :: System Administrators',
38 | 'Topic :: Internet',
39 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
40 | 'Programming Language :: Python :: 3',
41 | 'Environment :: Console',
42 | 'Natural Language :: English',
43 | 'Operating System :: POSIX'
44 | ],
45 | keywords='seedbox nas sync sftp',
46 |
47 | packages=find_packages(exclude=['ez_setup', 'tests*']),
48 | data_files=[('config', ['config/seedboxsync.yml.example'])],
49 | include_package_data=True,
50 | entry_points="""
51 | [console_scripts]
52 | seedboxsync = seedboxsync.main:main
53 | """,
54 |
55 | install_requires=[
56 | 'cement==3.0.14',
57 | 'pyyaml',
58 | 'colorlog',
59 | 'paramiko>=2.12',
60 | 'bcoding',
61 | 'tabulate',
62 | 'peewee'
63 | ],
64 | )
65 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | # Project informations
2 | sonar.projectKey=llaumgui_seedboxsync
3 | sonar.organization=llaumgui
4 |
5 | # This is the name and version displayed in the SonarCloud UI.
6 | sonar.projectName=SeedboxSync
7 | sonar.projectVersion=3.1.0
8 |
9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
10 | sonar.sources=seedboxsync
11 | sonar.tests=tests
12 | sonar.exclusions=tests/**/*,docs/**/*
13 | sonar.python.coverage.reportPaths=coverage.xml
14 |
15 | # Encoding of the source code. Default is default system encoding
16 | #sonar.sourceEncoding=UTF-8
17 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | PyTest Fixtures.
3 | """
4 |
5 | import pytest
6 | from cement import fs
7 |
8 |
9 | @pytest.fixture(scope="function")
10 | def tmp(request):
11 | """
12 | Create a `tmp` object that geneates a unique temporary directory, and file
13 | for each test function that requires it.
14 | """
15 | t = fs.Tmp()
16 | yield t
17 | t.remove()
18 |
--------------------------------------------------------------------------------
/tests/controlers/test_clean.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import os
3 | import shutil
4 | from seedboxsync.main import SeedboxSyncTest
5 |
6 |
7 | config_dirs = [os.getcwd() + '/tests/resources']
8 | db_path = config_dirs[0] + '/seedboxsync.db'
9 | db_backup = db_path + '.bak'
10 |
11 |
12 | def test_seedboxsync_clean():
13 | """
14 | Test clean command.
15 | """
16 |
17 | # Backup DB
18 | shutil.copyfile(db_path, db_backup)
19 |
20 | try:
21 | # seedboxsync clean progress
22 | argv = ['clean', 'progress']
23 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
24 | app.run()
25 | data, output = app.last_rendered
26 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'e8a1eb752a7fb6607ec41e63aaf45ebf'
27 |
28 | # seedboxsync clean downloaded 607
29 | argv = ['clean', 'downloaded', '607']
30 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
31 | app.run()
32 | data, output = app.last_rendered
33 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '48d2b91a68900eef6fc440546c305830'
34 |
35 | # seedboxsync clean downloaded 999
36 | argv = ['clean', 'downloaded', '999']
37 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
38 | app.run()
39 | data, output = app.last_rendered
40 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'ff4b034622291f06988a584777752ae5'
41 |
42 | finally:
43 | # Restore DB
44 | shutil.move(db_backup, db_path)
45 |
--------------------------------------------------------------------------------
/tests/controlers/test_search.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import os
3 | from seedboxsync.main import SeedboxSyncTest
4 |
5 |
6 | config_dirs = [os.getcwd() + '/tests/resources']
7 |
8 |
9 | def test_seedboxsync_search_downloaded():
10 | """
11 | Test search downloaded command.
12 | """
13 |
14 | # seedboxsync search downloaded
15 | argv = ['search', 'downloaded']
16 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
17 | app.run()
18 | data, output = app.last_rendered
19 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '7f5b9b8878af179e872b8110382cde4a'
20 |
21 | # seedboxsync search downloaded -n 5
22 | argv = ['search', 'downloaded', '-n', '5']
23 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
24 | app.run()
25 | data, output = app.last_rendered
26 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '319d69087d3a3273b44a4546a137a9fc'
27 | # seedboxsync search downloaded --number 5
28 | argv = ['search', 'downloaded', '--number', '5']
29 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
30 | app.run()
31 | data, output = app.last_rendered
32 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '319d69087d3a3273b44a4546a137a9fc'
33 |
34 | # seedboxsync search downloaded -s Nulla
35 | argv = ['search', 'downloaded', '-s', 'Nulla']
36 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
37 | app.run()
38 | data, output = app.last_rendered
39 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '766078235505717bc48981d14db3b0e3'
40 | # seedboxsync search downloaded --search 5
41 | argv = ['search', 'downloaded', '--search', 'nULLa']
42 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
43 | app.run()
44 | data, output = app.last_rendered
45 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '766078235505717bc48981d14db3b0e3'
46 |
47 |
48 | def test_seedboxsync_search_progress():
49 | """
50 | Test search progress command.
51 | """
52 |
53 | # seedboxsync search progress
54 | argv = ['search', 'progress']
55 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
56 | app.run()
57 | data, output = app.last_rendered
58 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'af0cf1e7182c2f7bc64f30ccf5bfa00a'
59 |
60 | # seedboxsync search progress -n 1
61 | argv = ['search', 'progress', '-n', '1']
62 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
63 | app.run()
64 | data, output = app.last_rendered
65 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'b4e4872e2831f30392f7301eb44dbf01'
66 | # seedboxsync search progress --number 1
67 | argv = ['search', 'progress', '--number', '1']
68 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
69 | app.run()
70 | data, output = app.last_rendered
71 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'b4e4872e2831f30392f7301eb44dbf01'
72 |
73 | # seedboxsync search progress -s ante
74 | argv = ['search', 'progress', '-s', 'ante']
75 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
76 | app.run()
77 | data, output = app.last_rendered
78 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '70d7a75d7e6dc46f094c26d894764205'
79 | # seedboxsync search progress --search AnTe
80 | argv = ['search', 'progress', '--search', 'AnTe']
81 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
82 | app.run()
83 | data, output = app.last_rendered
84 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '70d7a75d7e6dc46f094c26d894764205'
85 |
86 |
87 | def test_seedboxsync_search_uploaded():
88 | """
89 | Test search uploaded command.
90 | """
91 |
92 | # seedboxsync search uploaded
93 | argv = ['search', 'uploaded']
94 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
95 | app.run()
96 | data, output = app.last_rendered
97 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'eddf5be7df1164d0e13da4df497a8ce3'
98 |
99 | # seedboxsync search uploaded -n 5
100 | argv = ['search', 'uploaded', '-n', '5']
101 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
102 | app.run()
103 | data, output = app.last_rendered
104 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'd9da6c5c513564702263eb84417c133a'
105 | # seedboxsync search uploaded --number 5
106 | argv = ['search', 'uploaded', '--number', '5']
107 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
108 | app.run()
109 | data, output = app.last_rendered
110 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'd9da6c5c513564702263eb84417c133a'
111 |
112 | # seedboxsync search uploaded -s Vol
113 | argv = ['search', 'uploaded', '-s', 'Vol']
114 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
115 | app.run()
116 | data, output = app.last_rendered
117 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '674e18ee2dac8f603524d90d21131631'
118 | # seedboxsync search uploaded --search vOl
119 | argv = ['search', 'uploaded', '--search', 'Vol']
120 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
121 | app.run()
122 | data, output = app.last_rendered
123 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == '674e18ee2dac8f603524d90d21131631'
124 |
--------------------------------------------------------------------------------
/tests/controlers/test_stats.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import os
3 | from seedboxsync.main import SeedboxSyncTest
4 |
5 |
6 | config_dirs = [os.getcwd() + '/tests/resources']
7 |
8 |
9 | def test_seedboxsync_stats_by_month():
10 | """
11 | Test stats command.
12 | """
13 |
14 | # seedboxsync stats by-month
15 | argv = ['stats', 'by-month']
16 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
17 |
18 | app.run()
19 | data, output = app.last_rendered
20 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'fd5bbe21431cfcfff81455804ad30629'
21 |
22 | # seedboxsync stats by-year
23 | argv = ['stats', 'by-year']
24 | with SeedboxSyncTest(argv=argv, config_dirs=config_dirs) as app:
25 |
26 | app.run()
27 | data, output = app.last_rendered
28 | assert hashlib.md5(output.encode('utf-8')).hexdigest() == 'ec263be8819b09693ffe82cd131e211e'
29 |
--------------------------------------------------------------------------------
/tests/resources/Fedora-Server-dvd-x86_64-32.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/tests/resources/Fedora-Server-dvd-x86_64-32.torrent
--------------------------------------------------------------------------------
/tests/resources/seedboxsync.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llaumgui/seedboxsync/ccaf1b7f1e5e9e41ff07e71999a8428afe0acc86/tests/resources/seedboxsync.db
--------------------------------------------------------------------------------
/tests/resources/seedboxsync.yml:
--------------------------------------------------------------------------------
1 | ### SeedboxSync Configuration Settings
2 | ---
3 |
4 | #
5 | # Informations about your seedbox
6 | #
7 | seedbox:
8 |
9 | ### Informations about your seedbox connection
10 | # host: my-seedbox.ltd
11 | # port: 22
12 | # login: me
13 | # password: p4sw0rd
14 | # timeout: false
15 |
16 | ### For the moment, only sftp
17 | # protocol: sftp
18 |
19 | ### Chmod torrent after upload (false : disable)
20 | ### Use octal notation like https://docs.python.org/3.4/library/os.html#os.chmod
21 | # chmod: 0o777
22 | # chmod: false
23 |
24 | ### Use a tempory directory (you must create it !)
25 | # tmp_path: ./tmp
26 |
27 | ### Your "watch" folder you must create it!)
28 | # watch_path: ./watch
29 |
30 | ### Your finished folder you must create it!)
31 | # finished_path: ./files
32 |
33 | ### Exclude part files
34 | # part_suffix: .part
35 |
36 | ### Exclude pattern from sync
37 | ### Use re syntaxe: https://docs.python.org/3/library/re.html
38 | ### Example: .*missing$|^\..*\.swap$
39 | # exclude_syncing:
40 |
41 |
42 | #
43 | # Informations about local environment (NAS ?)
44 | #
45 | local:
46 |
47 | ### Your local "watch" folder
48 | # watch_path: ~/watch
49 |
50 | ### Path where download files
51 | # download_path: ~/Downloads/
52 |
53 | ### Use local sqlite database for store downloaded files
54 | db_file: tests/resources/seedboxsync.db
55 |
56 |
57 | #
58 | # PID and lock management to prevent several launch
59 | #
60 | pid:
61 |
62 | ### PID for blackhole sync
63 | # blackhole_path: ~/.config/seedboxsync/lock/blackhole.pid
64 |
65 | ### PID for seedbox downloaded sync
66 | # download_path: ~/.config/seedboxsync/lock/download.pid
67 |
68 |
69 | #
70 | # Healthchecks ping service
71 | #
72 | healthchecks:
73 |
74 | ### sync seedbox part
75 | sync_seedbox:
76 | ## Enable or disable service
77 | enabled: false
78 |
79 | ## Ping URL
80 | # ping_url:
81 |
82 | ### sync blackhole part
83 | sync_blackhole:
84 | ## Enable or disable service
85 | enabled: false
86 |
87 | ## Ping URL
88 | # ping_url:
89 |
90 |
91 | #
92 | # SeedboxSync tunning
93 | #
94 | seedboxsync:
95 |
96 | ### Toggle application level debug (does not toggle framework debugging)
97 | # debug: false
98 |
99 | ### Where external (third-party) plugins are loaded from
100 | # plugin_dir: /var/lib/seedboxsync/plugins/
101 |
102 | ### Where all plugin configurations are loaded from
103 | # plugin_config_dir: /etc/seedboxsync/plugins.d/
104 |
105 | ### The log handler label
106 | # log_handler: colorlog
107 |
108 |
109 | log.colorlog:
110 |
111 | ### Where the log file lives (no log file by default)
112 | # file: null
113 |
114 | ### The level for which to log. One of: info, warning, error, fatal, debug
115 | # level: info
116 |
117 | ### Whether or not to log to console
118 | # to_console: true
119 |
120 | ### Whether or not to rotate the log file when it reaches `max_bytes`
121 | # rotate: false
122 |
123 | ### Max size in bytes that a log file can grow until it is rotated.
124 | # max_bytes: 512000
125 |
126 | ### The maximun number of log files to maintain when rotating
127 | # max_files: 4
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | from seedboxsync.main import SeedboxSyncTest
2 |
3 |
4 | def test_seedboxsync():
5 | """
6 | Test seedboxsync without any subcommands or arguments.
7 | """
8 |
9 | with SeedboxSyncTest() as app:
10 | app.run()
11 | assert app.exit_code == 0
12 |
13 |
14 | def test_seedboxsync_debug():
15 | """
16 | Test that debug mode is functional.
17 | """
18 |
19 | with SeedboxSyncTest() as app:
20 | app.run()
21 | assert app.debug is False
22 |
23 | argv = ['--debug']
24 | with SeedboxSyncTest(argv=argv) as app:
25 | app.run()
26 | assert app.debug is True
27 |
--------------------------------------------------------------------------------