Compare commits
506 Commits
deluge-2.0
...
deluge-2.1
Author | SHA1 | Date | |
---|---|---|---|
897955f0a1 | |||
ff309ea4c5 | |||
3b11613cc7 | |||
a2d0cb7141 | |||
88ffd1b843 | |||
6a10e57f7e | |||
612e0061ed | |||
2eee7453cb | |||
58cc278145 | |||
a03e649da6 | |||
073bbbc09d | |||
bca0aa3532 | |||
cb588d0205 | |||
2b20e9689b | |||
65f7cf0d83 | |||
5ac8f4c81b | |||
c33c9082d9 | |||
62ae0f5ef6 | |||
24aa48187e | |||
342cca4367 | |||
9194092d7b | |||
5f6f65a065 | |||
967537a409 | |||
f74163489c | |||
7a110bd60f | |||
d56636426e | |||
de4fbd2e82 | |||
9c3982d4ff | |||
88fc21e993 | |||
54674576db | |||
89189adb24 | |||
a5a7da4a1a | |||
1e6cc03946 | |||
d8526ba653 | |||
c38f913948 | |||
0659fe4641 | |||
10501db63d | |||
2a312159b9 | |||
cb75192df4 | |||
588f600ba2 | |||
ea609cd3e0 | |||
4b6c7d01b2 | |||
b89b2c45b1 | |||
e38f1173cf | |||
e1e0999de6 | |||
5c9378ac5e | |||
f075f391cb | |||
8fb25f71f3 | |||
a3332079db | |||
0d6eec7a33 | |||
f16afc59ba | |||
e5388048a9 | |||
5374d237a7 | |||
2e466101fc | |||
8676a0d2a0 | |||
3ec23ad96b | |||
dcd3918f36 | |||
08c7f1960f | |||
8a4ec493c0 | |||
4d970754a4 | |||
f331b6c754 | |||
1022448e4f | |||
291540b601 | |||
092d07b68e | |||
da5d5bee20 | |||
6d9dc9bd42 | |||
937afd921c | |||
a4da8d29f8 | |||
8ec5ca9d08 | |||
9c90136f57 | |||
610a1bb313 | |||
23a48dd01c | |||
d02fa72e80 | |||
62d8749e74 | |||
76f0bf2e04 | |||
635f6d970d | |||
672e3c42a8 | |||
c1110e4ef3 | |||
742c8a941a | |||
3427ae4b90 | |||
034db27936 | |||
1e3c624613 | |||
3519f341d4 | |||
d6c96d6291 | |||
15c250e152 | |||
eb57412601 | |||
2f1c008a26 | |||
5e06aee5c8 | |||
351664ec07 | |||
5f1eada3ea | |||
bde4e4443e | |||
ed4bc5fa17 | |||
20afc31f3c | |||
9232a52fd6 | |||
23b3f144fc | |||
89d62eb509 | |||
00176ee2cd | |||
8737005b82 | |||
d08c3f72e9 | |||
40ebdf3f39 | |||
eeeb7fb69b | |||
3f9ae33793 | |||
0c7f53e305 | |||
63a4301a8b | |||
1b4ac88ce7 | |||
4b29436cd5 | |||
833b5a1f30 | |||
24b094a04a | |||
3365201011 | |||
c1ba403d4e | |||
8b62e50eb8 | |||
5b315e90c5 | |||
b711cd258a | |||
e1c4069a72 | |||
a2dee79439 | |||
7a54db3179 | |||
03e7952d26 | |||
7ee8750be4 | |||
f61001a15d | |||
86ddadacf7 | |||
632089940c | |||
5d7db3e727 | |||
4dd1f63b8b | |||
fc134cdffb | |||
36cb4c5a4f | |||
676bdb26e0 | |||
dff778ceeb | |||
bdadd2b515 | |||
a34543100c | |||
b8b044f451 | |||
2d87cde887 | |||
212efc4f52 | |||
879a397215 | |||
957cd5dd9c | |||
25087d3f2d | |||
d24109f0a2 | |||
647baebcf0 | |||
98ce3cd385 | |||
0c87d9bd7d | |||
aa35247e95 | |||
0dc4e18ac4 | |||
d4185505d1 | |||
04e58659fe | |||
5e738cf73a | |||
ce8595e8dd | |||
be74d96c6a | |||
4212bd6800 | |||
d40d40af31 | |||
cbf9ee8978 | |||
7abeb4ee0f | |||
bd4a3cba38 | |||
3cfa39a2ad | |||
a3b6d8d8e5 | |||
7e3692bb5a | |||
aa3a9a15cc | |||
0f92ea401f | |||
260d55aeae | |||
a9609a197d | |||
a8fac1381b | |||
65f6ede8b2 | |||
515dbcc5d9 | |||
827987fe7d | |||
1357ca7582 | |||
72d363968e | |||
c6b6902e9f | |||
6a5bb44d5b | |||
cbcf8eb863 | |||
09cfd9b89e | |||
b961e11df6 | |||
2ca683e8fe | |||
fd20addead | |||
535b13b5f1 | |||
d6a0276a78 | |||
9c0325b129 | |||
f885edd7fc | |||
2b171e58a3 | |||
d417c4b0f9 | |||
653f80eac8 | |||
76b89a7943 | |||
6ff7a5400f | |||
db021b9f41 | |||
ab4661f6fd | |||
396cadefda | |||
2296906ed3 | |||
1a134cab1b | |||
7d67792493 | |||
3c18e890e8 | |||
615500e6e6 | |||
1425fe5413 | |||
84643fb6f7 | |||
c8b621172e | |||
02e07dda2a | |||
b2e19561e6 | |||
389f4167b2 | |||
63cc745f5b | |||
1a4ac93fbb | |||
582f60ea0b | |||
157f6ff62a | |||
bf4244e8b2 | |||
25cfd58792 | |||
09d04aaac0 | |||
27b4e2d891 | |||
043344b986 | |||
3b8f71613b | |||
10fcbecc04 | |||
ab7f19fbb8 | |||
b665a4a6f7 | |||
2c45e59900 | |||
89868cc944 | |||
841cb889aa | |||
6b2f14e51e | |||
7e2192e875 | |||
f11a42b9bf | |||
845204178b | |||
d937a323fb | |||
d7c48d27d8 | |||
1bc766213c | |||
775aef5f9b | |||
83cac4978a | |||
2bb9a8e71c | |||
39783c7703 | |||
9f9f564e62 | |||
ab1b2bcf14 | |||
bb0c61bb3f | |||
a7dcf39a32 | |||
e43796ae51 | |||
6655fe67c3 | |||
2104b9831c | |||
e7127637cf | |||
6233e5c844 | |||
a01481b26f | |||
3d24998577 | |||
f24e9d152c | |||
f47089ae7d | |||
d70abd2986 | |||
7d998a45f2 | |||
3433a911cc | |||
967606fa0f | |||
97e7d95dd3 | |||
26c28445a5 | |||
74a459274c | |||
bb6e290bf8 | |||
4a79e1f100 | |||
bff93bb162 | |||
bffd091429 | |||
70d5931622 | |||
ce49cde49d | |||
a3bd2e547a | |||
64710ad226 | |||
cd6bad0e35 | |||
1310645f55 | |||
e6a7119595 | |||
0b39b529dd | |||
1d0e40c66b | |||
bcc89c73dd | |||
a6b47e18c9 | |||
5183c92543 | |||
7c1c3f62d1 | |||
729f062ea1 | |||
d879ee06a3 | |||
ed1b2a50fa | |||
c51e01ac46 | |||
4df5bd05ec | |||
cf4012bb60 | |||
bbcebe1306 | |||
bcaaeac852 | |||
4111f94597 | |||
dd7cc31918 | |||
d8d094cab6 | |||
dc6e93541b | |||
f6ffb940ab | |||
6fbb1bb370 | |||
8285b226eb | |||
194129c027 | |||
7d5a429466 | |||
ac5db1b262 | |||
a2857a318d | |||
13e1fa355d | |||
2e88fa1dfc | |||
366b10f07b | |||
92a048625a | |||
8199928160 | |||
545aca9a4c | |||
9f113eab23 | |||
bc6bc017cb | |||
535fda90e3 | |||
0ace086de4 | |||
bbb1b44a23 | |||
dfed17ac0d | |||
2f879c33f3 | |||
14b6ba10cf | |||
ae0b072b75 | |||
250afa6e0b | |||
b29b6fe69f | |||
f160d6312f | |||
a8d01fd52f | |||
3a5ec4f5f4 | |||
eebb93d4ee | |||
01fafd4fe0 | |||
ca0db4d1a7 | |||
ea72164798 | |||
e2ba980299 | |||
98051bdea2 | |||
20431cc771 | |||
82ecf8a416 | |||
9dcd90056d | |||
e2c7716ce2 | |||
e6c61c3f8c | |||
b834e33568 | |||
9ab2a50097 | |||
1838403e3b | |||
9e7c9fc1d3 | |||
9264cb749e | |||
c01679de1f | |||
3645feb486 | |||
d85f665091 | |||
5ec6ae3ad0 | |||
860730d43c | |||
c1ddcf6012 | |||
85bbdfe143 | |||
9f9827ca58 | |||
dcb3dad435 | |||
0e69b9199c | |||
88a3600ce3 | |||
91164d8dbf | |||
ec47720686 | |||
467ade1eb7 | |||
bb93a06fff | |||
80178f7310 | |||
ee354eb107 | |||
7d896599b8 | |||
55aee2b00f | |||
10d39c83cb | |||
0b2cb7539f | |||
6fdbf0ba5d | |||
a980f8e959 | |||
c90cf301df | |||
6f06cd5ebc | |||
86de5657ff | |||
4a335eeb61 | |||
86d582d52a | |||
673b6653a3 | |||
41732fe38b | |||
5964bcf897 | |||
3ed4a6e834 | |||
20fa106b8b | |||
654e2af4e5 | |||
d5dea44689 | |||
5743382c65 | |||
39f37e6133 | |||
0ed3554f95 | |||
ba6af99b05 | |||
9e29fe4111 | |||
a8a4fb69c0 | |||
6cf13d112b | |||
6973f96f8c | |||
0548bdb655 | |||
36606fc448 | |||
c415b097fe | |||
970fad7557 | |||
358ff74d0e | |||
b1cdc32f73 | |||
bcca07443c | |||
67d9c2efb4 | |||
34b0fdff1d | |||
f93e5e60b5 | |||
d8b1e2701c | |||
abf4c345f0 | |||
a09334e116 | |||
57ad9a25da | |||
5a2990ff90 | |||
759a618f74 | |||
23f1cfc926 | |||
57ea5ef5da | |||
944dc1659f | |||
2dc157578e | |||
8a59216061 | |||
cc1807cf97 | |||
63b7f6d382 | |||
5c4cbf58c5 | |||
5959a24d4c | |||
d4023e7dde | |||
0fd3c25684 | |||
4125e35ebd | |||
18d448d4a5 | |||
d5133f789a | |||
1cce6a297c | |||
ad20ec62f2 | |||
af2bed8a0f | |||
b93e868048 | |||
8d90ae5ffb | |||
ae4449642c | |||
bc2f4a30eb | |||
dc8766874e | |||
a33171732d | |||
b9a9e06c1d | |||
456e720b75 | |||
ae9bbdbae7 | |||
585ea88f1f | |||
f94f58918e | |||
3fc97672de | |||
e8e649a030 | |||
1e6c02ae83 | |||
b2e1f850d8 | |||
8bfa2cacbb | |||
c7e61f8c34 | |||
089c667d7f | |||
ebb955934d | |||
c7567ddee4 | |||
c655da38c8 | |||
4c0be7ddd4 | |||
38961d4253 | |||
6e81a11d8d | |||
be02be75be | |||
7b5ed9f1d6 | |||
e626f9fece | |||
3fab799dbf | |||
24c100d9b7 | |||
9bc2f62c80 | |||
1fa2de066f | |||
ae0b070c1b | |||
c3a2c67b98 | |||
200e8f552b | |||
4247013446 | |||
6ec32a85e4 | |||
633c56f54e | |||
23171ad205 | |||
277576268c | |||
74aa0db956 | |||
fe42fb2c31 | |||
4973538d6c | |||
de1e7c27df | |||
587b9afefe | |||
63b25311f5 | |||
d45dbfe064 | |||
3176b877a4 | |||
18541bce86 | |||
bebe08d92b | |||
0dbbb51cff | |||
bd78bd2643 | |||
7a3b164060 | |||
e7eb26416e | |||
b2b7703081 | |||
cbdde7bba5 | |||
4fd51a4ef9 | |||
333c81c1d7 | |||
21b5a15e5d | |||
edd431a304 | |||
d642fa3989 | |||
bae1647e99 | |||
decd7aca71 | |||
7cc9aaca49 | |||
196aa48727 | |||
af2972f697 | |||
d4addeedd6 | |||
8439698336 | |||
7d120690ab | |||
ee196f5035 | |||
ff85c334c7 | |||
0c574f33e1 | |||
a7c7309027 | |||
de2f998218 | |||
4982ba0b98 | |||
f57286fd51 | |||
12f7345d0c | |||
4e79ed8124 | |||
7787aa975f | |||
c13622a1e6 | |||
07a87fa15a | |||
2644169376 | |||
5988f5f04f | |||
95d826b77c | |||
9bcda41700 | |||
507c5df984 | |||
0ba87b424c | |||
53f818e176 | |||
00dcd60d56 | |||
1730230244 | |||
0728c03c1c | |||
354372b2ea | |||
d169aca8bd | |||
26720ca4c2 | |||
510a8b50b2 | |||
d190f149d1 | |||
24a31b1194 | |||
470490769f | |||
1259eca8ad | |||
f0316d3e31 | |||
9b580a87fa | |||
4bee1ce811 | |||
e3f537770f | |||
9b92bc2baf | |||
51b99caf24 | |||
850fd34522 | |||
9164dafe69 | |||
33e9545cd4 | |||
7b87a93862 | |||
51bde704b5 | |||
3f13c24362 | |||
6837d83f5b | |||
3c1d7da698 | |||
d6731b8cee | |||
fe80703f95 | |||
1808ac506a | |||
3174c7534d | |||
065729a389 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -2,3 +2,4 @@
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
*.py diff=python
|
||||
ext-all.js diff=minjs
|
||||
|
116
.github/workflows/ci.yml
vendored
Normal file
116
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-linux:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
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'))
|
||||
run: echo "SECURITY_TESTS=True" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements.txt -r requirements-tests.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Install security dependencies
|
||||
if: contains(env.SECURITY_TESTS, 'True')
|
||||
run: |
|
||||
wget -O- $TESTSSL_URL$TESTSSL_VER | tar xz
|
||||
mv -t deluge/tests/data testssl.sh-$TESTSSL_VER/testssl.sh testssl.sh-$TESTSSL_VER/etc/;
|
||||
env:
|
||||
TESTSSL_VER: 3.0.6
|
||||
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
|
||||
|
||||
- name: Setup core dump directory
|
||||
run: |
|
||||
sudo mkdir /cores/ && sudo chmod 777 /cores/
|
||||
echo "/cores/%E.%p" | sudo tee /proc/sys/kernel/core_pattern
|
||||
|
||||
- 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__)';
|
||||
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
# capture all crashes as build artifacts
|
||||
if: failure()
|
||||
with:
|
||||
name: crashes
|
||||
path: /cores
|
||||
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
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: |
|
||||
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 -m "not (todo or gtkui or security)" deluge
|
45
.github/workflows/docs.yml
vendored
Normal file
45
.github/workflows/docs.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Docs
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the main branch
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Test with tox
|
||||
env:
|
||||
TOX_ENV: docs
|
||||
run: |
|
||||
tox -e $TOX_ENV
|
17
.github/workflows/lint.yml
vendored
Normal file
17
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- name: Run pre-commit linting
|
||||
uses: pre-commit/action@v2.0.2
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -2,18 +2,25 @@
|
||||
build
|
||||
.cache
|
||||
dist
|
||||
docs/source/modules
|
||||
*egg-info
|
||||
docs/source/modules/deluge*.rst
|
||||
*.egg-info/
|
||||
*.dist-info/
|
||||
*.egg
|
||||
*.log
|
||||
*.pyc
|
||||
__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
|
||||
*.appdata.xml
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
||||
.venv*
|
||||
# used by setuptools to cache downloaded eggs
|
||||
/.eggs
|
||||
|
42
.pre-commit-config.yaml
Normal file
42
.pre-commit-config.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
default_language_version:
|
||||
python: python3
|
||||
exclude: >
|
||||
(?x)^(
|
||||
deluge/ui/web/docs/template/.*|
|
||||
)$
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
name: Fmt Black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: Fmt Prettier
|
||||
# Workaround to list modified files only.
|
||||
args: [--list-different]
|
||||
- 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]
|
||||
- id: mixed-line-ending
|
||||
name: Fix Line endings
|
||||
args: [--fix=auto]
|
||||
- id: trailing-whitespace
|
||||
name: Fix Trailing whitespace
|
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
deluge/ui/web/css/ext-*.css
|
||||
deluge/ui/web/js/extjs/ext-*.js
|
||||
deluge/ui/web/docs/
|
||||
deluge/ui/web/themes/images/
|
||||
*.py*
|
||||
*.html
|
13
.prettierrc.yaml
Normal file
13
.prettierrc.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
trailingComma: "es5"
|
||||
tabWidth: 4
|
||||
singleQuote: true
|
||||
overrides:
|
||||
- files:
|
||||
- "*.yaml"
|
||||
- ".*.yaml"
|
||||
- "*.yml"
|
||||
- ".*.yml"
|
||||
- "*.md"
|
||||
options:
|
||||
tabWidth: 2
|
||||
singleQuote: false
|
@ -69,7 +69,7 @@ confidence=
|
||||
# Arranged by category and use symbolic names instead of ids.
|
||||
disable=
|
||||
# Convention
|
||||
missing-docstring, invalid-name,
|
||||
missing-docstring, invalid-name, bad-continuation,
|
||||
# Error
|
||||
no-member, no-name-in-module,
|
||||
# Information
|
||||
@ -289,7 +289,7 @@ callbacks=cb_,_cb
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins,future_builtins
|
||||
redefining-builtins-modules=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
@ -359,11 +359,6 @@ known-standard-library=
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
|
22
.readthedocs.yml
Normal file
22
.readthedocs.yml
Normal file
@ -0,0 +1,22 @@
|
||||
# .readthedocs.yml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
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: setuptools
|
||||
path: .
|
51
.travis.yml
51
.travis.yml
@ -1,51 +0,0 @@
|
||||
dist: trusty
|
||||
sudo: required
|
||||
group: deprecated-2017Q2
|
||||
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
cache: pip
|
||||
|
||||
before_install:
|
||||
- lsb_release -a
|
||||
- sudo add-apt-repository ppa:deluge-team/develop -y
|
||||
- sudo apt-get update
|
||||
|
||||
# command to install dependencies
|
||||
install:
|
||||
- bash -c "echo $APTPACKAGES"
|
||||
- sudo apt-get install $APTPACKAGES
|
||||
- pip install "tox==2.1.1"
|
||||
|
||||
env:
|
||||
global:
|
||||
- APTPACKAGES="python-libtorrent"
|
||||
- APTPACKAGES_GTKUI="python-gobject python-glade2"
|
||||
- DISPLAY=:99.0
|
||||
matrix:
|
||||
- TOX_ENV=pydef
|
||||
- TOX_ENV=flake8
|
||||
# - TOX_ENV=flake8-complexity
|
||||
- TOX_ENV=docs
|
||||
# - TOX_ENV=todo
|
||||
- TOX_ENV=trial APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
|
||||
- TOX_ENV=pygtkui APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
|
||||
# - TOX_ENV=testcoverage APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
|
||||
- TOX_ENV=plugins
|
||||
|
||||
virtualenv:
|
||||
system_site_packages: true
|
||||
|
||||
# We use xvfb for the GTKUI tests
|
||||
before_script:
|
||||
- export PYTHONPATH=$PYTHONPATH:$PWD
|
||||
- python -c "import libtorrent as lt; print lt.__version__"
|
||||
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
|
||||
- echo '2.0.0.dev0' > RELEASE-VERSION
|
||||
|
||||
script:
|
||||
- bash -c "echo $DISPLAY"
|
||||
- tox -e $TOX_ENV
|
17
AUTHORS
17
AUTHORS
@ -39,14 +39,9 @@ Images Authors:
|
||||
* files: deluge/ui/data/pixmaps/*.svg, *.png
|
||||
deluge/ui/web/icons/active.png, alert.png, all.png, checking.png, dht.png,
|
||||
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
|
||||
exceptions: deluge/ui/data/pixmaps/deluge.svg and derivatives
|
||||
copyright: Andrew Resch
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/ui/data/pixmaps/deluge.svg and derivatives
|
||||
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
|
||||
deluge/ui/web/images/deluge*.png
|
||||
copyright: Andrew Wedderburn
|
||||
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
|
||||
copyright: Calum Lind
|
||||
license: GPLv3
|
||||
|
||||
* files: deluge/plugins/blocklist/blocklist/data/*.png
|
||||
@ -55,11 +50,9 @@ Images Authors:
|
||||
license: GPLv2
|
||||
url: http://ftp.acc.umu.se/pub/GNOME/sources/gnome-icon-theme
|
||||
|
||||
* files: deluge/ui/data/pixmaps/magnet.png
|
||||
copyright: Woothemes
|
||||
license: Freeware
|
||||
icon pack: WP Woothemes Ultimate
|
||||
url: http://www.woothemes.com/
|
||||
* files: deluge/ui/data/pixmaps/magnet*.svg, *.png
|
||||
copyright: Matias Wilkman
|
||||
license:
|
||||
|
||||
* files: deluge/ui/data/pixmaps/flags/*.png
|
||||
copyright: Mark James <mjames@gmail.com>
|
||||
|
181
CHANGELOG.md
Normal file
181
CHANGELOG.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.0 (WIP)
|
||||
|
||||
- Removed Python 2 support.
|
||||
|
||||
## 2.0.5 (2021-12-15)
|
||||
|
||||
### WebUI
|
||||
|
||||
- Fix js minifying error resulting in WebUI blank screen.
|
||||
- Silence erronous missing translations warning.
|
||||
|
||||
## 2.0.4 (2021-12-12)
|
||||
|
||||
### Packaging
|
||||
|
||||
- Fix python optional setup.py requirements
|
||||
|
||||
### Gtk UI
|
||||
|
||||
- Add detection of torrent URL on GTK UI focus
|
||||
- Fix piecesbar crashing when enabled
|
||||
- Remove num_blocks_cache_hits in stats
|
||||
- Fix unhandled error with empty clipboard
|
||||
- Add torrentdetails tabs position menu (#3441)
|
||||
- Hide pygame community banner in console
|
||||
- Fix cmp function for None types (#3309)
|
||||
- Fix loading config with double-quotes in string
|
||||
- Fix Status tab download speed and uploaded
|
||||
|
||||
### Web UI
|
||||
|
||||
- Handle torrent add failures
|
||||
- Add menu option to copy magnet URI
|
||||
- Fix md5sums in torrent files breaking file listing (#3388)
|
||||
- Add country flag alt/title for accessibility
|
||||
|
||||
### Console UI
|
||||
|
||||
- Fix allowing use of windows-curses on Windows
|
||||
- Fix hostlist status lookup errors
|
||||
- Fix AttributeError setting config values
|
||||
- Fix setting 'Skip' priority
|
||||
|
||||
### Core
|
||||
|
||||
- Add workaround libtorrent 2.0 file_progress error
|
||||
- Fix allow enabling any plugin Python version
|
||||
- Export torrent get_magnet_uri method
|
||||
- Fix loading magnet with resume_data and no metadata (#3478)
|
||||
- Fix httpdownloader reencoding torrent file downloads (#3440)
|
||||
- Fix lt listen_interfaces not comma-separated (#3337)
|
||||
- Fix unable to remove magnet with delete_copies enabled (#3325)
|
||||
- Fix Python 3.8 compatibility
|
||||
- Fix loading config with double-quotes in string
|
||||
- Fix pickle loading non-ascii state error (#3298)
|
||||
- Fix creation of pidfile via command option
|
||||
- Fix for peer.client UnicodeDecodeError
|
||||
- Fix show_file unhandled dbus error
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add How-to guides about services.
|
||||
|
||||
### Stats plugin
|
||||
|
||||
- Fix constant session status key warnings
|
||||
- Fix cairo error
|
||||
|
||||
### Notifications plugin
|
||||
|
||||
- Fix email KeyError with status name
|
||||
- Fix unhandled TypeErrors on Python 3
|
||||
|
||||
### Autoadd plugin
|
||||
|
||||
- Fix magnet missing applied labels
|
||||
|
||||
### Execute plugin
|
||||
|
||||
- Fix failing to run on Windows (#3439)
|
||||
|
||||
## 2.0.3 (2019-06-12)
|
||||
|
||||
### Gtk UI
|
||||
|
||||
- Fix errors running on Wayland (#3265).
|
||||
- Fix Peers Tab tooltip and context menu errors (#3266).
|
||||
|
||||
### Web UI
|
||||
|
||||
- Fix TypeError in Peers Tab setting country flag.
|
||||
- Fix reverse proxy header TypeError (#3260).
|
||||
- Fix request.base 'idna' codec error (#3261).
|
||||
- Fix unable to change password (#3262).
|
||||
|
||||
### Extractor plugin
|
||||
|
||||
- Fix potential error starting plugin.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix macOS install typo.
|
||||
- Fix Windows install instructions.
|
||||
|
||||
## 2.0.2 (2019-06-08)
|
||||
|
||||
### Packaging
|
||||
|
||||
- Add systemd deluged and deluge-web service files to package tarball (#2034)
|
||||
|
||||
### Core
|
||||
|
||||
- Fix Python 2 compatibility issue with SimpleNamespace.
|
||||
|
||||
## 2.0.1 (2019-06-07)
|
||||
|
||||
### Packaging
|
||||
|
||||
- Fix `setup.py` build error without git installed.
|
||||
|
||||
## 2.0.0 (2019-06-06)
|
||||
|
||||
### Codebase
|
||||
|
||||
- Ported to Python 3
|
||||
|
||||
### Core
|
||||
|
||||
- Improved Logging
|
||||
- Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
|
||||
plugin, which is also shipped with Deluge, and it does a better job and
|
||||
now, it even supports multiple users perfectly.
|
||||
- Authentication/Permission exceptions are now sent to clients and recreated
|
||||
there to allow acting upon them.
|
||||
- Updated SSL/TLS Protocol parameters for better security.
|
||||
- Make the distinction between adding to the session new unmanaged torrents
|
||||
and torrents loaded from state. This will break backwards compatibility.
|
||||
- Pass a copy of an event instead of passing the event arguments to the
|
||||
event handlers. This will break backwards compatibility.
|
||||
- Allow changing ownership of torrents.
|
||||
- File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
- Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which should then ask the username/password to the user.
|
||||
- Implemented sequential downloads.
|
||||
- Provide information about a torrent's pieces states
|
||||
- Add Option To Specify Outgoing Connection Interface.
|
||||
- Fix potential for host_id collision when creating hostlist entries.
|
||||
|
||||
### Gtk UI
|
||||
|
||||
- Ported to GTK3 (3rd-party plugins will need updated).
|
||||
- Allow changing ownership of torrents.
|
||||
- Host entries in the Connection Manager UI are now editable.
|
||||
- Implemented sequential downloads UI handling.
|
||||
- Add optional pieces bar instead of a regular progress bar in torrent status tab.
|
||||
- Make torrent opening compatible with all Unicode paths.
|
||||
- Fix magnet association button on Windows.
|
||||
- Add keyboard shortcuts for changing queue position:
|
||||
- Up: `Ctrl+Alt+Up`
|
||||
- Down: `Ctrl+Alt+Down`
|
||||
- Top: `Ctrl+Alt+Shift+Up`
|
||||
- Bottom: `Ctrl+Alt+Shift+Down`
|
||||
|
||||
### Web UI
|
||||
|
||||
- Server (deluge-web) now daemonizes by default, use '-d' or '--do-not-daemonize' to disable.
|
||||
- Fixed the '--base' option to work for regular use, not just with reverse proxies.
|
||||
|
||||
### Blocklist Plugin
|
||||
|
||||
- Implemented whitelist support to both core and GTK UI.
|
||||
- Implemented IP filter cleaning before each update. Restarting the deluge
|
||||
daemon is no longer needed.
|
||||
- If "check_after_days" is 0(zero), the timer is not started anymore. It
|
||||
would keep updating one call after the other. If the value changed, the
|
||||
timer is now stopped and restarted using the new value.
|
50
ChangeLog
50
ChangeLog
@ -1,50 +0,0 @@
|
||||
=== Deluge 2.0 (In Development) ===
|
||||
|
||||
* Improved Logging
|
||||
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
|
||||
plugin, which is also shipped with Deluge, and it does a better job and
|
||||
now, it even supports multiple users perfectly.
|
||||
* Authentication/Permission exceptions are now sent to clients and recreated
|
||||
there to allow acting upon them.
|
||||
* Enforced the use of the "deluge.plugins" namespace to reduce package
|
||||
names clashing beetween regular packages and deluge plugins.
|
||||
|
||||
==== Core ====
|
||||
* Make the distinction between adding to the session new unmanaged torrents
|
||||
and torrents loaded from state. This will break backwards compatability.
|
||||
* Pass a copy of an event instead of passing the event arguments to the
|
||||
event handlers. This will break backwards compatability.
|
||||
* Allow changing ownership of torrents.
|
||||
* File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
* Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which sould then ask the username/password to the user.
|
||||
* Implemented sequential downloads.
|
||||
* Provide information about a torrent's pieces states
|
||||
|
||||
==== GtkUI ====
|
||||
* Allow changing ownership of torrents.
|
||||
* Host entries in the Connection Manager UI are now editable.
|
||||
* Implemented sequential downloads UI handling.
|
||||
* Add optional pieces bar instead of a regular progress bar in torrent status tab.
|
||||
* Make torrent opening compatible with all unicode paths.
|
||||
* Fix magnet association button on Windows.
|
||||
* Add keyboard shortcuts for changing queue position:
|
||||
- Up: Ctrl+Alt+Up
|
||||
- Down: Ctrl+Alt+Down
|
||||
- Top: Ctrl+Alt+Shift+Up
|
||||
- Bottom: Ctrl+Alt+Shift+Down
|
||||
|
||||
==== WebUI ====
|
||||
* Server (deluge-web) now daemonizes by default, use '-d' or '--do-not-daemonize' to disable.
|
||||
* Fixed the '--base' option to work for regular use, not just with reverse proxies.
|
||||
|
||||
==== Blocklist Plugin ====
|
||||
* Implemented whitelist support to both core and GTK UI.
|
||||
* Implemented ip filter cleaning before each update. Restarting the deluge
|
||||
daemon is no longer needed.
|
||||
* If "check_after_days" is 0(zero), the timer is not started anymore. It
|
||||
would keep updating one call after the other. If the value changed, the
|
||||
timer is now stopped and restarted using the new value.
|
29
DEPENDS
29
DEPENDS
@ -1,29 +0,0 @@
|
||||
=== Core ===
|
||||
* libtorrent (rasterbar) >= 1.1.1
|
||||
* python >= 2.7.7
|
||||
* setuptools
|
||||
* twisted >= 11.1
|
||||
* pyopenssl
|
||||
* pyxdg
|
||||
* chardet
|
||||
* gettext
|
||||
* python-geoip (optional)
|
||||
* geoip-database (optional)
|
||||
* setproctitle (optional)
|
||||
* pillow (optional)
|
||||
* py2-ipaddress (optional, required for Windows IPv6)
|
||||
* rencode >= 1.0.2 (optional), python port bundled.
|
||||
|
||||
|
||||
=== Gtk UI ===
|
||||
* pygtk >= 2.16
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
* intltool
|
||||
* python-notify (optional)
|
||||
* pygame (optional)
|
||||
* python-appindicator (optional)
|
||||
|
||||
=== Web UI ===
|
||||
* mako
|
||||
* slimit (optional), minifies JS files.
|
98
DEPENDS.md
Normal file
98
DEPENDS.md
Normal file
@ -0,0 +1,98 @@
|
||||
# Deluge dependencies
|
||||
|
||||
The following are required to install and run Deluge. They are separated into
|
||||
sections to distinguish the precise requirements for each module.
|
||||
|
||||
All modules will require the [common](#common) section dependencies.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- [Python] _>= 3.5_
|
||||
|
||||
## Build
|
||||
|
||||
- [setuptools]
|
||||
- [intltool] - Optional: Desktop file translation for \*nix.
|
||||
- [closure-compiler] - Minify javascript (alternative is [rjsmin])
|
||||
|
||||
## Common
|
||||
|
||||
- [Twisted] _>= 17.1_ - Use `TLS` extras for `service_identity` and `idna`.
|
||||
- [OpenSSL] _>= 1.0.1_
|
||||
- [pyOpenSSL]
|
||||
- [rencode] _>= 1.0.2_ - Encoding library.
|
||||
- [PyXDG] - Access freedesktop.org standards for \*nix.
|
||||
- [xdg-utils] - Provides xdg-open for \*nix.
|
||||
- [zope.interface]
|
||||
- [chardet] - Optional: Encoding detection.
|
||||
- [setproctitle] - Optional: Renaming processes.
|
||||
- [Pillow] - Optional: Support for resizing tracker icons.
|
||||
- [dbus-python] - Optional: Show item location in filemanager.
|
||||
|
||||
### Linux and BSD
|
||||
|
||||
- [distro] - Optional: OS platform information.
|
||||
|
||||
### Windows OS
|
||||
|
||||
- [pywin32]
|
||||
- [certifi]
|
||||
|
||||
## Core (deluged daemon)
|
||||
|
||||
- [libtorrent] _>= 1.1.1_
|
||||
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_)
|
||||
|
||||
## GTK UI
|
||||
|
||||
- [GTK+] >= 3.10
|
||||
- [PyGObject]
|
||||
- [Pycairo]
|
||||
- [librsvg] _>= 2_
|
||||
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
|
||||
|
||||
### MacOS
|
||||
|
||||
- [GtkOSXApplication]
|
||||
|
||||
## Web UI
|
||||
|
||||
- [mako]
|
||||
|
||||
## Plugins
|
||||
|
||||
### Notifications
|
||||
|
||||
- [pygame] - Optional: Play sounds
|
||||
- [libnotify] w/GIR - Optional: Desktop popups.
|
||||
|
||||
[python]: https://www.python.org/
|
||||
[setuptools]: https://setuptools.readthedocs.io/en/latest/
|
||||
[intltool]: https://freedesktop.org/wiki/Software/intltool/
|
||||
[closure-compiler]: https://developers.google.com/closure/compiler/
|
||||
[rjsmin]: https://pypi.org/project/rjsmin/
|
||||
[openssl]: https://www.openssl.org/
|
||||
[pyopenssl]: https://pyopenssl.org
|
||||
[twisted]: https://twistedmatrix.com
|
||||
[pillow]: https://pypi.org/project/Pillow/
|
||||
[libtorrent]: https://libtorrent.org/
|
||||
[zope.interface]: https://pypi.org/project/zope.interface/
|
||||
[distro]: https://github.com/nir0s/distro
|
||||
[pywin32]: https://github.com/mhammond/pywin32
|
||||
[certifi]: https://pypi.org/project/certifi/
|
||||
[dbus-python]: https://pypi.org/project/dbus-python/
|
||||
[setproctitle]: https://pypi.org/project/setproctitle/
|
||||
[gtkosxapplication]: https://github.com/jralls/gtk-mac-integration
|
||||
[chardet]: https://chardet.github.io/
|
||||
[rencode]: https://github.com/aresch/rencode
|
||||
[pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/
|
||||
[xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/
|
||||
[gtk+]: https://www.gtk.org/
|
||||
[pycairo]: https://cairographics.org/pycairo/
|
||||
[pygobject]: https://pygobject.readthedocs.io/en/latest/
|
||||
[geoip]: https://pypi.org/project/GeoIP/
|
||||
[mako]: https://www.makotemplates.org/
|
||||
[pygame]: https://www.pygame.org/
|
||||
[libnotify]: https://developer.gnome.org/libnotify/
|
||||
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
|
||||
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
|
20
MANIFEST.in
20
MANIFEST.in
@ -1,23 +1,29 @@
|
||||
include AUTHORS ChangeLog DEPENDS LICENSE RELEASE-VERSION README.rst
|
||||
include msgfmt.py minify_web_js.py version.py
|
||||
exclude setup.cfg
|
||||
include *.md
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include RELEASE-VERSION
|
||||
include msgfmt.py
|
||||
include minify_web_js.py
|
||||
include version.py
|
||||
include gen_web_gettext.py
|
||||
|
||||
graft docs/man
|
||||
graft packaging/systemd
|
||||
|
||||
include deluge/i18n/*.po
|
||||
recursive-exclude deluge/i18n LC_MESSAGES *.mo
|
||||
recursive-exclude deluge/i18n *.mo
|
||||
|
||||
graft deluge/plugins
|
||||
recursive-exclude deluge/plugins create_dev_link.sh *.pyc *.egg
|
||||
prune deluge/plugins/*/build
|
||||
prune deluge/plugins/*/*.egg-info
|
||||
|
||||
graft deluge/tests/data
|
||||
graft deluge/tests/twisted
|
||||
graft deluge/tests/
|
||||
recursive-exclude deluge/tests *.pyc
|
||||
|
||||
graft deluge/ui/data
|
||||
recursive-exclude deluge/ui/data *.desktop *.xml
|
||||
graft deluge/ui/gtkui/glade
|
||||
graft deluge/ui/gtk3/glade
|
||||
|
||||
include deluge/ui/web/index.html
|
||||
include deluge/ui/web/css/*.css
|
||||
|
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Deluge BitTorrent Client
|
||||
|
||||
[![build-status]][github-ci] [![docs-status]][rtd-deluge]
|
||||
|
||||
Deluge is a BitTorrent client that utilizes a daemon/client model.
|
||||
It has various user interfaces available such as the GTK-UI, Web-UI and
|
||||
Console-UI. It uses [libtorrent][lt] at its core to handle the BitTorrent
|
||||
protocol.
|
||||
|
||||
## Install
|
||||
|
||||
From [PyPi](https://pypi.org/project/deluge):
|
||||
|
||||
pip install deluge
|
||||
|
||||
with all optional dependencies:
|
||||
|
||||
pip install deluge[all]
|
||||
|
||||
From source code:
|
||||
|
||||
pip install .
|
||||
|
||||
with all optional dependencies:
|
||||
|
||||
pip install .[all]
|
||||
|
||||
See [DEPENDS](DEPENDS.md) and [Installing/Source] for dependency details.
|
||||
|
||||
## Usage
|
||||
|
||||
The various user-interfaces and Deluge daemon can be started with the following commands.
|
||||
|
||||
Use the `--help` option for further command options.
|
||||
|
||||
### Gtk UI
|
||||
|
||||
`deluge` or `deluge-gtk`
|
||||
|
||||
### Console UI
|
||||
|
||||
`deluge-console`
|
||||
|
||||
### Web UI
|
||||
|
||||
`deluge-web`
|
||||
|
||||
Open http://localhost:8112 with default password `deluge`.
|
||||
|
||||
### Daemon
|
||||
|
||||
`deluged`
|
||||
|
||||
See the [Thinclient guide] to connect to the daemon from another computer.
|
||||
|
||||
## Contact
|
||||
|
||||
- [Homepage](https://deluge-torrent.org)
|
||||
- [User guide][user guide]
|
||||
- [Forum](https://forum.deluge-torrent.org)
|
||||
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
|
||||
|
||||
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
|
||||
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||
[installing/source]: https://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
[build-status]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml/badge.svg?branch=develop "CI"
|
||||
[github-ci]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml
|
||||
[docs-status]: https://readthedocs.org/projects/deluge/badge/?version=latest
|
||||
[rtd-deluge]: https://deluge.readthedocs.io/en/latest/?badge=latest "Documentation Status"
|
||||
[lt]: https://libtorrent.org
|
68
README.rst
68
README.rst
@ -1,68 +0,0 @@
|
||||
=========================
|
||||
Deluge BitTorrent Client
|
||||
=========================
|
||||
|
||||
|build-status| |docs|
|
||||
|
||||
Homepage: http://deluge-torrent.org
|
||||
|
||||
Authors:
|
||||
Andrew Resch
|
||||
Damien Churchill
|
||||
|
||||
For contributors and past developers see:
|
||||
AUTHORS
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
==========================
|
||||
|
||||
For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
|
||||
Ensure build dependencies are installed, see DEPENDS for a full listing.
|
||||
|
||||
Build and install by running::
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
================
|
||||
Contact/Support:
|
||||
================
|
||||
|
||||
:Forum: http://forum.deluge-torrent.org
|
||||
:IRC Channel: #deluge on irc.freenode.net
|
||||
|
||||
===
|
||||
FAQ
|
||||
===
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
|
||||
How to start the various user-interfaces:
|
||||
Gtk::
|
||||
|
||||
deluge or deluge-gtk
|
||||
|
||||
Console::
|
||||
|
||||
deluge-console
|
||||
|
||||
Web::
|
||||
|
||||
deluge-web
|
||||
Go to http://localhost:8112/ default-password = "deluge"
|
||||
|
||||
How do I start the daemon?:
|
||||
deluged
|
||||
|
||||
I can't connect to the daemon from another machine:
|
||||
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||
|
||||
|
||||
.. |build-status| image:: https://travis-ci.org/deluge-torrent/deluge.svg
|
||||
:target: https://travis-ci.org/deluge-torrent/deluge
|
||||
|
||||
.. |docs| image:: https://readthedocs.org/projects/deluge/badge/?version=develop
|
||||
:target: https://readthedocs.org/projects/deluge/?badge=develop
|
||||
:alt: Documentation Status
|
@ -1,7 +1 @@
|
||||
"""Deluge"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# this is a namespace package
|
||||
import pkg_resources
|
||||
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
|
@ -1,33 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from deluge.core.core import Core
|
||||
from deluge.core.daemon import Daemon
|
||||
|
||||
|
||||
class RpcApi(object):
|
||||
pass
|
||||
|
||||
|
||||
def scan_for_methods(obj):
|
||||
methods = {
|
||||
'__doc__': 'Methods available in %s' % obj.__name__.lower()
|
||||
}
|
||||
for d in dir(obj):
|
||||
if not hasattr(getattr(obj, d), '_rpcserver_export'):
|
||||
continue
|
||||
methods[d] = getattr(obj, d)
|
||||
cobj = type(obj.__name__.lower(), (object,), methods)
|
||||
setattr(RpcApi, obj.__name__.lower(), cobj)
|
||||
|
||||
|
||||
scan_for_methods(Core)
|
||||
scan_for_methods(Daemon)
|
@ -15,16 +15,22 @@ Example:
|
||||
>>> from deluge._libtorrent import lt
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from deluge.common import VersionSplit, get_version
|
||||
from deluge.error import LibtorrentImportError
|
||||
|
||||
try:
|
||||
import deluge.libtorrent as lt
|
||||
except ImportError:
|
||||
import libtorrent as lt
|
||||
try:
|
||||
import libtorrent as lt
|
||||
except ImportError as ex:
|
||||
raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
|
||||
|
||||
|
||||
REQUIRED_VERSION = '1.1.2.0'
|
||||
LT_VERSION = lt.__version__
|
||||
|
||||
if VersionSplit(lt.__version__) < VersionSplit(REQUIRED_VERSION):
|
||||
raise ImportError('Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION))
|
||||
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
|
||||
raise LibtorrentImportError(
|
||||
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
|
||||
)
|
||||
|
@ -7,8 +7,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
@ -86,15 +84,16 @@ argparse.ArgumentParser.find_subcommand = find_subcommand
|
||||
argparse.ArgumentParser.set_default_subparser = set_default_subparser
|
||||
|
||||
|
||||
def get_version():
|
||||
def _get_version_detail():
|
||||
version_str = '%s\n' % (common.get_version())
|
||||
try:
|
||||
from deluge._libtorrent import lt
|
||||
version_str += 'libtorrent: %s\n' % lt.__version__
|
||||
from deluge._libtorrent import LT_VERSION
|
||||
|
||||
version_str += 'libtorrent: %s\n' % LT_VERSION
|
||||
except ImportError:
|
||||
pass
|
||||
version_str += 'Python: %s\n' % platform.python_version()
|
||||
version_str += 'OS: %s %s\n' % (platform.system(), ' '.join(common.get_os_version()))
|
||||
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
|
||||
return version_str
|
||||
|
||||
|
||||
@ -121,7 +120,7 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
||||
|
||||
"""
|
||||
if not action.option_strings:
|
||||
metavar, = self._metavar_formatter(action, action.dest)(1)
|
||||
(metavar,) = self._metavar_formatter(action, action.dest)(1)
|
||||
return metavar
|
||||
else:
|
||||
parts = []
|
||||
@ -141,7 +140,6 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
||||
|
||||
|
||||
class HelpAction(argparse._HelpAction):
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if hasattr(parser, 'subparser'):
|
||||
subparser = getattr(parser, 'subparser')
|
||||
@ -151,11 +149,12 @@ class HelpAction(argparse._HelpAction):
|
||||
parser.exit()
|
||||
|
||||
|
||||
class BaseArgParser(argparse.ArgumentParser):
|
||||
|
||||
class ArgParserBase(argparse.ArgumentParser):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'formatter_class' not in kwargs:
|
||||
kwargs['formatter_class'] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90)
|
||||
kwargs['formatter_class'] = lambda prog: DelugeTextHelpFormatter(
|
||||
prog, max_help_position=33, width=90
|
||||
)
|
||||
|
||||
kwargs['add_help'] = kwargs.get('add_help', False)
|
||||
common_help = kwargs.pop('common_help', True)
|
||||
@ -164,32 +163,73 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
self.log_stream = kwargs['log_stream']
|
||||
del kwargs['log_stream']
|
||||
|
||||
super(BaseArgParser, self).__init__(*args, **kwargs)
|
||||
super(ArgParserBase, self).__init__(*args, **kwargs)
|
||||
|
||||
self.common_setup = False
|
||||
self.process_arg_group = False
|
||||
self.group = self.add_argument_group(_('Common Options'))
|
||||
if common_help:
|
||||
self.group.add_argument('-h', '--help', action=HelpAction,
|
||||
help=_('Print this help message'))
|
||||
self.group.add_argument('-V', '--version', action='version', version='%(prog)s ' + get_version(),
|
||||
help=_('Print version information'))
|
||||
self.group.add_argument('-v', action='version', version='%(prog)s ' + get_version(),
|
||||
help=argparse.SUPPRESS) # Deprecated arg
|
||||
self.group.add_argument('-c', '--config', metavar='<config>',
|
||||
help=_('Set the config directory path'))
|
||||
self.group.add_argument('-l', '--logfile', metavar='<logfile>',
|
||||
help=_('Output to specified logfile instead of stdout'))
|
||||
self.group.add_argument('-L', '--loglevel', 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>')
|
||||
self.group.add_argument('--logrotate', nargs='?', const='2M', metavar='<max-size>',
|
||||
help=_('Enable logfile rotation, with optional maximum logfile size, '
|
||||
'default: %(const)s (Logfile rotation count is 5)'))
|
||||
self.group.add_argument('-q', '--quiet', action='store_true',
|
||||
help=_('Quieten logging output (Same as `--loglevel none`)'))
|
||||
self.group.add_argument('--profile', metavar='<profile-file>', nargs='?', default=False,
|
||||
help=_('Profile %(prog)s with cProfile. Outputs to stdout '
|
||||
'unless a filename is specified'))
|
||||
self.group.add_argument(
|
||||
'-h', '--help', action=HelpAction, help=_('Print this help message')
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-V',
|
||||
'--version',
|
||||
action='version',
|
||||
version='%(prog)s ' + _get_version_detail(),
|
||||
help=_('Print version information'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-v',
|
||||
action='version',
|
||||
version='%(prog)s ' + _get_version_detail(),
|
||||
help=argparse.SUPPRESS,
|
||||
) # Deprecated arg
|
||||
self.group.add_argument(
|
||||
'-c',
|
||||
'--config',
|
||||
metavar='<config>',
|
||||
help=_('Set the config directory path'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-l',
|
||||
'--logfile',
|
||||
metavar='<logfile>',
|
||||
help=_('Output to specified logfile instead of stdout'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-L',
|
||||
'--loglevel',
|
||||
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>',
|
||||
)
|
||||
self.group.add_argument(
|
||||
'--logrotate',
|
||||
nargs='?',
|
||||
const='2M',
|
||||
metavar='<max-size>',
|
||||
help=_(
|
||||
'Enable logfile rotation, with optional maximum logfile size, '
|
||||
'default: %(const)s (Logfile rotation count is 5)'
|
||||
),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-q',
|
||||
'--quiet',
|
||||
action='store_true',
|
||||
help=_('Quieten logging output (Same as `--loglevel none`)'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'--profile',
|
||||
metavar='<profile-file>',
|
||||
nargs='?',
|
||||
default=False,
|
||||
help=_(
|
||||
'Profile %(prog)s with cProfile. Outputs to stdout '
|
||||
'unless a filename is specified'
|
||||
),
|
||||
)
|
||||
|
||||
def parse_args(self, args=None):
|
||||
"""Parse UI arguments and handle common and process group options.
|
||||
@ -204,7 +244,7 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
argparse.Namespace: The parsed arguments.
|
||||
|
||||
"""
|
||||
options = super(BaseArgParser, self).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):
|
||||
@ -220,9 +260,9 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
"""
|
||||
if withhold:
|
||||
args = [a for a in args if a not in withhold]
|
||||
options, remaining = super(BaseArgParser, self).parse_known_args(args=args)
|
||||
options, remaining = super(ArgParserBase, self).parse_known_args(args=args)
|
||||
options.remaining = remaining
|
||||
# Hanlde common and process group options
|
||||
# Handle common and process group options
|
||||
return self._handle_ui_options(options)
|
||||
|
||||
def _handle_ui_options(self, options):
|
||||
@ -251,8 +291,13 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
logrotate = common.parse_human_size(options.logrotate)
|
||||
|
||||
# Setup the logger
|
||||
deluge.log.setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode,
|
||||
logrotate=logrotate, output_stream=self.log_stream)
|
||||
deluge.log.setup_logger(
|
||||
level=options.loglevel,
|
||||
filename=options.logfile,
|
||||
filemode=logfile_mode,
|
||||
logrotate=logrotate,
|
||||
output_stream=self.log_stream,
|
||||
)
|
||||
|
||||
if options.config:
|
||||
if not set_config_dir(options.config):
|
||||
@ -278,20 +323,22 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
|
||||
# Write pid file before chuid
|
||||
if options.pidfile:
|
||||
with open(options.pidfile, 'wb') as _file:
|
||||
with open(options.pidfile, 'w') as _file:
|
||||
_file.write('%d\n' % os.getpid())
|
||||
|
||||
if not common.windows_check():
|
||||
if options.user:
|
||||
if not options.user.isdigit():
|
||||
import pwd
|
||||
options.user = pwd.getpwnam(options.user)[2]
|
||||
os.setuid(options.user)
|
||||
if options.group:
|
||||
if not options.group.isdigit():
|
||||
import grp
|
||||
|
||||
options.group = grp.getgrnam(options.group)[2]
|
||||
os.setuid(options.group)
|
||||
os.setgid(options.group)
|
||||
if options.user:
|
||||
if not options.user.isdigit():
|
||||
import pwd
|
||||
|
||||
options.user = pwd.getpwnam(options.user)[2]
|
||||
os.setuid(options.user)
|
||||
|
||||
return options
|
||||
|
||||
@ -300,14 +347,39 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||
|
||||
self.process_arg_group = True
|
||||
self.group = self.add_argument_group(_('Process Control Options'))
|
||||
self.group.add_argument('-P', '--pidfile', metavar='<pidfile>', action='store',
|
||||
help=_('Pidfile to store the process id'))
|
||||
self.group.add_argument(
|
||||
'-P',
|
||||
'--pidfile',
|
||||
metavar='<pidfile>',
|
||||
action='store',
|
||||
help=_('Pidfile to store the process id'),
|
||||
)
|
||||
if not common.windows_check():
|
||||
self.group.add_argument('-d', '--do-not-daemonize', dest='donotdaemonize', action='store_true',
|
||||
help=_('Do not daemonize (fork) this process'))
|
||||
self.group.add_argument('-f', '--fork', dest='donotdaemonize', action='store_false',
|
||||
help=argparse.SUPPRESS) # Deprecated arg
|
||||
self.group.add_argument('-U', '--user', metavar='<user>', action='store',
|
||||
help=_('Change to this user on startup (Requires root)'))
|
||||
self.group.add_argument('-g', '--group', metavar='<group>', action='store',
|
||||
help=_('Change to this group on startup (Requires root)'))
|
||||
self.group.add_argument(
|
||||
'-d',
|
||||
'--do-not-daemonize',
|
||||
dest='donotdaemonize',
|
||||
action='store_true',
|
||||
help=_('Do not daemonize (fork) this process'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-f',
|
||||
'--fork',
|
||||
dest='donotdaemonize',
|
||||
action='store_false',
|
||||
help=argparse.SUPPRESS,
|
||||
) # Deprecated arg
|
||||
self.group.add_argument(
|
||||
'-U',
|
||||
'--user',
|
||||
metavar='<user>',
|
||||
action='store',
|
||||
help=_('Change to this user on startup (Requires root)'),
|
||||
)
|
||||
self.group.add_argument(
|
||||
'-g',
|
||||
'--group',
|
||||
metavar='<group>',
|
||||
action='store',
|
||||
help=_('Change to this group on startup (Requires root)'),
|
||||
)
|
@ -9,11 +9,7 @@
|
||||
# License.
|
||||
|
||||
# Written by Petru Paler
|
||||
# Updated by Calum Lind to support both Python 2 and Python 3.
|
||||
|
||||
from sys import version_info
|
||||
|
||||
PY2 = version_info.major == 2
|
||||
# Updated by Calum Lind to support Python 3.
|
||||
|
||||
|
||||
class BTFailure(Exception):
|
||||
@ -31,9 +27,9 @@ def decode_int(x, f):
|
||||
f += 1
|
||||
newf = x.index(END_DELIM, f)
|
||||
n = int(x[f:newf])
|
||||
if x[f:f+1] == b'-' and x[f+1:f+2] == b'0':
|
||||
if x[f : f + 1] == b'-' and x[f + 1 : f + 2] == b'0':
|
||||
raise ValueError
|
||||
elif x[f:f+1] == b'0' and newf != f + 1:
|
||||
elif x[f : f + 1] == b'0' and newf != f + 1:
|
||||
raise ValueError
|
||||
return (n, newf + 1)
|
||||
|
||||
@ -41,25 +37,25 @@ def decode_int(x, f):
|
||||
def decode_string(x, f):
|
||||
colon = x.index(BYTE_SEP, f)
|
||||
n = int(x[f:colon])
|
||||
if x[f:f+1] == b'0' and colon != f + 1:
|
||||
if x[f : f + 1] == b'0' and colon != f + 1:
|
||||
raise ValueError
|
||||
colon += 1
|
||||
return (x[colon:colon + n], colon + n)
|
||||
return (x[colon : colon + n], colon + n)
|
||||
|
||||
|
||||
def decode_list(x, f):
|
||||
r, f = [], f + 1
|
||||
while x[f:f+1] != END_DELIM:
|
||||
v, f = decode_func[x[f:f+1]](x, f)
|
||||
while x[f : f + 1] != END_DELIM:
|
||||
v, f = decode_func[x[f : f + 1]](x, f)
|
||||
r.append(v)
|
||||
return (r, f + 1)
|
||||
|
||||
|
||||
def decode_dict(x, f):
|
||||
r, f = {}, f + 1
|
||||
while x[f:f+1] != END_DELIM:
|
||||
while x[f : f + 1] != END_DELIM:
|
||||
k, f = decode_string(x, f)
|
||||
r[k], f = decode_func[x[f:f+1]](x, f)
|
||||
r[k], f = decode_func[x[f : f + 1]](x, f)
|
||||
return (r, f + 1)
|
||||
|
||||
|
||||
@ -81,8 +77,8 @@ decode_func[b'9'] = decode_string
|
||||
|
||||
def bdecode(x):
|
||||
try:
|
||||
r, l = decode_func[x[0:1]](x, 0)
|
||||
except (IndexError, KeyError, ValueError):
|
||||
r, __ = decode_func[x[0:1]](x, 0)
|
||||
except (LookupError, TypeError, ValueError):
|
||||
raise BTFailure('Not a valid bencoded string')
|
||||
else:
|
||||
return r
|
||||
@ -109,7 +105,7 @@ def encode_bool(x, r):
|
||||
|
||||
|
||||
def encode_string(x, r):
|
||||
encode_string(x.encode('utf8'), r)
|
||||
encode_bytes(x.encode('utf8'), r)
|
||||
|
||||
|
||||
def encode_bytes(x, r):
|
||||
@ -126,6 +122,10 @@ def encode_list(x, r):
|
||||
def encode_dict(x, r):
|
||||
r.append(DICT_DELIM)
|
||||
for k, v in sorted(x.items()):
|
||||
try:
|
||||
k = k.encode('utf8')
|
||||
except AttributeError:
|
||||
pass
|
||||
r.extend((str(len(k)).encode('utf8'), BYTE_SEP, k))
|
||||
encode_func[type(v)](v, r)
|
||||
r.append(END_DELIM)
|
||||
@ -140,10 +140,6 @@ encode_func[dict] = encode_dict
|
||||
encode_func[bool] = encode_bool
|
||||
encode_func[str] = encode_string
|
||||
encode_func[bytes] = encode_bytes
|
||||
if PY2:
|
||||
encode_func[long] = encode_int
|
||||
encode_func[str] = encode_bytes
|
||||
encode_func[unicode] = encode_string
|
||||
|
||||
|
||||
def bencode(x):
|
||||
|
612
deluge/common.py
612
deluge/common.py
@ -8,10 +8,8 @@
|
||||
#
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import binascii
|
||||
import functools
|
||||
import glob
|
||||
import locale
|
||||
@ -24,33 +22,40 @@ import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
from contextlib import closing
|
||||
from datetime import datetime
|
||||
from io import BytesIO, open
|
||||
from urllib.parse import unquote_plus, urljoin
|
||||
from urllib.request import pathname2url
|
||||
|
||||
import chardet
|
||||
import pkg_resources
|
||||
|
||||
from deluge.decorators import deprecated
|
||||
from deluge.error import InvalidPathError
|
||||
|
||||
try:
|
||||
from urllib.parse import unquote_plus, urljoin
|
||||
from urllib.request import pathname2url
|
||||
import chardet
|
||||
except ImportError:
|
||||
# PY2 fallback
|
||||
from urlparse import urljoin # pylint: disable=ungrouped-imports
|
||||
from urllib import pathname2url, unquote_plus # pylint: disable=ungrouped-imports
|
||||
chardet = None
|
||||
|
||||
# Windows workaround for HTTPS requests requiring certificate authority bundle.
|
||||
# see: https://twistedmatrix.com/trac/ticket/9209
|
||||
if platform.system() in ('Windows', 'Microsoft'):
|
||||
from certifi import where
|
||||
|
||||
os.environ['SSL_CERT_FILE'] = where()
|
||||
|
||||
|
||||
DBUS_FILEMAN = None
|
||||
# gi makes dbus available on Window but don't import it as unused.
|
||||
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
|
||||
# gi makes dbus available on Window but don't import it as unused.
|
||||
try:
|
||||
import dbus
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
bus = dbus.SessionBus()
|
||||
DBUS_FILEMAN = bus.get_object('org.freedesktop.FileManager1', '/org/freedesktop/FileManager1')
|
||||
except dbus.DBusException:
|
||||
pass
|
||||
dbus = None
|
||||
try:
|
||||
import distro
|
||||
except ImportError:
|
||||
distro = None
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -62,24 +67,23 @@ TORRENT_STATE = [
|
||||
'Paused',
|
||||
'Error',
|
||||
'Queued',
|
||||
'Moving'
|
||||
'Moving',
|
||||
]
|
||||
|
||||
# The output formatting for json.dump
|
||||
JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False}
|
||||
|
||||
PY2 = sys.version_info.major == 2
|
||||
DBUS_FM_ID = 'org.freedesktop.FileManager1'
|
||||
DBUS_FM_PATH = '/org/freedesktop/FileManager1'
|
||||
|
||||
|
||||
def get_version():
|
||||
"""
|
||||
Returns the program version from the egg metadata
|
||||
|
||||
:returns: the version of Deluge
|
||||
:rtype: string
|
||||
"""The program version from the egg metadata.
|
||||
|
||||
Returns:
|
||||
str: The version of Deluge.
|
||||
"""
|
||||
return pkg_resources.require('Deluge')[0].version
|
||||
return pkg_resources.get_distribution('Deluge').version
|
||||
|
||||
|
||||
def get_default_config_dir(filename=None):
|
||||
@ -93,20 +97,21 @@ def get_default_config_dir(filename=None):
|
||||
"""
|
||||
|
||||
if windows_check():
|
||||
|
||||
def save_config_path(resource):
|
||||
app_data_path = os.environ.get('APPDATA')
|
||||
if not app_data_path:
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg # For Python 2.
|
||||
import winreg
|
||||
|
||||
hkey = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders')
|
||||
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
|
||||
)
|
||||
app_data_reg = winreg.QueryValueEx(hkey, 'AppData')
|
||||
app_data_path = app_data_reg[0]
|
||||
winreg.CloseKey(hkey)
|
||||
return os.path.join(app_data_path, resource)
|
||||
|
||||
else:
|
||||
from xdg.BaseDirectory import save_config_path
|
||||
if not filename:
|
||||
@ -127,11 +132,15 @@ def get_default_download_dir():
|
||||
download_dir = ''
|
||||
if not windows_check():
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
|
||||
try:
|
||||
with open(os.path.join(xdg_config_home, 'user-dirs.dirs'), 'r') as _file:
|
||||
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||
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('"'))
|
||||
download_dir = os.path.expandvars(
|
||||
line.partition('=')[2].rstrip().strip('"')
|
||||
)
|
||||
break
|
||||
except IOError:
|
||||
pass
|
||||
@ -141,47 +150,58 @@ def get_default_download_dir():
|
||||
return download_dir
|
||||
|
||||
|
||||
def archive_files(arc_name, filepaths):
|
||||
"""Compress a list of filepaths into timestamped tarball in config dir.
|
||||
def archive_files(arc_name, filepaths, message=None, rotate=10):
|
||||
"""Compress a list of filepaths into timestamped tarball in config dir.
|
||||
|
||||
The archiving config directory is 'archive'.
|
||||
The archiving config directory is 'archive'.
|
||||
|
||||
Args:
|
||||
arc_name (str): The archive output filename (appended with timestamp).
|
||||
filepaths (list): A list of the files to be archived into tarball.
|
||||
Args:
|
||||
arc_name (str): The archive output filename (appended with timestamp).
|
||||
filepaths (list): A list of the files to be archived into tarball.
|
||||
|
||||
Returns:
|
||||
str: The full archive filepath.
|
||||
Returns:
|
||||
str: The full archive filepath.
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from deluge.configmanager import get_config_dir
|
||||
from deluge.configmanager import get_config_dir
|
||||
|
||||
# Set archive compression to lzma with bz2 fallback.
|
||||
arc_comp = 'xz' if not PY2 else 'bz2'
|
||||
# Set archive compression to lzma
|
||||
arc_comp = 'xz'
|
||||
|
||||
archive_dir = os.path.join(get_config_dir(), 'archive')
|
||||
timestamp = datetime.datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
|
||||
arc_filepath = os.path.join(archive_dir, arc_name + '-' + timestamp + '.tar.' + arc_comp)
|
||||
max_num_arcs = 20
|
||||
archive_dir = os.path.join(get_config_dir(), 'archive')
|
||||
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
|
||||
arc_filepath = os.path.join(
|
||||
archive_dir, arc_name + '-' + timestamp + '.tar.' + arc_comp
|
||||
)
|
||||
|
||||
if not os.path.exists(archive_dir):
|
||||
os.makedirs(archive_dir)
|
||||
else:
|
||||
old_arcs = glob.glob(os.path.join(archive_dir, arc_name) + '*')
|
||||
if len(old_arcs) > max_num_arcs:
|
||||
# TODO: Remove oldest timestamped archives.
|
||||
log.warning('More than %s tarballs in config archive', max_num_arcs)
|
||||
if not os.path.exists(archive_dir):
|
||||
os.makedirs(archive_dir)
|
||||
else:
|
||||
all_arcs = glob.glob(os.path.join(archive_dir, arc_name) + '*')
|
||||
if len(all_arcs) >= rotate:
|
||||
log.warning(
|
||||
'Too many existing archives for %s. Deleting oldest archive.', arc_name
|
||||
)
|
||||
os.remove(sorted(all_arcs)[0])
|
||||
|
||||
try:
|
||||
with tarfile.open(arc_filepath, 'w:' + arc_comp) as tf:
|
||||
for filepath in filepaths:
|
||||
tf.add(filepath, arcname=os.path.basename(filepath))
|
||||
except OSError:
|
||||
log.error('Problem occurred archiving filepaths: %s', filepaths)
|
||||
return False
|
||||
else:
|
||||
return arc_filepath
|
||||
try:
|
||||
with tarfile.open(arc_filepath, 'w:' + arc_comp) as tar:
|
||||
for filepath in filepaths:
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
tar.add(filepath, arcname=os.path.basename(filepath))
|
||||
if message:
|
||||
with closing(BytesIO(message.encode('utf8'))) as fobj:
|
||||
tarinfo = tarfile.TarInfo('archive_message.txt')
|
||||
tarinfo.size = len(fobj.getvalue())
|
||||
tarinfo.mtime = time.time()
|
||||
tar.addfile(tarinfo, fileobj=fobj)
|
||||
except OSError:
|
||||
log.error('Problem occurred archiving filepaths: %s', filepaths)
|
||||
return False
|
||||
else:
|
||||
return arc_filepath
|
||||
|
||||
|
||||
def windows_check():
|
||||
@ -229,14 +249,25 @@ def linux_check():
|
||||
|
||||
|
||||
def get_os_version():
|
||||
"""Parse and return the os version information.
|
||||
|
||||
Converts the platform ver tuple to a string.
|
||||
|
||||
Returns:
|
||||
str: The os version info.
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return platform.win32_ver()
|
||||
os_version = platform.win32_ver()
|
||||
elif osx_check():
|
||||
return platform.mac_ver()
|
||||
elif linux_check():
|
||||
return platform.linux_distribution()
|
||||
os_version = list(platform.mac_ver())
|
||||
os_version[1] = '' # versioninfo always empty.
|
||||
elif distro:
|
||||
os_version = distro.linux_distribution()
|
||||
else:
|
||||
return (platform.release(), )
|
||||
os_version = (platform.release(),)
|
||||
|
||||
return ' '.join(filter(None, os_version))
|
||||
|
||||
|
||||
def get_pixmap(fname):
|
||||
@ -253,14 +284,17 @@ def get_pixmap(fname):
|
||||
|
||||
|
||||
def resource_filename(module, path):
|
||||
"""While developing, if there's a second deluge package, installed globally
|
||||
and another in develop mode somewhere else, while pkg_resources.require('Deluge')
|
||||
returns the proper deluge instance, pkg_resources.resource_filename does
|
||||
not, it returns the first found on the python path, which is not good
|
||||
enough.
|
||||
This is a work-around that.
|
||||
"""Get filesystem path for a resource.
|
||||
|
||||
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.
|
||||
"""
|
||||
return pkg_resources.require('Deluge>=%s' % get_version())[0].get_resource_filename(
|
||||
return pkg_resources.get_distribution('Deluge').get_resource_filename(
|
||||
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
|
||||
)
|
||||
|
||||
@ -281,8 +315,12 @@ def open_file(path, timestamp=None):
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
env = os.environ.copy()
|
||||
env['DESKTOP_STARTUP_ID'] = '%s-%u-%s-xdg_open_TIME%d' % \
|
||||
(os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
|
||||
env['DESKTOP_STARTUP_ID'] = '%s-%u-%s-xdg_open_TIME%d' % (
|
||||
os.path.basename(sys.argv[0]),
|
||||
os.getpid(),
|
||||
os.uname()[1],
|
||||
timestamp,
|
||||
)
|
||||
subprocess.Popen(['xdg-open', '%s' % path], env=env)
|
||||
|
||||
|
||||
@ -301,26 +339,42 @@ def show_file(path, timestamp=None):
|
||||
else:
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
startup_id = '%s_%u_%s-dbus_TIME%d' % (os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
|
||||
if DBUS_FILEMAN:
|
||||
paths = [urljoin('file:', pathname2url(path))]
|
||||
DBUS_FILEMAN.ShowItems(paths, startup_id, dbus_interface='org.freedesktop.FileManager1')
|
||||
else:
|
||||
env = os.environ.copy()
|
||||
env['DESKTOP_STARTUP_ID'] = startup_id.replace('dbus', 'xdg-open')
|
||||
# No option in xdg to highlight a file so just open parent folder.
|
||||
subprocess.Popen(['xdg-open', os.path.dirname(path.rstrip('/'))], env=env)
|
||||
startup_id = '%s_%u_%s-dbus_TIME%d TIMESTAMP=%d' % (
|
||||
os.path.basename(sys.argv[0]),
|
||||
os.getpid(),
|
||||
os.uname()[1],
|
||||
timestamp,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
if dbus:
|
||||
bus = dbus.SessionBus()
|
||||
try:
|
||||
filemanager1 = bus.get_object(DBUS_FM_ID, DBUS_FM_PATH)
|
||||
except dbus.exceptions.DBusException as ex:
|
||||
log.debug('Unable to get dbus file manager: %s', ex)
|
||||
# Fallback to xdg-open
|
||||
else:
|
||||
paths = [urljoin('file:', pathname2url(path))]
|
||||
filemanager1.ShowItems(paths, startup_id, dbus_interface=DBUS_FM_ID)
|
||||
return
|
||||
|
||||
env = os.environ.copy()
|
||||
env['DESKTOP_STARTUP_ID'] = startup_id.replace('dbus', 'xdg-open')
|
||||
# No option in xdg to highlight a file so just open parent folder.
|
||||
subprocess.Popen(['xdg-open', os.path.dirname(path.rstrip('/'))], env=env)
|
||||
|
||||
|
||||
def open_url_in_browser(url):
|
||||
"""
|
||||
Opens a url in the desktop's default browser
|
||||
Opens a URL in the desktop's default browser
|
||||
|
||||
:param url: the url to open
|
||||
:param url: the URL to open
|
||||
:type url: string
|
||||
|
||||
"""
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(url)
|
||||
|
||||
|
||||
@ -370,19 +424,35 @@ def fsize(fsize_b, precision=1, shortform=False):
|
||||
'110 KiB'
|
||||
|
||||
Note:
|
||||
This function has been refactored for perfomance with the
|
||||
This function has been refactored for performance with the
|
||||
fsize units being translated outside the function.
|
||||
|
||||
"""
|
||||
|
||||
if fsize_b >= 1024 ** 4:
|
||||
return '%.*f %s' % (precision, fsize_b / 1024 ** 4, tib_txt_short if shortform else tib_txt)
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
fsize_b / 1024 ** 4,
|
||||
tib_txt_short if shortform else tib_txt,
|
||||
)
|
||||
elif fsize_b >= 1024 ** 3:
|
||||
return '%.*f %s' % (precision, fsize_b / 1024 ** 3, gib_txt_short if shortform else gib_txt)
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
fsize_b / 1024 ** 3,
|
||||
gib_txt_short if shortform else gib_txt,
|
||||
)
|
||||
elif fsize_b >= 1024 ** 2:
|
||||
return '%.*f %s' % (precision, fsize_b / 1024 ** 2, mib_txt_short if shortform else mib_txt)
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
fsize_b / 1024 ** 2,
|
||||
mib_txt_short if shortform else mib_txt,
|
||||
)
|
||||
elif fsize_b >= 1024:
|
||||
return '%.*f %s' % (precision, fsize_b / 1024, kib_txt_short if shortform else kib_txt)
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
fsize_b / 1024,
|
||||
kib_txt_short if shortform else kib_txt,
|
||||
)
|
||||
else:
|
||||
return '%d %s' % (fsize_b, byte_txt)
|
||||
|
||||
@ -405,7 +475,7 @@ def fpcnt(dec, precision=2):
|
||||
|
||||
"""
|
||||
|
||||
pcnt = (dec * 100)
|
||||
pcnt = dec * 100
|
||||
if pcnt == 0 or pcnt == 100:
|
||||
precision = 0
|
||||
return '%.*f%%' % (precision, pcnt)
|
||||
@ -427,13 +497,29 @@ def fspeed(bps, precision=1, shortform=False):
|
||||
"""
|
||||
|
||||
if bps < 1024 ** 2:
|
||||
return '%.*f %s' % (precision, bps / 1024, _('K/s') if shortform else _('KiB/s'))
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
bps / 1024,
|
||||
_('K/s') if shortform else _('KiB/s'),
|
||||
)
|
||||
elif bps < 1024 ** 3:
|
||||
return '%.*f %s' % (precision, bps / 1024 ** 2, _('M/s') if shortform else _('MiB/s'))
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
bps / 1024 ** 2,
|
||||
_('M/s') if shortform else _('MiB/s'),
|
||||
)
|
||||
elif bps < 1024 ** 4:
|
||||
return '%.*f %s' % (precision, bps / 1024 ** 3, _('G/s') if shortform else _('GiB/s'))
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
bps / 1024 ** 3,
|
||||
_('G/s') if shortform else _('GiB/s'),
|
||||
)
|
||||
else:
|
||||
return '%.*f %s' % (precision, bps / 1024 ** 4, _('T/s') if shortform else _('TiB/s'))
|
||||
return '%.*f %s' % (
|
||||
precision,
|
||||
bps / 1024 ** 4,
|
||||
_('T/s') if shortform else _('TiB/s'),
|
||||
)
|
||||
|
||||
|
||||
def fpeer(num_peers, total_peers):
|
||||
@ -463,7 +549,7 @@ def ftime(secs):
|
||||
"""Formats a string to show time in a human readable form.
|
||||
|
||||
Args:
|
||||
secs (int): The number of seconds.
|
||||
secs (int or float): The number of seconds.
|
||||
|
||||
Returns:
|
||||
str: A formatted time string or empty string if value is 0.
|
||||
@ -473,24 +559,26 @@ def ftime(secs):
|
||||
'6h 23m'
|
||||
|
||||
Note:
|
||||
This function has been refactored for perfomance.
|
||||
This function has been refactored for performance.
|
||||
|
||||
"""
|
||||
|
||||
# Handle floats by truncating to an int
|
||||
secs = int(secs)
|
||||
if secs <= 0:
|
||||
time_str = ''
|
||||
elif secs < 60:
|
||||
time_str = '{:d}s'.format(secs)
|
||||
time_str = '{}s'.format(secs)
|
||||
elif secs < 3600:
|
||||
time_str = '{:d}m {:d}s'.format(secs // 60, secs % 60)
|
||||
time_str = '{}m {}s'.format(secs // 60, secs % 60)
|
||||
elif secs < 86400:
|
||||
time_str = '{:d}h {:d}m'.format(secs // 3600, secs // 60 % 60)
|
||||
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60)
|
||||
elif secs < 604800:
|
||||
time_str = '{:d}d {:d}h'.format(secs // 86400, secs // 3600 % 24)
|
||||
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24)
|
||||
elif secs < 31449600:
|
||||
time_str = '{:d}w {:d}d'.format(secs // 604800, secs // 86400 % 7)
|
||||
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7)
|
||||
else:
|
||||
time_str = '{:d}y {:d}w'.format(secs // 31449600, secs // 604800 % 52)
|
||||
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52)
|
||||
|
||||
return time_str
|
||||
|
||||
@ -542,18 +630,20 @@ def tokenize(text):
|
||||
return tokenized_input
|
||||
|
||||
|
||||
size_units = (dict(prefix='b', divider=1, singular='byte', plural='bytes'),
|
||||
dict(prefix='KiB', divider=1024**1),
|
||||
dict(prefix='MiB', divider=1024**2),
|
||||
dict(prefix='GiB', divider=1024**3),
|
||||
dict(prefix='TiB', divider=1024**4),
|
||||
dict(prefix='PiB', divider=1024**5),
|
||||
dict(prefix='KB', divider=1000**1),
|
||||
dict(prefix='MB', divider=1000**2),
|
||||
dict(prefix='GB', divider=1000**3),
|
||||
dict(prefix='TB', divider=1000**4),
|
||||
dict(prefix='PB', divider=1000**5),
|
||||
dict(prefix='m', divider=1000**2))
|
||||
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': '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},
|
||||
]
|
||||
|
||||
|
||||
class InvalidSize(Exception):
|
||||
@ -599,7 +689,7 @@ def is_url(url):
|
||||
"""
|
||||
A simple test to check if the URL is valid
|
||||
|
||||
:param url: the url to test
|
||||
:param url: the URL to test
|
||||
:type url: string
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
@ -635,9 +725,9 @@ TR_PARAM = 'tr='
|
||||
|
||||
def is_magnet(uri):
|
||||
"""
|
||||
A check to determine if a uri is a valid bittorrent magnet uri
|
||||
A check to determine if a URI is a valid bittorrent magnet URI
|
||||
|
||||
:param uri: the uri to check
|
||||
:param uri: the URI to check
|
||||
:type uri: string
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
@ -648,10 +738,10 @@ def is_magnet(uri):
|
||||
True
|
||||
|
||||
"""
|
||||
if not uri:
|
||||
return False
|
||||
|
||||
if uri.startswith(MAGNET_SCHEME) and XT_BTIH_PARAM in uri:
|
||||
return True
|
||||
return False
|
||||
return uri.startswith(MAGNET_SCHEME) and XT_BTIH_PARAM in uri
|
||||
|
||||
|
||||
def get_magnet_info(uri):
|
||||
@ -674,7 +764,7 @@ def get_magnet_info(uri):
|
||||
"""
|
||||
|
||||
tr0_param = 'tr.'
|
||||
tr0_param_regex = re.compile('^tr.(\d+)=(\S+)')
|
||||
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
|
||||
if not uri.startswith(MAGNET_SCHEME):
|
||||
return {}
|
||||
|
||||
@ -682,23 +772,24 @@ def get_magnet_info(uri):
|
||||
info_hash = None
|
||||
trackers = {}
|
||||
tier = 0
|
||||
for param in uri[len(MAGNET_SCHEME):].split('&'):
|
||||
for param in uri[len(MAGNET_SCHEME) :].split('&'):
|
||||
if param.startswith(XT_BTIH_PARAM):
|
||||
xt_hash = param[len(XT_BTIH_PARAM):]
|
||||
xt_hash = param[len(XT_BTIH_PARAM) :]
|
||||
if len(xt_hash) == 32:
|
||||
try:
|
||||
info_hash = base64.b32decode(xt_hash.upper()).encode('hex')
|
||||
infohash_str = base64.b32decode(xt_hash.upper())
|
||||
except TypeError as ex:
|
||||
log.debug('Invalid base32 magnet hash: %s, %s', xt_hash, ex)
|
||||
break
|
||||
info_hash = binascii.hexlify(infohash_str).decode()
|
||||
elif is_infohash(xt_hash):
|
||||
info_hash = xt_hash.lower()
|
||||
else:
|
||||
break
|
||||
elif param.startswith(DN_PARAM):
|
||||
name = unquote_plus(param[len(DN_PARAM):])
|
||||
name = unquote_plus(param[len(DN_PARAM) :])
|
||||
elif param.startswith(TR_PARAM):
|
||||
tracker = unquote_plus(param[len(TR_PARAM):])
|
||||
tracker = unquote_plus(param[len(TR_PARAM) :])
|
||||
trackers[tracker] = tier
|
||||
tier += 1
|
||||
elif param.startswith(tr0_param):
|
||||
@ -711,13 +802,18 @@ def get_magnet_info(uri):
|
||||
if info_hash:
|
||||
if not name:
|
||||
name = info_hash
|
||||
return {'name': name, 'info_hash': info_hash, 'files_tree': '', 'trackers': trackers}
|
||||
return {
|
||||
'name': name,
|
||||
'info_hash': info_hash,
|
||||
'files_tree': '',
|
||||
'trackers': trackers,
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def create_magnet_uri(infohash, name=None, trackers=None):
|
||||
"""Creates a magnet uri
|
||||
"""Creates a magnet URI
|
||||
|
||||
Args:
|
||||
infohash (str): The info-hash of the torrent.
|
||||
@ -725,15 +821,15 @@ def create_magnet_uri(infohash, name=None, trackers=None):
|
||||
trackers (list or dict, optional): A list of trackers or dict or {tracker: tier} pairs.
|
||||
|
||||
Returns:
|
||||
str: A magnet uri string.
|
||||
str: A magnet URI string.
|
||||
|
||||
"""
|
||||
try:
|
||||
infohash = infohash.decode('hex')
|
||||
except AttributeError:
|
||||
pass
|
||||
infohash = binascii.unhexlify(infohash)
|
||||
except TypeError:
|
||||
infohash.encode('utf-8')
|
||||
|
||||
uri = [MAGNET_SCHEME, XT_BTIH_PARAM, base64.b32encode(infohash)]
|
||||
uri = [MAGNET_SCHEME, XT_BTIH_PARAM, base64.b32encode(infohash).decode('utf-8')]
|
||||
if name:
|
||||
uri.extend(['&', DN_PARAM, name])
|
||||
if trackers:
|
||||
@ -788,6 +884,7 @@ def free_space(path):
|
||||
|
||||
if windows_check():
|
||||
from win32file import GetDiskFreeSpaceEx
|
||||
|
||||
return GetDiskFreeSpaceEx(path)[0]
|
||||
else:
|
||||
disk_data = os.statvfs(path.encode('utf8'))
|
||||
@ -831,6 +928,7 @@ def is_ipv4(ip):
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
try:
|
||||
if windows_check():
|
||||
return socket.inet_aton(ip)
|
||||
@ -859,6 +957,7 @@ def is_ipv6(ip):
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
import socket
|
||||
|
||||
try:
|
||||
return socket.inet_pton(socket.AF_INET6, ip)
|
||||
except (socket.error, AttributeError):
|
||||
@ -894,22 +993,34 @@ def decode_bytes(byte_str, encoding='utf8'):
|
||||
elif not isinstance(byte_str, bytes):
|
||||
return byte_str
|
||||
|
||||
encodings = [lambda: ('utf8', 'strict'),
|
||||
lambda: ('iso-8859-1', 'strict'),
|
||||
lambda: (chardet.detect(byte_str)['encoding'], 'strict'),
|
||||
lambda: (encoding, 'ignore')]
|
||||
encodings = [lambda: ('utf8', 'strict'), lambda: ('iso-8859-1', 'strict')]
|
||||
if chardet:
|
||||
encodings.append(lambda: (chardet.detect(byte_str)['encoding'], 'strict'))
|
||||
encodings.append(lambda: (encoding, 'ignore'))
|
||||
|
||||
if encoding is not 'utf8':
|
||||
if encoding.lower() not in ['utf8', 'utf-8']:
|
||||
encodings.insert(0, lambda: (encoding, 'strict'))
|
||||
|
||||
for l in encodings:
|
||||
for enc in encodings:
|
||||
try:
|
||||
return byte_str.decode(*l())
|
||||
return byte_str.decode(*enc())
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
@deprecated
|
||||
def decode_string(byte_str, encoding='utf8'):
|
||||
"""Deprecated: Use decode_bytes"""
|
||||
return decode_bytes(byte_str, encoding)
|
||||
|
||||
|
||||
@deprecated
|
||||
def utf8_encoded(str_, encoding='utf8'):
|
||||
"""Deprecated: Use encode or decode_bytes if needed"""
|
||||
return decode_bytes(str_, encoding).encode('utf8')
|
||||
|
||||
|
||||
def utf8_encode_structure(data):
|
||||
"""Recursively convert all unicode keys and values in a data structure to utf8.
|
||||
|
||||
@ -925,7 +1036,9 @@ def utf8_encode_structure(data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
return type(data)([utf8_encode_structure(d) for d in data])
|
||||
elif isinstance(data, dict):
|
||||
return dict([utf8_encode_structure(d) for d in data.items()])
|
||||
return {
|
||||
utf8_encode_structure(k): utf8_encode_structure(v) for k, v in data.items()
|
||||
}
|
||||
elif not isinstance(data, bytes):
|
||||
try:
|
||||
return data.encode('utf8')
|
||||
@ -943,8 +1056,10 @@ class VersionSplit(object):
|
||||
:type ver: string
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ver):
|
||||
version_re = re.compile(r"""
|
||||
version_re = re.compile(
|
||||
r"""
|
||||
^
|
||||
(?P<version>\d+\.\d+) # minimum 'N.N'
|
||||
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
|
||||
@ -954,7 +1069,9 @@ class VersionSplit(object):
|
||||
(?P<prerelversion>\d+(?:\.\d+)*)
|
||||
)?
|
||||
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
|
||||
$""", re.VERBOSE)
|
||||
$""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
# Check for PEP 386 compliant version
|
||||
match = re.search(version_re, ver)
|
||||
@ -968,25 +1085,27 @@ class VersionSplit(object):
|
||||
self.version = [int(x) for x in vs[0].split('.') if x.isdigit()]
|
||||
self.version_string = ''.join(str(x) for x in vs[0].split('.') if x.isdigit())
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
self.dev = None
|
||||
if len(vs) > 1:
|
||||
if vs[1].startswith(('rc', 'a', 'b', 'c')):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1].startswith('dev'):
|
||||
self.dev = vs[-1]
|
||||
try:
|
||||
# Store only the dev numeral.
|
||||
self.dev = int(vs[-1].rsplit('dev')[1])
|
||||
except ValueError:
|
||||
# Implicit dev numeral is 0.
|
||||
self.dev = 0
|
||||
|
||||
def get_comparable_versions(self, other):
|
||||
"""
|
||||
Returns a 2-tuple of lists for use in the comparison
|
||||
methods.
|
||||
"""
|
||||
# PEP 386 versions with .devN precede release version
|
||||
if bool(self.dev) != bool(other.dev):
|
||||
if self.dev != 'dev':
|
||||
self.dev = not self.dev
|
||||
if other.dev != 'dev':
|
||||
other.dev = not other.dev
|
||||
|
||||
# PEP 386 versions with .devN precede release version so default
|
||||
# non-dev versions to infinity while dev versions are ints.
|
||||
self.dev = float('inf') if self.dev is None else self.dev
|
||||
other.dev = float('inf') if other.dev is None else other.dev
|
||||
# If there is no suffix we use z because we want final
|
||||
# to appear after alpha, beta, and rc alphabetically.
|
||||
v1 = [self.version, self.suffix or 'z', self.dev]
|
||||
@ -1013,12 +1132,13 @@ AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
|
||||
def create_auth_file():
|
||||
import stat
|
||||
|
||||
import deluge.configmanager
|
||||
|
||||
auth_file = deluge.configmanager.get_config_dir('auth')
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
with open(auth_file, 'w') as _file:
|
||||
with open(auth_file, 'w', encoding='utf8') as _file:
|
||||
_file.flush()
|
||||
os.fsync(_file.fileno())
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
@ -1028,54 +1148,61 @@ def create_auth_file():
|
||||
def create_localclient_account(append=False):
|
||||
import random
|
||||
from hashlib import sha1 as sha
|
||||
|
||||
import deluge.configmanager
|
||||
|
||||
auth_file = deluge.configmanager.get_config_dir('auth')
|
||||
if not os.path.exists(auth_file):
|
||||
create_auth_file()
|
||||
|
||||
with open(auth_file, 'a' if append else 'w') as _file:
|
||||
_file.write(':'.join([
|
||||
'localclient',
|
||||
sha(str(random.random()).encode('utf8')).hexdigest(),
|
||||
str(AUTH_LEVEL_ADMIN)
|
||||
]) + '\n')
|
||||
with open(auth_file, 'a' if append else 'w', encoding='utf8') as _file:
|
||||
_file.write(
|
||||
':'.join(
|
||||
[
|
||||
'localclient',
|
||||
sha(str(random.random()).encode('utf8')).hexdigest(),
|
||||
str(AUTH_LEVEL_ADMIN),
|
||||
]
|
||||
)
|
||||
+ '\n'
|
||||
)
|
||||
_file.flush()
|
||||
os.fsync(_file.fileno())
|
||||
|
||||
|
||||
def get_localhost_auth():
|
||||
"""Grabs the localclient auth line from the 'auth' file and creates a localhost uri.
|
||||
"""Grabs the localclient auth line from the 'auth' file and creates a localhost URI.
|
||||
|
||||
Returns:
|
||||
tuple: With the username and password to login as.
|
||||
Returns:
|
||||
tuple: With the username and password to login as.
|
||||
"""
|
||||
from deluge.configmanager import get_config_dir
|
||||
|
||||
"""
|
||||
from deluge.configmanager import get_config_dir
|
||||
auth_file = get_config_dir('auth')
|
||||
if not os.path.exists(auth_file):
|
||||
from deluge.common import create_localclient_account
|
||||
create_localclient_account()
|
||||
auth_file = get_config_dir('auth')
|
||||
if not os.path.exists(auth_file):
|
||||
from deluge.common import create_localclient_account
|
||||
|
||||
with open(auth_file) as auth:
|
||||
for line in auth:
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
# This is a comment or blank line
|
||||
continue
|
||||
create_localclient_account()
|
||||
|
||||
lsplit = line.split(':')
|
||||
with open(auth_file, encoding='utf8') as auth:
|
||||
for line in auth:
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
# This is a comment or blank line
|
||||
continue
|
||||
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
else:
|
||||
log.error('Your auth file is malformed: Incorrect number of fields!')
|
||||
continue
|
||||
lsplit = line.split(':')
|
||||
|
||||
if username == 'localclient':
|
||||
return (username, password)
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
else:
|
||||
log.error('Your auth file is malformed: Incorrect number of fields!')
|
||||
continue
|
||||
|
||||
if username == 'localclient':
|
||||
return (username, password)
|
||||
|
||||
|
||||
def set_env_variable(name, value):
|
||||
@ -1100,92 +1227,50 @@ def set_env_variable(name, value):
|
||||
http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py
|
||||
"""
|
||||
# Update Python's copy of the environment variables
|
||||
try:
|
||||
os.environ[name] = value
|
||||
except UnicodeEncodeError:
|
||||
# Python 2
|
||||
os.environ[name] = value.encode('utf8')
|
||||
os.environ[name] = value
|
||||
|
||||
if windows_check():
|
||||
from ctypes import windll
|
||||
from ctypes import cdll
|
||||
from ctypes.util import find_msvcrt
|
||||
from ctypes import cdll, windll
|
||||
|
||||
# Update the copy maintained by Windows (so SysInternals Process Explorer sees it)
|
||||
try:
|
||||
result = windll.kernel32.SetEnvironmentVariableW(name, value)
|
||||
if result == 0:
|
||||
raise Warning
|
||||
except Exception:
|
||||
log.warning('Failed to set Env Var \'%s\' (\'kernel32.SetEnvironmentVariableW\')', name)
|
||||
result = windll.kernel32.SetEnvironmentVariableW(name, value)
|
||||
if result == 0:
|
||||
log.info(
|
||||
"Failed to set Env Var '%s' (kernel32.SetEnvironmentVariableW)", name
|
||||
)
|
||||
else:
|
||||
log.debug('Set Env Var \'%s\' to \'%s\' (\'kernel32.SetEnvironmentVariableW\')', name, value)
|
||||
log.debug(
|
||||
"Set Env Var '%s' to '%s' (kernel32.SetEnvironmentVariableW)",
|
||||
name,
|
||||
value,
|
||||
)
|
||||
|
||||
# Update the copy maintained by msvcrt (used by gtk+ runtime)
|
||||
try:
|
||||
result = cdll.msvcrt._putenv('%s=%s' % (name, value))
|
||||
if result != 0:
|
||||
raise Warning
|
||||
except Exception:
|
||||
log.warning('Failed to set Env Var \'%s\' (\'msvcrt._putenv\')', name)
|
||||
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)
|
||||
|
||||
# Update the copy maintained by whatever c runtime is used by Python
|
||||
try:
|
||||
msvcrt = find_msvcrt()
|
||||
msvcrtname = str(msvcrt).split('.')[0] if '.' in msvcrt else str(msvcrt)
|
||||
result = cdll.LoadLibrary(msvcrt)._putenv('%s=%s' % (name, value))
|
||||
if result != 0:
|
||||
raise Warning
|
||||
except Exception:
|
||||
log.warning('Failed to set Env Var \'%s\' (\'%s._putenv\')', name, msvcrtname)
|
||||
else:
|
||||
log.debug('Set Env Var \'%s\' to \'%s\' (\'%s._putenv\')', name, value, msvcrtname)
|
||||
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."""
|
||||
if windows_check():
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
# 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'
|
||||
|
||||
get_cmd_linew = cdll.kernel32.GetCommandLineW
|
||||
get_cmd_linew.argtypes = []
|
||||
get_cmd_linew.restype = LPCWSTR
|
||||
arg_list = []
|
||||
for arg in sys.argv:
|
||||
try:
|
||||
arg_list.append(arg.decode(encoding))
|
||||
except AttributeError:
|
||||
arg_list.append(arg)
|
||||
|
||||
cmdline_to_argvw = windll.shell32.CommandLineToArgvW
|
||||
cmdline_to_argvw.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
cmdline_to_argvw.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = get_cmd_linew()
|
||||
argc = c_int(0)
|
||||
argv = cmdline_to_argvw(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
else:
|
||||
# On other platforms, 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
|
||||
return arg_list
|
||||
|
||||
|
||||
def run_profiled(func, *args, **kwargs):
|
||||
@ -1201,6 +1286,7 @@ def run_profiled(func, *args, **kwargs):
|
||||
"""
|
||||
if kwargs.get('do_profile', True) is not False:
|
||||
import cProfile
|
||||
|
||||
profiler = cProfile.Profile()
|
||||
|
||||
def on_shutdown():
|
||||
@ -1212,6 +1298,7 @@ def run_profiled(func, *args, **kwargs):
|
||||
else:
|
||||
import pstats
|
||||
from io import StringIO
|
||||
|
||||
strio = StringIO()
|
||||
ps = pstats.Stats(profiler, stream=strio).sort_stats('cumulative')
|
||||
ps.print_stats()
|
||||
@ -1239,6 +1326,7 @@ def is_process_running(pid):
|
||||
|
||||
if windows_check():
|
||||
from win32process import EnumProcesses
|
||||
|
||||
return pid in EnumProcesses()
|
||||
else:
|
||||
try:
|
||||
|
@ -7,8 +7,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
@ -17,8 +15,6 @@ from twisted.internet import reactor
|
||||
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
|
||||
from twisted.internet.task import LoopingCall, deferLater
|
||||
|
||||
from deluge.common import PY2
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -27,7 +23,6 @@ class ComponentAlreadyRegistered(Exception):
|
||||
|
||||
|
||||
class ComponentException(Exception):
|
||||
|
||||
def __init__(self, message, tb):
|
||||
super(ComponentException, self).__init__(message)
|
||||
self.message = message
|
||||
@ -93,6 +88,7 @@ class Component(object):
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, interval=1, depend=None):
|
||||
"""Initialize component.
|
||||
|
||||
@ -146,10 +142,14 @@ class Component(object):
|
||||
elif self._component_state == 'Started':
|
||||
d = succeed(True)
|
||||
else:
|
||||
d = fail(ComponentException('Trying to start component "%s" but it is '
|
||||
'not in a stopped state. Current state: %s' %
|
||||
(self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4)))
|
||||
d = fail(
|
||||
ComponentException(
|
||||
'Trying to start component "%s" but it is '
|
||||
'not in a stopped state. Current state: %s'
|
||||
% (self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4),
|
||||
)
|
||||
)
|
||||
return d
|
||||
|
||||
def _component_stop(self):
|
||||
@ -193,10 +193,14 @@ class Component(object):
|
||||
elif self._component_state == 'Paused':
|
||||
d = succeed(None)
|
||||
else:
|
||||
d = fail(ComponentException('Trying to pause component "%s" but it is '
|
||||
'not in a started state. Current state: %s' %
|
||||
(self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4)))
|
||||
d = fail(
|
||||
ComponentException(
|
||||
'Trying to pause component "%s" but it is '
|
||||
'not in a started state. Current state: %s'
|
||||
% (self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4),
|
||||
)
|
||||
)
|
||||
return d
|
||||
|
||||
def _component_resume(self):
|
||||
@ -207,10 +211,14 @@ class Component(object):
|
||||
d = maybeDeferred(self._component_start_timer)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail(ComponentException('Trying to resume component "%s" but it is '
|
||||
'not in a paused state. Current state: %s' %
|
||||
(self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4)))
|
||||
d = fail(
|
||||
ComponentException(
|
||||
'Trying to resume component "%s" but it is '
|
||||
'not in a paused state. Current state: %s'
|
||||
% (self._component_name, self._component_state),
|
||||
traceback.format_stack(limit=4),
|
||||
)
|
||||
)
|
||||
return d
|
||||
|
||||
def _component_shutdown(self):
|
||||
@ -244,6 +252,7 @@ class ComponentRegistry(object):
|
||||
|
||||
It is used to manage the Components by starting, stopping, pausing and shutting them down.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
# Stores all of the components that are dependent on a particular component
|
||||
@ -264,7 +273,9 @@ class ComponentRegistry(object):
|
||||
"""
|
||||
name = obj._component_name
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered('Component already registered with name %s' % name)
|
||||
raise ComponentAlreadyRegistered(
|
||||
'Component already registered with name %s' % name
|
||||
)
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
if obj._component_depend:
|
||||
@ -279,7 +290,8 @@ class ComponentRegistry(object):
|
||||
obj (Component): a component object to deregister
|
||||
|
||||
Returns:
|
||||
Deferred: a deferred object that will fire once the Component has been sucessfully deregistered
|
||||
Deferred: a deferred object that will fire once the Component has been
|
||||
successfully deregistered
|
||||
|
||||
"""
|
||||
if obj in self.components.values():
|
||||
@ -289,6 +301,7 @@ class ComponentRegistry(object):
|
||||
def on_stop(result, name):
|
||||
# Component may have been removed, so pop to ensure it doesn't fail
|
||||
self.components.pop(name, None)
|
||||
|
||||
return d.addCallback(on_stop, obj._component_name)
|
||||
else:
|
||||
return succeed(None)
|
||||
@ -309,7 +322,7 @@ class ComponentRegistry(object):
|
||||
# Start all the components if names is empty
|
||||
if not names:
|
||||
names = list(self.components)
|
||||
elif isinstance(names, str if not PY2 else basestring):
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_depends_started(result, name):
|
||||
@ -343,7 +356,7 @@ class ComponentRegistry(object):
|
||||
"""
|
||||
if not names:
|
||||
names = list(self.components)
|
||||
elif isinstance(names, str if not PY2 else basestring):
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_dependents_stopped(result, name):
|
||||
@ -358,7 +371,9 @@ class ComponentRegistry(object):
|
||||
if name in self.components:
|
||||
if name in self.dependents:
|
||||
# If other components depend on this component, stop them first
|
||||
d = self.stop(self.dependents[name]).addCallback(on_dependents_stopped, name)
|
||||
d = self.stop(self.dependents[name]).addCallback(
|
||||
on_dependents_stopped, name
|
||||
)
|
||||
deferreds.append(d)
|
||||
stopped_in_deferred.update(self.dependents[name])
|
||||
else:
|
||||
@ -381,7 +396,7 @@ class ComponentRegistry(object):
|
||||
"""
|
||||
if not names:
|
||||
names = list(self.components)
|
||||
elif isinstance(names, str if not PY2 else basestring):
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
@ -407,7 +422,7 @@ class ComponentRegistry(object):
|
||||
"""
|
||||
if not names:
|
||||
names = list(self.components)
|
||||
elif isinstance(names, str if not PY2 else basestring):
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
@ -428,8 +443,11 @@ class ComponentRegistry(object):
|
||||
Deferred: Fired once all Components have been successfully shut down.
|
||||
|
||||
"""
|
||||
|
||||
def on_stopped(result):
|
||||
return DeferredList([comp._component_shutdown() for comp in self.components.values()])
|
||||
return DeferredList(
|
||||
[comp._component_shutdown() for comp in list(self.components.values())]
|
||||
)
|
||||
|
||||
return self.stop(list(self.components)).addCallback(on_stopped)
|
||||
|
||||
|
112
deluge/config.py
112
deluge/config.py
@ -39,20 +39,19 @@ this can only be done for the 'config file version' and not for the 'format'
|
||||
version as this will be done internally.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import cPickle as pickle
|
||||
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 # Necessary for the config tests
|
||||
callLater = None # noqa: N816 Necessary for the config tests
|
||||
|
||||
|
||||
def prop(func):
|
||||
@ -72,33 +71,33 @@ def prop(func):
|
||||
return property(doc=func.__doc__, **func())
|
||||
|
||||
|
||||
def find_json_objects(s):
|
||||
"""Find json objects in a string.
|
||||
def find_json_objects(text, decoder=json.JSONDecoder()):
|
||||
"""Find json objects in text.
|
||||
|
||||
Args:
|
||||
s (str): the string to find json objects in
|
||||
text (str): The text to find json objects within.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples containing start and end locations of json
|
||||
objects in string `s`. e.g. [(start, end), ...]
|
||||
objects in the text. e.g. [(start, end), ...]
|
||||
|
||||
|
||||
"""
|
||||
objects = []
|
||||
opens = 0
|
||||
start = s.find('{')
|
||||
offset = start
|
||||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
start = text.index('{', offset)
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
if start < 0:
|
||||
return []
|
||||
|
||||
for index, c in enumerate(s[offset:]):
|
||||
if c == '{':
|
||||
opens += 1
|
||||
elif c == '}':
|
||||
opens -= 1
|
||||
if opens == 0:
|
||||
objects.append((start, index + offset + 1))
|
||||
start = index + offset + 1
|
||||
try:
|
||||
__, index = decoder.raw_decode(text[start:])
|
||||
except json.decoder.JSONDecodeError:
|
||||
offset = start + 1
|
||||
else:
|
||||
offset = start + index
|
||||
objects.append((start, offset))
|
||||
|
||||
return objects
|
||||
|
||||
@ -115,16 +114,14 @@ class Config(object):
|
||||
setup to convert old config files. (default: 1)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
|
||||
self.__config = {}
|
||||
self.__set_functions = {}
|
||||
self.__change_callbacks = []
|
||||
|
||||
# These hold the version numbers and they will be set when loaded
|
||||
self.__version = {
|
||||
'format': 1,
|
||||
'file': file_version
|
||||
}
|
||||
self.__version = {'format': 1, 'file': file_version}
|
||||
|
||||
# This will get set with a reactor.callLater whenever a config option
|
||||
# is set.
|
||||
@ -182,18 +179,21 @@ class Config(object):
|
||||
if self.__config[key] == value:
|
||||
return
|
||||
|
||||
# Do not allow the type to change unless it is None
|
||||
if value is not None and not isinstance(
|
||||
self.__config[key], type(None)) and not isinstance(self.__config[key], type(value)):
|
||||
# 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:
|
||||
oldtype = type(self.__config[key])
|
||||
value = oldtype(value)
|
||||
# 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
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value.decode('utf8')
|
||||
value = value.decode('utf8')
|
||||
|
||||
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
|
||||
self.__config[key] = value
|
||||
@ -201,7 +201,9 @@ class Config(object):
|
||||
global callLater
|
||||
if callLater is None:
|
||||
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
|
||||
from twisted.internet.reactor import callLater # pylint: disable=redefined-outer-name
|
||||
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
|
||||
callLater,
|
||||
)
|
||||
# Run the set_function for this key if any
|
||||
try:
|
||||
for func in self.__set_functions[key]:
|
||||
@ -209,9 +211,11 @@ class Config(object):
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
|
||||
def do_change_callbacks(key, value):
|
||||
for func in self.__change_callbacks:
|
||||
func(key, value)
|
||||
|
||||
callLater(0, do_change_callbacks, key, value)
|
||||
except Exception:
|
||||
pass
|
||||
@ -297,7 +301,9 @@ class Config(object):
|
||||
global callLater
|
||||
if callLater is None:
|
||||
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
|
||||
from twisted.internet.reactor import callLater # pylint: disable=redefined-outer-name
|
||||
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():
|
||||
@ -422,8 +428,13 @@ class Config(object):
|
||||
log.exception(ex)
|
||||
log.warning('Unable to load config file: %s', filename)
|
||||
|
||||
log.debug('Config %s version: %s.%s loaded: %s', filename,
|
||||
self.__version['format'], self.__version['file'], self.__config)
|
||||
log.debug(
|
||||
'Config %s version: %s.%s loaded: %s',
|
||||
filename,
|
||||
self.__version['format'],
|
||||
self.__version['file'],
|
||||
self.__config,
|
||||
)
|
||||
|
||||
def save(self, filename=None):
|
||||
"""Save configuration to disk.
|
||||
@ -457,8 +468,11 @@ class Config(object):
|
||||
|
||||
# Save the new config and make sure it's written to disk
|
||||
try:
|
||||
log.debug('Saving new config file %s', filename + '.new')
|
||||
with open(filename + '.new', 'wb') as _file:
|
||||
with NamedTemporaryFile(
|
||||
prefix=os.path.basename(filename) + '.', delete=False
|
||||
) as _file:
|
||||
filename_tmp = _file.name
|
||||
log.debug('Saving new config file %s', filename_tmp)
|
||||
json.dump(self.__version, getwriter('utf8')(_file), **JSON_FORMAT)
|
||||
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
|
||||
_file.flush()
|
||||
@ -467,6 +481,9 @@ class Config(object):
|
||||
log.error('Error writing new config file: %s', ex)
|
||||
return False
|
||||
|
||||
# Resolve symlinked config files before backing up and saving.
|
||||
filename = os.path.realpath(filename)
|
||||
|
||||
# Make a backup of the old config
|
||||
try:
|
||||
log.debug('Backing up old config file to %s.bak', filename)
|
||||
@ -477,8 +494,8 @@ class Config(object):
|
||||
# The new config file has been written successfully, so let's move it over
|
||||
# the existing one.
|
||||
try:
|
||||
log.debug('Moving new config file %s to %s..', filename + '.new', filename)
|
||||
shutil.move(filename + '.new', filename)
|
||||
log.debug('Moving new config file %s to %s', filename_tmp, filename)
|
||||
shutil.move(filename_tmp, filename)
|
||||
except IOError as ex:
|
||||
log.error('Error moving new config file: %s', ex)
|
||||
return False
|
||||
@ -505,16 +522,23 @@ class Config(object):
|
||||
raise ValueError('output_version needs to be greater than input_range')
|
||||
|
||||
if self.__version['file'] not in input_range:
|
||||
log.debug('File version %s is not in input_range %s, ignoring converter function..',
|
||||
self.__version['file'], input_range)
|
||||
log.debug(
|
||||
'File version %s is not in input_range %s, ignoring converter function..',
|
||||
self.__version['file'],
|
||||
input_range,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
self.__config = func(self.__config)
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
log.error('There was an exception try to convert config file %s %s to %s',
|
||||
self.__config_file, self.__version['file'], output_version)
|
||||
log.error(
|
||||
'There was an exception try to convert config file %s %s to %s',
|
||||
self.__config_file,
|
||||
self.__version['file'],
|
||||
output_version,
|
||||
)
|
||||
raise ex
|
||||
else:
|
||||
self.__version['file'] = output_version
|
||||
@ -527,9 +551,11 @@ class Config(object):
|
||||
@prop
|
||||
def config(): # pylint: disable=no-method-argument
|
||||
"""The config dictionary"""
|
||||
|
||||
def fget(self):
|
||||
return self.__config
|
||||
|
||||
def fdel(self):
|
||||
return self.save()
|
||||
|
||||
return locals()
|
||||
|
@ -7,8 +7,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -94,9 +92,12 @@ class _ConfigManager(object):
|
||||
log.debug('Getting config: %s', config_file)
|
||||
# Create the config object if not already created
|
||||
if config_file not in self.config_files:
|
||||
self.config_files[config_file] = Config(config_file, defaults,
|
||||
config_dir=self.config_directory,
|
||||
file_version=file_version)
|
||||
self.config_files[config_file] = Config(
|
||||
config_file,
|
||||
defaults,
|
||||
config_dir=self.config_directory,
|
||||
file_version=file_version,
|
||||
)
|
||||
|
||||
return self.config_files[config_file]
|
||||
|
||||
@ -106,7 +107,9 @@ _configmanager = _ConfigManager()
|
||||
|
||||
|
||||
def ConfigManager(config, defaults=None, file_version=1): # NOQA: N802
|
||||
return _configmanager.get_config(config, defaults=defaults, file_version=file_version)
|
||||
return _configmanager.get_config(
|
||||
config, defaults=defaults, file_version=file_version
|
||||
)
|
||||
|
||||
|
||||
def set_config_dir(directory):
|
||||
|
@ -15,9 +15,8 @@ This should typically only be used by the Core. Plugins should utilize the
|
||||
`:mod:EventManager` for similar functionality.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
@ -30,6 +29,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class AlertManager(component.Component):
|
||||
"""AlertManager fetches and processes libtorrent alerts"""
|
||||
|
||||
def __init__(self):
|
||||
log.debug('AlertManager init...')
|
||||
component.Component.__init__(self, 'AlertManager', interval=0.3)
|
||||
@ -39,13 +39,15 @@ class AlertManager(component.Component):
|
||||
self.alert_queue_size = 10000
|
||||
self.set_alert_queue_size(self.alert_queue_size)
|
||||
|
||||
alert_mask = (lt.alert.category_t.error_notification |
|
||||
lt.alert.category_t.port_mapping_notification |
|
||||
lt.alert.category_t.storage_notification |
|
||||
lt.alert.category_t.tracker_notification |
|
||||
lt.alert.category_t.status_notification |
|
||||
lt.alert.category_t.ip_block_notification |
|
||||
lt.alert.category_t.performance_warning)
|
||||
alert_mask = (
|
||||
lt.alert.category_t.error_notification
|
||||
| lt.alert.category_t.port_mapping_notification
|
||||
| lt.alert.category_t.storage_notification
|
||||
| lt.alert.category_t.tracker_notification
|
||||
| lt.alert.category_t.status_notification
|
||||
| lt.alert.category_t.ip_block_notification
|
||||
| lt.alert.category_t.performance_warning
|
||||
)
|
||||
|
||||
self.session.apply_settings({'alert_mask': alert_mask})
|
||||
|
||||
@ -105,7 +107,10 @@ class AlertManager(component.Component):
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('Alerts queued: %s', num_alerts)
|
||||
if num_alerts > 0.9 * self.alert_queue_size:
|
||||
log.warning('Warning total alerts queued, %s, passes 90%% of queue size.', num_alerts)
|
||||
log.warning(
|
||||
'Warning total alerts queued, %s, passes 90%% of queue size.',
|
||||
num_alerts,
|
||||
)
|
||||
|
||||
# Loop through all alerts in the queue
|
||||
for alert in alerts:
|
||||
@ -118,10 +123,20 @@ class AlertManager(component.Component):
|
||||
for handler in self.handlers[alert_type]:
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('Handling alert: %s', alert_type)
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert))
|
||||
# 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"""
|
||||
log.info('Alert Queue Size set to %s', queue_size)
|
||||
self.alert_queue_size = queue_size
|
||||
component.get('Core').apply_session_setting('alert_queue_size', self.alert_queue_size)
|
||||
component.get('Core').apply_session_setting(
|
||||
'alert_queue_size', self.alert_queue_size
|
||||
)
|
||||
|
@ -8,8 +8,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@ -17,8 +15,14 @@ from io import open
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, create_localclient_account)
|
||||
from deluge.common import (
|
||||
AUTH_LEVEL_ADMIN,
|
||||
AUTH_LEVEL_DEFAULT,
|
||||
AUTH_LEVEL_NONE,
|
||||
AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY,
|
||||
create_localclient_account,
|
||||
)
|
||||
from deluge.error import AuthenticationRequired, AuthManagerError, BadLoginError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -28,7 +32,8 @@ AUTH_LEVELS_MAPPING = {
|
||||
'READONLY': AUTH_LEVEL_READONLY,
|
||||
'DEFAULT': AUTH_LEVEL_NORMAL,
|
||||
'NORMAL': AUTH_LEVEL_DEFAULT,
|
||||
'ADMIN': AUTH_LEVEL_ADMIN}
|
||||
'ADMIN': AUTH_LEVEL_ADMIN,
|
||||
}
|
||||
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
|
||||
|
||||
|
||||
@ -45,12 +50,14 @@ class Account(object):
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
|
||||
'authlevel_int': self.authlevel
|
||||
'authlevel_int': self.authlevel,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
|
||||
{'username': self.username, 'authlevel': self.authlevel})
|
||||
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
|
||||
'username': self.username,
|
||||
'authlevel': self.authlevel,
|
||||
}
|
||||
|
||||
|
||||
class AuthManager(component.Component):
|
||||
@ -92,7 +99,7 @@ class AuthManager(component.Component):
|
||||
int: The auth level for this user.
|
||||
|
||||
Raises:
|
||||
AuthenticationRequired: If aditional details are required to authenticate.
|
||||
AuthenticationRequired: If additional details are required to authenticate.
|
||||
BadLoginError: If the username does not exist or password does not match.
|
||||
|
||||
"""
|
||||
@ -129,8 +136,9 @@ class AuthManager(component.Component):
|
||||
if authlevel not in AUTH_LEVELS_MAPPING:
|
||||
raise AuthManagerError('Invalid auth level: %s' % authlevel)
|
||||
try:
|
||||
self.__auth[username] = Account(username, password,
|
||||
AUTH_LEVELS_MAPPING[authlevel])
|
||||
self.__auth[username] = Account(
|
||||
username, password, AUTH_LEVELS_MAPPING[authlevel]
|
||||
)
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception as ex:
|
||||
@ -181,7 +189,10 @@ class AuthManager(component.Component):
|
||||
try:
|
||||
with open(filepath_tmp, 'w', encoding='utf8') as _file:
|
||||
for account in self.__auth.values():
|
||||
_file.write('%(username)s:%(password)s:%(authlevel_int)s\n' % account.data())
|
||||
_file.write(
|
||||
'%(username)s:%(password)s:%(authlevel_int)s\n'
|
||||
% account.data()
|
||||
)
|
||||
_file.flush()
|
||||
os.fsync(_file.fileno())
|
||||
shutil.move(filepath_tmp, filepath)
|
||||
@ -232,8 +243,12 @@ class AuthManager(component.Component):
|
||||
lsplit = line.split(':')
|
||||
if len(lsplit) == 2:
|
||||
username, password = lsplit
|
||||
log.warning('Your auth entry for %s contains no auth level, '
|
||||
'using AUTH_LEVEL_DEFAULT(%s)..', username, AUTH_LEVEL_DEFAULT)
|
||||
log.warning(
|
||||
'Your auth entry for %s contains no auth level, '
|
||||
'using AUTH_LEVEL_DEFAULT(%s)..',
|
||||
username,
|
||||
AUTH_LEVEL_DEFAULT,
|
||||
)
|
||||
if username == 'localclient':
|
||||
authlevel = AUTH_LEVEL_ADMIN
|
||||
else:
|
||||
@ -254,7 +269,10 @@ class AuthManager(component.Component):
|
||||
try:
|
||||
authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
except KeyError:
|
||||
log.error('Your auth file is malformed: %r is not a valid auth level', authlevel)
|
||||
log.error(
|
||||
'Your auth file is malformed: %r is not a valid auth level',
|
||||
authlevel,
|
||||
)
|
||||
continue
|
||||
|
||||
self.__auth[username] = Account(username, password, authlevel)
|
||||
|
@ -8,28 +8,31 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
import base64
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.request import URLError, urlopen
|
||||
|
||||
from twisted.internet import defer, reactor, task
|
||||
from twisted.web.client import getPage
|
||||
from twisted.web.client import Agent, readBody
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge import path_chooser_common
|
||||
from deluge._libtorrent import lt
|
||||
from deluge.common import PY2
|
||||
from deluge._libtorrent import LT_VERSION, lt
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.alertmanager import AlertManager
|
||||
from deluge.core.authmanager import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVELS_MAPPING,
|
||||
AUTH_LEVELS_MAPPING_REVERSE, AuthManager)
|
||||
from deluge.core.authmanager import (
|
||||
AUTH_LEVEL_ADMIN,
|
||||
AUTH_LEVEL_NONE,
|
||||
AUTH_LEVELS_MAPPING,
|
||||
AUTH_LEVELS_MAPPING_REVERSE,
|
||||
AuthManager,
|
||||
)
|
||||
from deluge.core.eventmanager import EventManager
|
||||
from deluge.core.filtermanager import FilterManager
|
||||
from deluge.core.pluginmanager import PluginManager
|
||||
@ -37,19 +40,23 @@ from deluge.core.preferencesmanager import PreferencesManager
|
||||
from deluge.core.rpcserver import export
|
||||
from deluge.core.torrentmanager import TorrentManager
|
||||
from deluge.decorators import deprecated
|
||||
from deluge.error import AddTorrentError, DelugeError, InvalidPathError, InvalidTorrentError
|
||||
from deluge.event import NewVersionAvailableEvent, SessionPausedEvent, SessionResumedEvent, TorrentQueueChangedEvent
|
||||
from deluge.error import (
|
||||
AddTorrentError,
|
||||
DelugeError,
|
||||
InvalidPathError,
|
||||
InvalidTorrentError,
|
||||
)
|
||||
from deluge.event import (
|
||||
NewVersionAvailableEvent,
|
||||
SessionPausedEvent,
|
||||
SessionResumedEvent,
|
||||
TorrentQueueChangedEvent,
|
||||
)
|
||||
from deluge.httpdownloader import download_file
|
||||
|
||||
try:
|
||||
from urllib.request import urlopen, URLError
|
||||
except ImportError:
|
||||
# PY2 fallback
|
||||
from urllib2 import urlopen, URLError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
OLD_SESSION_STATUS_KEYS = {
|
||||
DEPR_SESSION_STATUS_KEYS = {
|
||||
# 'active_requests': None, # In dht_stats_alert, if required.
|
||||
'allowed_upload_slots': 'ses.num_unchoke_slots',
|
||||
# 'dht_global_nodes': None,
|
||||
@ -81,9 +88,7 @@ OLD_SESSION_STATUS_KEYS = {
|
||||
# 'utp_stats': None
|
||||
}
|
||||
|
||||
# TODO: replace with dynamic rate e.g.
|
||||
# 'dht.dht_bytes_in'.replace('_bytes', '') + '_rate'
|
||||
# would become 'dht.dht_in_rate'
|
||||
# Session status rate keys associated with session status counters.
|
||||
SESSION_RATES_MAPPING = {
|
||||
'dht_download_rate': 'dht.dht_bytes_in',
|
||||
'dht_upload_rate': 'dht.dht_bytes_out',
|
||||
@ -97,25 +102,24 @@ SESSION_RATES_MAPPING = {
|
||||
'upload_rate': 'net.sent_bytes',
|
||||
}
|
||||
|
||||
DELUGE_VER = deluge.common.get_version()
|
||||
|
||||
|
||||
class Core(component.Component):
|
||||
def __init__(self, listen_interface=None, read_only_config_keys=None):
|
||||
log.debug('Core init...')
|
||||
def __init__(
|
||||
self, listen_interface=None, outgoing_interface=None, read_only_config_keys=None
|
||||
):
|
||||
component.Component.__init__(self, 'Core')
|
||||
|
||||
deluge_version = deluge.common.get_version()
|
||||
split_version = deluge.common.VersionSplit(deluge_version).version
|
||||
while len(split_version) < 4:
|
||||
split_version.append(0)
|
||||
|
||||
deluge_fingerprint = lt.generate_fingerprint('DE', *split_version)
|
||||
user_agent = 'Deluge/{} libtorrent/{}'.format(deluge_version, self.get_libtorrent_version())
|
||||
|
||||
# Start the libtorrent session.
|
||||
log.debug('Starting session (fingerprint: %s, user_agent: %s)', deluge_fingerprint, user_agent)
|
||||
settings_pack = {'peer_fingerprint': deluge_fingerprint,
|
||||
'user_agent': user_agent,
|
||||
'ignore_resume_timestamps': True}
|
||||
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 = {
|
||||
'peer_fingerprint': peer_id,
|
||||
'user_agent': user_agent,
|
||||
'ignore_resume_timestamps': True,
|
||||
}
|
||||
self.session = lt.session(settings_pack, flags=0)
|
||||
|
||||
# Load the settings, if available.
|
||||
@ -141,7 +145,9 @@ class Core(component.Component):
|
||||
|
||||
# External IP Address from libtorrent
|
||||
self.external_ip = None
|
||||
self.eventmanager.register_event_handler('ExternalIPEvent', self._on_external_ip_event)
|
||||
self.eventmanager.register_event_handler(
|
||||
'ExternalIPEvent', self._on_external_ip_event
|
||||
)
|
||||
|
||||
# GeoIP instance with db loaded
|
||||
self.geoip_instance = None
|
||||
@ -157,23 +163,38 @@ class Core(component.Component):
|
||||
|
||||
# If there was an interface value from the command line, use it, but
|
||||
# store the one in the config so we can restore it on shutdown
|
||||
self.__old_interface = None
|
||||
self._old_listen_interface = None
|
||||
if listen_interface:
|
||||
if deluge.common.is_ip(listen_interface):
|
||||
self.__old_interface = self.config['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): %s', listen_interface)
|
||||
log.error(
|
||||
'Invalid listen interface (must be IP Address): %s',
|
||||
listen_interface,
|
||||
)
|
||||
|
||||
self._old_outgoing_interface = None
|
||||
if outgoing_interface:
|
||||
self._old_outgoing_interface = self.config['outgoing_interface']
|
||||
self.config['outgoing_interface'] = outgoing_interface
|
||||
|
||||
# New release check information
|
||||
self.__new_release = None
|
||||
|
||||
# Session status timer
|
||||
self.session_status = {}
|
||||
self.session_status = {k.name: 0 for k in lt.session_stats_metrics()}
|
||||
self._session_prev_bytes = {k: 0 for k in SESSION_RATES_MAPPING}
|
||||
# Initiate other session status keys.
|
||||
self.session_status.update(self._session_prev_bytes)
|
||||
hit_ratio_keys = ['write_hit_ratio', 'read_hit_ratio']
|
||||
self.session_status.update({k: 0.0 for k in hit_ratio_keys})
|
||||
|
||||
self.session_status_timer_interval = 0.5
|
||||
self.session_status_timer = task.LoopingCall(self.session.post_session_stats)
|
||||
self.alertmanager.register_handler('session_stats_alert', self._on_alert_session_stats)
|
||||
self._session_rates = {(k_rate, k_bytes): 0 for k_rate, k_bytes in SESSION_RATES_MAPPING.items()}
|
||||
self.alertmanager.register_handler(
|
||||
'session_stats_alert', self._on_alert_session_stats
|
||||
)
|
||||
self.session_rates_timer_interval = 2
|
||||
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
|
||||
|
||||
@ -195,8 +216,11 @@ class Core(component.Component):
|
||||
self._save_session_state()
|
||||
|
||||
# We stored a copy of the old interface value
|
||||
if self.__old_interface:
|
||||
self.config['listen_interface'] = self.__old_interface
|
||||
if self._old_listen_interface is not None:
|
||||
self.config['listen_interface'] = self._old_listen_interface
|
||||
|
||||
if self._old_outgoing_interface is not None:
|
||||
self.config['outgoing_interface'] = self._old_outgoing_interface
|
||||
|
||||
# Make sure the config file has been saved
|
||||
self.config.save()
|
||||
@ -216,6 +240,48 @@ class Core(component.Component):
|
||||
"""
|
||||
self.session.apply_settings(settings)
|
||||
|
||||
@staticmethod
|
||||
def _create_peer_id(version):
|
||||
"""Create a peer_id fingerprint.
|
||||
|
||||
This creates the peer_id and modifies the release char to identify
|
||||
pre-release and development version. Using ``D`` for dev, daily or
|
||||
nightly builds, ``a, b, r`` for pre-releases and ``s`` for
|
||||
stable releases.
|
||||
|
||||
Examples:
|
||||
``--<client><client><major><minor><micro><release>--``
|
||||
``--DE200D--`` (development version of 2.0.0)
|
||||
``--DE200s--`` (stable release of v2.0.0)
|
||||
``--DE201b--`` (beta pre-release of v2.0.1)
|
||||
|
||||
Args:
|
||||
version (str): The version string in PEP440 dotted notation.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
version_list = split.version + [0] * (4 - len(split.version))
|
||||
peer_id = lt.generate_fingerprint('DE', *version_list)
|
||||
|
||||
def substitute_chr(string, idx, char):
|
||||
"""Fast substitute single char in string."""
|
||||
return string[:idx] + char + string[idx + 1 :]
|
||||
|
||||
if split.dev:
|
||||
release_chr = 'D'
|
||||
elif split.suffix:
|
||||
# a (alpha), b (beta) or r (release candidate).
|
||||
release_chr = split.suffix[0].lower()
|
||||
else:
|
||||
release_chr = 's'
|
||||
peer_id = substitute_chr(peer_id, 6, release_chr)
|
||||
|
||||
return peer_id
|
||||
|
||||
def _save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
filename = 'session.state'
|
||||
@ -267,58 +333,71 @@ class Core(component.Component):
|
||||
|
||||
def _on_alert_session_stats(self, alert):
|
||||
"""The handler for libtorrent session stats alert"""
|
||||
if not self.session_status:
|
||||
# Empty dict on startup so needs populated with session rate keys and default value.
|
||||
self.session_status.update({key: 0 for key in list(SESSION_RATES_MAPPING)})
|
||||
self.session_status.update(alert.values)
|
||||
self._update_session_cache_hit_ratio()
|
||||
|
||||
def _update_session_cache_hit_ratio(self):
|
||||
"""Calculates the cache read/write hit ratios and updates session_status"""
|
||||
try:
|
||||
self.session_status['write_hit_ratio'] = ((self.session_status['disk.num_blocks_written'] -
|
||||
self.session_status['disk.num_write_ops']) /
|
||||
self.session_status['disk.num_blocks_written'])
|
||||
except ZeroDivisionError:
|
||||
"""Calculates the cache read/write hit ratios for session_status."""
|
||||
blocks_written = self.session_status['disk.num_blocks_written']
|
||||
blocks_read = self.session_status['disk.num_blocks_read']
|
||||
|
||||
if blocks_written:
|
||||
self.session_status['write_hit_ratio'] = (
|
||||
blocks_written - self.session_status['disk.num_write_ops']
|
||||
) / blocks_written
|
||||
else:
|
||||
self.session_status['write_hit_ratio'] = 0.0
|
||||
|
||||
try:
|
||||
self.session_status['read_hit_ratio'] = (self.session_status['disk.num_blocks_cache_hits'] /
|
||||
self.session_status['disk.num_blocks_read'])
|
||||
except ZeroDivisionError:
|
||||
if blocks_read:
|
||||
self.session_status['read_hit_ratio'] = (
|
||||
blocks_read - self.session_status['disk.num_read_ops']
|
||||
) / blocks_read
|
||||
else:
|
||||
self.session_status['read_hit_ratio'] = 0.0
|
||||
|
||||
def _update_session_rates(self):
|
||||
"""Calculates status rates based on interval and value difference for session_status"""
|
||||
if not self.session_status:
|
||||
return
|
||||
"""Calculate session status rates.
|
||||
|
||||
for (rate_key, status_key), prev_bytes in list(self._session_rates.items()):
|
||||
new_bytes = self.session_status[status_key]
|
||||
byte_rate = (new_bytes - prev_bytes) / self.session_rates_timer_interval
|
||||
self.session_status[rate_key] = byte_rate
|
||||
Uses polling interval and counter difference for session_status rates.
|
||||
"""
|
||||
for rate_key, prev_bytes in list(self._session_prev_bytes.items()):
|
||||
new_bytes = self.session_status[SESSION_RATES_MAPPING[rate_key]]
|
||||
self.session_status[rate_key] = (
|
||||
new_bytes - prev_bytes
|
||||
) / self.session_rates_timer_interval
|
||||
# Store current value for next update.
|
||||
self._session_rates[(rate_key, status_key)] = new_bytes
|
||||
self._session_prev_bytes[rate_key] = new_bytes
|
||||
|
||||
def get_new_release(self):
|
||||
log.debug('get_new_release')
|
||||
try:
|
||||
self.new_release = urlopen('http://download.deluge-torrent.org/version-2.0').read().strip()
|
||||
self.new_release = (
|
||||
urlopen('http://download.deluge-torrent.org/version-2.0')
|
||||
.read()
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
except URLError as ex:
|
||||
log.debug('Unable to get release info from website: %s', ex)
|
||||
return
|
||||
self.check_new_release()
|
||||
else:
|
||||
self.check_new_release()
|
||||
|
||||
def check_new_release(self):
|
||||
if self.new_release:
|
||||
log.debug('new_release: %s', self.new_release)
|
||||
if deluge.common.VersionSplit(self.new_release) > deluge.common.VersionSplit(deluge.common.get_version()):
|
||||
component.get('EventManager').emit(NewVersionAvailableEvent(self.new_release))
|
||||
if deluge.common.VersionSplit(
|
||||
self.new_release
|
||||
) > deluge.common.VersionSplit(deluge.common.get_version()):
|
||||
component.get('EventManager').emit(
|
||||
NewVersionAvailableEvent(self.new_release)
|
||||
)
|
||||
return self.new_release
|
||||
return False
|
||||
|
||||
def _add_torrent_file(self, filename, filedump, options, save_state=True):
|
||||
"""Adds a torrent file to the session.
|
||||
# Exported Methods
|
||||
@export
|
||||
def add_torrent_file_async(self, filename, filedump, options, save_state=True):
|
||||
"""Adds a torrent file to the session asynchronously.
|
||||
|
||||
Args:
|
||||
filename (str): The filename of the torrent.
|
||||
@ -327,17 +406,20 @@ class Core(component.Component):
|
||||
save_state (bool): If the state should be saved after adding the file.
|
||||
|
||||
Returns:
|
||||
str: The torrent ID or None.
|
||||
Deferred: The torrent ID or None.
|
||||
|
||||
"""
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception as ex:
|
||||
filedump = b64decode(filedump)
|
||||
except TypeError as ex:
|
||||
log.error('There was an error decoding the filedump string: %s', ex)
|
||||
|
||||
try:
|
||||
d = self.torrentmanager.add(
|
||||
filedump=filedump, options=options, filename=filename, save_state=save_state
|
||||
d = self.torrentmanager.add_async(
|
||||
filedump=filedump,
|
||||
options=options,
|
||||
filename=filename,
|
||||
save_state=save_state,
|
||||
)
|
||||
except RuntimeError as ex:
|
||||
log.error('There was an error adding the torrent file %s: %s', filename, ex)
|
||||
@ -345,7 +427,32 @@ class Core(component.Component):
|
||||
else:
|
||||
return d
|
||||
|
||||
# Exported Methods
|
||||
@export
|
||||
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.
|
||||
|
||||
Args:
|
||||
magnet (str): The magnet URI.
|
||||
timeout (int): Number of seconds to wait before canceling request.
|
||||
|
||||
Returns:
|
||||
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
|
||||
|
||||
"""
|
||||
|
||||
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, filedump, options):
|
||||
"""Adds a torrent file to the session.
|
||||
@ -357,42 +464,56 @@ class Core(component.Component):
|
||||
|
||||
Returns:
|
||||
str: The torrent_id or None.
|
||||
|
||||
"""
|
||||
return self._add_torrent_file(filename, filedump, options)
|
||||
try:
|
||||
filedump = b64decode(filedump)
|
||||
except Exception as ex:
|
||||
log.error('There was an error decoding the filedump string: %s', ex)
|
||||
|
||||
try:
|
||||
return self.torrentmanager.add(
|
||||
filedump=filedump, options=options, filename=filename
|
||||
)
|
||||
except RuntimeError as ex:
|
||||
log.error('There was an error adding the torrent file %s: %s', filename, ex)
|
||||
raise
|
||||
|
||||
@export
|
||||
def add_torrent_files(self, torrent_files):
|
||||
"""Adds multiple torrent files to the session.
|
||||
"""Adds multiple torrent files to the session asynchronously.
|
||||
|
||||
Args:
|
||||
torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options).
|
||||
torrent_files (list of tuples): Torrent files as tuple of
|
||||
``(filename, filedump, options)``.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
|
||||
"""
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_torrents():
|
||||
errors = []
|
||||
last_index = len(torrent_files) - 1
|
||||
for idx, torrent in enumerate(torrent_files):
|
||||
try:
|
||||
yield self._add_torrent_file(torrent[0], torrent[1],
|
||||
torrent[2], save_state=idx == last_index)
|
||||
yield self.add_torrent_file_async(
|
||||
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
|
||||
)
|
||||
except AddTorrentError as ex:
|
||||
log.warn('Error when adding torrent: %s', ex)
|
||||
log.warning('Error when adding torrent: %s', ex)
|
||||
errors.append(ex)
|
||||
defer.returnValue(errors)
|
||||
|
||||
return task.deferLater(reactor, 0, add_torrents)
|
||||
|
||||
@export
|
||||
def add_torrent_url(self, url, options, headers=None):
|
||||
"""
|
||||
Adds a torrent from a url. Deluge will attempt to fetch the torrent
|
||||
from url prior to adding it to the session.
|
||||
Adds a torrent from a URL. Deluge will attempt to fetch the torrent
|
||||
from the URL prior to adding it to the session.
|
||||
|
||||
:param url: the url pointing to the torrent file
|
||||
: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
|
||||
@ -401,7 +522,7 @@ class Core(component.Component):
|
||||
|
||||
:returns: a Deferred which returns the torrent_id as a str or None
|
||||
"""
|
||||
log.info('Attempting to add url %s', url)
|
||||
log.info('Attempting to add URL %s', url)
|
||||
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
@ -411,11 +532,11 @@ class Core(component.Component):
|
||||
os.remove(filename)
|
||||
except OSError as ex:
|
||||
log.warning('Could not remove temp file: %s', ex)
|
||||
return self.add_torrent_file(filename, base64.encodestring(data), options)
|
||||
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)
|
||||
log.error('Failed to add torrent from URL %s', url)
|
||||
return failure
|
||||
|
||||
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
|
||||
@ -438,7 +559,7 @@ class Core(component.Component):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
log.debug('Attempting to add by magnet uri: %s', uri)
|
||||
log.debug('Attempting to add by magnet URI: %s', uri)
|
||||
|
||||
return self.torrentmanager.add(magnet=uri, options=options)
|
||||
|
||||
@ -481,14 +602,19 @@ class Core(component.Component):
|
||||
errors = []
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
self.torrentmanager.remove(torrent_id, remove_data=remove_data, save_state=False)
|
||||
self.torrentmanager.remove(
|
||||
torrent_id, remove_data=remove_data, save_state=False
|
||||
)
|
||||
except InvalidTorrentError as ex:
|
||||
errors.append((torrent_id, str(ex)))
|
||||
# Save the session state
|
||||
self.torrentmanager.save_state()
|
||||
if errors:
|
||||
log.warn('Failed to remove %d of %d torrents.', len(errors), len(torrent_ids))
|
||||
log.warning(
|
||||
'Failed to remove %d of %d torrents.', len(errors), len(torrent_ids)
|
||||
)
|
||||
return errors
|
||||
|
||||
return task.deferLater(reactor, 0, do_remove_torrents)
|
||||
|
||||
@export
|
||||
@ -504,24 +630,22 @@ class Core(component.Component):
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
|
||||
if not self.session_status:
|
||||
return {key: 0 for key in keys}
|
||||
|
||||
if not keys:
|
||||
return self.session_status
|
||||
|
||||
status = {}
|
||||
for key in keys:
|
||||
if key in OLD_SESSION_STATUS_KEYS:
|
||||
new_key = OLD_SESSION_STATUS_KEYS[key]
|
||||
log.warning('Using deprecated session status key %s, please use %s', key, new_key)
|
||||
status[key] = self.session_status[new_key]
|
||||
else:
|
||||
try:
|
||||
status[key] = self.session_status[key]
|
||||
except KeyError:
|
||||
log.warning('Session status key does not exist: %s', key)
|
||||
try:
|
||||
status[key] = self.session_status[key]
|
||||
except KeyError:
|
||||
if key in DEPR_SESSION_STATUS_KEYS:
|
||||
new_key = DEPR_SESSION_STATUS_KEYS[key]
|
||||
log.debug(
|
||||
'Deprecated session status key %s, please use %s', key, new_key
|
||||
)
|
||||
status[key] = self.session_status[new_key]
|
||||
else:
|
||||
log.debug('Session status key not valid: %s', key)
|
||||
return status
|
||||
|
||||
@export
|
||||
@ -531,11 +655,21 @@ class Core(component.Component):
|
||||
self.torrentmanager[torrent_id].force_reannounce()
|
||||
|
||||
@export
|
||||
def pause_torrent(self, torrent_ids):
|
||||
log.debug('Pausing: %s', torrent_ids)
|
||||
def pause_torrent(self, torrent_id):
|
||||
"""Pauses a torrent"""
|
||||
log.debug('Pausing: %s', torrent_id)
|
||||
if not isinstance(torrent_id, str):
|
||||
self.pause_torrents(torrent_id)
|
||||
else:
|
||||
self.torrentmanager[torrent_id].pause()
|
||||
|
||||
@export
|
||||
def pause_torrents(self, torrent_ids=None):
|
||||
"""Pauses a list of torrents"""
|
||||
if not torrent_ids:
|
||||
torrent_ids = self.torrentmanager.get_torrent_list()
|
||||
for torrent_id in torrent_ids:
|
||||
if not self.torrentmanager[torrent_id].pause():
|
||||
log.warning('Error pausing torrent %s', torrent_id)
|
||||
self.pause_torrent(torrent_id)
|
||||
|
||||
@export
|
||||
def connect_peer(self, torrent_id, ip, port):
|
||||
@ -552,14 +686,14 @@ class Core(component.Component):
|
||||
|
||||
@export
|
||||
def pause_session(self):
|
||||
"""Pause all torrents in the session"""
|
||||
"""Pause the entire session"""
|
||||
if not self.session.is_paused():
|
||||
self.session.pause()
|
||||
component.get('EventManager').emit(SessionPausedEvent())
|
||||
|
||||
@export
|
||||
def resume_session(self):
|
||||
"""Resume all torrents in the session"""
|
||||
"""Resume the entire session"""
|
||||
if self.session.is_paused():
|
||||
self.session.resume()
|
||||
for torrent_id in self.torrentmanager.torrents:
|
||||
@ -567,18 +701,45 @@ class Core(component.Component):
|
||||
component.get('EventManager').emit(SessionResumedEvent())
|
||||
|
||||
@export
|
||||
def resume_torrent(self, torrent_ids):
|
||||
log.debug('Resuming: %s', torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
def is_session_paused(self):
|
||||
"""Returns the activity of the session"""
|
||||
return self.session.is_paused()
|
||||
|
||||
@export
|
||||
def resume_torrent(self, torrent_id):
|
||||
"""Resumes a torrent"""
|
||||
log.debug('Resuming: %s', torrent_id)
|
||||
if not isinstance(torrent_id, str):
|
||||
self.resume_torrents(torrent_id)
|
||||
else:
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
def create_torrent_status(self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False, all_keys=False):
|
||||
@export
|
||||
def resume_torrents(self, torrent_ids=None):
|
||||
"""Resumes a list of torrents"""
|
||||
if not torrent_ids:
|
||||
torrent_ids = self.torrentmanager.get_torrent_list()
|
||||
for torrent_id in torrent_ids:
|
||||
self.resume_torrent(torrent_id)
|
||||
|
||||
def create_torrent_status(
|
||||
self,
|
||||
torrent_id,
|
||||
torrent_keys,
|
||||
plugin_keys,
|
||||
diff=False,
|
||||
update=False,
|
||||
all_keys=False,
|
||||
):
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(torrent_keys, diff, update=update, all_keys=all_keys)
|
||||
status = self.torrentmanager[torrent_id].get_status(
|
||||
torrent_keys, diff, update=update, all_keys=all_keys
|
||||
)
|
||||
except KeyError:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
# Torrent was probaly removed meanwhile
|
||||
# Torrent was probably removed meanwhile
|
||||
return {}
|
||||
|
||||
# Ask the plugin manager to fill in the plugin keys
|
||||
@ -588,9 +749,17 @@ class Core(component.Component):
|
||||
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(keys, [torrent_id])
|
||||
return self.create_torrent_status(torrent_id, torrent_keys, plugin_keys, diff=diff, update=True,
|
||||
all_keys=not keys)
|
||||
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
|
||||
keys, [torrent_id]
|
||||
)
|
||||
return self.create_torrent_status(
|
||||
torrent_id,
|
||||
torrent_keys,
|
||||
plugin_keys,
|
||||
diff=diff,
|
||||
update=True,
|
||||
all_keys=not keys,
|
||||
)
|
||||
|
||||
@export
|
||||
def get_torrents_status(self, filter_dict, keys, diff=False):
|
||||
@ -605,8 +774,11 @@ class Core(component.Component):
|
||||
# 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))
|
||||
status_dict[key].update(
|
||||
self.pluginmanager.get_status(key, plugin_keys)
|
||||
)
|
||||
return status_dict
|
||||
|
||||
d.addCallback(add_plugin_fields)
|
||||
return d
|
||||
|
||||
@ -637,7 +809,7 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_config_values(self, keys):
|
||||
"""Get the config values for the entered keys"""
|
||||
return dict((key, self.config.get(key)) for key in keys)
|
||||
return {key: self.config.get(key) for key in keys}
|
||||
|
||||
@export
|
||||
def set_config(self, config):
|
||||
@ -668,7 +840,9 @@ class Core(component.Component):
|
||||
|
||||
settings = self.session.get_settings()
|
||||
proxy_type = settings['proxy_type']
|
||||
proxy_hostname = settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname']
|
||||
proxy_hostname = (
|
||||
settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname']
|
||||
)
|
||||
proxy_port = settings['i2p_port'] if proxy_type == 6 else settings['proxy_port']
|
||||
proxy_dict = {
|
||||
'type': proxy_type,
|
||||
@ -678,7 +852,7 @@ class Core(component.Component):
|
||||
'port': proxy_port,
|
||||
'proxy_hostnames': settings['proxy_hostnames'],
|
||||
'proxy_peer_connections': settings['proxy_peer_connections'],
|
||||
'proxy_tracker_connections': settings['proxy_tracker_connections']
|
||||
'proxy_tracker_connections': settings['proxy_tracker_connections'],
|
||||
}
|
||||
|
||||
return proxy_dict
|
||||
@ -713,12 +887,13 @@ class Core(component.Component):
|
||||
|
||||
Args:
|
||||
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.
|
||||
options (dict): A dict of torrent options to set. See
|
||||
``torrent.TorrentOptions`` class for valid keys.
|
||||
"""
|
||||
if 'owner' in options and not self.core.authmanager.has_account(options['owner']):
|
||||
if 'owner' in options and not self.authmanager.has_account(options['owner']):
|
||||
raise DelugeError('Username "%s" is not known.' % options['owner'])
|
||||
|
||||
if isinstance(torrent_ids, str if not PY2 else basestring):
|
||||
if isinstance(torrent_ids, str):
|
||||
torrent_ids = [torrent_ids]
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
@ -726,9 +901,13 @@ class Core(component.Component):
|
||||
|
||||
@export
|
||||
def set_torrent_trackers(self, torrent_id, trackers):
|
||||
"""Sets a torrents tracker list. trackers will be [{"url", "tier"}]"""
|
||||
"""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):
|
||||
return self.torrentmanager[torrent_id].get_magnet_uri()
|
||||
|
||||
@deprecated
|
||||
@export
|
||||
def set_torrent_max_connections(self, torrent_id, value):
|
||||
@ -804,30 +983,56 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
"""Returns the size of the file or folder 'path' and -1 if the path is
|
||||
unaccessible (non-existent or insufficient privs)"""
|
||||
inaccessible (non-existent or insufficient privileges)"""
|
||||
return deluge.common.get_path_size(path)
|
||||
|
||||
@export
|
||||
def create_torrent(self, path, tracker, piece_length, comment, target,
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
def create_torrent(
|
||||
self,
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
):
|
||||
|
||||
log.debug('creating torrent..')
|
||||
threading.Thread(target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session)).start()
|
||||
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, path, tracker, piece_length, comment, target,
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
def _create_torrent_thread(
|
||||
self,
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
):
|
||||
from deluge import metafile
|
||||
|
||||
metafile.make_meta_file(
|
||||
path,
|
||||
tracker,
|
||||
@ -837,25 +1042,26 @@ class Core(component.Component):
|
||||
webseeds=webseeds,
|
||||
private=private,
|
||||
created_by=created_by,
|
||||
trackers=trackers)
|
||||
trackers=trackers,
|
||||
)
|
||||
log.debug('torrent created!')
|
||||
if add_to_session:
|
||||
options = {}
|
||||
options['download_location'] = os.path.split(path)[0]
|
||||
with open(target, 'rb') as _file:
|
||||
filedump = base64.encodestring(_file.read())
|
||||
filedump = b64encode(_file.read())
|
||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||
|
||||
@export
|
||||
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,
|
||||
ie, plugin_file.read()"""
|
||||
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
|
||||
i.e. ``plugin_file.read()``"""
|
||||
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception as ex:
|
||||
filedump = b64decode(filedump)
|
||||
except TypeError as ex:
|
||||
log.error('There was an error decoding the filedump string!')
|
||||
log.exception(ex)
|
||||
return
|
||||
@ -867,14 +1073,14 @@ class Core(component.Component):
|
||||
@export
|
||||
def rescan_plugins(self):
|
||||
"""
|
||||
Rescans the plugin folders for new plugins
|
||||
Re-scans the plugin folders for new plugins
|
||||
"""
|
||||
component.get('CorePluginManager').scan_for_plugins()
|
||||
|
||||
@export
|
||||
def rename_files(self, torrent_id, filenames):
|
||||
"""
|
||||
Rename files in torrent_id. Since this is an asynchronous operation by
|
||||
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.
|
||||
|
||||
@ -920,7 +1126,9 @@ class Core(component.Component):
|
||||
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(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True):
|
||||
for torrent_id in sorted(
|
||||
torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True
|
||||
):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_top(torrent_id):
|
||||
@ -931,7 +1139,10 @@ class Core(component.Component):
|
||||
@export
|
||||
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) for torrent_id in torrent_ids)
|
||||
torrents = (
|
||||
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
||||
for torrent_id in torrent_ids
|
||||
)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
# torrent_ids must be sorted before moving.
|
||||
@ -941,7 +1152,9 @@ class Core(component.Component):
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_up(torrent_id)
|
||||
except KeyError:
|
||||
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
||||
log.warning(
|
||||
'torrent_id: %s does not exist in the queue', torrent_id
|
||||
)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get('EventManager').emit(TorrentQueueChangedEvent())
|
||||
@ -951,7 +1164,10 @@ class Core(component.Component):
|
||||
@export
|
||||
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) for torrent_id in torrent_ids)
|
||||
torrents = (
|
||||
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
||||
for torrent_id in torrent_ids
|
||||
)
|
||||
torrent_moved = True
|
||||
prev_queue_position = None
|
||||
# torrent_ids must be sorted before moving.
|
||||
@ -961,7 +1177,9 @@ class Core(component.Component):
|
||||
try:
|
||||
torrent_moved = self.torrentmanager.queue_down(torrent_id)
|
||||
except KeyError:
|
||||
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
||||
log.warning(
|
||||
'torrent_id: %s does not exist in the queue', torrent_id
|
||||
)
|
||||
# If the torrent moved, then we should emit a signal
|
||||
if torrent_moved:
|
||||
component.get('EventManager').emit(TorrentQueueChangedEvent())
|
||||
@ -972,7 +1190,9 @@ class Core(component.Component):
|
||||
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(torrent_ids, key=self.torrentmanager.get_queue_position):
|
||||
for torrent_id in sorted(
|
||||
torrent_ids, key=self.torrentmanager.get_queue_position
|
||||
):
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_bottom(torrent_id):
|
||||
@ -993,16 +1213,18 @@ class Core(component.Component):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
d = getPage(b'http://deluge-torrent.org/test_port.php?port=%s' %
|
||||
self.get_listen_port(), timeout=30)
|
||||
port = self.get_listen_port()
|
||||
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
|
||||
agent = Agent(reactor, connectTimeout=30)
|
||||
d = agent.request(b'GET', url.encode())
|
||||
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
def on_get_page(body):
|
||||
return bool(int(body))
|
||||
|
||||
def on_error(failure):
|
||||
log.warning('Error testing listen port: %s', failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addCallback(readBody).addCallback(on_get_page)
|
||||
d.addErrback(on_error)
|
||||
|
||||
return d
|
||||
@ -1034,7 +1256,7 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_external_ip(self):
|
||||
"""
|
||||
Returns the external ip address recieved from libtorrent.
|
||||
Returns the external IP address received from libtorrent.
|
||||
"""
|
||||
return self.external_ip
|
||||
|
||||
@ -1047,7 +1269,7 @@ class Core(component.Component):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return lt.__version__
|
||||
return LT_VERSION
|
||||
|
||||
@export
|
||||
def get_completion_paths(self, args):
|
||||
|
@ -8,8 +8,6 @@
|
||||
#
|
||||
|
||||
"""The Deluge daemon"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
@ -65,40 +63,59 @@ def is_daemon_running(pid_file):
|
||||
class Daemon(object):
|
||||
"""The Deluge Daemon class"""
|
||||
|
||||
def __init__(self, listen_interface=None, interface=None, port=None, standalone=False,
|
||||
read_only_config_keys=None):
|
||||
def __init__(
|
||||
self,
|
||||
listen_interface=None,
|
||||
outgoing_interface=None,
|
||||
interface=None,
|
||||
port=None,
|
||||
standalone=False,
|
||||
read_only_config_keys=None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
listen_interface (str, optional): The IP address to listen to bittorrent connections on.
|
||||
interface (str, optional): The IP address the daemon will listen for UI connections on.
|
||||
port (int, optional): The port the daemon will listen for UI connections on.
|
||||
standalone (bool, optional): If True the client is in Standalone mode otherwise, if
|
||||
False, start the daemon as separate process.
|
||||
read_only_config_keys (list of str, optional): A list of config keys that will not be
|
||||
altered by core.set_config() RPC method.
|
||||
listen_interface (str, optional): The IP address to listen to
|
||||
BitTorrent connections on.
|
||||
outgoing_interface (str, optional): The network interface name or
|
||||
IP address to open outgoing BitTorrent connections on.
|
||||
interface (str, optional): The IP address the daemon will
|
||||
listen for UI connections on.
|
||||
port (int, optional): The port the daemon will listen for UI
|
||||
connections on.
|
||||
standalone (bool, optional): If True the client is in Standalone
|
||||
mode otherwise, if False, start the daemon as separate process.
|
||||
read_only_config_keys (list of str, optional): A list of config
|
||||
keys that will not be altered by core.set_config() RPC method.
|
||||
"""
|
||||
self.standalone = standalone
|
||||
self.pid_file = get_config_dir('deluged.pid')
|
||||
log.info('Deluge daemon %s', get_version())
|
||||
if is_daemon_running(self.pid_file):
|
||||
raise DaemonRunningError('Deluge daemon already running with this config directory!')
|
||||
raise DaemonRunningError(
|
||||
'Deluge daemon already running with this config directory!'
|
||||
)
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown method.
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown)
|
||||
|
||||
# Catch some Windows specific signals
|
||||
if windows_check():
|
||||
|
||||
def win_handler(ctrl_type):
|
||||
"""Handle the Windows shutdown or close events."""
|
||||
log.debug('windows handler ctrl_type: %s', ctrl_type)
|
||||
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
|
||||
self._shutdown()
|
||||
return 1
|
||||
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
# Start the core as a thread and join it until it's done
|
||||
self.core = Core(listen_interface=listen_interface,
|
||||
read_only_config_keys=read_only_config_keys)
|
||||
self.core = Core(
|
||||
listen_interface=listen_interface,
|
||||
outgoing_interface=outgoing_interface,
|
||||
read_only_config_keys=read_only_config_keys,
|
||||
)
|
||||
|
||||
if port is None:
|
||||
port = self.core.config['daemon_port']
|
||||
@ -112,10 +129,16 @@ class Daemon(object):
|
||||
port=port,
|
||||
allow_remote=self.core.config['allow_remote'],
|
||||
listen=not standalone,
|
||||
interface=interface
|
||||
interface=interface,
|
||||
)
|
||||
|
||||
log.debug('Listening to UI on: %s:%s and bittorrent on: %s', interface, port, listen_interface)
|
||||
log.debug(
|
||||
'Listening to UI on: %s:%s and bittorrent on: %s Making connections out on: %s',
|
||||
interface,
|
||||
port,
|
||||
listen_interface,
|
||||
outgoing_interface,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
# Register the daemon and the core RPCs
|
||||
@ -157,6 +180,11 @@ class Daemon(object):
|
||||
"""Returns a list of the exported methods."""
|
||||
return self.rpcserver.get_method_list()
|
||||
|
||||
@export()
|
||||
def get_version(self):
|
||||
"""Returns the daemon version"""
|
||||
return get_version()
|
||||
|
||||
@export(1)
|
||||
def authorized_call(self, rpc):
|
||||
"""Determines if session auth_level is authorized to call RPC.
|
||||
@ -170,4 +198,7 @@ class Daemon(object):
|
||||
if rpc not in self.get_method_list():
|
||||
return False
|
||||
|
||||
return self.rpcserver.get_session_auth_level() >= self.rpcserver.get_rpc_auth_level(rpc)
|
||||
return (
|
||||
self.rpcserver.get_session_auth_level()
|
||||
>= self.rpcserver.get_rpc_auth_level(rpc)
|
||||
)
|
||||
|
@ -7,30 +7,61 @@
|
||||
# the additional special exception to link portions of this program with the OpenSSL library.
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
from logging import DEBUG, FileHandler, getLogger
|
||||
|
||||
from twisted.internet.error import CannotListenError
|
||||
|
||||
from deluge.argparserbase import ArgParserBase
|
||||
from deluge.common import run_profiled
|
||||
from deluge.configmanager import get_config_dir
|
||||
from deluge.ui.baseargparser import BaseArgParser
|
||||
from deluge.ui.translations_util import set_dummy_trans
|
||||
from deluge.i18n import setup_mock_translation
|
||||
|
||||
|
||||
def add_daemon_options(parser):
|
||||
group = parser.add_argument_group(_('Daemon Options'))
|
||||
group.add_argument('-u', '--ui-interface', metavar='<ip-addr>', action='store',
|
||||
help=_('IP address to listen for UI connections'))
|
||||
group.add_argument('-p', '--port', metavar='<port>', action='store', type=int,
|
||||
help=_('Port to listen for UI connections on'))
|
||||
group.add_argument('-i', '--interface', metavar='<ip-addr>', dest='listen_interface', action='store',
|
||||
help=_('IP address to listen for BitTorrent connections'))
|
||||
group.add_argument('--read-only-config-keys', metavar='<comma-separated-keys>', action='store',
|
||||
help=_('Config keys to be unmodified by `set_config` RPC'), type=str, default='')
|
||||
group.add_argument(
|
||||
'-u',
|
||||
'--ui-interface',
|
||||
metavar='<ip-addr>',
|
||||
action='store',
|
||||
help=_('IP address to listen for UI connections'),
|
||||
)
|
||||
group.add_argument(
|
||||
'-p',
|
||||
'--port',
|
||||
metavar='<port>',
|
||||
action='store',
|
||||
type=int,
|
||||
help=_('Port to listen for UI connections on'),
|
||||
)
|
||||
group.add_argument(
|
||||
'-i',
|
||||
'--interface',
|
||||
metavar='<ip-addr>',
|
||||
dest='listen_interface',
|
||||
action='store',
|
||||
help=_('IP address to listen for BitTorrent connections'),
|
||||
)
|
||||
group.add_argument(
|
||||
'-o',
|
||||
'--outgoing-interface',
|
||||
metavar='<interface>',
|
||||
dest='outgoing_interface',
|
||||
action='store',
|
||||
help=_(
|
||||
'The network interface name or IP address for outgoing BitTorrent connections.'
|
||||
),
|
||||
)
|
||||
group.add_argument(
|
||||
'--read-only-config-keys',
|
||||
metavar='<comma-separated-keys>',
|
||||
action='store',
|
||||
help=_('Config keys to be unmodified by `set_config` RPC'),
|
||||
type=str,
|
||||
default='',
|
||||
)
|
||||
parser.add_process_arg_group()
|
||||
|
||||
|
||||
@ -45,20 +76,23 @@ def start_daemon(skip_start=False):
|
||||
deluge.core.daemon.Daemon: A new daemon object
|
||||
|
||||
"""
|
||||
set_dummy_trans(warn_msg=True)
|
||||
setup_mock_translation()
|
||||
|
||||
# Setup the argument parser
|
||||
parser = BaseArgParser()
|
||||
parser = ArgParserBase()
|
||||
add_daemon_options(parser)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
# Check for any daemons running with this same config
|
||||
from deluge.core.daemon import is_daemon_running
|
||||
|
||||
pid_file = get_config_dir('deluged.pid')
|
||||
if is_daemon_running(pid_file):
|
||||
print('Cannot run multiple daemons with same config directory.\n'
|
||||
'If you believe this is an error, force starting by deleting: %s' % pid_file)
|
||||
print(
|
||||
'Cannot run multiple daemons with same config directory.\n'
|
||||
'If you believe this is an error, force starting by deleting: %s' % pid_file
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
log = getLogger(__name__)
|
||||
@ -72,18 +106,25 @@ def start_daemon(skip_start=False):
|
||||
def run_daemon(options):
|
||||
try:
|
||||
from deluge.core.daemon import Daemon
|
||||
daemon = Daemon(listen_interface=options.listen_interface,
|
||||
interface=options.ui_interface,
|
||||
port=options.port,
|
||||
read_only_config_keys=options.read_only_config_keys.split(','))
|
||||
|
||||
daemon = Daemon(
|
||||
listen_interface=options.listen_interface,
|
||||
outgoing_interface=options.outgoing_interface,
|
||||
interface=options.ui_interface,
|
||||
port=options.port,
|
||||
read_only_config_keys=options.read_only_config_keys.split(','),
|
||||
)
|
||||
if skip_start:
|
||||
return daemon
|
||||
else:
|
||||
daemon.start()
|
||||
except CannotListenError as ex:
|
||||
log.error('Cannot start deluged, listen port in use.\n'
|
||||
' Check for other running daemons or services using this port: %s:%s',
|
||||
ex.interface, ex.port)
|
||||
log.error(
|
||||
'Cannot start deluged, listen port in use.\n'
|
||||
' Check for other running daemons or services using this port: %s:%s',
|
||||
ex.interface,
|
||||
ex.port,
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as ex:
|
||||
log.error('Unable to start deluged: %s', ex)
|
||||
@ -95,4 +136,6 @@ def start_daemon(skip_start=False):
|
||||
if options.pidfile:
|
||||
os.remove(options.pidfile)
|
||||
|
||||
return run_profiled(run_daemon, options, output_file=options.profile, do_profile=options.profile)
|
||||
return run_profiled(
|
||||
run_daemon, options, output_file=options.profile, do_profile=options.profile
|
||||
)
|
||||
|
@ -7,8 +7,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
@ -36,7 +34,12 @@ class EventManager(component.Component):
|
||||
try:
|
||||
handler(*event.args)
|
||||
except Exception as ex:
|
||||
log.error('Event handler %s failed in %s with exception %s', event.name, handler, ex)
|
||||
log.error(
|
||||
'Event handler %s failed in %s with exception %s',
|
||||
event.name,
|
||||
handler,
|
||||
ex,
|
||||
)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -7,12 +7,10 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.common import PY2, TORRENT_STATE
|
||||
from deluge.common import TORRENT_STATE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -98,9 +96,8 @@ def tracker_error_filter(torrent_ids, values):
|
||||
|
||||
|
||||
class FilterManager(component.Component):
|
||||
"""FilterManager
|
||||
"""FilterManager"""
|
||||
|
||||
"""
|
||||
def __init__(self, core):
|
||||
component.Component.__init__(self, 'FilterManager')
|
||||
log.debug('FilterManager init..')
|
||||
@ -115,12 +112,14 @@ class FilterManager(component.Component):
|
||||
|
||||
def _init_tracker_tree():
|
||||
return {'Error': 0}
|
||||
|
||||
self.register_tree_field('tracker_host', _init_tracker_tree)
|
||||
|
||||
self.register_filter('tracker_host', tracker_error_filter)
|
||||
|
||||
def _init_users_tree():
|
||||
return {'': 0}
|
||||
|
||||
self.register_tree_field('owner', _init_users_tree)
|
||||
|
||||
def filter_torrent_ids(self, filter_dict):
|
||||
@ -133,7 +132,7 @@ class FilterManager(component.Component):
|
||||
|
||||
# Sanitize input: filter-value must be a list of strings
|
||||
for key, value in filter_dict.items():
|
||||
if isinstance(value, str if not PY2 else basestring):
|
||||
if isinstance(value, str):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
# Optimized filter for id
|
||||
@ -162,19 +161,25 @@ class FilterManager(component.Component):
|
||||
return torrent_ids
|
||||
|
||||
# Registered filters
|
||||
for field, values in filter_dict.items():
|
||||
for field, values in list(filter_dict.items()):
|
||||
if field in self.registered_filters:
|
||||
# Filters out doubles
|
||||
torrent_ids = list(set(self.registered_filters[field](torrent_ids, values)))
|
||||
torrent_ids = list(
|
||||
set(self.registered_filters[field](torrent_ids, values))
|
||||
)
|
||||
del filter_dict[field]
|
||||
|
||||
if not filter_dict:
|
||||
return torrent_ids
|
||||
|
||||
torrent_keys, plugin_keys = self.torrents.separate_keys(list(filter_dict), torrent_ids)
|
||||
torrent_keys, plugin_keys = self.torrents.separate_keys(
|
||||
list(filter_dict), torrent_ids
|
||||
)
|
||||
# Leftover filter arguments, default filter on status fields.
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys)
|
||||
status = self.core.create_torrent_status(
|
||||
torrent_id, torrent_keys, plugin_keys
|
||||
)
|
||||
for field, values in filter_dict.items():
|
||||
if field in status and status[field] in values:
|
||||
continue
|
||||
@ -194,17 +199,21 @@ class FilterManager(component.Component):
|
||||
tree_keys.remove(cat)
|
||||
|
||||
torrent_keys, plugin_keys = self.torrents.separate_keys(tree_keys, torrent_ids)
|
||||
items = dict((field, self.tree_fields[field]()) for field in tree_keys)
|
||||
items = {field: self.tree_fields[field]() for field in tree_keys}
|
||||
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys) # status={key:value}
|
||||
status = self.core.create_torrent_status(
|
||||
torrent_id, torrent_keys, plugin_keys
|
||||
) # status={key:value}
|
||||
for field in tree_keys:
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
if 'tracker_host' in items:
|
||||
items['tracker_host']['All'] = len(torrent_ids)
|
||||
items['tracker_host']['Error'] = len(tracker_error_filter(torrent_ids, ('Error',)))
|
||||
items['tracker_host']['Error'] = len(
|
||||
tracker_error_filter(torrent_ids, ('Error',))
|
||||
)
|
||||
|
||||
if not show_zero_hits:
|
||||
for cat in ['state', 'owner', 'tracker_host']:
|
||||
@ -215,7 +224,7 @@ class FilterManager(component.Component):
|
||||
sorted_items = {field: sorted(items[field].items()) for field in tree_keys}
|
||||
|
||||
if 'state' in tree_keys:
|
||||
sorted_items['state'].sort(self._sort_state_items)
|
||||
sorted_items['state'].sort(key=self._sort_state_item)
|
||||
|
||||
return sorted_items
|
||||
|
||||
@ -224,7 +233,9 @@ class FilterManager(component.Component):
|
||||
init_state['All'] = len(self.torrents.get_torrent_list())
|
||||
for state in TORRENT_STATE:
|
||||
init_state[state] = 0
|
||||
init_state['Active'] = len(self.filter_state_active(self.torrents.get_torrent_list()))
|
||||
init_state['Active'] = len(
|
||||
self.filter_state_active(self.torrents.get_torrent_list())
|
||||
)
|
||||
return init_state
|
||||
|
||||
def register_filter(self, filter_id, filter_func, filter_value=None):
|
||||
@ -242,7 +253,9 @@ class FilterManager(component.Component):
|
||||
|
||||
def filter_state_active(self, torrent_ids):
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = self.torrents[torrent_id].get_status(['download_payload_rate', 'upload_payload_rate'])
|
||||
status = self.torrents[torrent_id].get_status(
|
||||
['download_payload_rate', 'upload_payload_rate']
|
||||
)
|
||||
if status['download_payload_rate'] or status['upload_payload_rate']:
|
||||
pass
|
||||
else:
|
||||
@ -251,18 +264,12 @@ class FilterManager(component.Component):
|
||||
|
||||
def _hide_state_items(self, state_items):
|
||||
"""For hide(show)-zero hits"""
|
||||
for (value, count) in state_items.items():
|
||||
for value, count in list(state_items.items()):
|
||||
if value != 'All' and count == 0:
|
||||
del state_items[value]
|
||||
|
||||
def _sort_state_items(self, x, y):
|
||||
if x[0] in STATE_SORT:
|
||||
ix = STATE_SORT.index(x[0])
|
||||
else:
|
||||
ix = 99
|
||||
if y[0] in STATE_SORT:
|
||||
iy = STATE_SORT.index(y[0])
|
||||
else:
|
||||
iy = 99
|
||||
|
||||
return ix - iy
|
||||
def _sort_state_item(self, item):
|
||||
try:
|
||||
return STATE_SORT.index(item[0])
|
||||
except ValueError:
|
||||
return 99
|
||||
|
@ -9,8 +9,6 @@
|
||||
|
||||
|
||||
"""PluginManager for Core"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
@ -33,7 +31,8 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Compon
|
||||
|
||||
# Call the PluginManagerBase constructor
|
||||
deluge.pluginmanagerbase.PluginManagerBase.__init__(
|
||||
self, 'core.conf', 'deluge.plugin.core')
|
||||
self, 'core.conf', 'deluge.plugin.core'
|
||||
)
|
||||
|
||||
def start(self):
|
||||
# Enable plugins that are enabled in the config
|
||||
@ -76,6 +75,7 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Compon
|
||||
if name not in self.plugins:
|
||||
component.get('EventManager').emit(PluginDisabledEvent(name))
|
||||
return result
|
||||
|
||||
d.addBoth(on_disable_plugin)
|
||||
return d
|
||||
|
||||
|
@ -8,13 +8,13 @@
|
||||
#
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import threading
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.request import urlopen
|
||||
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
@ -29,13 +29,6 @@ try:
|
||||
except ImportError:
|
||||
GeoIP = None
|
||||
|
||||
try:
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib import quote_plus
|
||||
from urllib2 import urlopen
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
@ -47,6 +40,7 @@ DEFAULT_PREFS = {
|
||||
'download_location': deluge.common.get_default_download_dir(),
|
||||
'listen_ports': [6881, 6891],
|
||||
'listen_interface': '',
|
||||
'outgoing_interface': '',
|
||||
'random_port': True,
|
||||
'listen_random_port': None,
|
||||
'listen_use_sys_port': False,
|
||||
@ -71,8 +65,11 @@ DEFAULT_PREFS = {
|
||||
'max_upload_speed': -1.0,
|
||||
'max_download_speed': -1.0,
|
||||
'max_upload_slots_global': 4,
|
||||
'max_half_open_connections': (lambda: deluge.common.windows_check() and
|
||||
(lambda: deluge.common.vista_check() and 4 or 8)() or 50)(),
|
||||
'max_half_open_connections': (
|
||||
lambda: deluge.common.windows_check()
|
||||
and (lambda: deluge.common.vista_check() and 4 or 8)()
|
||||
or 50
|
||||
)(),
|
||||
'max_connections_per_second': 20,
|
||||
'ignore_limits_on_local_network': True,
|
||||
'max_connections_per_torrent': -1,
|
||||
@ -122,7 +119,7 @@ DEFAULT_PREFS = {
|
||||
'cache_expiry': 60,
|
||||
'auto_manage_prefer_seeds': False,
|
||||
'shared': False,
|
||||
'super_seeding': False
|
||||
'super_seeding': False,
|
||||
}
|
||||
|
||||
|
||||
@ -131,7 +128,9 @@ class PreferencesManager(component.Component):
|
||||
component.Component.__init__(self, 'PreferencesManager')
|
||||
self.config = deluge.configmanager.ConfigManager('core.conf', DEFAULT_PREFS)
|
||||
if 'proxies' in self.config:
|
||||
log.warning('Updating config file for proxy, using "peer" values to fill new "proxy" setting')
|
||||
log.warning(
|
||||
'Updating config file for proxy, using "peer" values to fill new "proxy" setting'
|
||||
)
|
||||
self.config['proxy'].update(self.config['proxies']['peer'])
|
||||
log.warning('New proxy config is: %s', self.config['proxy'])
|
||||
del self.config['proxies']
|
||||
@ -187,6 +186,11 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
self.__set_listen_on()
|
||||
|
||||
def _on_set_outgoing_interface(self, key, value):
|
||||
"""Set interface name or IP address for outgoing BitTorrent connections."""
|
||||
value = value.strip() if value else ''
|
||||
self.core.apply_session_settings({'outgoing_interfaces': value})
|
||||
|
||||
def _on_set_random_port(self, key, value):
|
||||
self.__set_listen_on()
|
||||
|
||||
@ -195,20 +199,34 @@ class PreferencesManager(component.Component):
|
||||
if self.config['random_port']:
|
||||
if not self.config['listen_random_port']:
|
||||
self.config['listen_random_port'] = random.randrange(49152, 65525)
|
||||
listen_ports = [self.config['listen_random_port']] * 2 # use single port range
|
||||
listen_ports = [
|
||||
self.config['listen_random_port']
|
||||
] * 2 # use single port range
|
||||
else:
|
||||
self.config['listen_random_port'] = None
|
||||
listen_ports = self.config['listen_ports']
|
||||
|
||||
interface = str(self.config['listen_interface'].strip())
|
||||
interface = interface if interface else '0.0.0.0'
|
||||
if self.config['listen_interface']:
|
||||
interface = self.config['listen_interface'].strip()
|
||||
else:
|
||||
interface = '0.0.0.0'
|
||||
|
||||
log.debug('Listen Interface: %s, Ports: %s with use_sys_port: %s',
|
||||
interface, listen_ports, self.config['listen_use_sys_port'])
|
||||
interfaces = ['%s:%s' % (interface, port) for port in range(listen_ports[0], listen_ports[1]+1)]
|
||||
log.debug(
|
||||
'Listen Interface: %s, Ports: %s with use_sys_port: %s',
|
||||
interface,
|
||||
listen_ports,
|
||||
self.config['listen_use_sys_port'],
|
||||
)
|
||||
interfaces = [
|
||||
'%s:%s' % (interface, port)
|
||||
for port in range(listen_ports[0], listen_ports[1] + 1)
|
||||
]
|
||||
self.core.apply_session_settings(
|
||||
{'listen_system_port_fallback': self.config['listen_use_sys_port'],
|
||||
'listen_interfaces': ''.join(interfaces)})
|
||||
{
|
||||
'listen_system_port_fallback': self.config['listen_use_sys_port'],
|
||||
'listen_interfaces': ','.join(interfaces),
|
||||
}
|
||||
)
|
||||
|
||||
def _on_set_outgoing_ports(self, key, value):
|
||||
self.__set_outgoing_ports()
|
||||
@ -217,14 +235,22 @@ class PreferencesManager(component.Component):
|
||||
self.__set_outgoing_ports()
|
||||
|
||||
def __set_outgoing_ports(self):
|
||||
port = 0 if self.config['random_outgoing_ports'] else self.config['outgoing_ports'][0]
|
||||
port = (
|
||||
0
|
||||
if self.config['random_outgoing_ports']
|
||||
else self.config['outgoing_ports'][0]
|
||||
)
|
||||
if port:
|
||||
num_ports = self.config['outgoing_ports'][1] - self.config['outgoing_ports'][0]
|
||||
num_ports = (
|
||||
self.config['outgoing_ports'][1] - self.config['outgoing_ports'][0]
|
||||
)
|
||||
num_ports = num_ports if num_ports > 1 else 5
|
||||
else:
|
||||
num_ports = 0
|
||||
log.debug('Outgoing port set to %s with range: %s', port, num_ports)
|
||||
self.core.apply_session_settings({'outgoing_port': port, 'num_outgoing_ports': num_ports})
|
||||
self.core.apply_session_settings(
|
||||
{'outgoing_port': port, 'num_outgoing_ports': num_ports}
|
||||
)
|
||||
|
||||
def _on_set_peer_tos(self, key, value):
|
||||
try:
|
||||
@ -233,8 +259,21 @@ class PreferencesManager(component.Component):
|
||||
log.error('Invalid tos byte: %s', ex)
|
||||
|
||||
def _on_set_dht(self, key, value):
|
||||
dht_bootstraps = 'router.bittorrent.com:6881,router.utorrent.com:6881,router.bitcomet.com:6881'
|
||||
self.core.apply_session_settings({'dht_bootstrap_nodes': dht_bootstraps, 'enable_dht': value})
|
||||
lt_bootstraps = self.core.session.get_settings()['dht_bootstrap_nodes']
|
||||
# Update list of lt bootstraps, using set to remove duplicates.
|
||||
dht_bootstraps = set(
|
||||
lt_bootstraps.split(',')
|
||||
+ [
|
||||
'router.bittorrent.com:6881',
|
||||
'router.utorrent.com:6881',
|
||||
'router.bitcomet.com:6881',
|
||||
'dht.transmissionbt.com:6881',
|
||||
'dht.aelitis.com:6881',
|
||||
]
|
||||
)
|
||||
self.core.apply_session_settings(
|
||||
{'dht_bootstrap_nodes': ','.join(dht_bootstraps), 'enable_dht': value}
|
||||
)
|
||||
|
||||
def _on_set_upnp(self, key, value):
|
||||
self.core.apply_session_setting('enable_upnp', value)
|
||||
@ -260,12 +299,21 @@ class PreferencesManager(component.Component):
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
# Convert Deluge enc_level values to libtorrent enc_level values.
|
||||
pe_enc_level = {0: lt.enc_level.plaintext, 1: lt.enc_level.rc4, 2: lt.enc_level.both}
|
||||
pe_enc_level = {
|
||||
0: lt.enc_level.plaintext,
|
||||
1: lt.enc_level.rc4,
|
||||
2: lt.enc_level.both,
|
||||
}
|
||||
self.core.apply_session_settings(
|
||||
{'out_enc_policy': lt.enc_policy(self.config['enc_out_policy']),
|
||||
'in_enc_policy': lt.enc_policy(self.config['enc_in_policy']),
|
||||
'allowed_enc_level': lt.enc_level(pe_enc_level[self.config['enc_level']]),
|
||||
'prefer_rc4': True})
|
||||
{
|
||||
'out_enc_policy': lt.enc_policy(self.config['enc_out_policy']),
|
||||
'in_enc_policy': lt.enc_policy(self.config['enc_in_policy']),
|
||||
'allowed_enc_level': lt.enc_level(
|
||||
pe_enc_level[self.config['enc_level']]
|
||||
),
|
||||
'prefer_rc4': True,
|
||||
}
|
||||
)
|
||||
|
||||
def _on_set_max_connections_global(self, key, value):
|
||||
self.core.apply_session_setting('connections_limit', value)
|
||||
@ -327,20 +375,29 @@ class PreferencesManager(component.Component):
|
||||
|
||||
def run(self):
|
||||
import time
|
||||
|
||||
now = time.time()
|
||||
# check if we've done this within the last week or never
|
||||
if (now - self.config['info_sent']) >= (60 * 60 * 24 * 7):
|
||||
try:
|
||||
url = 'http://deluge-torrent.org/stats_get.php?processor=' + \
|
||||
platform.machine() + '&python=' + platform.python_version() \
|
||||
+ '&deluge=' + deluge.common.get_version() \
|
||||
+ '&os=' + platform.system() \
|
||||
+ '&plugins=' + quote_plus(':'.join(self.config['enabled_plugins']))
|
||||
url = (
|
||||
'http://deluge-torrent.org/stats_get.php?processor='
|
||||
+ platform.machine()
|
||||
+ '&python='
|
||||
+ platform.python_version()
|
||||
+ '&deluge='
|
||||
+ deluge.common.get_version()
|
||||
+ '&os='
|
||||
+ platform.system()
|
||||
+ '&plugins='
|
||||
+ quote_plus(':'.join(self.config['enabled_plugins']))
|
||||
)
|
||||
urlopen(url)
|
||||
except IOError as ex:
|
||||
log.debug('Network error while trying to send info: %s', ex)
|
||||
else:
|
||||
self.config['info_sent'] = now
|
||||
|
||||
if value:
|
||||
SendInfoThread(self.config).start()
|
||||
|
||||
@ -352,7 +409,8 @@ class PreferencesManager(component.Component):
|
||||
self.new_release_timer.stop()
|
||||
# Set a timer to check for a new release every 3 days
|
||||
self.new_release_timer = LoopingCall(
|
||||
self._on_set_new_release_check, 'new_release_check', True)
|
||||
self._on_set_new_release_check, 'new_release_check', True
|
||||
)
|
||||
self.new_release_timer.start(72 * 60 * 60, False)
|
||||
else:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
@ -361,31 +419,34 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_proxy(self, key, value):
|
||||
# Initialise with type none and blank hostnames.
|
||||
proxy_settings = {
|
||||
'proxy_type': lt.proxy_type.none,
|
||||
'proxy_type': lt.proxy_type_t.none,
|
||||
'i2p_hostname': '',
|
||||
'proxy_hostname': '',
|
||||
'proxy_hostnames': value['proxy_hostnames'],
|
||||
'proxy_peer_connections': value['proxy_peer_connections'],
|
||||
'proxy_tracker_connections': value['proxy_tracker_connections'],
|
||||
'force_proxy': value['force_proxy'],
|
||||
'anonymous_mode': value['anonymous_mode']
|
||||
'anonymous_mode': value['anonymous_mode'],
|
||||
}
|
||||
|
||||
if value['type'] == lt.proxy_type.i2p_proxy:
|
||||
proxy_settings.update({
|
||||
'proxy_type': lt.proxy_type.i2p_proxy,
|
||||
'i2p_hostname': value['hostname'],
|
||||
'i2p_port': value['port'],
|
||||
})
|
||||
elif value['type'] != lt.proxy_type.none:
|
||||
proxy_settings.update({
|
||||
'proxy_type': value['type'],
|
||||
'proxy_hostname': value['hostname'],
|
||||
'proxy_port': value['port'],
|
||||
'proxy_username': value['username'],
|
||||
'proxy_password': value['password'],
|
||||
|
||||
})
|
||||
if value['type'] == lt.proxy_type_t.i2p_proxy:
|
||||
proxy_settings.update(
|
||||
{
|
||||
'proxy_type': lt.proxy_type_t.i2p_proxy,
|
||||
'i2p_hostname': value['hostname'],
|
||||
'i2p_port': value['port'],
|
||||
}
|
||||
)
|
||||
elif value['type'] != lt.proxy_type_t.none:
|
||||
proxy_settings.update(
|
||||
{
|
||||
'proxy_type': value['type'],
|
||||
'proxy_hostname': value['hostname'],
|
||||
'proxy_port': value['port'],
|
||||
'proxy_username': value['username'],
|
||||
'proxy_password': value['password'],
|
||||
}
|
||||
)
|
||||
|
||||
self.core.apply_session_settings(proxy_settings)
|
||||
|
||||
@ -396,7 +457,9 @@ 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.open(geoipdb_path, GeoIP.GEOIP_STANDARD)
|
||||
self.core.geoip_instance = GeoIP.open(
|
||||
geoipdb_path, GeoIP.GEOIP_STANDARD
|
||||
)
|
||||
except AttributeError:
|
||||
log.warning('GeoIP Unavailable')
|
||||
else:
|
||||
|
@ -8,24 +8,31 @@
|
||||
#
|
||||
|
||||
"""RPCServer Module"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
from types import FunctionType
|
||||
|
||||
from OpenSSL import SSL, crypto
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.protocol import Factory, connectionDone
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE
|
||||
from deluge.error import DelugeError, IncompatibleClient, NotAuthorizedError, WrappedException, _ClientSideRecreateError
|
||||
from deluge.core.authmanager import (
|
||||
AUTH_LEVEL_ADMIN,
|
||||
AUTH_LEVEL_DEFAULT,
|
||||
AUTH_LEVEL_NONE,
|
||||
)
|
||||
from deluge.crypto_utils import check_ssl_keys, get_context_factory
|
||||
from deluge.error import (
|
||||
DelugeError,
|
||||
IncompatibleClient,
|
||||
NotAuthorizedError,
|
||||
WrappedException,
|
||||
_ClientSideRecreateError,
|
||||
)
|
||||
from deluge.event import ClientDisconnectedEvent
|
||||
from deluge.transfer import DelugeTransferProtocol
|
||||
|
||||
@ -47,13 +54,23 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
:type auth_level: int
|
||||
|
||||
"""
|
||||
|
||||
def wrap(func, *args, **kwargs):
|
||||
func._rpcserver_export = True
|
||||
func._rpcserver_auth_level = auth_level
|
||||
doc = func.__doc__
|
||||
func.__doc__ = '**RPC Exported Function** (*Auth Level: %s*)\n\n' % auth_level
|
||||
if doc:
|
||||
func.__doc__ += doc
|
||||
|
||||
rpc_text = '**RPC exported method** (*Auth level: %s*)' % auth_level
|
||||
|
||||
# Append the RPC text while ensuring correct docstring formatting.
|
||||
if func.__doc__:
|
||||
if func.__doc__.endswith(' '):
|
||||
indent = func.__doc__.split('\n')[-1]
|
||||
func.__doc__ += '\n{}'.format(indent)
|
||||
else:
|
||||
func.__doc__ += '\n\n'
|
||||
func.__doc__ += rpc_text
|
||||
else:
|
||||
func.__doc__ = rpc_text
|
||||
|
||||
return func
|
||||
|
||||
@ -91,22 +108,6 @@ def format_request(call):
|
||||
return s
|
||||
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self): # NOQA: N802
|
||||
"""
|
||||
Create an SSL context.
|
||||
|
||||
This loads the servers cert/private key SSL files for use with the
|
||||
SSL transport.
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
||||
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
ctx.use_certificate_file(os.path.join(ssl_dir, 'daemon.cert'))
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, 'daemon.pkey'))
|
||||
return ctx
|
||||
|
||||
|
||||
class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
def __init__(self):
|
||||
super(DelugeRPCProtocol, self).__init__()
|
||||
@ -134,8 +135,10 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
|
||||
for call in request:
|
||||
if len(call) != 4:
|
||||
log.debug('Received invalid rpc request: number of items '
|
||||
'in request is %s', len(call))
|
||||
log.debug(
|
||||
'Received invalid rpc request: number of items ' 'in request is %s',
|
||||
len(call),
|
||||
)
|
||||
continue
|
||||
# log.debug('RPCRequest: %s', format_request(call))
|
||||
reactor.callLater(0, self.dispatch, *call)
|
||||
@ -152,7 +155,7 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
try:
|
||||
self.transfer_message(data)
|
||||
except Exception as ex:
|
||||
log.warn('Error occurred when sending message: %s.', ex)
|
||||
log.warning('Error occurred when sending message: %s.', ex)
|
||||
log.exception(ex)
|
||||
raise
|
||||
|
||||
@ -161,11 +164,11 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
This method is called when a new client connects.
|
||||
"""
|
||||
peer = self.transport.getPeer()
|
||||
log.info('Deluge Client connection made from: %s:%s',
|
||||
peer.host, peer.port)
|
||||
log.info('Deluge Client connection made from: %s:%s', peer.host, peer.port)
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE
|
||||
self.factory.authorized_sessions[
|
||||
self.transport.sessionno] = self.AuthLevel(AUTH_LEVEL_NONE, '')
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = self.AuthLevel(
|
||||
AUTH_LEVEL_NONE, ''
|
||||
)
|
||||
|
||||
def connectionLost(self, reason=connectionDone): # NOQA: N802
|
||||
"""
|
||||
@ -184,7 +187,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
del self.factory.interested_events[self.transport.sessionno]
|
||||
|
||||
if self.factory.state == 'running':
|
||||
component.get('EventManager').emit(ClientDisconnectedEvent(self.factory.session_id))
|
||||
component.get('EventManager').emit(
|
||||
ClientDisconnectedEvent(self.factory.session_id)
|
||||
)
|
||||
log.info('Deluge client disconnected: %s', reason.value)
|
||||
|
||||
def valid_session(self):
|
||||
@ -206,32 +211,42 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
:type kwargs: dict
|
||||
|
||||
"""
|
||||
|
||||
def send_error():
|
||||
"""
|
||||
Sends an error response with the contents of the exception that was raised.
|
||||
"""
|
||||
exceptionType, exceptionValue, dummy_exceptionTraceback = sys.exc_info()
|
||||
exc_type, exc_value, dummy_exc_trace = sys.exc_info()
|
||||
formated_tb = traceback.format_exc()
|
||||
try:
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
exceptionType.__name__,
|
||||
exceptionValue._args,
|
||||
exceptionValue._kwargs,
|
||||
formated_tb
|
||||
))
|
||||
self.sendData(
|
||||
(
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
exc_type.__name__,
|
||||
exc_value._args,
|
||||
exc_value._kwargs,
|
||||
formated_tb,
|
||||
)
|
||||
)
|
||||
except AttributeError:
|
||||
# This is not a deluge exception (object has no attribute '_args), let's wrap it
|
||||
log.warning('An exception occurred while sending RPC_ERROR to '
|
||||
'client. Wrapping it and resending. Error to '
|
||||
'send(causing exception goes next):\n%s', formated_tb)
|
||||
log.warning(
|
||||
'An exception occurred while sending RPC_ERROR to '
|
||||
'client. Wrapping it and resending. Error to '
|
||||
'send(causing exception goes next):\n%s',
|
||||
formated_tb,
|
||||
)
|
||||
try:
|
||||
raise WrappedException(str(exceptionValue), exceptionType.__name__, formated_tb)
|
||||
raise WrappedException(
|
||||
str(exc_value), exc_type.__name__, formated_tb
|
||||
)
|
||||
except WrappedException:
|
||||
send_error()
|
||||
except Exception as ex:
|
||||
log.error('An exception occurred while sending RPC_ERROR to client: %s', ex)
|
||||
log.error(
|
||||
'An exception occurred while sending RPC_ERROR to client: %s', ex
|
||||
)
|
||||
|
||||
if method == 'daemon.info':
|
||||
# This is a special case and used in the initial connection process
|
||||
@ -248,7 +263,8 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
ret = component.get('AuthManager').authorize(*args, **kwargs)
|
||||
if ret:
|
||||
self.factory.authorized_sessions[
|
||||
self.transport.sessionno] = self.AuthLevel(ret, args[0])
|
||||
self.transport.sessionno
|
||||
] = self.AuthLevel(ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception as ex:
|
||||
send_error()
|
||||
@ -290,11 +306,15 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
log.debug('RPC dispatch %s', method)
|
||||
try:
|
||||
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno].auth_level
|
||||
auth_level = self.factory.authorized_sessions[
|
||||
self.transport.sessionno
|
||||
].auth_level
|
||||
if auth_level < method_auth_requirement:
|
||||
# This session is not allowed to call this method
|
||||
log.debug('Session %s is attempting an unauthorized method call!',
|
||||
self.transport.sessionno)
|
||||
log.debug(
|
||||
'Session %s is attempting an unauthorized method call!',
|
||||
self.transport.sessionno,
|
||||
)
|
||||
raise NotAuthorizedError(auth_level, method_auth_requirement)
|
||||
# Set the session_id in the factory so that methods can know
|
||||
# which session is calling it.
|
||||
@ -310,6 +330,7 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||
# Check if the return value is a deferred, since we'll need to
|
||||
# wait for it to fire before sending the RPC_RESPONSE
|
||||
if isinstance(ret, defer.Deferred):
|
||||
|
||||
def on_success(result):
|
||||
try:
|
||||
self.sendData((RPC_RESPONSE, request_id, result))
|
||||
@ -379,8 +400,13 @@ class RPCServer(component.Component):
|
||||
# Check for SSL keys and generate some if needed
|
||||
check_ssl_keys()
|
||||
|
||||
cert = os.path.join(deluge.configmanager.get_config_dir('ssl'), 'daemon.cert')
|
||||
pkey = os.path.join(deluge.configmanager.get_config_dir('ssl'), 'daemon.pkey')
|
||||
|
||||
try:
|
||||
reactor.listenSSL(port, self.factory, ServerContextFactory(), interface=hostname)
|
||||
reactor.listenSSL(
|
||||
port, self.factory, get_context_factory(cert, pkey), interface=hostname
|
||||
)
|
||||
except Exception as ex:
|
||||
log.debug('Daemon already running or port not available.: %s', ex)
|
||||
raise
|
||||
@ -526,73 +552,35 @@ class RPCServer(component.Component):
|
||||
:type event: :class:`deluge.event.DelugeEvent`
|
||||
"""
|
||||
if not self.is_session_valid(session_id):
|
||||
log.debug('Session ID %s is not valid. Not sending event "%s".', session_id, event.name)
|
||||
log.debug(
|
||||
'Session ID %s is not valid. Not sending event "%s".',
|
||||
session_id,
|
||||
event.name,
|
||||
)
|
||||
return
|
||||
if session_id not in self.factory.interested_events:
|
||||
log.debug('Session ID %s is not interested in any events. Not sending event "%s".',
|
||||
session_id, event.name)
|
||||
log.debug(
|
||||
'Session ID %s is not interested in any events. Not sending event "%s".',
|
||||
session_id,
|
||||
event.name,
|
||||
)
|
||||
return
|
||||
if event.name not in self.factory.interested_events[session_id]:
|
||||
log.debug('Session ID %s is not interested in event "%s". Not sending it.', session_id, event.name)
|
||||
log.debug(
|
||||
'Session ID %s is not interested in event "%s". Not sending it.',
|
||||
session_id,
|
||||
event.name,
|
||||
)
|
||||
return
|
||||
log.debug('Sending event "%s" with args "%s" to session id "%s".',
|
||||
event.name, event.args, session_id)
|
||||
self.factory.session_protocols[session_id].sendData((RPC_EVENT, event.name, event.args))
|
||||
log.debug(
|
||||
'Sending event "%s" with args "%s" to session id "%s".',
|
||||
event.name,
|
||||
event.args,
|
||||
session_id,
|
||||
)
|
||||
self.factory.session_protocols[session_id].sendData(
|
||||
(RPC_EVENT, event.name, event.args)
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
self.factory.state = 'stopping'
|
||||
|
||||
|
||||
def check_ssl_keys():
|
||||
"""
|
||||
Check for SSL cert/key and create them if necessary
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||
if not os.path.exists(ssl_dir):
|
||||
# The ssl folder doesn't exist so we need to create it
|
||||
os.makedirs(ssl_dir)
|
||||
generate_ssl_keys()
|
||||
else:
|
||||
for f in ('daemon.pkey', 'daemon.cert'):
|
||||
if not os.path.exists(os.path.join(ssl_dir, f)):
|
||||
generate_ssl_keys()
|
||||
break
|
||||
|
||||
|
||||
def generate_ssl_keys():
|
||||
"""
|
||||
This method generates a new SSL key/cert.
|
||||
"""
|
||||
from deluge.common import PY2
|
||||
digest = 'sha256' if not PY2 else b'sha256'
|
||||
|
||||
# Generate key pair
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||
|
||||
# Generate cert request
|
||||
req = crypto.X509Req()
|
||||
subj = req.get_subject()
|
||||
setattr(subj, 'CN', 'Deluge Daemon')
|
||||
req.set_pubkey(pkey)
|
||||
req.sign(pkey, digest)
|
||||
|
||||
# Generate certificate
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(0)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
||||
cert.set_issuer(req.get_subject())
|
||||
cert.set_subject(req.get_subject())
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
cert.sign(pkey, digest)
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
|
||||
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
|
||||
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
# Make the files only readable by this user
|
||||
for f in ('daemon.pkey', 'daemon.cert'):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||
|
@ -14,11 +14,10 @@ Attributes:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from twisted.internet.defer import Deferred, DeferredList
|
||||
|
||||
@ -28,19 +27,11 @@ from deluge.common import decode_bytes
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||
from deluge.decorators import deprecated
|
||||
from deluge.event import TorrentFolderRenamedEvent, TorrentStateChangedEvent, TorrentTrackerStatusEvent
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
# PY2 fallback
|
||||
from urlparse import urlparse # pylint: disable=ungrouped-imports
|
||||
|
||||
try:
|
||||
from future_builtins import zip
|
||||
except ImportError:
|
||||
# Ignore on Py3.
|
||||
pass
|
||||
from deluge.event import (
|
||||
TorrentFolderRenamedEvent,
|
||||
TorrentStateChangedEvent,
|
||||
TorrentTrackerStatusEvent,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -52,7 +43,7 @@ LT_TORRENT_STATE_MAP = {
|
||||
'finished': 'Seeding',
|
||||
'seeding': 'Seeding',
|
||||
'allocating': 'Allocating',
|
||||
'checking_resume_data': 'Checking'
|
||||
'checking_resume_data': 'Checking',
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +56,7 @@ def sanitize_filepath(filepath, folder=False):
|
||||
Args:
|
||||
folder (bool): A trailing slash is appended to the returned filepath.
|
||||
"""
|
||||
|
||||
def clean_filename(filename):
|
||||
"""Strips whitespace and discards dotted filenames"""
|
||||
filename = filename.strip()
|
||||
@ -110,12 +102,14 @@ def convert_lt_files(files):
|
||||
except AttributeError:
|
||||
file_path = _file.path
|
||||
|
||||
filelist.append({
|
||||
'index': index,
|
||||
'path': file_path.replace('\\', '/'),
|
||||
'size': _file.size,
|
||||
'offset': _file.offset
|
||||
})
|
||||
filelist.append(
|
||||
{
|
||||
'index': index,
|
||||
'path': file_path.replace('\\', '/'),
|
||||
'size': _file.size,
|
||||
'offset': _file.offset,
|
||||
}
|
||||
)
|
||||
|
||||
return filelist
|
||||
|
||||
@ -128,7 +122,7 @@ class TorrentOptions(dict):
|
||||
auto_managed (bool): Set torrent to auto managed mode, i.e. will be started or queued automatically.
|
||||
download_location (str): The path for the torrent data to be stored while downloading.
|
||||
file_priorities (list of int): The priority for files in torrent, range is [0..7] however
|
||||
only [0, 1, 5, 7] are normally used and correspond to [Do Not Download, Normal, High, Highest]
|
||||
only [0, 1, 4, 7] are normally used and correspond to [Skip, Low, Normal, High]
|
||||
mapped_files (dict): A mapping of the renamed filenames in 'index:filename' pairs.
|
||||
max_connections (int): Sets maximum number of connections this torrent will open.
|
||||
This must be at least 2. The default is unlimited (-1).
|
||||
@ -152,6 +146,7 @@ class TorrentOptions(dict):
|
||||
stop_ratio (float): The seeding ratio to stop (or remove) the torrent at.
|
||||
super_seeding (bool): Enable super seeding/initial seeding.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(TorrentOptions, self).__init__()
|
||||
config = ConfigManager('core.conf').config
|
||||
@ -172,7 +167,7 @@ class TorrentOptions(dict):
|
||||
'shared': 'shared',
|
||||
'stop_at_ratio': 'stop_seed_at_ratio',
|
||||
'stop_ratio': 'stop_seed_ratio',
|
||||
'super_seeding': 'super_seeding'
|
||||
'super_seeding': 'super_seeding',
|
||||
}
|
||||
for opt_k, conf_k in options_conf_map.items():
|
||||
self[opt_k] = config[conf_k]
|
||||
@ -198,12 +193,12 @@ class Torrent(object):
|
||||
options (dict): The torrent options.
|
||||
state (TorrentState): The torrent state.
|
||||
filename (str): The filename of the torrent file.
|
||||
magnet (str): The magnet uri.
|
||||
magnet (str): The magnet URI.
|
||||
|
||||
Attributes:
|
||||
torrent_id (str): The torrent_id for this torrent
|
||||
handle: Holds the libtorrent torrent handle
|
||||
magnet (str): The magnet uri used to add this torrent (if available).
|
||||
magnet (str): The magnet URI used to add this torrent (if available).
|
||||
status: Holds status info so that we don"t need to keep getting it from libtorrent.
|
||||
torrent_info: store the torrent info.
|
||||
has_metadata (bool): True if the metadata for the torrent is available, False otherwise.
|
||||
@ -227,6 +222,7 @@ class Torrent(object):
|
||||
we can re-pause it after its done if necessary
|
||||
forced_error (TorrentError): Keep track if we have forced this torrent to be in Error state.
|
||||
"""
|
||||
|
||||
def __init__(self, handle, options, state=None, filename=None, magnet=None):
|
||||
self.torrent_id = str(handle.info_hash())
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
@ -237,7 +233,6 @@ class Torrent(object):
|
||||
self.rpcserver = component.get('RPCServer')
|
||||
|
||||
self.handle = handle
|
||||
self.handle.resolve_countries(True)
|
||||
|
||||
self.magnet = magnet
|
||||
self.status = self.handle.status()
|
||||
@ -258,6 +253,9 @@ class Torrent(object):
|
||||
self.is_finished = False
|
||||
self.filename = filename
|
||||
|
||||
if not self.filename:
|
||||
self.filename = ''
|
||||
|
||||
self.forced_error = None
|
||||
self.statusmsg = None
|
||||
self.state = None
|
||||
@ -296,7 +294,9 @@ class Torrent(object):
|
||||
|
||||
# Skip set_prioritize_first_last if set_file_priorities is in options as it also calls the method.
|
||||
if 'file_priorities' in options and 'prioritize_first_last_pieces' in options:
|
||||
self.options['prioritize_first_last_pieces'] = options.pop('prioritize_first_last_pieces')
|
||||
self.options['prioritize_first_last_pieces'] = options.pop(
|
||||
'prioritize_first_last_pieces'
|
||||
)
|
||||
|
||||
for key, value in options.items():
|
||||
if key in self.options:
|
||||
@ -408,20 +408,24 @@ class Torrent(object):
|
||||
|
||||
# Set the pieces in first and last ranges to priority 7
|
||||
# if they are not marked as do not download
|
||||
priorities[first_start:first_end] = [p and 7 for p in priorities[first_start:first_end]]
|
||||
priorities[last_start:last_end] = [p and 7 for p in priorities[last_start:last_end]]
|
||||
priorities[first_start:first_end] = [
|
||||
p and 7 for p in priorities[first_start:first_end]
|
||||
]
|
||||
priorities[last_start:last_end] = [
|
||||
p and 7 for p in priorities[last_start:last_end]
|
||||
]
|
||||
|
||||
# Setting the priorites for all the pieces of this torrent
|
||||
self.handle.prioritize_pieces(priorities)
|
||||
|
||||
def set_sequential_download(self, set_sequencial):
|
||||
def set_sequential_download(self, sequential):
|
||||
"""Sets whether to download the pieces of the torrent in order.
|
||||
|
||||
Args:
|
||||
set_sequencial (bool): Enable sequencial downloading.
|
||||
sequential (bool): Enable sequential downloading.
|
||||
"""
|
||||
self.options['sequential_download'] = set_sequencial
|
||||
self.handle.set_sequential_download(set_sequencial)
|
||||
self.options['sequential_download'] = 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.
|
||||
@ -440,11 +444,8 @@ class Torrent(object):
|
||||
Args:
|
||||
super_seeding (bool): Enable super seeding.
|
||||
"""
|
||||
if self.status.is_seeding:
|
||||
self.options['super_seeding'] = super_seeding
|
||||
self.handle.super_seeding(super_seeding)
|
||||
else:
|
||||
self.options['super_seeding'] = False
|
||||
self.options['super_seeding'] = 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.
|
||||
@ -493,32 +494,35 @@ class Torrent(object):
|
||||
Args:
|
||||
file_priorities (list of int): List of file priorities.
|
||||
"""
|
||||
if not self.has_metadata:
|
||||
return
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('Setting %s file priorities to: %s', self.torrent_id, file_priorities)
|
||||
log.debug(
|
||||
'Setting %s file priorities to: %s', self.torrent_id, file_priorities
|
||||
)
|
||||
|
||||
if (self.handle.has_metadata() and file_priorities and
|
||||
len(file_priorities) == len(self.get_files())):
|
||||
if file_priorities and len(file_priorities) == len(self.get_files()):
|
||||
self.handle.prioritize_files(file_priorities)
|
||||
else:
|
||||
log.debug('Unable to set new file priorities.')
|
||||
file_priorities = self.handle.file_priorities()
|
||||
|
||||
if 0 in self.options['file_priorities']:
|
||||
# Previously marked a file 'Do Not Download' so check if changed any 0's to >0.
|
||||
# Previously marked a file 'skip' so check for any 0's now >0.
|
||||
for index, priority in enumerate(self.options['file_priorities']):
|
||||
if priority == 0 and file_priorities[index] > 0:
|
||||
# Changed 'Do Not Download' to a download priority so update state.
|
||||
# Changed priority from skip to download so update state.
|
||||
self.is_finished = False
|
||||
self.update_state()
|
||||
break
|
||||
|
||||
# Ensure stored options are in sync in case file_priorities were faulty (old state?).
|
||||
self.options['file_priorities'] = self.handle.file_priorities()
|
||||
# Store the priorities.
|
||||
self.options['file_priorities'] = file_priorities
|
||||
|
||||
# Set the first/last priorities if needed.
|
||||
if self.options['prioritize_first_last_pieces']:
|
||||
self.set_prioritize_first_last_pieces(self.options['prioritize_first_last_pieces'])
|
||||
self.set_prioritize_first_last_pieces(True)
|
||||
|
||||
@deprecated
|
||||
def set_save_path(self, download_location):
|
||||
@ -594,11 +598,16 @@ class Torrent(object):
|
||||
|
||||
if self.tracker_status != status:
|
||||
self.tracker_status = status
|
||||
component.get('EventManager').emit(TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status))
|
||||
component.get('EventManager').emit(
|
||||
TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status)
|
||||
)
|
||||
|
||||
def merge_trackers(self, torrent_info):
|
||||
"""Merges new trackers in torrent_info into torrent"""
|
||||
log.info('Adding any new trackers to torrent (%s) already in session...', self.torrent_id)
|
||||
log.info(
|
||||
'Adding any new trackers to torrent (%s) already in session...',
|
||||
self.torrent_id,
|
||||
)
|
||||
if not torrent_info:
|
||||
return
|
||||
# Don't merge trackers if either torrent has private flag set.
|
||||
@ -636,13 +645,23 @@ class Torrent(object):
|
||||
self.state = LT_TORRENT_STATE_MAP.get(str(status.state), str(status.state))
|
||||
|
||||
if self.state != old_state:
|
||||
component.get('EventManager').emit(TorrentStateChangedEvent(self.torrent_id, self.state))
|
||||
component.get('EventManager').emit(
|
||||
TorrentStateChangedEvent(self.torrent_id, self.state)
|
||||
)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('State from lt was: %s | Session is paused: %s\nTorrent state set from "%s" to "%s" (%s)',
|
||||
'error' if status_error else status.state, session_paused, old_state, self.state, self.torrent_id)
|
||||
log.debug(
|
||||
'State from lt was: %s | Session is paused: %s\nTorrent state set from "%s" to "%s" (%s)',
|
||||
'error' if status_error else status.state,
|
||||
session_paused,
|
||||
old_state,
|
||||
self.state,
|
||||
self.torrent_id,
|
||||
)
|
||||
if self.forced_error:
|
||||
log.debug('Torrent Error state message: %s', self.forced_error.error_message)
|
||||
log.debug(
|
||||
'Torrent Error state message: %s', self.forced_error.error_message
|
||||
)
|
||||
|
||||
def set_status_message(self, message=None):
|
||||
"""Sets the torrent status message.
|
||||
@ -697,16 +716,23 @@ class Torrent(object):
|
||||
"""
|
||||
status = self.status
|
||||
eta = 0
|
||||
if self.is_finished and self.options['stop_at_ratio'] and status.upload_payload_rate:
|
||||
if (
|
||||
self.is_finished
|
||||
and self.options['stop_at_ratio']
|
||||
and status.upload_payload_rate
|
||||
):
|
||||
# We're a seed, so calculate the time to the 'stop_share_ratio'
|
||||
eta = ((status.all_time_download * self.options['stop_ratio']) -
|
||||
status.all_time_upload) // status.upload_payload_rate
|
||||
eta = (
|
||||
int(status.all_time_download * self.options['stop_ratio'])
|
||||
- status.all_time_upload
|
||||
) // status.upload_payload_rate
|
||||
elif status.download_payload_rate:
|
||||
left = status.total_wanted - status.total_wanted_done
|
||||
if left > 0:
|
||||
eta = left // status.download_payload_rate
|
||||
|
||||
return eta
|
||||
# Limit to 1 year, avoid excessive values and prevent GTK int overflow.
|
||||
return eta if eta < 31557600 else -1
|
||||
|
||||
def get_ratio(self):
|
||||
"""Get the ratio of upload/download for this torrent.
|
||||
@ -774,27 +800,37 @@ class Torrent(object):
|
||||
if peer.flags & peer.connecting or peer.flags & peer.handshake:
|
||||
continue
|
||||
|
||||
client = decode_bytes(peer.client)
|
||||
try:
|
||||
client = decode_bytes(peer.client)
|
||||
except UnicodeDecodeError:
|
||||
# libtorrent on Py3 can raise UnicodeDecodeError for peer_info.client
|
||||
client = 'unknown'
|
||||
|
||||
try:
|
||||
country = component.get('Core').geoip_instance.country_code_by_addr(peer.ip[0])
|
||||
country = component.get('Core').geoip_instance.country_code_by_addr(
|
||||
peer.ip[0]
|
||||
)
|
||||
except AttributeError:
|
||||
country = ''
|
||||
else:
|
||||
try:
|
||||
country = ''.join([char if char.isalpha() else ' ' for char in country])
|
||||
country = ''.join(
|
||||
[char if char.isalpha() else ' ' for char in country]
|
||||
)
|
||||
except TypeError:
|
||||
country = ''
|
||||
|
||||
ret.append({
|
||||
'client': client,
|
||||
'country': country,
|
||||
'down_speed': peer.payload_down_speed,
|
||||
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
|
||||
'progress': peer.progress,
|
||||
'seed': peer.flags & peer.seed,
|
||||
'up_speed': peer.payload_up_speed,
|
||||
})
|
||||
ret.append(
|
||||
{
|
||||
'client': client,
|
||||
'country': country,
|
||||
'down_speed': peer.payload_down_speed,
|
||||
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
|
||||
'progress': peer.progress,
|
||||
'seed': peer.flags & peer.seed,
|
||||
'up_speed': peer.payload_up_speed,
|
||||
}
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
@ -825,8 +861,19 @@ class Torrent(object):
|
||||
"""
|
||||
if not self.has_metadata:
|
||||
return []
|
||||
return [progress / _file.size if _file.size else 0.0 for progress, _file in
|
||||
zip(self.handle.file_progress(), self.torrent_info.files())]
|
||||
|
||||
try:
|
||||
files_progresses = zip(
|
||||
self.handle.file_progress(), self.torrent_info.files()
|
||||
)
|
||||
except Exception:
|
||||
# Handle libtorrent >=2.0.0,<=2.0.4 file_progress error
|
||||
files_progresses = zip(iter(lambda: 0, 1), self.torrent_info.files())
|
||||
|
||||
return [
|
||||
progress / _file.size if _file.size else 0.0
|
||||
for progress, _file in files_progresses
|
||||
]
|
||||
|
||||
def get_tracker_host(self):
|
||||
"""Get the hostname of the currently connected tracker.
|
||||
@ -846,7 +893,7 @@ class Torrent(object):
|
||||
if tracker:
|
||||
url = urlparse(tracker.replace('udp://', 'http://'))
|
||||
if hasattr(url, 'hostname'):
|
||||
host = (url.hostname or 'DHT')
|
||||
host = url.hostname or 'DHT'
|
||||
# Check if hostname is an IP address and just return it if that's the case
|
||||
try:
|
||||
socket.inet_aton(host)
|
||||
@ -867,7 +914,7 @@ class Torrent(object):
|
||||
return ''
|
||||
|
||||
def get_magnet_uri(self):
|
||||
"""Returns a magnet uri for this torrent"""
|
||||
"""Returns a magnet URI for this torrent"""
|
||||
return lt.make_magnet_uri(self.handle)
|
||||
|
||||
def get_name(self):
|
||||
@ -881,14 +928,18 @@ class Torrent(object):
|
||||
str: the name of the torrent.
|
||||
|
||||
"""
|
||||
if not self.options['name']:
|
||||
handle_name = self.handle.name()
|
||||
if handle_name:
|
||||
name = decode_bytes(handle_name)
|
||||
else:
|
||||
name = self.torrent_id
|
||||
if self.options['name']:
|
||||
return self.options['name']
|
||||
|
||||
if self.has_metadata:
|
||||
# Use the top-level folder as torrent name.
|
||||
filename = decode_bytes(self.torrent_info.file_at(0).path)
|
||||
name = filename.replace('\\', '/', 1).split('/', 1)[0]
|
||||
else:
|
||||
name = self.options['name']
|
||||
name = decode_bytes(self.handle.name())
|
||||
|
||||
if not name:
|
||||
name = self.torrent_id
|
||||
|
||||
return name
|
||||
|
||||
@ -937,6 +988,8 @@ class Torrent(object):
|
||||
call to get_status based on the session_id
|
||||
update (bool): If True the status will be updated from libtorrent
|
||||
if False, the cached values will be returned
|
||||
all_keys (bool): If True return all keys while ignoring the keys param
|
||||
if False, return only the requested keys
|
||||
|
||||
Returns:
|
||||
dict: a dictionary of the status keys and their values
|
||||
@ -987,7 +1040,9 @@ class Torrent(object):
|
||||
'seeding_time': lambda: self.status.seeding_time,
|
||||
'finished_time': lambda: self.status.finished_time,
|
||||
'all_time_download': lambda: self.status.all_time_download,
|
||||
'storage_mode': lambda: self.status.storage_mode.name.split('_')[2], # sparse or allocate
|
||||
'storage_mode': lambda: self.status.storage_mode.name.split('_')[
|
||||
2
|
||||
], # sparse or allocate
|
||||
'distributed_copies': lambda: max(0.0, self.status.distributed_copies),
|
||||
'download_payload_rate': lambda: self.status.download_payload_rate,
|
||||
'file_priorities': self.get_file_priorities,
|
||||
@ -1000,8 +1055,12 @@ class Torrent(object):
|
||||
'max_upload_slots': lambda: self.options['max_upload_slots'],
|
||||
'max_upload_speed': lambda: self.options['max_upload_speed'],
|
||||
'message': lambda: self.statusmsg,
|
||||
'move_on_completed_path': lambda: self.options['move_completed_path'], # Deprecated: move_completed_path
|
||||
'move_on_completed': lambda: self.options['move_completed'], # Deprecated: Use move_completed
|
||||
'move_on_completed_path': lambda: self.options[
|
||||
'move_completed_path'
|
||||
], # Deprecated: move_completed_path
|
||||
'move_on_completed': lambda: self.options[
|
||||
'move_completed'
|
||||
], # Deprecated: Use move_completed
|
||||
'move_completed_path': lambda: self.options['move_completed_path'],
|
||||
'move_completed': lambda: self.options['move_completed'],
|
||||
'next_announce': lambda: self.status.next_announce.seconds,
|
||||
@ -1009,17 +1068,26 @@ class Torrent(object):
|
||||
'num_seeds': lambda: self.status.num_seeds,
|
||||
'owner': lambda: self.options['owner'],
|
||||
'paused': lambda: self.status.paused,
|
||||
'prioritize_first_last': lambda: self.options['prioritize_first_last_pieces'],
|
||||
'prioritize_first_last': lambda: self.options[
|
||||
'prioritize_first_last_pieces'
|
||||
],
|
||||
# Deprecated: Use prioritize_first_last_pieces
|
||||
'prioritize_first_last_pieces': lambda: self.options['prioritize_first_last_pieces'],
|
||||
'prioritize_first_last_pieces': lambda: self.options[
|
||||
'prioritize_first_last_pieces'
|
||||
],
|
||||
'sequential_download': lambda: self.options['sequential_download'],
|
||||
'progress': self.get_progress,
|
||||
'shared': lambda: self.options['shared'],
|
||||
'remove_at_ratio': lambda: self.options['remove_at_ratio'],
|
||||
'save_path': lambda: self.options['download_location'], # Deprecated: Use download_location
|
||||
'save_path': lambda: self.options[
|
||||
'download_location'
|
||||
], # Deprecated: Use download_location
|
||||
'download_location': lambda: self.options['download_location'],
|
||||
'seeds_peers_ratio': lambda: -1.0 if self.status.num_incomplete == 0 else ( # Use -1.0 to signify infinity
|
||||
self.status.num_complete / self.status.num_incomplete),
|
||||
'seeds_peers_ratio': lambda: -1.0
|
||||
if self.status.num_incomplete == 0
|
||||
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'],
|
||||
@ -1032,19 +1100,32 @@ class Torrent(object):
|
||||
'total_seeds': lambda: self.status.num_complete,
|
||||
'total_uploaded': lambda: self.status.all_time_upload,
|
||||
'total_wanted': lambda: self.status.total_wanted,
|
||||
'total_remaining': lambda: self.status.total_wanted - self.status.total_wanted_done,
|
||||
'total_remaining': lambda: self.status.total_wanted
|
||||
- self.status.total_wanted_done,
|
||||
'tracker': lambda: self.status.current_tracker,
|
||||
'tracker_host': self.get_tracker_host,
|
||||
'trackers': lambda: self.trackers,
|
||||
'tracker_status': lambda: self.tracker_status,
|
||||
'upload_payload_rate': lambda: self.status.upload_payload_rate,
|
||||
'comment': lambda: decode_bytes(self.torrent_info.comment()) if self.has_metadata else '',
|
||||
'creator': lambda: decode_bytes(self.torrent_info.creator()) if self.has_metadata else '',
|
||||
'num_files': lambda: self.torrent_info.num_files() if self.has_metadata else 0,
|
||||
'num_pieces': lambda: self.torrent_info.num_pieces() if self.has_metadata else 0,
|
||||
'piece_length': lambda: self.torrent_info.piece_length() if self.has_metadata else 0,
|
||||
'comment': lambda: decode_bytes(self.torrent_info.comment())
|
||||
if self.has_metadata
|
||||
else '',
|
||||
'creator': lambda: decode_bytes(self.torrent_info.creator())
|
||||
if self.has_metadata
|
||||
else '',
|
||||
'num_files': lambda: self.torrent_info.num_files()
|
||||
if self.has_metadata
|
||||
else 0,
|
||||
'num_pieces': lambda: self.torrent_info.num_pieces()
|
||||
if self.has_metadata
|
||||
else 0,
|
||||
'piece_length': lambda: self.torrent_info.piece_length()
|
||||
if self.has_metadata
|
||||
else 0,
|
||||
'private': lambda: self.torrent_info.priv() if self.has_metadata else False,
|
||||
'total_size': lambda: self.torrent_info.total_size() if self.has_metadata else 0,
|
||||
'total_size': lambda: self.torrent_info.total_size()
|
||||
if self.has_metadata
|
||||
else 0,
|
||||
'eta': self.get_eta,
|
||||
'file_progress': self.get_file_progress,
|
||||
'files': self.get_files,
|
||||
@ -1061,7 +1142,7 @@ class Torrent(object):
|
||||
'super_seeding': lambda: self.status.super_seeding,
|
||||
'time_since_download': lambda: self.status.time_since_download,
|
||||
'time_since_upload': lambda: self.status.time_since_upload,
|
||||
'time_since_transfer': self.get_time_since_transfer
|
||||
'time_since_transfer': self.get_time_since_transfer,
|
||||
}
|
||||
|
||||
def pause(self):
|
||||
@ -1074,30 +1155,35 @@ class Torrent(object):
|
||||
# Turn off auto-management so the torrent will not be unpaused by lt queueing
|
||||
self.handle.auto_managed(False)
|
||||
if self.state == 'Error':
|
||||
return False
|
||||
log.debug('Unable to pause torrent while in Error state')
|
||||
elif self.status.paused:
|
||||
# This torrent was probably paused due to being auto managed by lt
|
||||
# Since we turned auto_managed off, we should update the state which should
|
||||
# show it as 'Paused'. We need to emit a torrent_paused signal because
|
||||
# the torrent_paused alert from libtorrent will not be generated.
|
||||
self.update_state()
|
||||
component.get('EventManager').emit(TorrentStateChangedEvent(self.torrent_id, 'Paused'))
|
||||
component.get('EventManager').emit(
|
||||
TorrentStateChangedEvent(self.torrent_id, 'Paused')
|
||||
)
|
||||
else:
|
||||
try:
|
||||
self.handle.pause()
|
||||
except RuntimeError as ex:
|
||||
log.debug('Unable to pause torrent: %s', ex)
|
||||
return False
|
||||
return True
|
||||
|
||||
def resume(self):
|
||||
"""Resumes this torrent."""
|
||||
if self.status.paused and self.status.auto_managed:
|
||||
log.debug('Resume not possible for auto-managed torrent!')
|
||||
elif self.forced_error and self.forced_error.was_paused:
|
||||
log.debug('Resume skipped for forced_error torrent as it was originally paused.')
|
||||
elif (self.status.is_finished and self.options['stop_at_ratio'] and
|
||||
self.get_ratio() >= self.options['stop_ratio']):
|
||||
log.debug(
|
||||
'Resume skipped for forced_error torrent as it was originally paused.'
|
||||
)
|
||||
elif (
|
||||
self.status.is_finished
|
||||
and self.options['stop_at_ratio']
|
||||
and self.get_ratio() >= self.options['stop_ratio']
|
||||
):
|
||||
log.debug('Resume skipped for torrent as it has reached "stop_seed_ratio".')
|
||||
else:
|
||||
# Check if torrent was originally being auto-managed.
|
||||
@ -1125,8 +1211,8 @@ class Torrent(object):
|
||||
bool: True is successful, otherwise False
|
||||
"""
|
||||
try:
|
||||
self.handle.connect_peer((peer_ip, peer_port), 0)
|
||||
except RuntimeError as ex:
|
||||
self.handle.connect_peer((peer_ip, int(peer_port)), 0)
|
||||
except (RuntimeError, ValueError) as ex:
|
||||
log.debug('Unable to connect to peer: %s', ex)
|
||||
return False
|
||||
return True
|
||||
@ -1147,9 +1233,13 @@ class Torrent(object):
|
||||
try:
|
||||
os.makedirs(dest)
|
||||
except OSError as ex:
|
||||
log.error('Could not move storage for torrent %s since %s does '
|
||||
'not exist and could not create the directory: %s',
|
||||
self.torrent_id, dest, ex)
|
||||
log.error(
|
||||
'Could not move storage for torrent %s since %s does '
|
||||
'not exist and could not create the directory: %s',
|
||||
self.torrent_id,
|
||||
dest,
|
||||
ex,
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -1182,8 +1272,9 @@ class Torrent(object):
|
||||
flags = lt.save_resume_flags_t.flush_disk_cache if flush_disk_cache else 0
|
||||
# Don't generate fastresume data if torrent is in a Deluge Error state.
|
||||
if self.forced_error:
|
||||
component.get('TorrentManager').waiting_on_resume_data[self.torrent_id].errback(
|
||||
UserWarning('Skipped creating resume_data while in Error state'))
|
||||
component.get('TorrentManager').waiting_on_resume_data[
|
||||
self.torrent_id
|
||||
].errback(UserWarning('Skipped creating resume_data while in Error state'))
|
||||
else:
|
||||
self.handle.save_resume_data(flags)
|
||||
|
||||
@ -1205,12 +1296,11 @@ class Torrent(object):
|
||||
log.error('Unable to save torrent file to: %s', ex)
|
||||
|
||||
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
|
||||
# Regenerate the file priorities
|
||||
self.set_file_priorities([])
|
||||
|
||||
if filedump is None:
|
||||
metadata = lt.bdecode(self.torrent_info.metadata())
|
||||
torrent_file = {b'info': metadata}
|
||||
filedump = lt.bencode(torrent_file)
|
||||
lt_ct = lt.create_torrent(self.torrent_info)
|
||||
filedump = lt.bencode(lt_ct.generate())
|
||||
|
||||
write_file(filepath, filedump)
|
||||
|
||||
# If the user has requested a copy of the torrent be saved elsewhere we need to do that.
|
||||
@ -1222,9 +1312,13 @@ class Torrent(object):
|
||||
|
||||
def delete_torrentfile(self, delete_copies=False):
|
||||
"""Deletes the .torrent file in the state directory in config"""
|
||||
torrent_files = [os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')]
|
||||
if delete_copies:
|
||||
torrent_files.append(os.path.join(self.config['torrentfiles_location'], self.filename))
|
||||
torrent_files = [
|
||||
os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
|
||||
]
|
||||
if delete_copies and self.filename:
|
||||
torrent_files.append(
|
||||
os.path.join(self.config['torrentfiles_location'], self.filename)
|
||||
)
|
||||
|
||||
for torrent_file in torrent_files:
|
||||
log.debug('Deleting torrent file: %s', torrent_file)
|
||||
@ -1245,8 +1339,8 @@ class Torrent(object):
|
||||
def scrape_tracker(self):
|
||||
"""Scrape the tracker
|
||||
|
||||
A scrape request queries the tracker for statistics such as total
|
||||
number of incomplete peers, complete peers, number of downloads etc.
|
||||
A scrape request queries the tracker for statistics such as total
|
||||
number of incomplete peers, complete peers, number of downloads etc.
|
||||
"""
|
||||
try:
|
||||
self.handle.scrape_tracker()
|
||||
@ -1284,7 +1378,7 @@ class Torrent(object):
|
||||
# lt needs utf8 byte-string. Otherwise if wstrings enabled, unicode string.
|
||||
try:
|
||||
self.handle.rename_file(index, filename.encode('utf8'))
|
||||
except TypeError:
|
||||
except (UnicodeDecodeError, TypeError):
|
||||
self.handle.rename_file(index, filename)
|
||||
|
||||
def rename_folder(self, folder, new_folder):
|
||||
@ -1293,7 +1387,7 @@ class Torrent(object):
|
||||
This basically does a file rename on all of the folders children.
|
||||
|
||||
Args:
|
||||
folder (str): The orignal folder name
|
||||
folder (str): The original folder name
|
||||
new_folder (str): The new folder name
|
||||
|
||||
Returns:
|
||||
@ -1320,15 +1414,19 @@ class Torrent(object):
|
||||
new_path = _file['path'].replace(folder, new_folder, 1)
|
||||
try:
|
||||
self.handle.rename_file(_file['index'], new_path.encode('utf8'))
|
||||
except TypeError:
|
||||
except (UnicodeDecodeError, TypeError):
|
||||
self.handle.rename_file(_file['index'], new_path)
|
||||
|
||||
def on_folder_rename_complete(dummy_result, torrent, folder, new_folder):
|
||||
"""Folder rename complete"""
|
||||
component.get('EventManager').emit(TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder))
|
||||
component.get('EventManager').emit(
|
||||
TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder)
|
||||
)
|
||||
# Empty folders are removed after libtorrent folder renames
|
||||
self.remove_empty_folders(folder)
|
||||
torrent.waiting_on_folder_rename = [_dir for _dir in torrent.waiting_on_folder_rename if _dir]
|
||||
torrent.waiting_on_folder_rename = [
|
||||
_dir for _dir in torrent.waiting_on_folder_rename if _dir
|
||||
]
|
||||
component.get('TorrentManager').save_resume_data((self.torrent_id,))
|
||||
|
||||
d = DeferredList(list(wait_on_folder.values()))
|
||||
@ -1345,7 +1443,9 @@ class Torrent(object):
|
||||
"""
|
||||
# Removes leading slashes that can cause join to ignore download_location
|
||||
download_location = self.options['download_location']
|
||||
folder_full_path = os.path.normpath(os.path.join(download_location, folder.lstrip('\\/')))
|
||||
folder_full_path = os.path.normpath(
|
||||
os.path.join(download_location, folder.lstrip('\\/'))
|
||||
)
|
||||
|
||||
try:
|
||||
if not os.listdir(folder_full_path):
|
||||
@ -1356,7 +1456,9 @@ class Torrent(object):
|
||||
for name in dirs:
|
||||
try:
|
||||
os.removedirs(os.path.join(root, name))
|
||||
log.debug('Removed Empty Folder %s', os.path.join(root, name))
|
||||
log.debug(
|
||||
'Removed Empty Folder %s', os.path.join(root, name)
|
||||
)
|
||||
except OSError as ex:
|
||||
log.debug(ex)
|
||||
|
||||
@ -1379,16 +1481,24 @@ class Torrent(object):
|
||||
pieces = None
|
||||
else:
|
||||
pieces = []
|
||||
for piece, avail_piece in zip(self.status.pieces, self.handle.piece_availability()):
|
||||
for piece, avail_piece in zip(
|
||||
self.status.pieces, self.handle.piece_availability()
|
||||
):
|
||||
if piece:
|
||||
pieces.append(3) # Completed.
|
||||
elif avail_piece:
|
||||
pieces.append(1) # Available, just not downloaded nor being downloaded.
|
||||
pieces.append(
|
||||
1
|
||||
) # Available, just not downloaded nor being downloaded.
|
||||
else:
|
||||
pieces.append(0) # Missing, no known peer with piece, or not asked for yet.
|
||||
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:
|
||||
pieces[peer_info.downloading_piece_index] = 2 # Being downloaded from peer.
|
||||
pieces[
|
||||
peer_info.downloading_piece_index
|
||||
] = 2 # Being downloaded from peer.
|
||||
|
||||
return pieces
|
||||
|
File diff suppressed because it is too large
Load Diff
137
deluge/crypto_utils.py
Normal file
137
deluge/crypto_utils.py
Normal file
@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# 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 os
|
||||
import stat
|
||||
|
||||
from OpenSSL import crypto
|
||||
from OpenSSL.crypto import FILETYPE_PEM
|
||||
from twisted.internet.ssl import (
|
||||
AcceptableCiphers,
|
||||
Certificate,
|
||||
CertificateOptions,
|
||||
KeyPair,
|
||||
TLSVersion,
|
||||
)
|
||||
|
||||
import deluge.configmanager
|
||||
|
||||
# A TLS ciphers list.
|
||||
# Sources for more information on TLS ciphers:
|
||||
# - https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# - https://www.ssllabs.com/projects/best-practices/index.html
|
||||
# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
|
||||
#
|
||||
# This list was inspired by the `urllib3` library
|
||||
# - https://github.com/urllib3/urllib3/blob/master/urllib3/util/ssl_.py#L79
|
||||
#
|
||||
# The general intent is:
|
||||
# - prefer cipher suites that offer perfect forward secrecy (ECDHE),
|
||||
# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
|
||||
# - disable NULL authentication, MD5 MACs and DSS for security reasons.
|
||||
TLS_CIPHERS = ':'.join(
|
||||
[
|
||||
'ECDH+AESGCM',
|
||||
'ECDH+CHACHA20',
|
||||
'AES256-GCM-SHA384',
|
||||
'AES128-GCM-SHA256',
|
||||
'!DSS' '!aNULL',
|
||||
'!eNULL',
|
||||
'!MD5',
|
||||
]
|
||||
)
|
||||
|
||||
# This value tells OpenSSL to disable all SSL/TLS renegotiation.
|
||||
SSL_OP_NO_RENEGOTIATION = 0x40000000
|
||||
|
||||
|
||||
def get_context_factory(cert_path, pkey_path):
|
||||
"""OpenSSL context factory.
|
||||
|
||||
Generates an OpenSSL context factory using Twisted's CertificateOptions class.
|
||||
This will keep a server cipher order.
|
||||
|
||||
Args:
|
||||
cert_path (string): The path to the certificate file
|
||||
pkey_path (string): The path to the private key file
|
||||
|
||||
Returns:
|
||||
twisted.internet.ssl.CertificateOptions: An OpenSSL context factory
|
||||
"""
|
||||
|
||||
with open(cert_path) as cert:
|
||||
certificate = Certificate.loadPEM(cert.read()).original
|
||||
with open(pkey_path) as pkey:
|
||||
private_key = KeyPair.load(pkey.read(), FILETYPE_PEM).original
|
||||
ciphers = AcceptableCiphers.fromOpenSSLCipherString(TLS_CIPHERS)
|
||||
cert_options = CertificateOptions(
|
||||
privateKey=private_key,
|
||||
certificate=certificate,
|
||||
raiseMinimumTo=TLSVersion.TLSv1_2,
|
||||
acceptableCiphers=ciphers,
|
||||
)
|
||||
ctx = cert_options.getContext()
|
||||
ctx.use_certificate_chain_file(cert_path)
|
||||
ctx.set_options(SSL_OP_NO_RENEGOTIATION)
|
||||
|
||||
return cert_options
|
||||
|
||||
|
||||
def check_ssl_keys():
|
||||
"""
|
||||
Check for SSL cert/key and create them if necessary
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||
if not os.path.exists(ssl_dir):
|
||||
# The ssl folder doesn't exist so we need to create it
|
||||
os.makedirs(ssl_dir)
|
||||
generate_ssl_keys()
|
||||
else:
|
||||
for f in ('daemon.pkey', 'daemon.cert'):
|
||||
if not os.path.exists(os.path.join(ssl_dir, f)):
|
||||
generate_ssl_keys()
|
||||
break
|
||||
|
||||
|
||||
def generate_ssl_keys():
|
||||
"""
|
||||
This method generates a new SSL key/cert.
|
||||
"""
|
||||
digest = 'sha256'
|
||||
|
||||
# Generate key pair
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||
|
||||
# Generate cert request
|
||||
req = crypto.X509Req()
|
||||
subj = req.get_subject()
|
||||
setattr(subj, 'CN', 'Deluge Daemon')
|
||||
req.set_pubkey(pkey)
|
||||
req.sign(pkey, digest)
|
||||
|
||||
# Generate certificate
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(0)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
||||
cert.set_issuer(req.get_subject())
|
||||
cert.set_subject(req.get_subject())
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
cert.sign(pkey, digest)
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
|
||||
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
|
||||
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
# Make the files only readable by this user
|
||||
for f in ('daemon.pkey', 'daemon.cert'):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
@ -7,8 +7,6 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
@ -23,11 +21,14 @@ def proxy(proxy_func):
|
||||
:param proxy_func: the proxy function
|
||||
:type proxy_func: function
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return proxy_func(func, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@ -53,10 +54,11 @@ def overrides(*args):
|
||||
if inspect.isfunction(args[0]):
|
||||
return _overrides(stack, args[0])
|
||||
else:
|
||||
# One or more classes are specifed, so return a function that will be
|
||||
# One or more classes are specified, so return a function that will be
|
||||
# called with the real function as argument
|
||||
def ret_func(func, **kwargs):
|
||||
return _overrides(stack, func, explicit_base_classes=args)
|
||||
|
||||
return ret_func
|
||||
|
||||
|
||||
@ -75,7 +77,10 @@ def _overrides(stack, method, explicit_base_classes=None):
|
||||
check_classes = base_classes
|
||||
|
||||
if not base_classes:
|
||||
raise ValueError('overrides decorator: unable to determine base class of class "%s"' % class_name)
|
||||
raise ValueError(
|
||||
'overrides decorator: unable to determine base class of class "%s"'
|
||||
% class_name
|
||||
)
|
||||
|
||||
def get_class(cls_name):
|
||||
if '.' not in cls_name:
|
||||
@ -91,46 +96,66 @@ def _overrides(stack, method, explicit_base_classes=None):
|
||||
|
||||
if explicit_base_classes:
|
||||
# One or more base classes are explicitly given, check only those classes
|
||||
override_classes = re.search(r'\s*@overrides\((.+)\)\s*', stack[1][4][0]).group(1)
|
||||
override_classes = re.search(r'\s*@overrides\((.+)\)\s*', stack[1][4][0]).group(
|
||||
1
|
||||
)
|
||||
override_classes = [c.strip() for c in override_classes.split(',')]
|
||||
check_classes = override_classes
|
||||
|
||||
for c in base_classes + check_classes:
|
||||
classes[c] = get_class(c)
|
||||
|
||||
# Verify that the excplicit override class is one of base classes
|
||||
# Verify that the explicit override class is one of base classes
|
||||
if explicit_base_classes:
|
||||
from itertools import product
|
||||
|
||||
for bc, cc in product(base_classes, check_classes):
|
||||
if issubclass(classes[bc], classes[cc]):
|
||||
break
|
||||
else:
|
||||
raise Exception('Excplicit override class "%s" is not a super class of: %s'
|
||||
% (explicit_base_classes, class_name))
|
||||
raise Exception(
|
||||
'Excplicit override class "%s" is not a super class of: %s'
|
||||
% (explicit_base_classes, class_name)
|
||||
)
|
||||
if not all(hasattr(classes[cls], method.__name__) for cls in check_classes):
|
||||
for cls in check_classes:
|
||||
if not hasattr(classes[cls], method.__name__):
|
||||
raise Exception('Function override "%s" not found in superclass: %s\n%s'
|
||||
% (method.__name__, cls, 'File: %s:%s' % (stack[1][1], stack[1][2])))
|
||||
raise Exception(
|
||||
'Function override "%s" not found in superclass: %s\n%s'
|
||||
% (
|
||||
method.__name__,
|
||||
cls,
|
||||
'File: %s:%s' % (stack[1][1], stack[1][2]),
|
||||
)
|
||||
)
|
||||
|
||||
if not any(hasattr(classes[cls], method.__name__) for cls in check_classes):
|
||||
raise Exception('Function override "%s" not found in any superclass: %s\n%s'
|
||||
% (method.__name__, check_classes, 'File: %s:%s' % (stack[1][1], stack[1][2])))
|
||||
raise Exception(
|
||||
'Function override "%s" not found in any superclass: %s\n%s'
|
||||
% (
|
||||
method.__name__,
|
||||
check_classes,
|
||||
'File: %s:%s' % (stack[1][1], stack[1][2]),
|
||||
)
|
||||
)
|
||||
return method
|
||||
|
||||
|
||||
def deprecated(func):
|
||||
"""This is a decorator which can be used to mark function as deprecated.
|
||||
|
||||
It will result in a warning being emmitted when the function is used.
|
||||
It will result in a warning being emitted when the function is used.
|
||||
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def depr_func(*args, **kwargs):
|
||||
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
|
||||
warnings.warn('Call to deprecated function {}.'.format(func.__name__),
|
||||
category=DeprecationWarning, stacklevel=2)
|
||||
warnings.warn(
|
||||
'Call to deprecated function {}.'.format(func.__name__),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
warnings.simplefilter('default', DeprecationWarning) # Reset filter
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
@ -9,11 +9,7 @@
|
||||
#
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
|
||||
inst._args = args
|
||||
@ -45,7 +41,6 @@ class InvalidPathError(DelugeError):
|
||||
|
||||
|
||||
class WrappedException(DelugeError):
|
||||
|
||||
def __init__(self, message, exception_type, traceback):
|
||||
super(WrappedException, self).__init__(message)
|
||||
self.type = exception_type
|
||||
@ -60,27 +55,27 @@ class _ClientSideRecreateError(DelugeError):
|
||||
|
||||
|
||||
class IncompatibleClient(_ClientSideRecreateError):
|
||||
|
||||
def __init__(self, daemon_version):
|
||||
self.daemon_version = daemon_version
|
||||
msg = 'Your deluge client is not compatible with the daemon. '\
|
||||
'Please upgrade your client to %(daemon_version)s' % \
|
||||
dict(daemon_version=self.daemon_version)
|
||||
msg = (
|
||||
'Your deluge client is not compatible with the daemon. '
|
||||
'Please upgrade your client to %(daemon_version)s'
|
||||
) % {'daemon_version': self.daemon_version}
|
||||
super(IncompatibleClient, self).__init__(message=msg)
|
||||
|
||||
|
||||
class NotAuthorizedError(_ClientSideRecreateError):
|
||||
|
||||
def __init__(self, current_level, required_level):
|
||||
msg = 'Auth level too low: %(current_level)s < %(required_level)s' % \
|
||||
dict(current_level=current_level, required_level=required_level)
|
||||
msg = ('Auth level too low: %(current_level)s < %(required_level)s') % {
|
||||
'current_level': current_level,
|
||||
'required_level': required_level,
|
||||
}
|
||||
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(_UsernameBasedPasstroughError, self).__init__(message)
|
||||
self.username = username
|
||||
@ -96,3 +91,7 @@ class AuthenticationRequired(_UsernameBasedPasstroughError):
|
||||
|
||||
class AuthManagerError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
|
||||
class LibtorrentImportError(ImportError):
|
||||
pass
|
||||
|
@ -14,8 +14,6 @@ This module describes the types of events that can be generated by the daemon
|
||||
and subsequently emitted to the clients.
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
known_events = {}
|
||||
|
||||
|
||||
@ -23,13 +21,14 @@ class DelugeEventMetaClass(type):
|
||||
"""
|
||||
This metaclass simply keeps a list of all events classes created.
|
||||
"""
|
||||
def __init__(self, name, bases, dct): # pylint: disable=bad-mcs-method-argument
|
||||
super(DelugeEventMetaClass, self).__init__(name, bases, dct)
|
||||
|
||||
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
|
||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
||||
if name != 'DelugeEvent':
|
||||
known_events[name] = self
|
||||
known_events[name] = cls
|
||||
|
||||
|
||||
class DelugeEvent(object):
|
||||
class DelugeEvent(metaclass=DelugeEventMetaClass):
|
||||
"""
|
||||
The base class for all events.
|
||||
|
||||
@ -39,7 +38,6 @@ class DelugeEvent(object):
|
||||
:type args: list
|
||||
|
||||
"""
|
||||
__metaclass__ = DelugeEventMetaClass
|
||||
|
||||
def _get_name(self):
|
||||
return self.__class__.__name__
|
||||
@ -57,6 +55,7 @@ class TorrentAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new torrent is successfully added to the session.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, from_state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id of the torrent that was added
|
||||
@ -71,6 +70,7 @@ class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent has been removed from the session.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -83,6 +83,7 @@ class PreTorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent is about to be removed from the session.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -95,6 +96,7 @@ class TorrentStateChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent changes state.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -109,6 +111,7 @@ class TorrentTrackerStatusEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrents tracker status changes.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, status):
|
||||
"""
|
||||
Args:
|
||||
@ -122,6 +125,7 @@ class TorrentQueueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the queue order has changed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -129,6 +133,7 @@ class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a folder within a torrent has been renamed.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, old, new):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -145,6 +150,7 @@ class TorrentFileRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file within a torrent has been renamed.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, index, name):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -161,6 +167,7 @@ class TorrentFinishedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent finishes downloading.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -173,6 +180,7 @@ class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent resumes from a paused state.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -185,6 +193,7 @@ class TorrentFileCompletedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file completes.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, index):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -199,6 +208,7 @@ class TorrentStorageMovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the storage location for a torrent has been moved.
|
||||
"""
|
||||
|
||||
def __init__(self, torrent_id, path):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
@ -213,6 +223,7 @@ class CreateTorrentProgressEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when creating a torrent file remotely.
|
||||
"""
|
||||
|
||||
def __init__(self, piece_count, num_pieces):
|
||||
self._args = [piece_count, num_pieces]
|
||||
|
||||
@ -221,6 +232,7 @@ class NewVersionAvailableEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a more recent version of Deluge is available.
|
||||
"""
|
||||
|
||||
def __init__(self, new_release):
|
||||
"""
|
||||
:param new_release: the new version that is available
|
||||
@ -234,6 +246,7 @@ class SessionStartedEvent(DelugeEvent):
|
||||
Emitted when a session has started. This typically only happens once when
|
||||
the daemon is initially started.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -241,6 +254,7 @@ class SessionPausedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been paused.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -248,6 +262,7 @@ class SessionResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been resumed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -255,6 +270,7 @@ class ConfigValueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a config value changes in the Core.
|
||||
"""
|
||||
|
||||
def __init__(self, key, value):
|
||||
"""
|
||||
:param key: the key that changed
|
||||
@ -268,6 +284,7 @@ class PluginEnabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is enabled in the Core.
|
||||
"""
|
||||
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
@ -276,6 +293,7 @@ class PluginDisabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is disabled in the Core.
|
||||
"""
|
||||
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
@ -284,6 +302,7 @@ class ClientDisconnectedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a client disconnects.
|
||||
"""
|
||||
|
||||
def __init__(self, session_id):
|
||||
self._args = [session_id]
|
||||
|
||||
@ -292,6 +311,7 @@ class ExternalIPEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the external ip address is received from libtorrent.
|
||||
"""
|
||||
|
||||
def __init__(self, external_ip):
|
||||
"""
|
||||
Args:
|
||||
|
@ -7,129 +7,196 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import cgi
|
||||
import logging
|
||||
import os.path
|
||||
import zlib
|
||||
|
||||
from twisted.internet import reactor
|
||||
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 PageRedirect
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.iweb import IAgent
|
||||
from zope.interface import implementer
|
||||
|
||||
from deluge.common import get_version, utf8_encode_structure
|
||||
|
||||
try:
|
||||
from urllib.parse import urljoin
|
||||
except ImportError:
|
||||
# PY2 fallback
|
||||
from urlparse import urljoin # pylint: disable=ungrouped-imports
|
||||
from deluge.common import get_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPDownloader(client.HTTPDownloader):
|
||||
"""
|
||||
Factory class for downloading files and keeping track of progress.
|
||||
"""
|
||||
def __init__(self, url, filename, part_callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
class CompressionDecoder(client.GzipDecoder):
|
||||
"""A compression decoder for gzip, x-gzip and deflate."""
|
||||
|
||||
def deliverBody(self, protocol): # NOQA: N802
|
||||
self.original.deliverBody(CompressionDecoderProtocol(protocol, self.original))
|
||||
|
||||
|
||||
class CompressionDecoderProtocol(client._GzipProtocol):
|
||||
"""A compression decoder protocol for CompressionDecoder."""
|
||||
|
||||
def __init__(self, protocol, response):
|
||||
super(CompressionDecoderProtocol, self).__init__(protocol, response)
|
||||
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
|
||||
|
||||
|
||||
class BodyHandler(HTTPClientParser, object):
|
||||
"""An HTTP parser that saves the response to a file."""
|
||||
|
||||
def __init__(self, request, finished, length, agent, encoding=None):
|
||||
"""BodyHandler init.
|
||||
|
||||
Args:
|
||||
request (t.w.i.IClientRequest): The parser request.
|
||||
finished (Deferred): A Deferred to handle the finished response.
|
||||
length (int): The length of the response.
|
||||
agent (t.w.i.IAgent): The agent from which the request was sent.
|
||||
"""
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param force_filename: forces use of the supplied filename, regardless of header content
|
||||
:type force_filename: bool
|
||||
:param part_callback: a function to be called when a part of data
|
||||
is received, it's signature should be: func(data, current_length, total_length)
|
||||
:type part_callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
super(BodyHandler, self).__init__(request, finished)
|
||||
self.agent = agent
|
||||
self.finished = finished
|
||||
self.total_length = length
|
||||
self.current_length = 0
|
||||
self.data = b''
|
||||
self.encoding = encoding
|
||||
|
||||
def dataReceived(self, data): # NOQA: N802
|
||||
self.current_length += len(data)
|
||||
self.data += data
|
||||
if self.agent.part_callback:
|
||||
self.agent.part_callback(data, self.current_length, self.total_length)
|
||||
|
||||
def connectionLost(self, reason): # NOQA: N802
|
||||
if self.encoding:
|
||||
self.data = self.data.decode(self.encoding).encode('utf8')
|
||||
with open(self.agent.filename, 'wb') as _file:
|
||||
_file.write(self.data)
|
||||
self.finished.callback(self.agent.filename)
|
||||
self.state = u'DONE'
|
||||
HTTPClientParser.connectionLost(self, reason)
|
||||
|
||||
|
||||
@implementer(IAgent)
|
||||
class HTTPDownloaderAgent(object):
|
||||
"""A File Downloader Agent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent,
|
||||
filename,
|
||||
part_callback=None,
|
||||
force_filename=False,
|
||||
allow_compression=True,
|
||||
handle_redirect=True,
|
||||
):
|
||||
"""HTTPDownloaderAgent init.
|
||||
|
||||
Args:
|
||||
agent (t.w.c.Agent): The agent which will send the requests.
|
||||
filename (str): The filename to save the file as.
|
||||
force_filename (bool): Forces use of the supplied filename,
|
||||
regardless of header content.
|
||||
part_callback (func): A function to be called when a part of data
|
||||
is received, it's signature should be:
|
||||
func(data, current_length, total_length)
|
||||
"""
|
||||
|
||||
self.handle_redirect = handle_redirect
|
||||
self.agent = agent
|
||||
self.filename = filename
|
||||
self.part_callback = part_callback
|
||||
self.current_length = 0
|
||||
self.total_length = 0
|
||||
self.decoder = None
|
||||
self.value = filename
|
||||
self.force_filename = force_filename
|
||||
self.allow_compression = allow_compression
|
||||
self.code = None
|
||||
agent = b'Deluge/%s (http://deluge-torrent.org)' % get_version().encode('utf8')
|
||||
self.decoder = None
|
||||
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
|
||||
def request_callback(self, response):
|
||||
finished = Deferred()
|
||||
|
||||
def gotStatus(self, version, status, message): # NOQA: N802
|
||||
self.code = int(status)
|
||||
client.HTTPDownloader.gotStatus(self, version, status, message)
|
||||
if not self.handle_redirect and response.code in (
|
||||
http.MOVED_PERMANENTLY,
|
||||
http.FOUND,
|
||||
http.SEE_OTHER,
|
||||
http.TEMPORARY_REDIRECT,
|
||||
):
|
||||
location = response.headers.getRawHeaders(b'location')[0]
|
||||
error = PageRedirect(response.code, location=location)
|
||||
finished.errback(Failure(error))
|
||||
else:
|
||||
headers = response.headers
|
||||
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
|
||||
|
||||
def gotHeaders(self, headers): # NOQA: N802
|
||||
if self.code == http.OK:
|
||||
if 'content-length' in headers:
|
||||
self.total_length = int(headers['content-length'][0])
|
||||
else:
|
||||
self.total_length = 0
|
||||
if headers.hasHeader(b'content-disposition') and not self.force_filename:
|
||||
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
|
||||
'utf-8'
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
if self.allow_compression and 'content-encoding' in headers and \
|
||||
headers['content-encoding'][0] in ('gzip', 'x-gzip', 'deflate'):
|
||||
# Adding 32 to the wbits enables gzip & zlib decoding (with automatic header detection)
|
||||
# Adding 16 just enables gzip decoding (no zlib)
|
||||
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
|
||||
count = 1
|
||||
fileroot = os.path.splitext(new_file_name)[0]
|
||||
fileext = os.path.splitext(new_file_name)[1]
|
||||
while os.path.isfile(new_file_name):
|
||||
# Increment filename if already exists
|
||||
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
|
||||
count += 1
|
||||
|
||||
if 'content-disposition' in headers and not self.force_filename:
|
||||
new_file_name = str(headers['content-disposition'][0]).split(';')[1].split('=')[1]
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(os.path.split(self.value)[0], new_file_name)
|
||||
self.filename = new_file_name
|
||||
|
||||
count = 1
|
||||
fileroot = os.path.splitext(new_file_name)[0]
|
||||
fileext = os.path.splitext(new_file_name)[1]
|
||||
while os.path.isfile(new_file_name):
|
||||
# Increment filename if already exists
|
||||
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
|
||||
count += 1
|
||||
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
|
||||
cont_type, params = cgi.parse_header(cont_type_header)
|
||||
# Only re-ecode text content types.
|
||||
encoding = None
|
||||
if cont_type.startswith('text/'):
|
||||
encoding = params.get('charset', None)
|
||||
response.deliverBody(
|
||||
BodyHandler(response.request, finished, body_length, self, encoding)
|
||||
)
|
||||
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
return finished
|
||||
|
||||
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
|
||||
location = headers['location'][0]
|
||||
error = PageRedirect(self.code, location=location)
|
||||
self.noPage(Failure(error))
|
||||
def request(self, method, uri, headers=None, body_producer=None):
|
||||
"""Issue a new request to the wrapped agent.
|
||||
|
||||
return client.HTTPDownloader.gotHeaders(self, headers)
|
||||
Args:
|
||||
method (bytes): The HTTP method to use.
|
||||
uri (bytes): The url to download from.
|
||||
headers (t.w.h.Headers, optional): Any extra headers to send.
|
||||
body_producer (t.w.i.IBodyProducer, optional): Request body data.
|
||||
|
||||
def pagePart(self, data): # NOQA: N802
|
||||
if self.code == http.OK:
|
||||
self.current_length += len(data)
|
||||
if self.decoder:
|
||||
data = self.decoder.decompress(data)
|
||||
if self.part_callback:
|
||||
self.part_callback(data, self.current_length, self.total_length)
|
||||
Returns:
|
||||
Deferred: The filename of the of the downloaded file.
|
||||
"""
|
||||
if headers is None:
|
||||
headers = Headers()
|
||||
|
||||
return client.HTTPDownloader.pagePart(self, data)
|
||||
if not headers.hasHeader(b'User-Agent'):
|
||||
version = get_version()
|
||||
user_agent = 'Deluge/%s (https://deluge-torrent.org)' % version
|
||||
headers.addRawHeader('User-Agent', user_agent)
|
||||
|
||||
def pageEnd(self): # NOQA: N802
|
||||
if self.decoder:
|
||||
data = self.decoder.flush()
|
||||
self.current_length -= len(data)
|
||||
self.decoder = None
|
||||
self.pagePart(data)
|
||||
|
||||
return client.HTTPDownloader.pageEnd(self)
|
||||
d = self.agent.request(
|
||||
method=method, uri=uri, headers=headers, bodyProducer=body_producer
|
||||
)
|
||||
d.addCallback(self.request_callback)
|
||||
return d
|
||||
|
||||
|
||||
def sanitise_filename(filename):
|
||||
"""
|
||||
Sanitises a filename to use as a download destination file.
|
||||
"""Sanitises a filename to use as a download destination file.
|
||||
|
||||
Logs any filenames that could be considered malicious.
|
||||
|
||||
:param filename: the filename to sanitise
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
filename (str): The filename to sanitise.
|
||||
|
||||
Returns:
|
||||
str: The sanitised filename.
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
@ -137,136 +204,128 @@ def sanitise_filename(filename):
|
||||
|
||||
if os.path.basename(filename) != filename:
|
||||
# Dodgy server, log it
|
||||
log.warning('Potentially malicious server: trying to write to file: %s', filename)
|
||||
log.warning(
|
||||
'Potentially malicious server: trying to write to file: %s', filename
|
||||
)
|
||||
# Only use the basename
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
filename = filename.strip()
|
||||
if filename.startswith('.') or ';' in filename or '|' in filename:
|
||||
# Dodgy server, log it
|
||||
log.warning('Potentially malicious server: trying to write to file: %s', filename)
|
||||
log.warning(
|
||||
'Potentially malicious server: trying to write to file: %s', filename
|
||||
)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def _download_file(url, filename, callback=None, headers=None, force_filename=False, allow_compression=True):
|
||||
"""
|
||||
Downloads a file from a specific URL and returns a Deferred. A callback
|
||||
function can be specified to be called as parts are received.
|
||||
def _download_file(
|
||||
url,
|
||||
filename,
|
||||
callback=None,
|
||||
headers=None,
|
||||
force_filename=False,
|
||||
allow_compression=True,
|
||||
handle_redirects=True,
|
||||
):
|
||||
"""Downloads a file from a specific URL and returns a Deferred.
|
||||
|
||||
A callback function can be specified to be called as parts are received.
|
||||
|
||||
Args:
|
||||
url (str): The url to download from
|
||||
filename (str): The filename to save the file as
|
||||
callback (func): A function to be called when a part of data is received,
|
||||
url (str): The url to download from.
|
||||
filename (str): The filename to save the file as.
|
||||
callback (func): A function to be called when partial data is received,
|
||||
it's signature should be: func(data, current_length, total_length)
|
||||
headers (dict): Any optional headers to send
|
||||
force_filename (bool): force us to use the filename specified rather than
|
||||
one the server may suggest
|
||||
allow_compression (bool): Allows gzip & deflate decoding
|
||||
headers (dict): Any optional headers to send.
|
||||
force_filename (bool): Force using the filename specified rather than
|
||||
one the server may suggest.
|
||||
allow_compression (bool): Allows gzip & deflate decoding.
|
||||
|
||||
Returns:
|
||||
Deferred: the filename of the downloaded file
|
||||
Deferred: The filename of the downloaded file.
|
||||
|
||||
Raises:
|
||||
t.w.e.PageRedirect
|
||||
t.w.e.Error: for all other HTTP response errors
|
||||
|
||||
"""
|
||||
|
||||
agent = client.Agent(reactor)
|
||||
|
||||
if allow_compression:
|
||||
if not headers:
|
||||
headers = {}
|
||||
headers['accept-encoding'] = 'deflate, gzip, x-gzip'
|
||||
enc_accepted = ['gzip', 'x-gzip', 'deflate']
|
||||
decoders = [(enc.encode(), CompressionDecoder) for enc in enc_accepted]
|
||||
agent = client.ContentDecoderAgent(agent, decoders)
|
||||
if handle_redirects:
|
||||
agent = client.RedirectAgent(agent)
|
||||
|
||||
url = url.encode('utf8')
|
||||
filename = filename.encode('utf8')
|
||||
headers = utf8_encode_structure(headers) if headers else headers
|
||||
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
|
||||
agent = HTTPDownloaderAgent(
|
||||
agent, filename, callback, force_filename, allow_compression, handle_redirects
|
||||
)
|
||||
|
||||
# In Twisted 13.1.0 _parse() function replaced by _URI class.
|
||||
# In Twisted 15.0.0 _URI class renamed to URI.
|
||||
if hasattr(client, '_parse'):
|
||||
scheme, host, port, dummy_path = client._parse(url)
|
||||
else:
|
||||
try:
|
||||
from twisted.web.client import _URI as URI
|
||||
except ImportError:
|
||||
from twisted.web.client import URI
|
||||
finally:
|
||||
uri = URI.fromBytes(url)
|
||||
scheme = uri.scheme
|
||||
host = uri.host
|
||||
port = uri.port
|
||||
# The Headers init expects dict values to be a list.
|
||||
if headers:
|
||||
for name, value in list(headers.items()):
|
||||
if not isinstance(value, list):
|
||||
headers[name] = [value]
|
||||
|
||||
if scheme == 'https':
|
||||
from twisted.internet import ssl
|
||||
# ClientTLSOptions in Twisted >= 14, see ticket #2765 for details on this addition.
|
||||
try:
|
||||
from twisted.internet._sslverify import ClientTLSOptions
|
||||
except ImportError:
|
||||
ctx_factory = ssl.ClientContextFactory()
|
||||
else:
|
||||
class TLSSNIContextFactory(ssl.ClientContextFactory): # pylint: disable=no-init
|
||||
"""
|
||||
A custom context factory to add a server name for TLS connections.
|
||||
"""
|
||||
def getContext(self): # NOQA: N802
|
||||
ctx = ssl.ClientContextFactory.getContext(self)
|
||||
ClientTLSOptions(host, ctx)
|
||||
return ctx
|
||||
ctx_factory = TLSSNIContextFactory()
|
||||
|
||||
reactor.connectSSL(host, port, factory, ctx_factory)
|
||||
else:
|
||||
reactor.connectTCP(host, port, factory)
|
||||
|
||||
return factory.deferred
|
||||
return agent.request(b'GET', url.encode(), Headers(headers))
|
||||
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None, force_filename=False,
|
||||
allow_compression=True, handle_redirects=True):
|
||||
"""
|
||||
Downloads a file from a specific URL and returns a Deferred. A callback
|
||||
function can be specified to be called as parts are received.
|
||||
def download_file(
|
||||
url,
|
||||
filename,
|
||||
callback=None,
|
||||
headers=None,
|
||||
force_filename=False,
|
||||
allow_compression=True,
|
||||
handle_redirects=True,
|
||||
):
|
||||
"""Downloads a file from a specific URL and returns a Deferred.
|
||||
|
||||
A callback function can be specified to be called as parts are received.
|
||||
|
||||
Args:
|
||||
url (str): The url to download from
|
||||
filename (str): The filename to save the file as
|
||||
callback (func): A function to be called when a part of data is received,
|
||||
it's signature should be: func(data, current_length, total_length)
|
||||
headers (dict): Any optional headers to send
|
||||
force_filename (bool): force us to use the filename specified rather than
|
||||
one the server may suggest
|
||||
allow_compression (bool): Allows gzip & deflate decoding
|
||||
handle_redirects (bool): If HTTP redirects should be handled automatically
|
||||
url (str): The url to download from.
|
||||
filename (str): The filename to save the file as.
|
||||
callback (func): A function to be called when partial data is received,
|
||||
it's signature should be: func(data, current_length, total_length).
|
||||
headers (dict): Any optional headers to send.
|
||||
force_filename (bool): Force the filename specified rather than one the
|
||||
server may suggest.
|
||||
allow_compression (bool): Allows gzip & deflate decoding.
|
||||
handle_redirects (bool): HTTP redirects handled automatically or not.
|
||||
|
||||
Returns:
|
||||
Deferred: the filename of the downloaded file
|
||||
Deferred: The filename of the downloaded file.
|
||||
|
||||
Raises:
|
||||
t.w.e.PageRedirect: Unless handle_redirects=True
|
||||
t.w.e.Error: for all other HTTP response errors
|
||||
|
||||
t.w.e.PageRedirect: If handle_redirects is False.
|
||||
t.w.e.Error: For all other HTTP response errors.
|
||||
"""
|
||||
|
||||
def on_download_success(result):
|
||||
log.debug('Download success!')
|
||||
return result
|
||||
|
||||
def on_download_fail(failure):
|
||||
if failure.check(PageRedirect) and handle_redirects:
|
||||
new_url = urljoin(url, failure.getErrorMessage().split(' to ')[1])
|
||||
result = _download_file(new_url, filename, callback=callback, headers=headers,
|
||||
force_filename=force_filename,
|
||||
allow_compression=allow_compression)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the failure and pass to the caller
|
||||
log.warning('Error occurred downloading file from "%s": %s',
|
||||
url, failure.getErrorMessage())
|
||||
result = failure
|
||||
log.warning(
|
||||
'Error occurred downloading file from "%s": %s',
|
||||
url,
|
||||
failure.getErrorMessage(),
|
||||
)
|
||||
result = failure
|
||||
return result
|
||||
|
||||
d = _download_file(url, filename, callback=callback, headers=headers,
|
||||
force_filename=force_filename, allow_compression=allow_compression)
|
||||
d = _download_file(
|
||||
url,
|
||||
filename,
|
||||
callback=callback,
|
||||
headers=headers,
|
||||
force_filename=force_filename,
|
||||
allow_compression=allow_compression,
|
||||
handle_redirects=handle_redirects,
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
15
deluge/i18n/__init__.py
Normal file
15
deluge/i18n/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
from .util import (
|
||||
I18N_DOMAIN,
|
||||
get_languages,
|
||||
set_language,
|
||||
setup_mock_translation,
|
||||
setup_translation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'I18N_DOMAIN',
|
||||
'set_language',
|
||||
'get_languages',
|
||||
'setup_translation',
|
||||
'setup_mock_translation',
|
||||
]
|
6178
deluge/i18n/af.po
Normal file
6178
deluge/i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
7788
deluge/i18n/ar.po
7788
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
8048
deluge/i18n/ast.po
8048
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
7272
deluge/i18n/be.po
7272
deluge/i18n/be.po
File diff suppressed because it is too large
Load Diff
8134
deluge/i18n/bg.po
8134
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
6763
deluge/i18n/bn.po
6763
deluge/i18n/bn.po
File diff suppressed because it is too large
Load Diff
6820
deluge/i18n/bs.po
6820
deluge/i18n/bs.po
File diff suppressed because it is too large
Load Diff
7930
deluge/i18n/ca.po
7930
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
8064
deluge/i18n/cs.po
8064
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
6812
deluge/i18n/cy.po
6812
deluge/i18n/cy.po
File diff suppressed because it is too large
Load Diff
8291
deluge/i18n/da.po
8291
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
8177
deluge/i18n/de.po
8177
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
8244
deluge/i18n/el.po
8244
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
8240
deluge/i18n/en_AU.po
8240
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
8268
deluge/i18n/en_CA.po
8268
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
8326
deluge/i18n/en_GB.po
8326
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
6757
deluge/i18n/eo.po
6757
deluge/i18n/eo.po
File diff suppressed because it is too large
Load Diff
8237
deluge/i18n/es.po
8237
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
8225
deluge/i18n/et.po
8225
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
7470
deluge/i18n/eu.po
7470
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
6981
deluge/i18n/fa.po
6981
deluge/i18n/fa.po
File diff suppressed because it is too large
Load Diff
8211
deluge/i18n/fi.po
8211
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
6164
deluge/i18n/fo.po
Normal file
6164
deluge/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
8451
deluge/i18n/fr.po
8451
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
7674
deluge/i18n/fy.po
7674
deluge/i18n/fy.po
File diff suppressed because it is too large
Load Diff
6164
deluge/i18n/ga.po
Normal file
6164
deluge/i18n/ga.po
Normal file
File diff suppressed because it is too large
Load Diff
7874
deluge/i18n/gl.po
7874
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
7963
deluge/i18n/he.po
7963
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
7935
deluge/i18n/hi.po
7935
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
7699
deluge/i18n/hr.po
7699
deluge/i18n/hr.po
File diff suppressed because it is too large
Load Diff
8279
deluge/i18n/hu.po
8279
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
7078
deluge/i18n/id.po
7078
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
8160
deluge/i18n/is.po
8160
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
8249
deluge/i18n/it.po
8249
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
6798
deluge/i18n/iu.po
6798
deluge/i18n/iu.po
File diff suppressed because it is too large
Load Diff
8009
deluge/i18n/ja.po
8009
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
7555
deluge/i18n/ka.po
7555
deluge/i18n/ka.po
File diff suppressed because it is too large
Load Diff
8077
deluge/i18n/kk.po
8077
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
6172
deluge/i18n/km.po
Normal file
6172
deluge/i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
6962
deluge/i18n/kn.po
6962
deluge/i18n/kn.po
File diff suppressed because it is too large
Load Diff
8147
deluge/i18n/ko.po
8147
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
6774
deluge/i18n/ku.po
6774
deluge/i18n/ku.po
File diff suppressed because it is too large
Load Diff
6164
deluge/i18n/ky.po
Normal file
6164
deluge/i18n/ky.po
Normal file
File diff suppressed because it is too large
Load Diff
6755
deluge/i18n/la.po
6755
deluge/i18n/la.po
File diff suppressed because it is too large
Load Diff
@ -3,12 +3,16 @@
|
||||
# This file is public domain.
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
|
||||
# Deferred translation
|
||||
def _(message):
|
||||
return message
|
||||
|
||||
|
||||
# Languages we provide translations for, out of the box.
|
||||
LANGUAGES = {
|
||||
'af': _('Afrikaans'),
|
||||
@ -107,3 +111,5 @@ LANGUAGES = {
|
||||
'zh-hant': _('Traditional Chinese'),
|
||||
'zh_TW': _('Chinese (Taiwan)'),
|
||||
}
|
||||
|
||||
del _
|
6164
deluge/i18n/lb.po
Normal file
6164
deluge/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
8222
deluge/i18n/lt.po
8222
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
8283
deluge/i18n/lv.po
8283
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
7457
deluge/i18n/mk.po
7457
deluge/i18n/mk.po
File diff suppressed because it is too large
Load Diff
6164
deluge/i18n/ml.po
Normal file
6164
deluge/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load Diff
8371
deluge/i18n/ms.po
8371
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
6172
deluge/i18n/nap.po
Normal file
6172
deluge/i18n/nap.po
Normal file
File diff suppressed because it is too large
Load Diff
8225
deluge/i18n/nb.po
8225
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
6755
deluge/i18n/nds.po
6755
deluge/i18n/nds.po
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
Reference in New Issue
Block a user