├── .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 | ![ruTorrent settings / Downloads](images/rutorrent_1.png) 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 | ![ruTorrent settings / Autotools](images/rutorrent_2.png) 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 | ![SeedboxSync logo](images/seedboxsync.png) 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 | --------------------------------------------------------------------------------