Compare commits

..

No commits in common. "develop" and "deluge-2.1.0.dev0" have entirely different histories.

810 changed files with 340741 additions and 354108 deletions

1
.gitattributes vendored
View file

@ -3,4 +3,3 @@
.gitignore export-ignore
*.py diff=python
ext-all.js diff=minjs
*.state -merge -text

View file

@ -1,104 +0,0 @@
name: Package
on:
push:
tags:
- "deluge-*"
- "!deluge*-dev*"
branches:
- develop
pull_request:
types: [labeled, opened, synchronize, reopened]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
ref:
description: "Enter a tag or commit to package"
default: ""
jobs:
windows_package:
runs-on: windows-2022
if: (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'package'))
strategy:
matrix:
arch: [x64, x86]
python: ["3.9"]
libtorrent: [2.0.7, 1.2.19]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Checkout Deluge source to subdir to enable packaging any tag/commit
- name: Checkout Deluge source
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
path: deluge_src
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python}}
architecture: ${{ matrix.arch }}
cache: pip
- name: Prepare pip
run: python -m pip install wheel setuptools==68.*
- name: Install GTK
run: |
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile("https://github.com/deluge-torrent/gvsbuild-release/releases/download/latest/gvsbuild-py${{ matrix.python }}-vs16-${{ matrix.arch }}.zip","C:\GTK.zip")
7z x C:\GTK.zip -oc:\GTK
echo "C:\GTK\release\lib" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "C:\GTK\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "C:\GTK\release" | Out-File -FilePath $env:GITHUB_PATH -Append
python -m pip install --no-index --find-links="C:\GTK\release\python" pycairo PyGObject
- name: Install Python dependencies
# Pillow no longer provides 32-bit wheels for Windows
# so specify only-binary to install old version.
run: >
python -m pip install
--only-binary=pillow
twisted[tls]==22.8.0
libtorrent==${{ matrix.libtorrent }}
pyinstaller
pygame
-r requirements.txt
- name: Install Deluge
working-directory: deluge_src
run: |
python -m pip install .
python setup.py install_scripts
- name: Freeze Deluge
working-directory: packaging/win
run: |
pyinstaller --clean delugewin.spec --distpath freeze
- name: Verify Deluge exes
working-directory: packaging/win/freeze/Deluge/
run: |
deluge-debug.exe -v
deluged-debug.exe -v
deluge-web-debug.exe -v
deluge-console -v
- name: Make Deluge Installer
working-directory: ./packaging/win
run: |
python setup_nsis.py
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
- uses: actions/upload-artifact@v4
with:
name: deluge-py${{ matrix.python }}-lt${{ matrix.libtorrent }}-${{ matrix.arch }}
path: packaging/win/*.exe

View file

@ -6,29 +6,40 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
core-dump:
description: "Set to 1 to enable retrieving core dump from crashes"
default: "0"
jobs:
test-linux:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.7", "3.10"]
runs-on: ubuntu-20.04
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: "requirements*.txt"
python-version: "3.8"
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Add libtorrent deb repository
uses: myci-actions/add-deb-repo@8
with:
repo: deb http://ppa.launchpad.net/libtorrent.org/1.2-daily/ubuntu focal main
repo-name: libtorrent
keys: 58E5430D9667FAEFFCA0B93F32309D6B9E009EDB
key-server: keyserver.ubuntu.com
install: python3-libtorrent-dbg
- name: Sets env var for security
if: (github.event_name == 'pull_request' && contains(github.event.pull_request.body, 'security_test')) || (github.event_name == 'push' && contains(github.event.head_commit.message, 'security_test'))
@ -36,8 +47,8 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip wheel setuptools
pip install -r requirements-ci.txt
pip install --upgrade pip wheel
pip install -r requirements.txt -r requirements-tests.txt
pip install -e .
- name: Install security dependencies
@ -49,21 +60,19 @@ jobs:
TESTSSL_VER: 3.0.6
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
- name: Setup core dump catch and store
if: github.event.inputs.core-dump == '1'
- name: Setup core dump directory
run: |
sudo mkdir /cores/ && sudo chmod 777 /cores/
echo "/cores/%E.%p" | sudo tee /proc/sys/kernel/core_pattern
ulimit -c unlimited
sudo apt install glibc-tools
echo "DEBUG_PREFIX=catchsegv python -X dev -m" >> $GITHUB_ENV
- name: Test with pytest
run: |
ulimit -c unlimited # Enable core dumps to be captured
cp /usr/lib/python3/dist-packages/libtorrent*.so $GITHUB_WORKSPACE/deluge
python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
$DEBUG_PREFIX pytest -v -m "not (todo or gtkui)" deluge
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
# capture all crashes as build artifacts
if: failure()
with:
@ -71,31 +80,37 @@ jobs:
path: /cores
test-windows:
runs-on: windows-2022
strategy:
matrix:
python-version: ["3.7", "3.10"]
runs-on: windows-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: "requirements*.txt"
python-version: "3.7"
- name: Cache pip
uses: actions/cache@v2
with:
path: '%LOCALAPPDATA%\pip\Cache'
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install dependencies
run: |
pip install --upgrade pip wheel setuptools
pip install -r requirements-ci.txt
python -m pip install --upgrade pip wheel
python -m pip install libtorrent==1.2.*
pip install -r requirements.txt -r requirements-tests.txt
pip install -e .
- name: Test with pytest
run: |
python -c 'import libtorrent as lt; print(lt.__version__)';
pytest -v -m "not (todo or gtkui or security)" deluge
pytest -m "not (todo or gtkui or security)" deluge

View file

@ -15,23 +15,30 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@v2
with:
python-version: "3.10"
cache: "pip"
cache-dependency-path: "requirements*.txt"
python-version: "3.8"
- name: Cache pip
uses: actions/cache@v2
with:
# This path is specific to Ubuntu
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install dependencies
run: |
pip install --upgrade pip wheel
pip install tox
sudo apt-get install enchant-2
sudo apt-get install enchant
- name: Build docs with tox
- name: Test with tox
env:
TOX_ENV: docs
run: |

View file

@ -11,7 +11,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Run pre-commit linting
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@v2.0.2

5
.gitignore vendored
View file

@ -10,16 +10,17 @@ docs/source/modules/deluge*.rst
__pycache__/
*.py[cod]
*.tar.*
_trial_temp
.tox/
deluge/i18n/*/
deluge.pot
deluge/ui/web/js/*.js
deluge/ui/web/js/extjs/ext-extensions*.js
*.desktop
*.metainfo.xml
*.appdata.xml
.build_data*
osx/app
RELEASE-VERSION
.venv*
# used by setuptools to cache downloaded eggs
/.eggs
_pytest_temp/

View file

@ -3,28 +3,35 @@ default_language_version:
exclude: >
(?x)^(
deluge/ui/web/docs/template/.*|
deluge/tests/data/.*svg|
)$
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.6.4
- repo: https://github.com/ambv/black
rev: 20.8b1
hooks:
- id: ruff
name: Chk Ruff
args: [--fix]
- id: ruff-format
name: Fmt Ruff
- id: black
name: Fmt Black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
rev: v2.2.1
hooks:
- id: prettier
name: Fmt Prettier
# Workaround to list modified files only.
args: [--list-different]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
- repo: https://gitlab.com/pycqa/flake8
# v3.7.9 due to E402 issue: https://gitlab.com/pycqa/flake8/-/issues/638
rev: 3.7.9
hooks:
- id: flake8
name: Chk Flake8
additional_dependencies:
- flake8-isort==4.0.0
- pep8-naming==0.11.1
args: [--isort-show-traceback]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
hooks:
- id: double-quote-string-fixer
name: Fix Double-quotes
- id: end-of-file-fixer
name: Fix End-of-files
exclude_types: [javascript, css]
@ -33,9 +40,3 @@ repos:
args: [--fix=auto]
- id: trailing-whitespace
name: Fix Trailing whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py37-plus]
stages: [manual]

View file

@ -5,14 +5,6 @@
# Required
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
jobs:
post_checkout:
- git fetch --unshallow || true
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
@ -22,8 +14,9 @@ formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: requirements.txt
- requirements: docs/requirements.txt
- method: pip
- method: setuptools
path: .

View file

@ -1,96 +1,8 @@
# Changelog
## 2.2.x (TBA)
## 2.1.0 (WIP)
### Breaking changes
- Python 3.6 support removed (Python >= 3.7)
### Web UI
- Accept network interface name in addition to IP adress in "Incoming Address"
## 2.1.1 (2022-07-10)
### Core
- Fix missing trackers added via magnet
- Fix handling magnets with tracker tiers
## 2.1.0 (2022-06-28)
### Breaking changes
- Python 2 support removed (Python >= 3.6)
- libtorrent minimum requirement increased (>= 1.2).
### Core
- Add support for SVG tracker icons.
- Fix tracker icon error handling.
- Fix cleaning-up tracker icon temp files.
- Fix Plugin manager to handle new metadata 2.1.
- Hide passwords in config logs.
- Fix cleaning-up temp files in add_torrent_url.
- Fix KeyError in sessionproxy after torrent delete.
- Remove libtorrent deprecated functions.
- Fix file_completed_alert handling.
- Add plugin keys to get_torrents_status.
- Add support for pygeoip dependency.
- Fix crash logging to Windows protected folder.
- Add is_interface and is_interface_name to validate network interfaces.
- Fix is_url and is_infohash error with None value.
- Fix load_libintl error.
- Add support for IPv6 in host lists.
- Add systemd user services.
- Fix refresh and expire the torrent status cache.
- Fix crash when logging errors initializing gettext.
### Web UI
- Fix ETA column sorting in correct order (#3413).
- Fix defining foreground and background colors.
- Accept charset in content-type for json messages.
- Fix 'Complete Seen' and 'Completed' sorting.
- Fix encoding HTML entities for torrent attributes to prevent XSS.
### Gtk UI
- Fix download location textbox width.
- Fix obscured port number in Connection Manager.
- Increase connection manager default height.
- Fix bug with setting move completed in Options tab.
- Fix adding daemon accounts.
- Add workaround for crash on Windows with ico or gif icons.
- Hide account password length in log.
- Added a torrent menu option for magnet copy.
- Fix unable to prefetch magnet in thinclient mode.
- Use GtkSpinner when testing open port.
- Update About Dialog year.
- Fix Edit Torrents dialogs close issues.
- Fix ETA being copied to neighboring empty cells.
- Disable GTK CSD by default on Windows.
### Console UI
- Fix curses.init_pair raise ValueError on Py3.10.
- Swap j and k key's behavior to fit vim mode.
- Fix torrent details status error.
- Fix incorrect test for when a host is online.
- Add the torrent label to info command.
### AutoAdd
- Fix handling torrent decode errors.
- Fix error dialog not being shown on error.
### Blocklist
- Add frequency unit to interval label.
### Notifications
- Fix UnicodeEncodeError upon non-ascii torrent name.
- Removed Python 2 support.
## 2.0.5 (2021-12-15)

View file

@ -7,7 +7,7 @@ All modules will require the [common](#common) section dependencies.
## Prerequisite
- [Python] _>= 3.6_
- [Python] _>= 3.5_
## Build
@ -28,7 +28,6 @@ All modules will require the [common](#common) section dependencies.
- [setproctitle] - Optional: Renaming processes.
- [Pillow] - Optional: Support for resizing tracker icons.
- [dbus-python] - Optional: Show item location in filemanager.
- [ifaddr] - Optional: Verify network interfaces.
### Linux and BSD
@ -41,8 +40,8 @@ All modules will require the [common](#common) section dependencies.
## Core (deluged daemon)
- [libtorrent] _>= 1.2.0_
- [GeoIP] or [pygeoip] - Optional: IP address country lookup. (_Debian: `python-geoip`_)
- [libtorrent] _>= 1.1.1_
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_)
## GTK UI
@ -50,7 +49,7 @@ All modules will require the [common](#common) section dependencies.
- [PyGObject]
- [Pycairo]
- [librsvg] _>= 2_
- [ayatanaappindicator3] w/GIR - Optional: Ubuntu system tray icon.
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
### MacOS
@ -95,6 +94,5 @@ All modules will require the [common](#common) section dependencies.
[mako]: https://www.makotemplates.org/
[pygame]: https://www.pygame.org/
[libnotify]: https://developer.gnome.org/libnotify/
[ayatanaappindicator3]: https://lazka.github.io/pgi-docs/AyatanaAppIndicator3-0.1/index.html
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
[ifaddr]: https://pypi.org/project/ifaddr/

View file

@ -59,7 +59,6 @@ See the [Thinclient guide] to connect to the daemon from another computer.
- [User guide][user guide]
- [Forum](https://forum.deluge-torrent.org)
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
- [Discord](https://discord.gg/nwaHSE6tqn)
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient

View file

@ -1,6 +0,0 @@
from twisted.web.http import Request
__request__: Request
def _(string: str) -> str: ...
def _n(string: str) -> str: ...

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -14,7 +15,6 @@ Example:
>>> from deluge._libtorrent import lt
"""
from deluge.common import VersionSplit, get_version
from deluge.error import LibtorrentImportError
@ -27,10 +27,10 @@ except ImportError:
raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
REQUIRED_VERSION = '1.2.0.0'
REQUIRED_VERSION = '1.1.2.0'
LT_VERSION = lt.__version__
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
raise LibtorrentImportError(
f'Deluge {get_version()} requires libtorrent >= {REQUIRED_VERSION}'
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -92,7 +93,7 @@ def _get_version_detail():
except ImportError:
pass
version_str += 'Python: %s\n' % platform.python_version()
version_str += f'OS: {platform.system()} {common.get_os_version()}\n'
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
return version_str
@ -106,8 +107,8 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
line instead. This way list formatting is not mangled by textwrap.wrap.
"""
wrapped_lines = []
for line in text.splitlines():
wrapped_lines.extend(textwrap.wrap(line, width, subsequent_indent=' '))
for l in text.splitlines():
wrapped_lines.extend(textwrap.wrap(l, width, subsequent_indent=' '))
return wrapped_lines
def _format_action_invocation(self, action):
@ -134,7 +135,7 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
default = action.dest.upper()
args_string = self._format_args(action, default)
opt = ', '.join(action.option_strings)
parts.append(f'{opt} {args_string}')
parts.append('%s %s' % (opt, args_string))
return ', '.join(parts)
@ -162,7 +163,7 @@ class ArgParserBase(argparse.ArgumentParser):
self.log_stream = kwargs['log_stream']
del kwargs['log_stream']
super().__init__(*args, **kwargs)
super(ArgParserBase, self).__init__(*args, **kwargs)
self.common_setup = False
self.process_arg_group = False
@ -199,7 +200,7 @@ class ArgParserBase(argparse.ArgumentParser):
self.group.add_argument(
'-L',
'--loglevel',
choices=[level for k in deluge.log.levels for level in (k, k.upper())],
choices=[l for k in deluge.log.levels for l in (k, k.upper())],
help=_('Set the log level (none, error, warning, info, debug)'),
metavar='<level>',
)
@ -243,7 +244,7 @@ class ArgParserBase(argparse.ArgumentParser):
argparse.Namespace: The parsed arguments.
"""
options = super().parse_args(args=args)
options = super(ArgParserBase, self).parse_args(args=args)
return self._handle_ui_options(options)
def parse_known_ui_args(self, args, withhold=None):
@ -259,7 +260,7 @@ class ArgParserBase(argparse.ArgumentParser):
"""
if withhold:
args = [a for a in args if a not in withhold]
options, remaining = super().parse_known_args(args=args)
options, remaining = super(ArgParserBase, self).parse_known_args(args=args)
options.remaining = remaining
# Handle common and process group options
return self._handle_ui_options(options)

View file

@ -84,7 +84,8 @@ def bdecode(x):
return r
class Bencached:
class Bencached(object):
__slots__ = ['bencoded']
def __init__(self, s):

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
#
@ -7,38 +8,31 @@
#
"""Common functions for various parts of Deluge to use."""
import base64
import binascii
import functools
import glob
import locale
import logging
import numbers
import os
import platform
import re
import socket
import subprocess
import sys
import tarfile
import time
from contextlib import closing
from datetime import datetime
from importlib import resources
from io import BytesIO
from pathlib import Path
from io import BytesIO, open
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
import pkg_resources
from deluge.decorators import deprecated
from deluge.error import InvalidPathError
try:
from importlib.metadata import distribution
except ImportError:
from pkg_resources import get_distribution as distribution
try:
import chardet
except ImportError:
@ -51,11 +45,6 @@ if platform.system() in ('Windows', 'Microsoft'):
os.environ['SSL_CERT_FILE'] = where()
try:
import ifaddr
except ImportError:
ifaddr = None
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
# gi makes dbus available on Window but don't import it as unused.
@ -87,9 +76,6 @@ JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False}
DBUS_FM_ID = 'org.freedesktop.FileManager1'
DBUS_FM_PATH = '/org/freedesktop/FileManager1'
# Retained for plugin backward compatibility
PY2 = False
def get_version():
"""The program version from the egg metadata.
@ -97,7 +83,7 @@ def get_version():
Returns:
str: The version of Deluge.
"""
return distribution('Deluge').version
return pkg_resources.get_distribution('Deluge').version
def get_default_config_dir(filename=None):
@ -149,14 +135,14 @@ def get_default_download_dir():
try:
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
with open(user_dirs_path, encoding='utf8') as _file:
with open(user_dirs_path, 'r', encoding='utf8') as _file:
for line in _file:
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
download_dir = os.path.expandvars(
line.partition('=')[2].rstrip().strip('"')
)
break
except OSError:
except IOError:
pass
if not download_dir:
@ -277,7 +263,7 @@ def get_os_version():
os_version = list(platform.mac_ver())
os_version[1] = '' # versioninfo always empty.
elif distro:
os_version = (distro.name(), distro.version(), distro.codename())
os_version = distro.linux_distribution()
else:
os_version = (platform.release(),)
@ -297,22 +283,20 @@ def get_pixmap(fname):
return resource_filename('deluge', os.path.join('ui', 'data', 'pixmaps', fname))
def resource_filename(module: str, path: str) -> str:
"""Get filesystem path for a non-python resource.
def resource_filename(module, path):
"""Get filesystem path for a resource.
Abstracts getting module resource files. Originally created to
workaround pkg_resources.resource_filename limitations with
multiple Deluge packages installed.
This function contains a work-around for pkg_resources.resource_filename
not returning the correct path with multiple packages installed.
So if there's a second deluge package, installed globally and another in
develop mode somewhere else, while pkg_resources.get_distribution('Deluge')
returns the proper deluge instance, pkg_resources.resource_filename
does not, it returns the first found on the python path, which is wrong.
"""
path = Path(path)
try:
with resources.as_file(resources.files(module) / path) as resource_file:
return str(resource_file)
except AttributeError:
# Python <= 3.8
with resources.path(module, path.parts[0]) as resource_file:
return str(resource_file.joinpath(*path.parts[1:]))
return pkg_resources.get_distribution('Deluge').get_resource_filename(
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
)
def open_file(path, timestamp=None):
@ -424,49 +408,43 @@ def translate_size_units():
def fsize(fsize_b, precision=1, shortform=False):
"""Formats the bytes value into a string with KiB, MiB, GiB or TiB units.
"""Formats the bytes value into a string with KiB, MiB or GiB units.
Args:
fsize_b (int): The filesize in bytes.
precision (int): The output float precision, 1 by default.
shortform (bool): The output short|long form, False (long form) by default.
precision (int): The filesize float precision.
Returns:
str: A formatted string in KiB, MiB, GiB or TiB units.
str: A formatted string in KiB, MiB or GiB units.
Examples:
>>> fsize(112245)
'109.6 KiB'
>>> fsize(112245, precision=0)
'110 KiB'
>>> fsize(112245, shortform=True)
'109.6 K'
Note:
This function has been refactored for performance with the
fsize units being translated outside the function.
Notice that short forms K|M|G|T are synonymous here with
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
"""
if fsize_b >= 1024**4:
if fsize_b >= 1024 ** 4:
return '%.*f %s' % (
precision,
fsize_b / 1024**4,
fsize_b / 1024 ** 4,
tib_txt_short if shortform else tib_txt,
)
elif fsize_b >= 1024**3:
elif fsize_b >= 1024 ** 3:
return '%.*f %s' % (
precision,
fsize_b / 1024**3,
fsize_b / 1024 ** 3,
gib_txt_short if shortform else gib_txt,
)
elif fsize_b >= 1024**2:
elif fsize_b >= 1024 ** 2:
return '%.*f %s' % (
precision,
fsize_b / 1024**2,
fsize_b / 1024 ** 2,
mib_txt_short if shortform else mib_txt,
)
elif fsize_b >= 1024:
@ -484,7 +462,7 @@ def fpcnt(dec, precision=2):
Args:
dec (float): The ratio in the range [0.0, 1.0].
precision (int): The output float precision, 2 by default.
precision (int): The percentage float precision.
Returns:
str: A formatted string representing a percentage.
@ -508,8 +486,6 @@ def fspeed(bps, precision=1, shortform=False):
Args:
bps (int): The speed in bytes per second.
precision (int): The output float precision, 1 by default.
shortform (bool): The output short|long form, False (long form) by default.
Returns:
str: A formatted string representing transfer speed.
@ -518,34 +494,30 @@ def fspeed(bps, precision=1, shortform=False):
>>> fspeed(43134)
'42.1 KiB/s'
Note:
Notice that short forms K|M|G|T are synonymous here with
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
"""
if bps < 1024**2:
if bps < 1024 ** 2:
return '%.*f %s' % (
precision,
bps / 1024,
_('K/s') if shortform else _('KiB/s'),
)
elif bps < 1024**3:
elif bps < 1024 ** 3:
return '%.*f %s' % (
precision,
bps / 1024**2,
bps / 1024 ** 2,
_('M/s') if shortform else _('MiB/s'),
)
elif bps < 1024**4:
elif bps < 1024 ** 4:
return '%.*f %s' % (
precision,
bps / 1024**3,
bps / 1024 ** 3,
_('G/s') if shortform else _('GiB/s'),
)
else:
return '%.*f %s' % (
precision,
bps / 1024**4,
bps / 1024 ** 4,
_('T/s') if shortform else _('TiB/s'),
)
@ -558,7 +530,7 @@ def fpeer(num_peers, total_peers):
total_peers (int): The total number of peers.
Returns:
str: A formatted string 'num_peers (total_peers)' or if total_peers < 0, just 'num_peers'.
str: A formatted string 'num_peers (total_peers)' or total_peers < 0, just 'num_peers'.
Examples:
>>> fpeer(10, 20)
@ -568,9 +540,9 @@ def fpeer(num_peers, total_peers):
"""
if total_peers > -1:
return f'{num_peers:d} ({total_peers:d})'
return '{:d} ({:d})'.format(num_peers, total_peers)
else:
return f'{num_peers:d}'
return '{:d}'.format(num_peers)
def ftime(secs):
@ -596,27 +568,27 @@ def ftime(secs):
if secs <= 0:
time_str = ''
elif secs < 60:
time_str = f'{secs}s'
time_str = '{}s'.format(secs)
elif secs < 3600:
time_str = f'{secs // 60}m {secs % 60}s'
time_str = '{}m {}s'.format(secs // 60, secs % 60)
elif secs < 86400:
time_str = f'{secs // 3600}h {secs // 60 % 60}m'
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60)
elif secs < 604800:
time_str = f'{secs // 86400}d {secs // 3600 % 24}h'
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24)
elif secs < 31449600:
time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7)
else:
time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52)
return time_str
def fdate(seconds, date_only=False, precision_secs=False):
"""Formats a date time string in the locale's date representation based on the system's timezone.
"""Formats a date time string in the locale's date representation based on the systems timezone.
Args:
seconds (float): Time in seconds since the Epoch.
date_only (bool): Whether to include only the date, False by default.
precision_secs (bool): Include seconds in time format, False by default.
precision_secs (bool): Include seconds in time format.
Returns:
str: A string in the locale's datetime representation or "" if seconds < 0
@ -641,14 +613,10 @@ def tokenize(text):
Returns:
list: A list of strings and/or numbers.
Note:
This function is used to implement robust tokenization of user input
It automatically coerces integer and floating point numbers, ignores
whitespace and knows how to separate numbers from strings even without
whitespace.
Possible optimization: move the 2 regexes outside of function.
This function is used to implement robust tokenization of user input
It automatically coerces integer and floating point numbers, ignores
whitespace and knows how to separate numbers from strings even without
whitespace.
"""
tokenized_input = []
for token in re.split(r'(\d+(?:\.\d+)?)', text):
@ -664,21 +632,17 @@ def tokenize(text):
size_units = [
{'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'},
{'prefix': 'KiB', 'divider': 1024**1},
{'prefix': 'MiB', 'divider': 1024**2},
{'prefix': 'GiB', 'divider': 1024**3},
{'prefix': 'TiB', 'divider': 1024**4},
{'prefix': 'PiB', 'divider': 1024**5},
{'prefix': 'k', 'divider': 1000**1},
{'prefix': 'm', 'divider': 1000**2},
{'prefix': 'g', 'divider': 1000**3},
{'prefix': 't', 'divider': 1000**4},
{'prefix': 'p', 'divider': 1000**5},
{'prefix': 'KB', 'divider': 1000**1},
{'prefix': 'MB', 'divider': 1000**2},
{'prefix': 'GB', 'divider': 1000**3},
{'prefix': 'TB', 'divider': 1000**4},
{'prefix': 'PB', 'divider': 1000**5},
{'prefix': 'KiB', 'divider': 1024 ** 1},
{'prefix': 'MiB', 'divider': 1024 ** 2},
{'prefix': 'GiB', 'divider': 1024 ** 3},
{'prefix': 'TiB', 'divider': 1024 ** 4},
{'prefix': 'PiB', 'divider': 1024 ** 5},
{'prefix': 'KB', 'divider': 1000 ** 1},
{'prefix': 'MB', 'divider': 1000 ** 2},
{'prefix': 'GB', 'divider': 1000 ** 3},
{'prefix': 'TB', 'divider': 1000 ** 4},
{'prefix': 'PB', 'divider': 1000 ** 5},
{'prefix': 'm', 'divider': 1000 ** 2},
]
@ -721,16 +685,6 @@ def parse_human_size(size):
raise InvalidSize(msg % (size, tokens))
def anchorify_urls(text: str) -> str:
"""
Wrap all occurrences of text URLs with HTML
"""
url_pattern = r'((htt)|(ft)|(ud))ps?://\S+'
html_href_pattern = r'<a href="\g<0>">\g<0></a>'
return re.sub(url_pattern, html_href_pattern, text)
def is_url(url):
"""
A simple test to check if the URL is valid
@ -746,9 +700,6 @@ def is_url(url):
True
"""
if not url:
return False
return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp')
@ -763,9 +714,6 @@ def is_infohash(infohash):
bool: True if valid infohash, False otherwise.
"""
if not infohash:
return False
return len(infohash) == 40 and infohash.isalnum()
@ -773,8 +721,6 @@ MAGNET_SCHEME = 'magnet:?'
XT_BTIH_PARAM = 'xt=urn:btih:'
DN_PARAM = 'dn='
TR_PARAM = 'tr='
TR_TIER_PARAM = 'tr.'
TR_TIER_REGEX = re.compile(r'^tr.(\d+)=(\S+)')
def is_magnet(uri):
@ -817,6 +763,8 @@ def get_magnet_info(uri):
"""
tr0_param = 'tr.'
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
if not uri.startswith(MAGNET_SCHEME):
return {}
@ -844,14 +792,12 @@ def get_magnet_info(uri):
tracker = unquote_plus(param[len(TR_PARAM) :])
trackers[tracker] = tier
tier += 1
elif param.startswith(TR_TIER_PARAM):
tracker_match = re.match(TR_TIER_REGEX, param)
if not tracker_match:
continue
tier, tracker = tracker_match.groups()
tracker = unquote_plus(tracker)
trackers[tracker] = int(tier)
elif param.startswith(tr0_param):
try:
tier, tracker = re.match(tr0_param_regex, param).groups()
trackers[tracker] = tier
except AttributeError:
pass
if info_hash:
if not name:
@ -872,7 +818,7 @@ def create_magnet_uri(infohash, name=None, trackers=None):
Args:
infohash (str): The info-hash of the torrent.
name (str, optional): The name of the torrent.
trackers (list or dict, optional): A list of trackers or a dict or some {tracker: tier} pairs.
trackers (list or dict, optional): A list of trackers or dict or {tracker: tier} pairs.
Returns:
str: A magnet URI string.
@ -914,7 +860,7 @@ def get_path_size(path):
return os.path.getsize(path)
dir_size = 0
for p, dummy_dirs, files in os.walk(path):
for (p, dummy_dirs, files) in os.walk(path):
for _file in files:
filename = os.path.join(p, _file)
dir_size += os.path.getsize(filename)
@ -946,29 +892,6 @@ def free_space(path):
return disk_data.f_bavail * block_size
def is_interface(interface):
"""Check if interface is a valid IP or network adapter.
Args:
interface (str): The IP or interface name to test.
Returns:
bool: Whether interface is valid is not.
Examples:
Windows:
>>> is_interface('{7A30AE62-23ZA-3744-Z844-A5B042524871}')
>>> is_interface('127.0.0.1')
True
Linux:
>>> is_interface('lo')
>>> is_interface('127.0.0.1')
True
"""
return is_ip(interface) or is_interface_name(interface)
def is_ip(ip):
"""A test to see if 'ip' is a valid IPv4 or IPv6 address.
@ -1004,12 +927,15 @@ def is_ipv4(ip):
"""
import socket
try:
socket.inet_pton(socket.AF_INET, ip)
except OSError:
if windows_check():
return socket.inet_aton(ip)
else:
return socket.inet_pton(socket.AF_INET, ip)
except socket.error:
return False
else:
return True
def is_ipv6(ip):
@ -1028,51 +954,23 @@ def is_ipv6(ip):
"""
try:
socket.inet_pton(socket.AF_INET6, ip)
except OSError:
return False
import ipaddress
except ImportError:
import socket
try:
return socket.inet_pton(socket.AF_INET6, ip)
except (socket.error, AttributeError):
if windows_check():
log.warning('Unable to verify IPv6 Address on Windows.')
return True
else:
return True
def is_interface_name(name):
"""Returns True if an interface name exists.
Args:
name (str): The Interface to test. eg. eth0 linux. GUID on Windows.
Returns:
bool: Whether name is valid or not.
Examples:
>>> is_interface_name("eth0")
True
>>> is_interface_name("{7A30AE62-23ZA-3744-Z844-A5B042524871}")
True
"""
if not windows_check():
try:
socket.if_nametoindex(name)
except OSError:
return ipaddress.IPv6Address(decode_bytes(ip))
except ipaddress.AddressValueError:
pass
else:
return True
if ifaddr:
try:
adapters = ifaddr.get_adapters()
except OSError:
return True
else:
return any([name == a.name for a in adapters])
if windows_check():
regex = '^{[0-9A-Z]{8}-([0-9A-Z]{4}-){3}[0-9A-Z]{12}}$'
return bool(re.search(regex, str(name)))
return True
return False
def decode_bytes(byte_str, encoding='utf8'):
@ -1150,7 +1048,7 @@ def utf8_encode_structure(data):
@functools.total_ordering
class VersionSplit:
class VersionSplit(object):
"""
Used for comparing version numbers.
@ -1348,13 +1246,33 @@ def set_env_variable(name, value):
)
# Update the copy maintained by msvcrt (used by gtk+ runtime)
result = cdll.msvcrt._wputenv(f'{name}={value}')
result = cdll.msvcrt._wputenv('%s=%s' % (name, value))
if result != 0:
log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name)
else:
log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value)
def unicode_argv():
""" Gets sys.argv as list of unicode objects on any platform."""
# On platforms other than Windows, we have to find the likely encoding of the args and decode
# First check if sys.stdout or stdin have encoding set
encoding = getattr(sys.stdout, 'encoding') or getattr(sys.stdin, 'encoding')
# If that fails, check what the locale is set to
encoding = encoding or locale.getpreferredencoding()
# As a last resort, just default to utf-8
encoding = encoding or 'utf-8'
arg_list = []
for arg in sys.argv:
try:
arg_list.append(arg.decode(encoding))
except AttributeError:
arg_list.append(arg)
return arg_list
def run_profiled(func, *args, **kwargs):
"""
Profile a function with cProfile

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
#
@ -23,13 +24,13 @@ class ComponentAlreadyRegistered(Exception):
class ComponentException(Exception):
def __init__(self, message, tb):
super().__init__(message)
super(ComponentException, self).__init__(message)
self.message = message
self.tb = tb
def __str__(self):
s = super().__str__()
return '{}\n{}'.format(s, ''.join(self.tb))
s = super(ComponentException, self).__str__()
return '%s\n%s' % (s, ''.join(self.tb))
def __eq__(self, other):
if isinstance(other, self.__class__):
@ -41,7 +42,7 @@ class ComponentException(Exception):
return not self.__eq__(other)
class Component:
class Component(object):
"""Component objects are singletons managed by the :class:`ComponentRegistry`.
When a new Component object is instantiated, it will be automatically
@ -59,16 +60,11 @@ class Component:
Deluge core.
**update()** - This method is called every 1 second by default while the
Component is in a *Started* state. The interval can be
Componented is in a *Started* state. The interval can be
specified during instantiation. The update() timer can be
paused by instructing the :class:`ComponentRegistry` to pause
this Component.
**pause()** - This method is called when the component is being paused.
**resume()** - This method is called when the component resumes from a Paused
state.
**shutdown()** - This method is called when the client is exiting. If the
Component is in a "Started" state when this is called, a
call to stop() will be issued prior to shutdown().
@ -85,10 +81,10 @@ class Component:
**Stopped** - The Component has either been stopped or has yet to be started.
**Stopping** - The Component has had its stop method called, but it hasn't
**Stopping** - The Component has had it's stop method called, but it hasn't
fully stopped yet.
**Paused** - The Component has had its update timer stopped, but will
**Paused** - The Component has had it's update timer stopped, but will
still be considered in a Started state.
"""
@ -116,8 +112,9 @@ class Component:
_ComponentRegistry.deregister(self)
def _component_start_timer(self):
self._component_timer = LoopingCall(self.update)
self._component_timer.start(self._component_interval)
if hasattr(self, 'update'):
self._component_timer = LoopingCall(self.update)
self._component_timer.start(self._component_interval)
def _component_start(self):
def on_start(result):
@ -133,10 +130,13 @@ class Component:
return fail(result)
if self._component_state == 'Stopped':
self._component_state = 'Starting'
d = deferLater(reactor, 0, self.start)
d.addCallbacks(on_start, on_start_fail)
self._component_starting_deferred = d
if hasattr(self, 'start'):
self._component_state = 'Starting'
d = deferLater(reactor, 0, self.start)
d.addCallbacks(on_start, on_start_fail)
self._component_starting_deferred = d
else:
d = maybeDeferred(on_start, None)
elif self._component_state == 'Starting':
return self._component_starting_deferred
elif self._component_state == 'Started':
@ -166,11 +166,14 @@ class Component:
return result
if self._component_state != 'Stopped' and self._component_state != 'Stopping':
self._component_state = 'Stopping'
d = maybeDeferred(self.stop)
d.addCallback(on_stop)
d.addErrback(on_stop_fail)
self._component_stopping_deferred = d
if hasattr(self, 'stop'):
self._component_state = 'Stopping'
d = maybeDeferred(self.stop)
d.addCallback(on_stop)
d.addErrback(on_stop_fail)
self._component_stopping_deferred = d
else:
d = maybeDeferred(on_stop, None)
if self._component_state == 'Stopping':
return self._component_stopping_deferred
@ -180,12 +183,13 @@ class Component:
def _component_pause(self):
def on_pause(result):
self._component_state = 'Paused'
if self._component_timer and self._component_timer.running:
self._component_timer.stop()
if self._component_state == 'Started':
d = maybeDeferred(self.pause)
d.addCallback(on_pause)
if self._component_timer and self._component_timer.running:
d = maybeDeferred(self._component_timer.stop)
d.addCallback(on_pause)
else:
d = succeed(None)
elif self._component_state == 'Paused':
d = succeed(None)
else:
@ -202,10 +206,9 @@ class Component:
def _component_resume(self):
def on_resume(result):
self._component_state = 'Started'
self._component_start_timer()
if self._component_state == 'Paused':
d = maybeDeferred(self.resume)
d = maybeDeferred(self._component_start_timer)
d.addCallback(on_resume)
else:
d = fail(
@ -220,7 +223,9 @@ class Component:
def _component_shutdown(self):
def on_stop(result):
return maybeDeferred(self.shutdown)
if hasattr(self, 'shutdown'):
return maybeDeferred(self.shutdown)
return succeed(None)
d = self._component_stop()
d.addCallback(on_stop)
@ -241,14 +246,8 @@ class Component:
def shutdown(self):
pass
def pause(self):
pass
def resume(self):
pass
class ComponentRegistry:
class ComponentRegistry(object):
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
It is used to manage the Components by starting, stopping, pausing and shutting them down.

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
#
@ -38,18 +39,36 @@ this can only be done for the 'config file version' and not for the 'format'
version as this will be done internally.
"""
import json
import logging
import os
import pickle
import shutil
from codecs import getwriter
from io import open
from tempfile import NamedTemporaryFile
from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__)
callLater = None # noqa: N816 Necessary for the config tests
def prop(func):
"""Function decorator for defining property attributes
The decorated function is expected to return a dictionary
containing one or more of the following pairs:
fget - function for getting attribute value
fset - function for setting attribute value
fdel - function for deleting attribute
This can be conveniently constructed by the locals() builtin
function; see:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
"""
return property(doc=func.__doc__, **func())
def find_json_objects(text, decoder=json.JSONDecoder()):
@ -83,22 +102,7 @@ def find_json_objects(text, decoder=json.JSONDecoder()):
return objects
def cast_to_existing_type(value, old_value):
"""Attempt to convert new value type to match old value type"""
types_match = isinstance(old_value, (type(None), type(value)))
if value is not None and not types_match:
old_type = type(old_value)
# Skip convert to bytes since requires knowledge of encoding and value should
# be unicode anyway.
if old_type is bytes:
return value
return old_type(value)
return value
class Config:
class Config(object):
"""This class is used to access/create/modify config files.
Args:
@ -108,23 +112,13 @@ class Config:
file_version (int): The file format for the default config values when creating
a fresh config. This value should be increased whenever a new migration function is
setup to convert old config files. (default: 1)
log_mask_funcs (dict): A dict of key:function, used to mask sensitive
key values (e.g. passwords) when logging is enabled.
"""
def __init__(
self,
filename,
defaults=None,
config_dir=None,
file_version=1,
log_mask_funcs=None,
):
def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
self.__config = {}
self.__set_functions = {}
self.__change_callbacks = []
self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {}
# These hold the version numbers and they will be set when loaded
self.__version = {'format': 1, 'file': file_version}
@ -135,7 +129,7 @@ class Config:
if defaults:
for key, value in defaults.items():
self.set_item(key, value, default=True)
self.set_item(key, value)
# Load the config from file in the config_dir
if config_dir:
@ -145,12 +139,6 @@ class Config:
self.load()
def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase
"""Wrapper around reactor.callLater for test purpose."""
from twisted.internet import reactor
return reactor.callLater(period, func, *args, **kwargs)
def __contains__(self, item):
return item in self.__config
@ -159,7 +147,7 @@ class Config:
return self.set_item(key, value)
def set_item(self, key, value, default=False):
def set_item(self, key, value):
"""Sets item 'key' to 'value' in the config dictionary.
Does not allow changing the item's type unless it is None.
@ -171,8 +159,6 @@ class Config:
key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is
currently in the config.
default (optional, bool): When setting a default value skip func or save
callbacks.
Raises:
ValueError: Raised when the type of value is not the same as what is
@ -185,54 +171,61 @@ class Config:
5
"""
if isinstance(value, bytes):
value = value.decode()
if key not in self.__config:
self.__config[key] = value
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
return
if key in self.__config:
if self.__config[key] == value:
return
# Change the value type if it is not None and does not match.
type_match = isinstance(self.__config[key], (type(None), type(value)))
if value is not None and not type_match:
try:
value = cast_to_existing_type(value, self.__config[key])
oldtype = type(self.__config[key])
# Don't convert to bytes as requires encoding and value will
# be decoded anyway.
if oldtype is not bytes:
value = oldtype(value)
except ValueError:
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise
else:
if self.__config[key] == value:
return
if log.isEnabledFor(logging.DEBUG):
if key in self.__log_mask_funcs:
value = self.__log_mask_funcs[key](value)
log.debug(
'Setting key "%s" to: %s (of type: %s)',
key,
value,
type(value),
)
if isinstance(value, bytes):
value = value.decode('utf8')
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
self.__config[key] = value
# Skip save or func callbacks if setting default value for keys
if default:
return
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# Run the set_function for this key if any
for func in self.__set_functions.get(key, []):
self.callLater(0, func, key, value)
try:
for func in self.__set_functions[key]:
callLater(0, func, key, value)
except KeyError:
pass
try:
def do_change_callbacks(key, value):
for func in self.__change_callbacks:
func(key, value)
self.callLater(0, do_change_callbacks, key, value)
callLater(0, do_change_callbacks, key, value)
except Exception:
pass
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = self.callLater(5, self.save)
self._save_timer = callLater(5, self.save)
def __getitem__(self, key):
"""See get_item"""
"""See get_item """
return self.get_item(key)
def get_item(self, key):
@ -305,9 +298,16 @@ class Config:
del self.__config[key]
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = self.callLater(5, self.save)
self._save_timer = callLater(5, self.save)
def register_change_callback(self, callback):
"""Registers a callback function for any changed value.
@ -353,6 +353,7 @@ class Config:
# Run the function now if apply_now is set
if apply_now:
function(key, self.__config[key])
return
def apply_all(self):
"""Calls all set functions.
@ -395,9 +396,9 @@ class Config:
filename = self.__config_file
try:
with open(filename, encoding='utf8') as _file:
with open(filename, 'r', encoding='utf8') as _file:
data = _file.read()
except OSError as ex:
except IOError as ex:
log.warning('Unable to open config file %s: %s', filename, ex)
return
@ -427,24 +428,12 @@ class Config:
log.exception(ex)
log.warning('Unable to load config file: %s', filename)
if not log.isEnabledFor(logging.DEBUG):
return
config = self.__config
if self.__log_mask_funcs:
config = {
key: self.__log_mask_funcs[key](config[key])
if key in self.__log_mask_funcs
else config[key]
for key in config
}
log.debug(
'Config %s version: %s.%s loaded: %s',
filename,
self.__version['format'],
self.__version['file'],
config,
self.__config,
)
def save(self, filename=None):
@ -462,7 +451,7 @@ class Config:
# Check to see if the current config differs from the one on disk
# We will only write a new config file if there is a difference
try:
with open(filename, encoding='utf8') as _file:
with open(filename, 'r', encoding='utf8') as _file:
data = _file.read()
objects = find_json_objects(data)
start, end = objects[0]
@ -474,7 +463,7 @@ class Config:
if self._save_timer and self._save_timer.active():
self._save_timer.cancel()
return True
except (OSError, IndexError) as ex:
except (IOError, IndexError) as ex:
log.warning('Unable to open config file: %s because: %s', filename, ex)
# Save the new config and make sure it's written to disk
@ -488,7 +477,7 @@ class Config:
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
_file.flush()
os.fsync(_file.fileno())
except OSError as ex:
except IOError as ex:
log.error('Error writing new config file: %s', ex)
return False
@ -499,7 +488,7 @@ class Config:
try:
log.debug('Backing up old config file to %s.bak', filename)
shutil.move(filename, filename + '.bak')
except OSError as ex:
except IOError as ex:
log.warning('Unable to backup old config: %s', ex)
# The new config file has been written successfully, so let's move it over
@ -507,7 +496,7 @@ class Config:
try:
log.debug('Moving new config file %s to %s', filename_tmp, filename)
shutil.move(filename_tmp, filename)
except OSError as ex:
except IOError as ex:
log.error('Error moving new config file: %s', ex)
return False
else:
@ -559,11 +548,14 @@ class Config:
def config_file(self):
return self.__config_file
@property
def config(self):
@prop
def config(): # pylint: disable=no-method-argument
"""The config dictionary"""
return self.__config
@config.deleter
def config(self):
return self.save()
def fget(self):
return self.__config
def fdel(self):
return self.save()
return locals()

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -16,7 +17,7 @@ from deluge.config import Config
log = logging.getLogger(__name__)
class _ConfigManager:
class _ConfigManager(object):
def __init__(self):
log.debug('ConfigManager started..')
self.config_files = {}

View file

@ -1,215 +0,0 @@
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import asyncio
import tempfile
import warnings
from unittest.mock import Mock, patch
import pytest
import pytest_twisted
from twisted.internet import reactor
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.error import CannotListenError, ProcessTerminated
from twisted.python.failure import Failure
import deluge.component as _component
import deluge.configmanager
from deluge.common import get_localhost_auth
from deluge.tests import common
from deluge.ui.client import client as _client
DEFAULT_LISTEN_PORT = 58900
@pytest.fixture
def listen_port(request):
if request and 'daemon' in request.fixturenames:
try:
return request.getfixturevalue('daemon').listen_port
except Exception:
pass
return DEFAULT_LISTEN_PORT
@pytest.fixture
def mock_callback():
"""Returns a `Mock` object which can be registered as a callback to test against.
If callback was not called within `timeout` seconds, it will raise a TimeoutError.
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
"""
def reset(timeout=0.5, *args, **kwargs):
if mock.called:
original_reset_mock(*args, **kwargs)
if mock.deferred:
mock.deferred.cancel()
deferred = Deferred(canceller=lambda x: deferred.callback(None))
deferred.addTimeout(timeout, reactor)
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
mock.deferred = deferred
mock = Mock()
mock.__qualname__ = 'mock'
original_reset_mock = mock.reset_mock
mock.reset_mock = reset
mock.reset_mock()
return mock
@pytest.fixture
def config_dir(tmp_path):
config_dir = tmp_path / 'config'
deluge.configmanager.set_config_dir(config_dir)
yield config_dir
@pytest_twisted.async_yield_fixture()
async def client(request, config_dir, monkeypatch, listen_port):
# monkeypatch.setattr(
# _client, 'connect', functools.partial(_client.connect, port=listen_port)
# )
try:
username, password = get_localhost_auth()
except Exception:
username, password = '', ''
await _client.connect(
'localhost',
port=listen_port,
username=username,
password=password,
)
yield _client
if _client.connected():
await _client.disconnect()
@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir, tmp_path):
listen_port = DEFAULT_LISTEN_PORT
logfile = tmp_path / 'daemon.log'
if hasattr(request.cls, 'daemon_custom_script'):
custom_script = request.cls.daemon_custom_script
else:
custom_script = ''
for dummy in range(10):
try:
d, daemon = common.start_core(
listen_port=listen_port,
logfile=logfile,
timeout=5,
timeout_msg='Timeout!',
custom_script=custom_script,
print_stdout=True,
print_stderr=True,
config_directory=config_dir,
)
await d
except CannotListenError as ex:
exception_error = ex
listen_port += 1
except (KeyboardInterrupt, SystemExit):
raise
else:
break
else:
raise exception_error
daemon.listen_port = listen_port
yield daemon
try:
await daemon.kill()
except ProcessTerminated:
pass
@pytest.fixture(autouse=True)
def common_fixture(config_dir, request, monkeypatch, listen_port):
"""Adds some instance attributes to test classes for backwards compatibility with old testing."""
def fail(self, reason):
if isinstance(reason, Failure):
reason = reason.value
return pytest.fail(str(reason))
if request.instance:
request.instance.patch = monkeypatch.setattr
request.instance.config_dir = config_dir
request.instance.listen_port = listen_port
request.instance.id = lambda: request.node.name
request.cls.fail = fail
@pytest_twisted.async_yield_fixture(scope='function')
async def component():
"""Verify component registry is clean, and clean up after test."""
if len(_component._ComponentRegistry.components) != 0:
warnings.warn(
'The component._ComponentRegistry.components is not empty on test setup.\n'
'This is probably caused by another test that did not clean up after finishing!: %s'
% _component._ComponentRegistry.components
)
yield _component
await _component.shutdown()
_component._ComponentRegistry.components.clear()
_component._ComponentRegistry.dependents.clear()
@pytest_twisted.async_yield_fixture(scope='function')
async def base_fixture(common_fixture, component, request):
"""This fixture is autoused on all tests that subclass BaseTestCase"""
self = request.instance
if hasattr(self, 'set_up'):
try:
await maybeDeferred(self.set_up)
except Exception as exc:
warnings.warn('Error caught in test setup!\n%s' % exc)
pytest.fail('Error caught in test setup!\n%s' % exc)
yield
if hasattr(self, 'tear_down'):
try:
await maybeDeferred(self.tear_down)
except Exception as exc:
pytest.fail('Error caught in test teardown!\n%s' % exc)
@pytest.mark.usefixtures('base_fixture')
class BaseTestCase:
"""This is the base class that should be used for all test classes
that create classes that inherit from deluge.component.Component. It
ensures that the component registry has been cleaned up when tests
have finished.
"""
@pytest.fixture
def mock_mkstemp(tmp_path):
"""Return known tempfile location to verify file deleted"""
tmp_file = tempfile.mkstemp(dir=tmp_path)
with patch('tempfile.mkstemp', return_value=tmp_file):
yield tmp_file
def pytest_collection_modifyitems(session, config, items) -> None:
"""
Automatically runs async tests with pytest_twisted.ensureDeferred
"""
function_items = (item for item in items if isinstance(item, pytest.Function))
for function_item in function_items:
function = function_item.obj
if hasattr(function, '__func__'):
# methods need to be unwrapped.
function = function.__func__
if asyncio.iscoroutinefunction(function):
# This is how pytest_twisted marks ensureDeferred tests
setattr(function, '_pytest_twisted_mark', 'async_test')

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -14,16 +15,10 @@ This should typically only be used by the Core. Plugins should utilize the
`:mod:EventManager` for similar functionality.
"""
import contextlib
import logging
import threading
import time
from collections import defaultdict
from functools import partial
from typing import Any, Callable
from types import SimpleNamespace
from twisted.internet import reactor, task, threads
from twisted.internet import reactor
import deluge.component as component
from deluge._libtorrent import lt
@ -37,7 +32,7 @@ class AlertManager(component.Component):
def __init__(self):
log.debug('AlertManager init...')
component.Component.__init__(self, 'AlertManager')
component.Component.__init__(self, 'AlertManager', interval=0.3)
self.session = component.get('Core').session
# Increase the alert queue size so that alerts don't get lost.
@ -52,94 +47,53 @@ class AlertManager(component.Component):
| lt.alert.category_t.status_notification
| lt.alert.category_t.ip_block_notification
| lt.alert.category_t.performance_warning
| lt.alert.category_t.file_progress_notification
)
self.session.apply_settings({'alert_mask': alert_mask})
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
self.handlers = defaultdict(list)
self.handlers_timeout_secs = 2
self.handlers = {}
self.delayed_calls = []
self._event = threading.Event()
def update(self):
pass
def start(self):
thread = threading.Thread(
target=self.wait_for_alert_in_thread, name='alert-poller', daemon=True
)
thread.start()
self._event.set()
def stop(self):
self.cancel_delayed_calls()
def pause(self):
self._event.clear()
def resume(self):
self._event.set()
def wait_for_alert_in_thread(self):
while self._component_state not in ('Stopping', 'Stopped'):
if self.check_delayed_calls():
time.sleep(0.05)
continue
if self.session.wait_for_alert(1000) is None:
continue
if self._event.wait():
threads.blockingCallFromThread(reactor, self.maybe_handle_alerts)
def on_delayed_call_timeout(self, result, timeout, **kwargs):
log.warning('Alert handler was timed-out before being called %s', kwargs)
def cancel_delayed_calls(self):
"""Cancel all delayed handlers."""
for delayed_call in self.delayed_calls:
delayed_call.cancel()
self.delayed_calls = []
def check_delayed_calls(self) -> bool:
"""Returns True if any handler calls are delayed."""
self.delayed_calls = [dc for dc in self.delayed_calls if not dc.called]
return len(self.delayed_calls) > 0
def maybe_handle_alerts(self) -> None:
if self._component_state != 'Started':
return
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
self.handle_alerts()
def register_handler(self, alert_type: str, handler: Callable[[Any], None]) -> None:
def stop(self):
for delayed_call in self.delayed_calls:
if delayed_call.active():
delayed_call.cancel()
self.delayed_calls = []
def register_handler(self, alert_type, handler):
"""
Registers a function that will be called when 'alert_type' is pop'd
in handle_alerts. The handler function should look like: handler(alert)
Where 'alert' is the actual alert object from libtorrent.
Args:
alert_type: String representation of the libtorrent alert name.
Can be supplied with or without `_alert` suffix.
handler: Callback function when the alert is raised.
:param alert_type: str, this is string representation of the alert name
:param handler: func(alert), the function to be called when the alert is raised
"""
if alert_type and alert_type.endswith('_alert'):
alert_type = alert_type[: -len('_alert')]
if alert_type not in self.handlers:
# There is no entry for this alert type yet, so lets make it with an
# empty list.
self.handlers[alert_type] = []
# Append the handler to the list in the handlers dictionary
self.handlers[alert_type].append(handler)
log.debug('Registered handler for alert %s', alert_type)
def deregister_handler(self, handler: Callable[[Any], None]):
def deregister_handler(self, handler):
"""
De-registers the `handler` function from all alert types.
De-registers the `:param:handler` function from all alert types.
Args:
handler: The handler function to deregister.
:param handler: func, the handler function to deregister
"""
for alert_type_handlers in self.handlers.values():
with contextlib.suppress(ValueError):
alert_type_handlers.remove(handler)
# Iterate through all handlers and remove 'handler' where found
for (dummy_key, value) in self.handlers.items():
if handler in value:
# Handler is in this alert type list
value.remove(handler)
def handle_alerts(self):
"""
@ -158,32 +112,26 @@ class AlertManager(component.Component):
num_alerts,
)
# Loop through all alerts in the queue
for alert in alerts:
alert_type = alert.what()
alert_type = type(alert).__name__
# Display the alert message
if log.isEnabledFor(logging.DEBUG):
log.debug('%s: %s', alert_type, decode_bytes(alert.message()))
if alert_type not in self.handlers:
continue
# Call any handlers for this alert type
for handler in self.handlers[alert_type]:
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
d = task.deferLater(reactor, 0, handler, alert)
on_handler_timeout = partial(
self.on_delayed_call_timeout,
handler=handler.__qualname__,
alert_type=alert_type,
)
d.addTimeout(
self.handlers_timeout_secs,
reactor,
onTimeoutCancel=on_handler_timeout,
)
self.delayed_calls.append(d)
if alert_type in self.handlers:
for handler in self.handlers[alert_type]:
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
# Copy alert attributes
alert_copy = SimpleNamespace(
**{
attr: getattr(alert, attr)
for attr in dir(alert)
if not attr.startswith('__')
}
)
self.delayed_calls.append(reactor.callLater(0, handler, alert_copy))
def set_alert_queue_size(self, queue_size):
"""Sets the maximum size of the libtorrent alert queue"""

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -10,6 +11,7 @@
import logging
import os
import shutil
from io import open
import deluge.component as component
import deluge.configmanager as configmanager
@ -28,14 +30,14 @@ log = logging.getLogger(__name__)
AUTH_LEVELS_MAPPING = {
'NONE': AUTH_LEVEL_NONE,
'READONLY': AUTH_LEVEL_READONLY,
'DEFAULT': AUTH_LEVEL_DEFAULT,
'NORMAL': AUTH_LEVEL_NORMAL,
'DEFAULT': AUTH_LEVEL_NORMAL,
'NORMAL': AUTH_LEVEL_DEFAULT,
'ADMIN': AUTH_LEVEL_ADMIN,
}
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
class Account:
class Account(object):
__slots__ = ('username', 'password', 'authlevel')
def __init__(self, username, password, authlevel):
@ -52,10 +54,10 @@ class Account:
}
def __repr__(self):
return '<Account username="{username}" authlevel={authlevel}>'.format(
username=self.username,
authlevel=self.authlevel,
)
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
'username': self.username,
'authlevel': self.authlevel,
}
class AuthManager(component.Component):
@ -180,7 +182,7 @@ class AuthManager(component.Component):
if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak)
except OSError as ex:
except IOError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else:
log.info('Saving the %s at: %s', filename, filepath)
@ -194,7 +196,7 @@ class AuthManager(component.Component):
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
except OSError as ex:
except IOError as ex:
log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
@ -223,9 +225,9 @@ class AuthManager(component.Component):
for _filepath in (auth_file, auth_file_bak):
log.info('Opening %s for load: %s', filename, _filepath)
try:
with open(_filepath, encoding='utf8') as _file:
with open(_filepath, 'r', encoding='utf8') as _file:
file_data = _file.readlines()
except OSError as ex:
except IOError as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
file_data = []
else:

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -12,16 +13,16 @@ import logging
import os
import shutil
import tempfile
import threading
from base64 import b64decode, b64encode
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.request import URLError, urlopen
from twisted.internet import defer, reactor, task, threads
from twisted.internet import defer, reactor, task
from twisted.web.client import Agent, readBody
import deluge.common
import deluge.component as component
from deluge import metafile, path_chooser_common
from deluge import path_chooser_common
from deluge._libtorrent import LT_VERSION, lt
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.alertmanager import AlertManager
@ -38,7 +39,7 @@ from deluge.core.pluginmanager import PluginManager
from deluge.core.preferencesmanager import PreferencesManager
from deluge.core.rpcserver import export
from deluge.core.torrentmanager import TorrentManager
from deluge.decorators import deprecated, maybe_coroutine
from deluge.decorators import deprecated
from deluge.error import (
AddTorrentError,
DelugeError,
@ -111,7 +112,7 @@ class Core(component.Component):
component.Component.__init__(self, 'Core')
# Start the libtorrent session.
user_agent = f'Deluge/{DELUGE_VER} libtorrent/{LT_VERSION}'
user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION)
peer_id = self._create_peer_id(DELUGE_VER)
log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent)
settings_pack = {
@ -164,25 +165,19 @@ class Core(component.Component):
# store the one in the config so we can restore it on shutdown
self._old_listen_interface = None
if listen_interface:
if deluge.common.is_interface(listen_interface):
if deluge.common.is_ip(listen_interface):
self._old_listen_interface = self.config['listen_interface']
self.config['listen_interface'] = listen_interface
else:
log.error(
'Invalid listen interface (must be IP Address or Interface Name): %s',
'Invalid listen interface (must be IP Address): %s',
listen_interface,
)
self._old_outgoing_interface = None
if outgoing_interface:
if deluge.common.is_interface(outgoing_interface):
self._old_outgoing_interface = self.config['outgoing_interface']
self.config['outgoing_interface'] = outgoing_interface
else:
log.error(
'Invalid outgoing interface (must be IP Address or Interface Name): %s',
outgoing_interface,
)
self._old_outgoing_interface = self.config['outgoing_interface']
self.config['outgoing_interface'] = outgoing_interface
# New release check information
self.__new_release = None
@ -198,7 +193,7 @@ class Core(component.Component):
self.session_status_timer_interval = 0.5
self.session_status_timer = task.LoopingCall(self.session.post_session_stats)
self.alertmanager.register_handler(
'session_stats', self._on_alert_session_stats
'session_stats_alert', self._on_alert_session_stats
)
self.session_rates_timer_interval = 2
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
@ -240,12 +235,13 @@ class Core(component.Component):
"""Apply libtorrent session settings.
Args:
settings: A dict of lt session settings to apply.
settings (dict): A dict of lt session settings to apply.
"""
self.session.apply_settings(settings)
@staticmethod
def _create_peer_id(version: str) -> str:
def _create_peer_id(version):
"""Create a peer_id fingerprint.
This creates the peer_id and modifies the release char to identify
@ -260,10 +256,11 @@ class Core(component.Component):
``--DE201b--`` (beta pre-release of v2.0.1)
Args:
version: The version string in PEP440 dotted notation.
version (str): The version string in PEP440 dotted notation.
Returns:
The formatted peer_id with Deluge prefix e.g. '--DE200s--'
str: The formatted peer_id with Deluge prefix e.g. '--DE200s--'
"""
split = deluge.common.VersionSplit(version)
# Fill list with zeros to length of 4 and use lt to create fingerprint.
@ -296,7 +293,7 @@ class Core(component.Component):
if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak)
except OSError as ex:
except IOError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else:
log.info('Saving the %s at: %s', filename, filepath)
@ -306,17 +303,18 @@ class Core(component.Component):
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
except (OSError, EOFError) as ex:
except (IOError, EOFError) as ex:
log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
shutil.move(filepath_bak, filepath)
def _load_session_state(self) -> dict:
def _load_session_state(self):
"""Loads the libtorrent session state
Returns:
A libtorrent sesion state, empty dict if unable to load it.
dict: A libtorrent sesion state, empty dict if unable to load it.
"""
filename = 'session.state'
filepath = get_config_dir(filename)
@ -327,7 +325,7 @@ class Core(component.Component):
try:
with open(_filepath, 'rb') as _file:
state = lt.bdecode(_file.read())
except (OSError, EOFError, RuntimeError) as ex:
except (IOError, EOFError, RuntimeError) as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
else:
log.info('Successfully loaded %s: %s', filename, _filepath)
@ -398,19 +396,18 @@ class Core(component.Component):
# Exported Methods
@export
def add_torrent_file_async(
self, filename: str, filedump: str, options: dict, save_state: bool = True
) -> 'defer.Deferred[Optional[str]]':
def add_torrent_file_async(self, filename, filedump, options, save_state=True):
"""Adds a torrent file to the session asynchronously.
Args:
filename: The filename of the torrent.
filedump: A base64 encoded string of torrent file contents.
options: The options to apply to the torrent upon adding.
save_state: If the state should be saved after adding the file.
filename (str): The filename of the torrent.
filedump (str): A base64 encoded string of torrent file contents.
options (dict): The options to apply to the torrent upon adding.
save_state (bool): If the state should be saved after adding the file.
Returns:
The torrent ID or None.
Deferred: The torrent ID or None.
"""
try:
filedump = b64decode(filedump)
@ -431,39 +428,42 @@ class Core(component.Component):
return d
@export
@maybe_coroutine
async def prefetch_magnet_metadata(
self, magnet: str, timeout: int = 30
) -> Tuple[str, bytes]:
def prefetch_magnet_metadata(self, magnet, timeout=30):
"""Download magnet metadata without adding to Deluge session.
Used by UIs to get magnet files for selection before adding to session.
The metadata is bencoded and for transfer base64 encoded.
Args:
magnet: The magnet URI.
timeout: Number of seconds to wait before canceling request.
magnet (str): The magnet URI.
timeout (int): Number of seconds to wait before canceling request.
Returns:
A tuple of (torrent_id, metadata) for the magnet.
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
"""
return await self.torrentmanager.prefetch_metadata(magnet, timeout)
def on_metadata(result, result_d):
"""Return result of torrent_id and metadata"""
result_d.callback(result)
return result
d = self.torrentmanager.prefetch_metadata(magnet, timeout)
# Use a separate callback chain to handle existing prefetching magnet.
result_d = defer.Deferred()
d.addBoth(on_metadata, result_d)
return result_d
@export
def add_torrent_file(
self, filename: str, filedump: Union[str, bytes], options: dict
) -> Optional[str]:
def add_torrent_file(self, filename, filedump, options):
"""Adds a torrent file to the session.
Args:
filename: The filename of the torrent.
filedump: A base64 encoded string of the torrent file contents.
options: The options to apply to the torrent upon adding.
filename (str): The filename of the torrent.
filedump (str): A base64 encoded string of the torrent file contents.
options (dict): The options to apply to the torrent upon adding.
Returns:
The torrent_id or None.
str: The torrent_id or None.
"""
try:
filedump = b64decode(filedump)
@ -479,26 +479,25 @@ class Core(component.Component):
raise
@export
def add_torrent_files(
self, torrent_files: List[Tuple[str, Union[str, bytes], dict]]
) -> 'defer.Deferred[List[AddTorrentError]]':
def add_torrent_files(self, torrent_files):
"""Adds multiple torrent files to the session asynchronously.
Args:
torrent_files: Torrent files as tuple of
``(filename, filedump, options)``.
torrent_files (list of tuples): Torrent files as tuple of
``(filename, filedump, options)``.
Returns:
A list of errors (if there were any)
Deferred
"""
@maybe_coroutine
async def add_torrents():
@defer.inlineCallbacks
def add_torrents():
errors = []
last_index = len(torrent_files) - 1
for idx, torrent in enumerate(torrent_files):
try:
await self.add_torrent_file_async(
yield self.add_torrent_file_async(
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
)
except AddTorrentError as ex:
@ -509,89 +508,93 @@ class Core(component.Component):
return task.deferLater(reactor, 0, add_torrents)
@export
@maybe_coroutine
async def add_torrent_url(
self, url: str, options: dict, headers: dict = None
) -> 'defer.Deferred[Optional[str]]':
"""Adds a torrent from a URL. Deluge will attempt to fetch the torrent
def add_torrent_url(self, url, options, headers=None):
"""
Adds a torrent from a URL. Deluge will attempt to fetch the torrent
from the URL prior to adding it to the session.
Args:
url: the URL pointing to the torrent file
options: the options to apply to the torrent on add
headers: any optional headers to send
:param url: the URL pointing to the torrent file
:type url: string
:param options: the options to apply to the torrent on add
:type options: dict
:param headers: any optional headers to send
:type headers: dict
Returns:
a Deferred which returns the torrent_id as a str or None
:returns: a Deferred which returns the torrent_id as a str or None
"""
log.info('Attempting to add URL %s', url)
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
try:
filename = await download_file(
url, tmp_file, headers=headers, force_filename=True
)
except Exception:
log.error('Failed to add torrent from URL %s', url)
raise
else:
def on_download_success(filename):
# We got the file, so add it to the session
with open(filename, 'rb') as _file:
data = _file.read()
return self.add_torrent_file(filename, b64encode(data), options)
finally:
try:
os.close(tmp_fd)
os.remove(tmp_file)
os.remove(filename)
except OSError as ex:
log.warning(f'Unable to delete temp file {tmp_file}: , {ex}')
log.warning('Could not remove temp file: %s', ex)
return self.add_torrent_file(filename, b64encode(data), options)
def on_download_fail(failure):
# Log the error and pass the failure onto the client
log.error('Failed to add torrent from URL %s', url)
return failure
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
os.close(tmp_fd)
d = download_file(url, tmp_file, headers=headers, force_filename=True)
d.addCallbacks(on_download_success, on_download_fail)
return d
@export
def add_torrent_magnet(self, uri: str, options: dict) -> str:
"""Adds a torrent from a magnet link.
def add_torrent_magnet(self, uri, options):
"""
Adds a torrent from a magnet link.
Args:
uri: the magnet link
options: the options to apply to the torrent on add
:param uri: the magnet link
:type uri: string
:param options: the options to apply to the torrent on add
:type options: dict
:returns: the torrent_id
:rtype: string
Returns:
the torrent_id
"""
log.debug('Attempting to add by magnet URI: %s', uri)
return self.torrentmanager.add(magnet=uri, options=options)
@export
def remove_torrent(self, torrent_id: str, remove_data: bool) -> bool:
def remove_torrent(self, torrent_id, remove_data):
"""Removes a single torrent from the session.
Args:
torrent_id: The torrent ID to remove.
remove_data: If True, also remove the downloaded data.
torrent_id (str): The torrent ID to remove.
remove_data (bool): If True, also remove the downloaded data.
Returns:
True if removed successfully.
bool: True if removed successfully.
Raises:
InvalidTorrentError: If the torrent ID does not exist in the session.
"""
log.debug('Removing torrent %s from the core.', torrent_id)
return self.torrentmanager.remove(torrent_id, remove_data)
@export
def remove_torrents(
self, torrent_ids: List[str], remove_data: bool
) -> 'defer.Deferred[List[Tuple[str, str]]]':
def remove_torrents(self, torrent_ids, remove_data):
"""Remove multiple torrents from the session.
Args:
torrent_ids: The torrent IDs to remove.
remove_data: If True, also remove the downloaded data.
torrent_ids (list): The torrent IDs to remove.
remove_data (bool): If True, also remove the downloaded data.
Returns:
An empty list if no errors occurred otherwise the list contains
tuples of strings, a torrent ID and an error message. For example:
list: An empty list if no errors occurred otherwise the list contains
tuples of strings, a torrent ID and an error message. For example:
[('<torrent_id>', 'Error removing torrent')]
[('<torrent_id>', 'Error removing torrent')]
"""
log.info('Removing %d torrents from core.', len(torrent_ids))
@ -615,17 +618,17 @@ class Core(component.Component):
return task.deferLater(reactor, 0, do_remove_torrents)
@export
def get_session_status(self, keys: List[str]) -> Dict[str, Union[int, float]]:
def get_session_status(self, keys):
"""Gets the session status values for 'keys', these keys are taking
from libtorrent's session status.
See: http://www.rasterbar.com/products/libtorrent/manual.html#status
Args:
keys: the keys for which we want values
:param keys: the keys for which we want values
:type keys: list
:returns: a dictionary of {key: value, ...}
:rtype: dict
Returns:
a dictionary of {key: value, ...}
"""
if not keys:
return self.session_status
@ -646,13 +649,13 @@ class Core(component.Component):
return status
@export
def force_reannounce(self, torrent_ids: List[str]) -> None:
def force_reannounce(self, torrent_ids):
log.debug('Forcing reannouncment to: %s', torrent_ids)
for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_reannounce()
@export
def pause_torrent(self, torrent_id: str) -> None:
def pause_torrent(self, torrent_id):
"""Pauses a torrent"""
log.debug('Pausing: %s', torrent_id)
if not isinstance(torrent_id, str):
@ -661,7 +664,7 @@ class Core(component.Component):
self.torrentmanager[torrent_id].pause()
@export
def pause_torrents(self, torrent_ids: List[str] = None) -> None:
def pause_torrents(self, torrent_ids=None):
"""Pauses a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
@ -669,27 +672,27 @@ class Core(component.Component):
self.pause_torrent(torrent_id)
@export
def connect_peer(self, torrent_id: str, ip: str, port: int):
def connect_peer(self, torrent_id, ip, port):
log.debug('adding peer %s to %s', ip, torrent_id)
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
@export
def move_storage(self, torrent_ids: List[str], dest: str):
def move_storage(self, torrent_ids, dest):
log.debug('Moving storage %s to %s', torrent_ids, dest)
for torrent_id in torrent_ids:
if not self.torrentmanager[torrent_id].move_storage(dest):
log.warning('Error moving torrent %s to %s', torrent_id, dest)
@export
def pause_session(self) -> None:
def pause_session(self):
"""Pause the entire session"""
if not self.session.is_paused():
self.session.pause()
component.get('EventManager').emit(SessionPausedEvent())
@export
def resume_session(self) -> None:
def resume_session(self):
"""Resume the entire session"""
if self.session.is_paused():
self.session.resume()
@ -698,12 +701,12 @@ class Core(component.Component):
component.get('EventManager').emit(SessionResumedEvent())
@export
def is_session_paused(self) -> bool:
def is_session_paused(self):
"""Returns the activity of the session"""
return self.session.is_paused()
@export
def resume_torrent(self, torrent_id: str) -> None:
def resume_torrent(self, torrent_id):
"""Resumes a torrent"""
log.debug('Resuming: %s', torrent_id)
if not isinstance(torrent_id, str):
@ -712,7 +715,7 @@ class Core(component.Component):
self.torrentmanager[torrent_id].resume()
@export
def resume_torrents(self, torrent_ids: List[str] = None) -> None:
def resume_torrents(self, torrent_ids=None):
"""Resumes a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
@ -745,9 +748,7 @@ class Core(component.Component):
return status
@export
def get_torrent_status(
self, torrent_id: str, keys: List[str], diff: bool = False
) -> dict:
def get_torrent_status(self, torrent_id, keys, diff=False):
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
keys, [torrent_id]
)
@ -761,54 +762,57 @@ class Core(component.Component):
)
@export
@maybe_coroutine
async def get_torrents_status(
self, filter_dict: dict, keys: List[str], diff: bool = False
) -> dict:
"""returns all torrents , optionally filtered by filter_dict."""
all_keys = not keys
def get_torrents_status(self, filter_dict, keys, diff=False):
"""
returns all torrents , optionally filtered by filter_dict.
"""
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
status_dict, plugin_keys = await self.torrentmanager.torrents_status_update(
torrent_ids, keys, diff=diff
)
# Ask the plugin manager to fill in the plugin keys
if len(plugin_keys) > 0 or all_keys:
for key in status_dict:
status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
return status_dict
d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff)
def add_plugin_fields(args):
status_dict, plugin_keys = args
# Ask the plugin manager to fill in the plugin keys
if len(plugin_keys) > 0:
for key in status_dict:
status_dict[key].update(
self.pluginmanager.get_status(key, plugin_keys)
)
return status_dict
d.addCallback(add_plugin_fields)
return d
@export
def get_filter_tree(
self, show_zero_hits: bool = True, hide_cat: List[str] = None
) -> Dict:
"""returns {field: [(value,count)] }
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
"""
returns {field: [(value,count)] }
for use in sidebar(s)
"""
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
@export
def get_session_state(self) -> List[str]:
def get_session_state(self):
"""Returns a list of torrent_ids in the session."""
# Get the torrent list from the TorrentManager
return self.torrentmanager.get_torrent_list()
@export
def get_config(self) -> dict:
def get_config(self):
"""Get all the preferences as a dictionary"""
return self.config.config
@export
def get_config_value(self, key: str) -> Any:
def get_config_value(self, key):
"""Get the config value for key"""
return self.config.get(key)
@export
def get_config_values(self, keys: List[str]) -> Dict[str, Any]:
def get_config_values(self, keys):
"""Get the config values for the entered keys"""
return {key: self.config.get(key) for key in keys}
@export
def set_config(self, config: Dict[str, Any]):
def set_config(self, config):
"""Set the config with values from dictionary"""
# Load all the values into the configuration
for key in config:
@ -817,20 +821,21 @@ class Core(component.Component):
self.config[key] = config[key]
@export
def get_listen_port(self) -> int:
def get_listen_port(self):
"""Returns the active listen port"""
return self.session.listen_port()
@export
def get_proxy(self) -> Dict[str, Any]:
def get_proxy(self):
"""Returns the proxy settings
Returns:
Proxy settings.
dict: Contains proxy settings.
Notes:
Proxy type names:
0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P
"""
settings = self.session.get_settings()
@ -853,38 +858,36 @@ class Core(component.Component):
return proxy_dict
@export
def get_available_plugins(self) -> List[str]:
def get_available_plugins(self):
"""Returns a list of plugins available in the core"""
return self.pluginmanager.get_available_plugins()
@export
def get_enabled_plugins(self) -> List[str]:
def get_enabled_plugins(self):
"""Returns a list of enabled plugins in the core"""
return self.pluginmanager.get_enabled_plugins()
@export
def enable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
def enable_plugin(self, plugin):
return self.pluginmanager.enable_plugin(plugin)
@export
def disable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
def disable_plugin(self, plugin):
return self.pluginmanager.disable_plugin(plugin)
@export
def force_recheck(self, torrent_ids: List[str]) -> None:
def force_recheck(self, torrent_ids):
"""Forces a data recheck on torrent_ids"""
for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_recheck()
@export
def set_torrent_options(
self, torrent_ids: List[str], options: Dict[str, Any]
) -> None:
def set_torrent_options(self, torrent_ids, options):
"""Sets the torrent options for torrent_ids
Args:
torrent_ids: A list of torrent_ids to set the options for.
options: A dict of torrent options to set. See
torrent_ids (list): A list of torrent_ids to set the options for.
options (dict): A dict of torrent options to set. See
``torrent.TorrentOptions`` class for valid keys.
"""
if 'owner' in options and not self.authmanager.has_account(options['owner']):
@ -897,14 +900,12 @@ class Core(component.Component):
self.torrentmanager[torrent_id].set_options(options)
@export
def set_torrent_trackers(
self, torrent_id: str, trackers: List[Dict[str, Any]]
) -> None:
def set_torrent_trackers(self, torrent_id, trackers):
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
return self.torrentmanager[torrent_id].set_trackers(trackers)
@export
def get_magnet_uri(self, torrent_id: str) -> str:
def get_magnet_uri(self, torrent_id):
return self.torrentmanager[torrent_id].get_magnet_uri()
@deprecated
@ -991,33 +992,31 @@ class Core(component.Component):
path,
tracker,
piece_length,
comment=None,
target=None,
webseeds=None,
private=False,
created_by=None,
trackers=None,
add_to_session=False,
torrent_format=metafile.TorrentFormat.V1,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
):
if isinstance(torrent_format, str):
torrent_format = metafile.TorrentFormat(torrent_format)
log.debug('creating torrent..')
return threads.deferToThread(
self._create_torrent_thread,
path,
tracker,
piece_length,
comment=comment,
target=target,
webseeds=webseeds,
private=private,
created_by=created_by,
trackers=trackers,
add_to_session=add_to_session,
torrent_format=torrent_format,
)
threading.Thread(
target=self._create_torrent_thread,
args=(
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
),
).start()
def _create_torrent_thread(
self,
@ -1031,44 +1030,30 @@ class Core(component.Component):
created_by,
trackers,
add_to_session,
torrent_format,
):
from deluge import metafile
filecontent = metafile.make_meta_file_content(
metafile.make_meta_file(
path,
tracker,
piece_length,
comment=comment,
target=target,
webseeds=webseeds,
private=private,
created_by=created_by,
trackers=trackers,
torrent_format=torrent_format,
)
write_file = False
if target or not add_to_session:
write_file = True
if not target:
target = metafile.default_meta_file_path(path)
filename = os.path.split(target)[-1]
if write_file:
with open(target, 'wb') as _file:
_file.write(filecontent)
filedump = b64encode(filecontent)
log.debug('torrent created!')
if add_to_session:
options = {}
options['download_location'] = os.path.split(path)[0]
self.add_torrent_file(filename, filedump, options)
return filename, filedump
with open(target, 'rb') as _file:
filedump = b64encode(_file.read())
self.add_torrent_file(os.path.split(target)[1], filedump, options)
@export
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
def upload_plugin(self, filename, filedump):
"""This method is used to upload new plugins to the daemon. It is used
when connecting to the daemon remotely and installing a new plugin on
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
@ -1086,24 +1071,26 @@ class Core(component.Component):
component.get('CorePluginManager').scan_for_plugins()
@export
def rescan_plugins(self) -> None:
"""Re-scans the plugin folders for new plugins"""
def rescan_plugins(self):
"""
Re-scans the plugin folders for new plugins
"""
component.get('CorePluginManager').scan_for_plugins()
@export
def rename_files(
self, torrent_id: str, filenames: List[Tuple[int, str]]
) -> defer.Deferred:
"""Rename files in ``torrent_id``. Since this is an asynchronous operation by
def rename_files(self, torrent_id, filenames):
"""
Rename files in ``torrent_id``. Since this is an asynchronous operation by
libtorrent, watch for the TorrentFileRenamedEvent to know when the
files have been renamed.
Args:
torrent_id: the torrent_id to rename files
filenames: a list of index, filename pairs
:param torrent_id: the torrent_id to rename files
:type torrent_id: string
:param filenames: a list of index, filename pairs
:type filenames: ((index, filename), ...)
:raises InvalidTorrentError: if torrent_id is invalid
Raises:
InvalidTorrentError: if torrent_id is invalid
"""
if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session')
@ -1114,20 +1101,21 @@ class Core(component.Component):
return task.deferLater(reactor, 0, rename)
@export
def rename_folder(
self, torrent_id: str, folder: str, new_folder: str
) -> defer.Deferred:
"""Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
def rename_folder(self, torrent_id, folder, new_folder):
"""
Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
TorrentFolderRenamedEvent which is emitted when the folder has been
renamed successfully.
Args:
torrent_id: the torrent to rename folder in
folder: the folder to rename
new_folder: the new folder name
:param torrent_id: the torrent to rename folder in
:type torrent_id: string
:param folder: the folder to rename
:type folder: string
:param new_folder: the new folder name
:type new_folder: string
:raises InvalidTorrentError: if the torrent_id is invalid
Raises:
InvalidTorrentError: if the torrent_id is invalid
"""
if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session')
@ -1135,7 +1123,7 @@ class Core(component.Component):
return self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
@export
def queue_top(self, torrent_ids: List[str]) -> None:
def queue_top(self, torrent_ids):
log.debug('Attempting to queue %s to top', torrent_ids)
# torrent_ids must be sorted in reverse before moving to preserve order
for torrent_id in sorted(
@ -1149,7 +1137,7 @@ class Core(component.Component):
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export
def queue_up(self, torrent_ids: List[str]) -> None:
def queue_up(self, torrent_ids):
log.debug('Attempting to queue %s to up', torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1174,7 +1162,7 @@ class Core(component.Component):
prev_queue_position = queue_position
@export
def queue_down(self, torrent_ids: List[str]) -> None:
def queue_down(self, torrent_ids):
log.debug('Attempting to queue %s to down', torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1199,7 +1187,7 @@ class Core(component.Component):
prev_queue_position = queue_position
@export
def queue_bottom(self, torrent_ids: List[str]) -> None:
def queue_bottom(self, torrent_ids):
log.debug('Attempting to queue %s to bottom', torrent_ids)
# torrent_ids must be sorted before moving to preserve order
for torrent_id in sorted(
@ -1213,15 +1201,17 @@ class Core(component.Component):
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export
def glob(self, path: str) -> List[str]:
def glob(self, path):
return glob.glob(path)
@export
def test_listen_port(self) -> 'defer.Deferred[Optional[bool]]':
"""Checks if the active port is open
def test_listen_port(self):
"""
Checks if the active port is open
:returns: True if the port is open, False if not
:rtype: bool
Returns:
True if the port is open, False if not
"""
port = self.get_listen_port()
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
@ -1240,17 +1230,18 @@ class Core(component.Component):
return d
@export
def get_free_space(self, path: str = None) -> int:
"""Returns the number of free bytes at path
def get_free_space(self, path=None):
"""
Returns the number of free bytes at path
Args:
path: the path to check free space at, if None, use the default download location
:param path: the path to check free space at, if None, use the default download location
:type path: string
Returns:
the number of free bytes at path
:returns: the number of free bytes at path
:rtype: int
:raises InvalidPathError: if the path is invalid
Raises:
InvalidPathError: if the path is invalid
"""
if not path:
path = self.config['download_location']
@ -1263,40 +1254,46 @@ class Core(component.Component):
self.external_ip = external_ip
@export
def get_external_ip(self) -> str:
"""Returns the external IP address received from libtorrent."""
def get_external_ip(self):
"""
Returns the external IP address received from libtorrent.
"""
return self.external_ip
@export
def get_libtorrent_version(self) -> str:
"""Returns the libtorrent version.
def get_libtorrent_version(self):
"""
Returns the libtorrent version.
:returns: the version
:rtype: string
Returns:
the version
"""
return LT_VERSION
@export
def get_completion_paths(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""Returns the available path completions for the input value."""
def get_completion_paths(self, args):
"""
Returns the available path completions for the input value.
"""
return path_chooser_common.get_completion_paths(args)
@export(AUTH_LEVEL_ADMIN)
def get_known_accounts(self) -> List[Dict[str, Any]]:
def get_known_accounts(self):
return self.authmanager.get_known_accounts()
@export(AUTH_LEVEL_NONE)
def get_auth_levels_mappings(self) -> Tuple[Dict[str, int], Dict[int, str]]:
def get_auth_levels_mappings(self):
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
@export(AUTH_LEVEL_ADMIN)
def create_account(self, username: str, password: str, authlevel: str) -> bool:
def create_account(self, username, password, authlevel):
return self.authmanager.create_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def update_account(self, username: str, password: str, authlevel: str) -> bool:
def update_account(self, username, password, authlevel):
return self.authmanager.update_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def remove_account(self, username: str) -> bool:
def remove_account(self, username):
return self.authmanager.remove_account(username)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,7 +8,6 @@
#
"""The Deluge daemon"""
import logging
import os
import socket
@ -42,8 +42,8 @@ def is_daemon_running(pid_file):
try:
with open(pid_file) as _file:
pid, port = (int(x) for x in _file.readline().strip().split(';'))
except (OSError, ValueError):
pid, port = [int(x) for x in _file.readline().strip().split(';')]
except (EnvironmentError, ValueError):
return False
if is_process_running(pid):
@ -51,7 +51,7 @@ def is_daemon_running(pid_file):
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
_socket.connect(('127.0.0.1', port))
except OSError:
except socket.error:
# Can't connect, so pid is not a deluged process.
return False
else:
@ -60,7 +60,7 @@ def is_daemon_running(pid_file):
return True
class Daemon:
class Daemon(object):
"""The Deluge Daemon class"""
def __init__(
@ -154,7 +154,7 @@ class Daemon:
pid = os.getpid()
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
with open(self.pid_file, 'w') as _file:
_file.write(f'{pid};{self.port}\n')
_file.write('%s;%s\n' % (pid, self.port))
component.start()

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -8,7 +9,6 @@
"""PluginManager for Core"""
import logging
from twisted.internet import defer

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
#
@ -23,14 +24,10 @@ import deluge.configmanager
from deluge._libtorrent import lt
from deluge.event import ConfigValueChangedEvent
GeoIP = None
try:
from GeoIP import GeoIP
import GeoIP
except ImportError:
try:
from pygeoip import GeoIP
except ImportError:
pass
GeoIP = None
log = logging.getLogger(__name__)
@ -198,7 +195,7 @@ class PreferencesManager(component.Component):
self.__set_listen_on()
def __set_listen_on(self):
"""Set the ports and interface address to listen for incoming connections on."""
""" Set the ports and interface address to listen for incoming connections on."""
if self.config['random_port']:
if not self.config['listen_random_port']:
self.config['listen_random_port'] = random.randrange(49152, 65525)
@ -221,7 +218,7 @@ class PreferencesManager(component.Component):
self.config['listen_use_sys_port'],
)
interfaces = [
f'{interface}:{port}'
'%s:%s' % (interface, port)
for port in range(listen_ports[0], listen_ports[1] + 1)
]
self.core.apply_session_settings(
@ -396,7 +393,7 @@ class PreferencesManager(component.Component):
+ quote_plus(':'.join(self.config['enabled_plugins']))
)
urlopen(url)
except OSError as ex:
except IOError as ex:
log.debug('Network error while trying to send info: %s', ex)
else:
self.config['info_sent'] = now
@ -460,9 +457,11 @@ class PreferencesManager(component.Component):
# Load the GeoIP DB for country look-ups if available
if os.path.exists(geoipdb_path):
try:
self.core.geoip_instance = GeoIP(geoipdb_path, 0)
except Exception as ex:
log.warning('GeoIP Unavailable: %s', ex)
self.core.geoip_instance = GeoIP.open(
geoipdb_path, GeoIP.GEOIP_STANDARD
)
except AttributeError:
log.warning('GeoIP Unavailable')
else:
log.warning('Unable to find GeoIP database file: %s', geoipdb_path)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,14 +8,12 @@
#
"""RPCServer Module"""
import logging
import os
import sys
import traceback
from collections import namedtuple
from types import FunctionType
from typing import Callable, TypeVar, overload
from twisted.internet import defer, reactor
from twisted.internet.protocol import Factory, connectionDone
@ -43,16 +42,6 @@ RPC_EVENT = 3
log = logging.getLogger(__name__)
TCallable = TypeVar('TCallable', bound=Callable)
@overload
def export(func: TCallable) -> TCallable: ...
@overload
def export(auth_level: int) -> Callable[[TCallable], TCallable]: ...
def export(auth_level=AUTH_LEVEL_DEFAULT):
"""
@ -76,7 +65,7 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
if func.__doc__:
if func.__doc__.endswith(' '):
indent = func.__doc__.split('\n')[-1]
func.__doc__ += f'\n{indent}'
func.__doc__ += '\n{}'.format(indent)
else:
func.__doc__ += '\n\n'
func.__doc__ += rpc_text
@ -121,7 +110,7 @@ def format_request(call):
class DelugeRPCProtocol(DelugeTransferProtocol):
def __init__(self):
super().__init__()
super(DelugeRPCProtocol, self).__init__()
# namedtuple subclass with auth_level, username for the connected session.
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
@ -273,9 +262,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
raise IncompatibleClient(deluge.common.get_version())
ret = component.get('AuthManager').authorize(*args, **kwargs)
if ret:
self.factory.authorized_sessions[self.transport.sessionno] = (
self.AuthLevel(ret, args[0])
)
self.factory.authorized_sessions[
self.transport.sessionno
] = self.AuthLevel(ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self
except Exception as ex:
send_error()
@ -544,8 +533,8 @@ class RPCServer(component.Component):
:type event: :class:`deluge.event.DelugeEvent`
"""
log.debug('intevents: %s', self.factory.interested_events)
# Use copy of `interested_events` since it can mutate while iterating.
for session_id, interest in self.factory.interested_events.copy().items():
# Find sessions interested in this event
for session_id, interest in self.factory.interested_events.items():
if event.name in interest:
log.debug('Emit Event: %s %s', event.name, event.args)
# This session is interested so send a RPC_EVENT

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -16,8 +17,6 @@ Attributes:
import logging
import os
import socket
import time
from typing import Optional
from urllib.parse import urlparse
from twisted.internet.defer import Deferred, DeferredList
@ -82,7 +81,7 @@ def convert_lt_files(files):
"""Indexes and decodes files from libtorrent get_files().
Args:
files (file_storage): The libtorrent torrent files.
files (list): The libtorrent torrent files.
Returns:
list of dict: The files.
@ -97,18 +96,18 @@ def convert_lt_files(files):
}
"""
filelist = []
for index in range(files.num_files()):
for index, _file in enumerate(files):
try:
file_path = files.file_path(index).decode('utf8')
file_path = _file.path.decode('utf8')
except AttributeError:
file_path = files.file_path(index)
file_path = _file.path
filelist.append(
{
'index': index,
'path': file_path.replace('\\', '/'),
'size': files.file_size(index),
'offset': files.file_offset(index),
'size': _file.size,
'offset': _file.offset,
}
)
@ -149,7 +148,7 @@ class TorrentOptions(dict):
"""
def __init__(self):
super().__init__()
super(TorrentOptions, self).__init__()
config = ConfigManager('core.conf').config
options_conf_map = {
'add_paused': 'add_paused',
@ -179,14 +178,14 @@ class TorrentOptions(dict):
self['seed_mode'] = False
class TorrentError:
class TorrentError(object):
def __init__(self, error_message, was_paused=False, restart_to_resume=False):
self.error_message = error_message
self.was_paused = was_paused
self.restart_to_resume = restart_to_resume
class Torrent:
class Torrent(object):
"""Torrent holds information about torrents added to the libtorrent session.
Args:
@ -236,10 +235,9 @@ class Torrent:
self.handle = handle
self.magnet = magnet
self._status: Optional['lt.torrent_status'] = None
self._status_last_update: float = 0.0
self.status = self.handle.status()
self.torrent_info = self.handle.torrent_file()
self.torrent_info = self.handle.get_torrent_info()
self.has_metadata = self.status.has_metadata
self.options = TorrentOptions()
@ -270,6 +268,7 @@ class Torrent:
self.prev_status = {}
self.waiting_on_folder_rename = []
self.update_status(self.handle.status())
self._create_status_funcs()
self.set_options(self.options)
self.update_state()
@ -277,18 +276,6 @@ class Torrent:
if log.isEnabledFor(logging.DEBUG):
log.debug('Torrent object created.')
def _set_handle_flags(self, flag: lt.torrent_flags, set_flag: bool):
"""set or unset a flag to the lt handle
Args:
flag (lt.torrent_flags): the flag to set/unset
set_flag (bool): True for setting the flag, False for unsetting it
"""
if set_flag:
self.handle.set_flags(flag)
else:
self.handle.unset_flags(flag)
def on_metadata_received(self):
"""Process the metadata received alert for this torrent"""
self.has_metadata = True
@ -373,7 +360,7 @@ class Torrent:
"""Sets maximum download speed for this torrent.
Args:
m_down_speed (float): Maximum download speed in KiB/s.
m_up_speed (float): Maximum download speed in KiB/s.
"""
self.options['max_download_speed'] = m_down_speed
if m_down_speed < 0:
@ -405,7 +392,7 @@ class Torrent:
return
# A list of priorities for each piece in the torrent
priorities = self.handle.get_piece_priorities()
priorities = self.handle.piece_priorities()
def get_file_piece(idx, byte_offset):
return self.torrent_info.map_file(idx, byte_offset, 0).piece
@ -438,10 +425,7 @@ class Torrent:
sequential (bool): Enable sequential downloading.
"""
self.options['sequential_download'] = sequential
self._set_handle_flags(
flag=lt.torrent_flags.sequential_download,
set_flag=sequential,
)
self.handle.set_sequential_download(sequential)
def set_auto_managed(self, auto_managed):
"""Set auto managed mode, i.e. will be started or queued automatically.
@ -451,10 +435,7 @@ class Torrent:
"""
self.options['auto_managed'] = auto_managed
if not (self.status.paused and not self.status.auto_managed):
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=auto_managed,
)
self.handle.auto_managed(auto_managed)
self.update_state()
def set_super_seeding(self, super_seeding):
@ -464,10 +445,7 @@ class Torrent:
super_seeding (bool): Enable super seeding.
"""
self.options['super_seeding'] = super_seeding
self._set_handle_flags(
flag=lt.torrent_flags.super_seeding,
set_flag=super_seeding,
)
self.handle.super_seeding(super_seeding)
def set_stop_ratio(self, stop_ratio):
"""The seeding ratio to stop (or remove) the torrent at.
@ -528,7 +506,7 @@ class Torrent:
self.handle.prioritize_files(file_priorities)
else:
log.debug('Unable to set new file priorities.')
file_priorities = self.handle.get_file_priorities()
file_priorities = self.handle.file_priorities()
if 0 in self.options['file_priorities']:
# Previously marked a file 'skip' so check for any 0's now >0.
@ -578,7 +556,7 @@ class Torrent:
trackers (list of dicts): A list of trackers.
"""
if trackers is None:
self.trackers = list(self.handle.trackers())
self.trackers = [tracker for tracker in self.handle.trackers()]
self.tracker_host = None
return
@ -643,7 +621,7 @@ class Torrent:
def update_state(self):
"""Updates the state, based on libtorrent's torrent state"""
status = self.get_lt_status()
status = self.handle.status()
session_paused = component.get('Core').session.is_paused()
old_state = self.state
self.set_status_message()
@ -655,10 +633,7 @@ class Torrent:
elif status_error:
self.state = 'Error'
# auto-manage status will be reverted upon resuming.
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.handle.auto_managed(False)
self.set_status_message(decode_bytes(status_error))
elif status.moving_storage:
self.state = 'Moving'
@ -711,11 +686,8 @@ class Torrent:
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
session can resume.
"""
status = self.get_lt_status()
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
status = self.handle.status()
self.handle.auto_managed(False)
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
if not status.paused:
self.handle.pause()
@ -729,10 +701,7 @@ class Torrent:
log.error('Restart deluge to clear this torrent error')
if not self.forced_error.was_paused and self.options['auto_managed']:
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
self.handle.auto_managed(True)
self.forced_error = None
self.set_status_message('OK')
if update_state:
@ -856,7 +825,7 @@ class Torrent:
'client': client,
'country': country,
'down_speed': peer.payload_down_speed,
'ip': f'{peer.ip[0]}:{peer.ip[1]}',
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
'progress': peer.progress,
'seed': peer.flags & peer.seed,
'up_speed': peer.payload_up_speed,
@ -875,7 +844,7 @@ class Torrent:
def get_file_priorities(self):
"""Return the file priorities"""
if not self.handle.status().has_metadata:
if not self.handle.has_metadata():
return []
if not self.options['file_priorities']:
@ -928,7 +897,7 @@ class Torrent:
# Check if hostname is an IP address and just return it if that's the case
try:
socket.inet_aton(host)
except OSError:
except socket.error:
pass
else:
# This is an IP address because an exception wasn't raised
@ -964,10 +933,10 @@ class Torrent:
if self.has_metadata:
# Use the top-level folder as torrent name.
filename = decode_bytes(self.torrent_info.files().file_path(0))
filename = decode_bytes(self.torrent_info.file_at(0).path)
name = filename.replace('\\', '/', 1).split('/', 1)[0]
else:
name = decode_bytes(self.handle.status().name)
name = decode_bytes(self.handle.name())
if not name:
name = self.torrent_id
@ -1026,7 +995,7 @@ class Torrent:
dict: a dictionary of the status keys and their values
"""
if update:
self.get_lt_status()
self.update_status(self.handle.status())
if all_keys:
keys = list(self.status_funcs)
@ -1056,35 +1025,13 @@ class Torrent:
return status_dict
def get_lt_status(self) -> 'lt.torrent_status':
"""Get the torrent status fresh, not from cache.
This should be used when a guaranteed fresh status is needed rather than
`torrent.handle.status()` because it will update the cache as well.
"""
self.status = self.handle.status()
return self.status
@property
def status(self) -> 'lt.torrent_status':
"""Cached copy of the libtorrent status for this torrent.
If it has not been updated within the last five seconds, it will be
automatically refreshed.
"""
if self._status_last_update < (time.time() - 5):
self.status = self.handle.status()
return self._status
@status.setter
def status(self, status: 'lt.torrent_status') -> None:
def update_status(self, status):
"""Updates the cached status.
Args:
status: a libtorrent torrent status
status (libtorrent.torrent_status): a libtorrent torrent status
"""
self._status = status
self._status_last_update = time.time()
self.status = status
def _create_status_funcs(self):
"""Creates the functions for getting torrent status"""
@ -1138,8 +1085,9 @@ class Torrent:
'download_location': lambda: self.options['download_location'],
'seeds_peers_ratio': lambda: -1.0
if self.status.num_incomplete == 0
# Use -1.0 to signify infinity
else (self.status.num_complete / self.status.num_incomplete),
else ( # Use -1.0 to signify infinity
self.status.num_complete / self.status.num_incomplete
),
'seed_rank': lambda: self.status.seed_rank,
'state': lambda: self.state,
'stop_at_ratio': lambda: self.options['stop_at_ratio'],
@ -1205,10 +1153,7 @@ class Torrent:
"""
# Turn off auto-management so the torrent will not be unpaused by lt queueing
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.handle.auto_managed(False)
if self.state == 'Error':
log.debug('Unable to pause torrent while in Error state')
elif self.status.paused:
@ -1243,10 +1188,7 @@ class Torrent:
else:
# Check if torrent was originally being auto-managed.
if self.options['auto_managed']:
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
self.handle.auto_managed(True)
try:
self.handle.resume()
except RuntimeError as ex:
@ -1350,7 +1292,7 @@ class Torrent:
try:
with open(filepath, 'wb') as save_file:
save_file.write(filedump)
except OSError as ex:
except IOError as ex:
log.error('Unable to save torrent file to: %s', ex)
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
@ -1543,18 +1485,20 @@ class Torrent:
self.status.pieces, self.handle.piece_availability()
):
if piece:
# Completed.
pieces.append(3)
pieces.append(3) # Completed.
elif avail_piece:
# Available, just not downloaded nor being downloaded.
pieces.append(1)
pieces.append(
1
) # Available, just not downloaded nor being downloaded.
else:
# Missing, no known peer with piece, or not asked for yet.
pieces.append(0)
pieces.append(
0
) # Missing, no known peer with piece, or not asked for yet.
for peer_info in self.handle.get_peer_info():
if peer_info.downloading_piece_index >= 0:
# Being downloaded from peer.
pieces[peer_info.downloading_piece_index] = 2
pieces[
peer_info.downloading_piece_index
] = 2 # Being downloaded from peer.
return pieces

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,18 +8,16 @@
#
"""TorrentManager handles Torrent objects"""
import datetime
import logging
import operator
import os
import pickle
import time
from base64 import b64encode
from collections import namedtuple
from tempfile import gettempdir
from typing import Dict, List, NamedTuple, Tuple
from twisted.internet import defer, reactor, threads
from twisted.internet import defer, error, reactor, threads
from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.task import LoopingCall
@ -34,7 +33,6 @@ from deluge.common import (
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath
from deluge.decorators import maybe_coroutine
from deluge.error import AddTorrentError, InvalidTorrentError
from deluge.event import (
ExternalIPEvent,
@ -51,18 +49,13 @@ from deluge.event import (
log = logging.getLogger(__name__)
LT_DEFAULT_ADD_TORRENT_FLAGS = (
lt.torrent_flags.paused
| lt.torrent_flags.auto_managed
| lt.torrent_flags.update_subscribe
| lt.torrent_flags.apply_ip_filter
lt.add_torrent_params_flags_t.flag_paused
| lt.add_torrent_params_flags_t.flag_auto_managed
| lt.add_torrent_params_flags_t.flag_update_subscribe
| lt.add_torrent_params_flags_t.flag_apply_ip_filter
)
class PrefetchQueueItem(NamedTuple):
alert_deferred: Deferred
result_queue: List[Deferred]
class TorrentState: # pylint: disable=old-style-class
"""Create a torrent state.
@ -140,8 +133,7 @@ class TorrentManager(component.Component):
"""
# This is used in the test to mock out timeouts
clock = reactor
callLater = reactor.callLater # noqa: N815
def __init__(self):
component.Component.__init__(
@ -170,7 +162,7 @@ class TorrentManager(component.Component):
self.is_saving_state = False
self.save_resume_data_file_lock = defer.DeferredLock()
self.torrents_loading = {}
self.prefetching_metadata: Dict[str, PrefetchQueueItem] = {}
self.prefetching_metadata = {}
# This is a map of torrent_ids to Deferreds used to track needed resume data.
# The Deferreds will be completed when resume data has been saved.
@ -203,32 +195,34 @@ class TorrentManager(component.Component):
# Register alert functions
alert_handles = [
'external_ip',
'performance',
'add_torrent',
'metadata_received',
'torrent_finished',
'torrent_paused',
'torrent_checked',
'torrent_resumed',
'tracker_reply',
'tracker_announce',
'tracker_warning',
'tracker_error',
'file_renamed',
'file_error',
'file_completed',
'storage_moved',
'storage_moved_failed',
'state_update',
'state_changed',
'save_resume_data',
'save_resume_data_failed',
'fastresume_rejected',
'external_ip_alert',
'performance_alert',
'add_torrent_alert',
'metadata_received_alert',
'torrent_finished_alert',
'torrent_paused_alert',
'torrent_checked_alert',
'torrent_resumed_alert',
'tracker_reply_alert',
'tracker_announce_alert',
'tracker_warning_alert',
'tracker_error_alert',
'file_renamed_alert',
'file_error_alert',
'file_completed_alert',
'storage_moved_alert',
'storage_moved_failed_alert',
'state_update_alert',
'state_changed_alert',
'save_resume_data_alert',
'save_resume_data_failed_alert',
'fastresume_rejected_alert',
]
for alert_handle in alert_handles:
on_alert_func = getattr(self, ''.join(['on_alert_', alert_handle]))
on_alert_func = getattr(
self, ''.join(['on_alert_', alert_handle.replace('_alert', '')])
)
self.alerts.register_handler(alert_handle, on_alert_func)
# Define timers
@ -253,8 +247,8 @@ class TorrentManager(component.Component):
self.save_resume_data_timer.start(190, False)
self.prev_status_cleanup_loop.start(10)
@maybe_coroutine
async def stop(self):
@defer.inlineCallbacks
def stop(self):
# Stop timers
if self.save_state_timer.running:
self.save_state_timer.stop()
@ -266,11 +260,11 @@ class TorrentManager(component.Component):
self.prev_status_cleanup_loop.stop()
# Save state on shutdown
await self.save_state()
yield self.save_state()
self.session.pause()
result = await self.save_resume_data(flush_disk_cache=True)
result = yield self.save_resume_data(flush_disk_cache=True)
# Remove the temp_file to signify successfully saved state
if result and os.path.isfile(self.temp_file):
os.remove(self.temp_file)
@ -284,6 +278,11 @@ class TorrentManager(component.Component):
'Paused',
'Queued',
):
# If the global setting is set, but the per-torrent isn't...
# Just skip to the next torrent.
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
if not torrent.options['stop_at_ratio']:
continue
if (
torrent.get_ratio() >= torrent.options['stop_ratio']
and torrent.is_finished
@ -291,8 +290,8 @@ class TorrentManager(component.Component):
if torrent.options['remove_at_ratio']:
self.remove(torrent_id)
break
torrent.pause()
if not torrent.handle.status().paused:
torrent.pause()
def __getitem__(self, torrent_id):
"""Return the Torrent with torrent_id.
@ -344,64 +343,66 @@ class TorrentManager(component.Component):
else:
return torrent_info
@maybe_coroutine
async def prefetch_metadata(self, magnet: str, timeout: int) -> Tuple[str, bytes]:
def prefetch_metadata(self, magnet, timeout):
"""Download the metadata for a magnet URI.
Args:
magnet: A magnet URI to download the metadata for.
timeout: Number of seconds to wait before canceling.
magnet (str): A magnet URI to download the metadata for.
timeout (int): Number of seconds to wait before canceling.
Returns:
A tuple of (torrent_id, metadata)
Deferred: A tuple of (torrent_id (str), metadata (dict))
"""
torrent_id = get_magnet_info(magnet)['info_hash']
if torrent_id in self.prefetching_metadata:
d = Deferred()
self.prefetching_metadata[torrent_id].result_queue.append(d)
return await d
return self.prefetching_metadata[torrent_id].defer
add_torrent_params = lt.parse_magnet_uri(magnet)
add_torrent_params.save_path = gettempdir()
add_torrent_params.flags = (
add_torrent_params = {}
add_torrent_params['save_path'] = gettempdir()
add_torrent_params['url'] = magnet.strip().encode('utf8')
add_torrent_params['flags'] = (
(
LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.torrent_flags.duplicate_is_error
| lt.torrent_flags.upload_mode
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
| lt.add_torrent_params_flags_t.flag_upload_mode
)
^ lt.torrent_flags.auto_managed
^ lt.torrent_flags.paused
^ lt.add_torrent_params_flags_t.flag_auto_managed
^ lt.add_torrent_params_flags_t.flag_paused
)
torrent_handle = self.session.add_torrent(add_torrent_params)
d = Deferred()
# Cancel the defer if timeout reached.
d.addTimeout(timeout, self.clock)
self.prefetching_metadata[torrent_id] = PrefetchQueueItem(d, [])
defer_timeout = self.callLater(timeout, d.cancel)
d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout)
Prefetch = namedtuple('Prefetch', 'defer handle')
self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle)
return d
def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout):
# Cancel reactor.callLater.
try:
torrent_info = await d
except (defer.TimeoutError, defer.CancelledError):
log.debug(f'Prefetching metadata for {torrent_id} timed out or cancelled.')
metadata = b''
else:
log.debug('prefetch metadata received')
if VersionSplit(LT_VERSION) < VersionSplit('2.0.0.0'):
metadata = torrent_info.metadata()
else:
metadata = torrent_info.info_section()
defer_timeout.cancel()
except error.AlreadyCalled:
pass
log.debug('remove prefetch magnet from session')
result_queue = self.prefetching_metadata.pop(torrent_id).result_queue
self.session.remove_torrent(torrent_handle, 1)
result = torrent_id, b64encode(metadata)
try:
torrent_handle = self.prefetching_metadata.pop(torrent_id).handle
except KeyError:
pass
else:
self.session.remove_torrent(torrent_handle, 1)
for d in result_queue:
d.callback(result)
return result
metadata = None
if isinstance(torrent_info, lt.torrent_info):
log.debug('prefetch metadata received')
metadata = lt.bdecode(torrent_info.metadata())
return torrent_id, metadata
def _build_torrent_options(self, options):
"""Load default options and update if needed."""
@ -434,10 +435,14 @@ class TorrentManager(component.Component):
elif magnet:
magnet_info = get_magnet_info(magnet)
if magnet_info:
add_torrent_params['url'] = magnet.strip().encode('utf8')
add_torrent_params['name'] = magnet_info['name']
add_torrent_params['trackers'] = list(magnet_info['trackers'])
torrent_id = magnet_info['info_hash']
add_torrent_params['info_hash'] = bytes(bytearray.fromhex(torrent_id))
# Workaround lt 1.2 bug for magnet resume data with no metadata
if resume_data and VersionSplit(LT_VERSION) >= VersionSplit('1.2.10.0'):
add_torrent_params['info_hash'] = bytes(
bytearray.fromhex(torrent_id)
)
else:
raise AddTorrentError(
'Unable to add magnet, invalid magnet info: %s' % magnet
@ -452,7 +457,7 @@ class TorrentManager(component.Component):
raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
elif torrent_id in self.prefetching_metadata:
# Cancel and remove metadata fetching torrent.
self.prefetching_metadata[torrent_id].alert_deferred.cancel()
self.prefetching_metadata[torrent_id].defer.cancel()
# Check for renamed files and if so, rename them in the torrent_info before adding.
if options['mapped_files'] and torrent_info:
@ -480,12 +485,16 @@ class TorrentManager(component.Component):
# Set flags: enable duplicate_is_error & override_resume_data, disable auto_managed.
add_torrent_params['flags'] = (
LT_DEFAULT_ADD_TORRENT_FLAGS | lt.torrent_flags.duplicate_is_error
) ^ lt.torrent_flags.auto_managed
LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
| lt.add_torrent_params_flags_t.flag_override_resume_data
) ^ lt.add_torrent_params_flags_t.flag_auto_managed
if options['seed_mode']:
add_torrent_params['flags'] |= lt.torrent_flags.seed_mode
add_torrent_params['flags'] |= lt.add_torrent_params_flags_t.flag_seed_mode
if options['super_seeding']:
add_torrent_params['flags'] |= lt.torrent_flags.super_seeding
add_torrent_params[
'flags'
] |= lt.add_torrent_params_flags_t.flag_super_seeding
return torrent_id, add_torrent_params
@ -810,8 +819,8 @@ class TorrentManager(component.Component):
try:
with open(filepath, 'rb') as _file:
state = pickle.load(_file, encoding='utf8')
except (OSError, EOFError, pickle.UnpicklingError) as ex:
message = f'Unable to load {filepath}: {ex}'
except (IOError, EOFError, pickle.UnpicklingError) as ex:
message = 'Unable to load {}: {}'.format(filepath, ex)
log.error(message)
if not filepath.endswith('.bak'):
self.archive_state(message)
@ -1067,7 +1076,7 @@ class TorrentManager(component.Component):
try:
with open(_filepath, 'rb') as _file:
resume_data = lt.bdecode(_file.read())
except (OSError, EOFError, RuntimeError) as ex:
except (IOError, EOFError, RuntimeError) as ex:
if self.torrents:
log.warning('Unable to load %s: %s', _filepath, ex)
resume_data = None
@ -1351,8 +1360,10 @@ class TorrentManager(component.Component):
torrent.set_tracker_status('Announce OK')
# Check for peer information from the tracker, if none then send a scrape request.
torrent.get_lt_status()
if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
if (
alert.handle.status().num_complete == -1
or alert.handle.status().num_incomplete == -1
):
torrent.scrape_tracker()
def on_alert_tracker_announce(self, alert):
@ -1387,18 +1398,22 @@ class TorrentManager(component.Component):
log.debug(
'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message
)
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
# we will need to verify that at least one endpoint to the errored tracker is working
for tracker in torrent.handle.trackers():
if tracker['url'] == alert.url:
if any(
endpoint['last_error']['value'] == 0
for endpoint in tracker['endpoints']
):
torrent.set_tracker_status('Announce OK')
else:
torrent.set_tracker_status('Error: ' + error_message)
break
if VersionSplit(LT_VERSION) >= VersionSplit('1.2.0.0'):
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
# we will need to verify that at least one endpoint to the errored tracker is working
for tracker in torrent.handle.trackers():
if tracker['url'] == alert.url:
if any(
endpoint['last_error']['value'] == 0
for endpoint in tracker['endpoints']
):
torrent.set_tracker_status('Announce OK')
else:
torrent.set_tracker_status('Error: ' + error_message)
break
else:
# preserve old functionality for libtorrent < 1.2
torrent.set_tracker_status('Error: ' + error_message)
def on_alert_storage_moved(self, alert):
"""Alert handler for libtorrent storage_moved_alert"""
@ -1472,9 +1487,7 @@ class TorrentManager(component.Component):
return
if torrent_id in self.torrents:
# libtorrent add_torrent expects bencoded resume_data.
self.resume_data[torrent_id] = lt.bencode(
lt.write_resume_data(alert.params)
)
self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
if torrent_id in self.waiting_on_resume_data:
self.waiting_on_resume_data[torrent_id].callback(None)
@ -1556,7 +1569,7 @@ class TorrentManager(component.Component):
# Try callback to prefetch_metadata method.
try:
d = self.prefetching_metadata[torrent_id].alert_deferred
d = self.prefetching_metadata[torrent_id].defer
except KeyError:
pass
else:
@ -1602,7 +1615,7 @@ class TorrentManager(component.Component):
except RuntimeError:
continue
if torrent_id in self.torrents:
self.torrents[torrent_id].status = t_status
self.torrents[torrent_id].update_status(t_status)
self.handle_torrents_status_callback(self.torrents_status_requests.pop())

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
#

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
#
@ -10,9 +11,6 @@ import inspect
import re
import warnings
from functools import wraps
from typing import Any, Callable, Coroutine, TypeVar
from twisted.internet import defer
def proxy(proxy_func):
@ -127,7 +125,7 @@ def _overrides(stack, method, explicit_base_classes=None):
% (
method.__name__,
cls,
f'File: {stack[1][1]}:{stack[1][2]}',
'File: %s:%s' % (stack[1][1], stack[1][2]),
)
)
@ -137,7 +135,7 @@ def _overrides(stack, method, explicit_base_classes=None):
% (
method.__name__,
check_classes,
f'File: {stack[1][1]}:{stack[1][2]}',
'File: %s:%s' % (stack[1][1], stack[1][2]),
)
)
return method
@ -154,7 +152,7 @@ def deprecated(func):
def depr_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
warnings.warn(
f'Call to deprecated function {func.__name__}.',
'Call to deprecated function {}.'.format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
@ -162,74 +160,3 @@ def deprecated(func):
return func(*args, **kwargs)
return depr_func
class CoroutineDeferred(defer.Deferred):
"""Wraps a coroutine in a Deferred.
It will dynamically pass through the underlying coroutine without wrapping where apporpriate.
"""
def __init__(self, coro: Coroutine):
# Delay this import to make sure a reactor was installed first
from twisted.internet import reactor
super().__init__()
self.coro = coro
self.awaited = None
self.activate_deferred = reactor.callLater(0, self.activate)
def __await__(self):
if self.awaited in [None, True]:
self.awaited = True
return self.coro.__await__()
# Already in deferred mode
return super().__await__()
def activate(self):
"""If the result wasn't awaited before the next context switch, we turn it into a deferred."""
if self.awaited is None:
self.awaited = False
try:
d = defer.Deferred.fromCoroutine(self.coro)
except AttributeError:
# Fallback for Twisted <= 21.2 without fromCoroutine
d = defer.ensureDeferred(self.coro)
d.chainDeferred(self)
def _callback_activate(self):
"""Verify awaited status before calling activate."""
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
self.activate()
def addCallback(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addCallback(*args, **kwargs)
def addCallbacks(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addCallbacks(*args, **kwargs)
def addErrback(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addErrback(*args, **kwargs)
def addBoth(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addBoth(*args, **kwargs)
_RetT = TypeVar('_RetT')
def maybe_coroutine(
f: Callable[..., Coroutine[Any, Any, _RetT]],
) -> 'Callable[..., defer.Deferred[_RetT]]':
"""Wraps a coroutine function to make it usable as a normal function that returns a Deferred."""
@wraps(f)
def wrapper(*args, **kwargs):
# Uncomment for quick testing to make sure CoroutineDeferred magic isn't at fault
# return defer.ensureDeferred(f(*args, **kwargs))
return CoroutineDeferred(f(*args, **kwargs))
return wrapper

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -10,13 +11,13 @@
class DelugeError(Exception):
def __new__(cls, *args, **kwargs):
inst = super().__new__(cls, *args, **kwargs)
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
inst._args = args
inst._kwargs = kwargs
return inst
def __init__(self, message=None):
super().__init__(message)
super(DelugeError, self).__init__(message)
self.message = message
def __str__(self):
@ -41,12 +42,12 @@ class InvalidPathError(DelugeError):
class WrappedException(DelugeError):
def __init__(self, message, exception_type, traceback):
super().__init__(message)
super(WrappedException, self).__init__(message)
self.type = exception_type
self.traceback = traceback
def __str__(self):
return f'{self.message}\n{self.traceback}'
return '%s\n%s' % (self.message, self.traceback)
class _ClientSideRecreateError(DelugeError):
@ -60,7 +61,7 @@ class IncompatibleClient(_ClientSideRecreateError):
'Your deluge client is not compatible with the daemon. '
'Please upgrade your client to %(daemon_version)s'
) % {'daemon_version': self.daemon_version}
super().__init__(message=msg)
super(IncompatibleClient, self).__init__(message=msg)
class NotAuthorizedError(_ClientSideRecreateError):
@ -69,14 +70,14 @@ class NotAuthorizedError(_ClientSideRecreateError):
'current_level': current_level,
'required_level': required_level,
}
super().__init__(message=msg)
super(NotAuthorizedError, self).__init__(message=msg)
self.current_level = current_level
self.required_level = required_level
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
def __init__(self, message, username):
super().__init__(message)
super(_UsernameBasedPasstroughError, self).__init__(message)
self.username = username

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -13,7 +14,6 @@ This module describes the types of events that can be generated by the daemon
and subsequently emitted to the clients.
"""
known_events = {}
@ -23,7 +23,7 @@ class DelugeEventMetaClass(type):
"""
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super().__init__(name, bases, dct)
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
if name != 'DelugeEvent':
known_events[name] = cls

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -6,7 +7,7 @@
# See LICENSE for more details.
#
import email.message
import cgi
import logging
import os.path
import zlib
@ -16,7 +17,7 @@ from twisted.internet.defer import Deferred
from twisted.python.failure import Failure
from twisted.web import client, http
from twisted.web._newclient import HTTPClientParser
from twisted.web.error import Error, PageRedirect
from twisted.web.error import PageRedirect
from twisted.web.http_headers import Headers
from twisted.web.iweb import IAgent
from zope.interface import implementer
@ -37,11 +38,11 @@ class CompressionDecoderProtocol(client._GzipProtocol):
"""A compression decoder protocol for CompressionDecoder."""
def __init__(self, protocol, response):
super().__init__(protocol, response)
super(CompressionDecoderProtocol, self).__init__(protocol, response)
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
class BodyHandler(HTTPClientParser):
class BodyHandler(HTTPClientParser, object):
"""An HTTP parser that saves the response to a file."""
def __init__(self, request, finished, length, agent, encoding=None):
@ -53,7 +54,7 @@ class BodyHandler(HTTPClientParser):
length (int): The length of the response.
agent (t.w.i.IAgent): The agent from which the request was sent.
"""
super().__init__(request, finished)
super(BodyHandler, self).__init__(request, finished)
self.agent = agent
self.finished = finished
self.total_length = length
@ -73,12 +74,12 @@ class BodyHandler(HTTPClientParser):
with open(self.agent.filename, 'wb') as _file:
_file.write(self.data)
self.finished.callback(self.agent.filename)
self.state = 'DONE'
self.state = u'DONE'
HTTPClientParser.connectionLost(self, reason)
@implementer(IAgent)
class HTTPDownloaderAgent:
class HTTPDownloaderAgent(object):
"""A File Downloader Agent."""
def __init__(
@ -122,9 +123,6 @@ class HTTPDownloaderAgent:
location = response.headers.getRawHeaders(b'location')[0]
error = PageRedirect(response.code, location=location)
finished.errback(Failure(error))
elif response.code >= 400:
error = Error(response.code)
finished.errback(Failure(error))
else:
headers = response.headers
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
@ -133,10 +131,9 @@ class HTTPDownloaderAgent:
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
'utf-8'
)
message = email.message.EmailMessage()
message['content-disposition'] = content_disp
new_file_name = message.get_filename()
if new_file_name:
content_disp_params = cgi.parse_header(content_disp)[1]
if 'filename' in content_disp_params:
new_file_name = content_disp_params['filename']
new_file_name = sanitise_filename(new_file_name)
new_file_name = os.path.join(
os.path.split(self.filename)[0], new_file_name
@ -147,16 +144,13 @@ class HTTPDownloaderAgent:
fileext = os.path.splitext(new_file_name)[1]
while os.path.isfile(new_file_name):
# Increment filename if already exists
new_file_name = f'{fileroot}-{count}{fileext}'
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
count += 1
self.filename = new_file_name
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
message = email.message.EmailMessage()
message['content-type'] = cont_type_header
cont_type = message.get_content_type()
params = message['content-type'].params
cont_type, params = cgi.parse_header(cont_type_header)
# Only re-ecode text content types.
encoding = None
if cont_type.startswith('text/'):

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# This file is public domain.
#

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more