Compare commits
699 Commits
deluge-1.3
...
extjs4-por
Author | SHA1 | Date | |
---|---|---|---|
6892834d4d | |||
609f54a704 | |||
fa08f7de43 | |||
a91037843b | |||
4a43d6a635 | |||
1837d833c2 | |||
e3e20aa14f | |||
c865486f82 | |||
c06d4dfea5 | |||
c405425993 | |||
d10752fc1a | |||
ab665384d4 | |||
f5e6eabee4 | |||
717e66b836 | |||
fea0b41425 | |||
d37c3e0213 | |||
b724f74700 | |||
d44a357284 | |||
d6715fcbb9 | |||
2853d028fd | |||
4ab5c6d9de | |||
f53dc5faaf | |||
c46bc049d1 | |||
39d19b5afd | |||
e8506b925f | |||
eb9071fcb0 | |||
9362ec0103 | |||
fe0332bccf | |||
64bacbfbf4 | |||
280377ad6f | |||
0c3d2322cc | |||
07b6db0c98 | |||
713953ec03 | |||
6324737031 | |||
428681aca3 | |||
317e9ee423 | |||
08e774bbda | |||
43cb787b44 | |||
fb8f1e7ebc | |||
881bcee160 | |||
42b8af25aa | |||
bf4b826809 | |||
8ae14de208 | |||
5b45670a85 | |||
6cdf9940d3 | |||
8b69d66bae | |||
3ee434975c | |||
dda4620d98 | |||
7ac0083239 | |||
6ae58248a1 | |||
1c78bcbb29 | |||
7227c97cac | |||
4fcfb677a4 | |||
808ff02130 | |||
08a0a2de99 | |||
fd56ccaabf | |||
cebddf9c79 | |||
e9b602d85f | |||
5b2d37954c | |||
fcc13f454b | |||
15ef668fef | |||
bf145c0715 | |||
192f3d88e5 | |||
d9cf3a8c08 | |||
a41b1357b5 | |||
c3c21dae72 | |||
4daa7e2470 | |||
b301051cdd | |||
456f660878 | |||
f7ce07c68f | |||
9eb85cb6eb | |||
40fd945f70 | |||
78944f47f3 | |||
acb747bfd5 | |||
0c1055511d | |||
f0c327a024 | |||
b81159f295 | |||
ca86aa5714 | |||
fc7fa94319 | |||
c6ee8cf39d | |||
bd7bbc4e33 | |||
312a57aa50 | |||
f87ed6d5a6 | |||
4234311050 | |||
a47da57c0d | |||
13528fe7f8 | |||
99358dcbb0 | |||
16cc8f6eea | |||
a384cd70b3 | |||
0e00aa479b | |||
807bc095b4 | |||
5a81ab3c35 | |||
bad228645c | |||
e016b2106f | |||
f63f247ac5 | |||
0228af6b50 | |||
90fb40b741 | |||
367631c9aa | |||
b36d62be9b | |||
b4cc1d4358 | |||
39ad5a3596 | |||
dbad4684db | |||
12d0e9574b | |||
dd50b7bea1 | |||
4dc4049851 | |||
27a6e398ee | |||
7035b1f166 | |||
a701fddbe8 | |||
b512a664c6 | |||
5bffa3757d | |||
8b6d6e3836 | |||
37b9277c0e | |||
cf891125e6 | |||
f75ec9d484 | |||
9a1ae06033 | |||
55f456d851 | |||
c346687510 | |||
08ee3d8f69 | |||
795f633bc4 | |||
b6596a27bc | |||
b7fd2d1bf1 | |||
0f625943c0 | |||
420447e386 | |||
a79520e3ee | |||
8ae26c368e | |||
981ad6d7d2 | |||
3b5e70580e | |||
71f9ef6499 | |||
7dd54b4b34 | |||
c64ed6adc5 | |||
a82c753ac0 | |||
96b5f617f2 | |||
842734c4e4 | |||
095f4ff20a | |||
ed0b017fe1 | |||
ce9b540b97 | |||
5112ed48d1 | |||
dfa8834db8 | |||
5bc63fa910 | |||
24c945f139 | |||
2542ad9234 | |||
acb4ab44d2 | |||
16fbf27b90 | |||
3397c2487b | |||
66e8b34a54 | |||
59f9d4e5cc | |||
221dea1f1a | |||
4420aae092 | |||
ddc0957e3e | |||
2f71ef4264 | |||
bc56b749ee | |||
34c95a08a3 | |||
9ae19e173f | |||
6672aaba1b | |||
0712fc9dee | |||
07dc9005f3 | |||
274a76ab3b | |||
777993f74a | |||
d1037ae213 | |||
15e9f5f218 | |||
4aab110aaf | |||
8933ac3123 | |||
2e896b520e | |||
16d27b9657 | |||
d3e8afdda1 | |||
b86ba13376 | |||
f736576436 | |||
9d1715405f | |||
ee0d757b0e | |||
32c95fac1e | |||
df3214168c | |||
9e9261e6f8 | |||
087e94f6a1 | |||
abe0031c2b | |||
13db148a11 | |||
84c5078667 | |||
cebdc89b18 | |||
87e767d4c1 | |||
ce406674ec | |||
ac5f9a2828 | |||
6d55c44983 | |||
1557d0da1f | |||
2f785216f6 | |||
8f1730591b | |||
9ec44894d4 | |||
bb981127db | |||
a96aeed706 | |||
f14de6553a | |||
b521b3065b | |||
ea438609bf | |||
0ba51d0e51 | |||
53370e4639 | |||
c70c8ea45d | |||
937b53b355 | |||
27cd89c4ad | |||
c4dbf017a5 | |||
ec74f9aae3 | |||
cfd955a605 | |||
feed806983 | |||
c66637116b | |||
4d4c6404b1 | |||
042ddd2891 | |||
af24542856 | |||
6dc393ed23 | |||
c13eade81c | |||
eb639c3722 | |||
dc514d308c | |||
67b5cde128 | |||
ef98d19ed4 | |||
94a7b2ebf1 | |||
e0443943b5 | |||
dd78a75ca8 | |||
82712c80e1 | |||
a710bcaed4 | |||
3a7c182f83 | |||
d42778afa3 | |||
724025092a | |||
bd43f3c464 | |||
8464a938b2 | |||
b8fad45eaa | |||
b08e90ac2a | |||
13a379ef6c | |||
09e24df4bb | |||
019f2a0619 | |||
2fb874d486 | |||
85b4ceec30 | |||
b0599313bc | |||
974f48380f | |||
b3865d0a7f | |||
79c9dd3076 | |||
edb0c2e71d | |||
1c58dce3c1 | |||
445f3c0123 | |||
eb15c96403 | |||
71f411e458 | |||
856a6cd1ab | |||
99f2dbd178 | |||
0e4747bf22 | |||
81637f4572 | |||
25f086fa85 | |||
6d57a29f1d | |||
9b3f5783d5 | |||
b3492b07a1 | |||
28def22625 | |||
427fe23bdc | |||
da5c5d4b84 | |||
438cbd2238 | |||
2d59b62317 | |||
19f32b1446 | |||
9b812a4eec | |||
e383187796 | |||
6151050ad4 | |||
9a3bf35cdf | |||
6391970fad | |||
0ba0e013b5 | |||
552c898998 | |||
bc5b4d902f | |||
6a8e3f1c49 | |||
81ca9952e9 | |||
74618d5a65 | |||
0c110c2408 | |||
1ac997e7d7 | |||
d4692bef42 | |||
77fc53afc0 | |||
3b676eca40 | |||
ce3ce2c035 | |||
c8735b5cab | |||
cc5f2ffe18 | |||
89b79c76a3 | |||
837c39fdda | |||
110026edbe | |||
3b8ebf68a6 | |||
ffd344d0b5 | |||
9d29ca7b29 | |||
38906468c1 | |||
95d7caf3ac | |||
4044f52f77 | |||
a7bd953169 | |||
8922717ff2 | |||
117d50b728 | |||
04af8965bc | |||
d6f5e5b4ec | |||
1f3a7bf44c | |||
2e62ced811 | |||
95819c79e5 | |||
5ad21303c6 | |||
922e64a07e | |||
30d70d2b9b | |||
a06b350858 | |||
06f025f4bd | |||
d362a6ceba | |||
138b8ae314 | |||
6f3bc5620f | |||
f2249d5803 | |||
f26de83509 | |||
f6826a4f48 | |||
dd3f78bd36 | |||
63d0d0c69b | |||
1be59bb116 | |||
751345fc28 | |||
12ea65d188 | |||
e950cca059 | |||
a063095dad | |||
39978d5ade | |||
9fa8748432 | |||
18b27d4b49 | |||
f41f6ad46a | |||
bb9a8509c8 | |||
6694ac7a58 | |||
81d22eb730 | |||
47a9b18b89 | |||
292929ba59 | |||
cbcf413ffd | |||
4d8b34209b | |||
98a8be7131 | |||
2e68e0181c | |||
e6773dfce1 | |||
f56be66556 | |||
67a4fd49e9 | |||
e992ac3eab | |||
d05352db65 | |||
b1e0dd66eb | |||
897c2f981f | |||
91801e1632 | |||
fa20e49a93 | |||
4432e6e6e3 | |||
c225c045cb | |||
e552c21f66 | |||
89d04a393b | |||
f1730dc4d4 | |||
fb5005e3f6 | |||
51b5b23f76 | |||
78e966946f | |||
936bd925d9 | |||
43e3fe2a1a | |||
6ed3136c8e | |||
8195421c99 | |||
342da12d0c | |||
5296fc7d4c | |||
233e814547 | |||
03325c5f48 | |||
1a6742b1e2 | |||
154688a3e2 | |||
fe12552590 | |||
e63c33c496 | |||
105cb52cb0 | |||
3e0ea26e5f | |||
e44cac0eaa | |||
86a1b801f5 | |||
b3870ad6dd | |||
67ff83360f | |||
b2a16a0240 | |||
e17c035521 | |||
249398489e | |||
d44f59a0e7 | |||
6c99204828 | |||
1794f09b21 | |||
b08a4679de | |||
bfc221fc18 | |||
5ad3a1666c | |||
49d5ed6bde | |||
4b9209674e | |||
b9a688013f | |||
387b746fae | |||
796109649d | |||
d258794517 | |||
98f80c0eb6 | |||
d18becc861 | |||
0503db85ea | |||
bcb636dda4 | |||
5bc304470c | |||
42e1e2fd20 | |||
bb0746c3e8 | |||
19799d74b4 | |||
0d560bcd6f | |||
69b79756f2 | |||
fd248eb1fd | |||
45ccd3b84a | |||
298b85c368 | |||
67add964de | |||
e81a279dc2 | |||
280781ded9 | |||
f30a2858ce | |||
32b41fabd6 | |||
a0f9689664 | |||
08843ccad5 | |||
e0bb8869aa | |||
255af3c485 | |||
f35145b0a6 | |||
f2d560351e | |||
62da60a0e4 | |||
d9c1a56d44 | |||
84f278dbcc | |||
356f298e9c | |||
5fb01dacc0 | |||
8d541ad419 | |||
426eea154e | |||
ccc047848a | |||
11d8332e43 | |||
2193240c66 | |||
e43c532e63 | |||
930addb389 | |||
fab1e8412d | |||
1cce30393b | |||
510c81776f | |||
e7096d9509 | |||
5ae242472f | |||
ee75786e40 | |||
87473f2cde | |||
b2f349c05d | |||
9ac0d62149 | |||
db46a97263 | |||
4c2f9a1a0a | |||
64e38eac20 | |||
499a58f50d | |||
5f0f7204a8 | |||
62f6683730 | |||
60d96c6f20 | |||
d1efe5f1b4 | |||
62421080ef | |||
9e4ea0a671 | |||
b11468c19b | |||
40a5722987 | |||
956ea10a00 | |||
543fcf722c | |||
7f52472e9e | |||
5619991f2a | |||
2b04955128 | |||
e83d540fe4 | |||
4e5d88da82 | |||
10816cb8f4 | |||
5f8eda9204 | |||
b0c561dbbc | |||
1173f1c714 | |||
3da5cd9816 | |||
d9d8762c8e | |||
e16ee523a5 | |||
3db7bcbfc7 | |||
e1a3a431f0 | |||
9a3316f950 | |||
1789e8d03c | |||
837322478b | |||
ce2516ab2c | |||
962bfc3d2c | |||
4ff0fb19ee | |||
7a4006439b | |||
c015c3a57d | |||
8a9e732f95 | |||
0b3c408e64 | |||
d3a61bbda4 | |||
c523958bf6 | |||
06003b3650 | |||
1e0005f572 | |||
4a071ecba1 | |||
77eb1a5f82 | |||
ac8c928a5b | |||
23f64a5440 | |||
98ca371b15 | |||
f8737777b1 | |||
e198ea14e4 | |||
553f35eae5 | |||
376a23e6fd | |||
077f35ec5c | |||
00ab9ff499 | |||
20302021c4 | |||
9e5455793b | |||
7f6a1db89a | |||
cdcab320fb | |||
ea22bb0b10 | |||
554f34a261 | |||
ad2b13eb2c | |||
ce636ccd57 | |||
053700342a | |||
cd7805bfda | |||
79869faa53 | |||
b77f8929d6 | |||
e1d8025309 | |||
183a97785b | |||
f748660cac | |||
cea6c817df | |||
b9ff47e10f | |||
14746bf94d | |||
87f871f40a | |||
9f3ac37f25 | |||
417a9f6e63 | |||
ba6389bcac | |||
9bca1a72b1 | |||
e688b45448 | |||
4c54cfedb9 | |||
88039a0eda | |||
9a54beef78 | |||
b0a0574ae0 | |||
db64745862 | |||
0353a388b3 | |||
b41ebe1b89 | |||
1952357f35 | |||
6c8529b3ba | |||
78ea5c9bd3 | |||
b35875e300 | |||
ad498c6e42 | |||
d1b3aa54ad | |||
d0346a104f | |||
5f888faceb | |||
f6f3a8e084 | |||
5dcc935852 | |||
eba7c2bf17 | |||
00fa074452 | |||
5d46d2aee5 | |||
007dd67ea1 | |||
ff3c3f7148 | |||
68c04acf50 | |||
44676f282a | |||
182ec0cd97 | |||
6f0b1fd7f2 | |||
ba3a093746 | |||
b7e7a4bc49 | |||
ac18ecd1f0 | |||
2f6283ea39 | |||
b30499c6ac | |||
8c12c47d3e | |||
356808b02c | |||
1f800bf49a | |||
d1b4523733 | |||
c00391a852 | |||
5841521133 | |||
7e2411289d | |||
2fa8ca6753 | |||
1c15df8e00 | |||
f282487806 | |||
078ed6ba71 | |||
67ea05921c | |||
0f36a65aaf | |||
90d23ce582 | |||
860457ff48 | |||
e52018bfcd | |||
9bd11ab204 | |||
c164013725 | |||
b9a8bf2409 | |||
4c3d068f0c | |||
c9e4d286c3 | |||
e43146a4ac | |||
1c2eb0c737 | |||
b0d77a4f20 | |||
20635773b3 | |||
f17634ea63 | |||
16f617d240 | |||
1c7676bfe5 | |||
63fa5bf85b | |||
6cefb49f28 | |||
eeed72a977 | |||
49e10ea0cf | |||
26e45dcbc8 | |||
b7bc1fdb1d | |||
3b00a7de59 | |||
14ec9464aa | |||
3d64f0d8da | |||
87e3a5f515 | |||
75e9ff57de | |||
b180d2a900 | |||
1822c2bde9 | |||
40e6777c48 | |||
f88b24d507 | |||
593452ed63 | |||
4197e129fe | |||
0360cbe0b8 | |||
d2f41fe7e5 | |||
0a2e9a5324 | |||
3c302088f6 | |||
3b6bad2f13 | |||
6fd4b298f3 | |||
2beec764c9 | |||
e5760ee341 | |||
45940b9064 | |||
c97f809bdc | |||
ae6837c88c | |||
a827cf6c7a | |||
f52e3c4aa0 | |||
8f7e307f33 | |||
f1f6f137c3 | |||
ff7ff8eac7 | |||
4cb2bcae25 | |||
df95222849 | |||
463fd3ac04 | |||
eb37c91866 | |||
7cd210a59b | |||
eee27868a8 | |||
e5dec3f020 | |||
def1127c78 | |||
847f2c2ebd | |||
fb49aa02a8 | |||
f8dc66b773 | |||
c17b466bae | |||
d9cdff9525 | |||
915db80a55 | |||
350d4d7260 | |||
4b92912577 | |||
a794223d96 | |||
5811d372f9 | |||
0b2f2f2c8a | |||
64022d7bc7 | |||
bf715d90fd | |||
fdbd9e6687 | |||
0a0383d075 | |||
5b1bed5a48 | |||
7ed33192ec | |||
c82ba44be8 | |||
729daf331c | |||
db1835d942 | |||
7d4a316733 | |||
da8629db97 | |||
df573c66c6 | |||
29f61b58fb | |||
15ce2b71f9 | |||
116ccc21fd | |||
dee33745c8 | |||
8586cda4e0 | |||
db9b5580d7 | |||
10aebd600a | |||
f0fe3c7879 | |||
33fd852bda | |||
65c9dc5fa8 | |||
7e2eea46d3 | |||
01773e433f | |||
ca5eaf4270 | |||
7d64f057c7 | |||
48d016e97d | |||
e9ce506d1c | |||
e0eb0bd06a | |||
9f992ec40d | |||
ce8ef4f95b | |||
4d0560eff2 | |||
d49cde1994 | |||
16a1173f1d | |||
333d2f5562 | |||
c7fe1bdef5 | |||
46a967fb8c | |||
ca22e84858 | |||
20bd962e6a | |||
22a1448372 | |||
722a5cd9e1 | |||
efecf38bcd | |||
dfb75d67b9 | |||
961d405921 | |||
e025b6b9db | |||
bc5aa1bf71 | |||
3cd30ea96a | |||
504751424f | |||
37a00a48a7 | |||
2a2f5d90ae | |||
f0920f5638 | |||
43fb998651 | |||
148fcdbe37 | |||
de79bba540 | |||
d5881142aa | |||
494c468da8 | |||
672668ccdb | |||
538aed9147 | |||
d800273891 | |||
94f96c5165 | |||
4b1d60c727 | |||
cfe547b31a | |||
f6195f775f | |||
87879ab3b8 | |||
81a837faed | |||
c06f905702 | |||
f6f9e0234a | |||
e1e1472a8f | |||
b7e1fe1696 | |||
0314d0440f | |||
3226b1819d | |||
4b8a85763c | |||
ae4f2c3bb0 | |||
bc28b83062 | |||
2603c36e7d | |||
81b56cce62 | |||
649a2b6f8e | |||
4caf81ef89 | |||
65c33a37a1 | |||
eff17931eb | |||
b33c2abf82 | |||
ba514f0b0e | |||
7f60867ae9 | |||
71d8836118 | |||
97d6f8ce80 | |||
ca7f009e74 | |||
f08e5176c3 | |||
70161a54fa | |||
a945d0a78d | |||
245b799ccf | |||
0dc6c3ecfd | |||
98f000cc70 | |||
8d4daff068 | |||
79d68a5b9b | |||
412d0ee4f9 | |||
e8788bde08 | |||
815a71fe8b | |||
fce16ba51f | |||
50cfd9c9b1 | |||
369b03bffb |
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@ dist
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "libtorrent"]
|
||||
path = libtorrent
|
||||
url = git://deluge-torrent.org/libtorrent
|
42
ChangeLog
42
ChangeLog
@ -1,4 +1,13 @@
|
||||
=== Deluge 1.3.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 ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
* Implement #457 progress bars for folders
|
||||
@ -7,16 +16,39 @@
|
||||
* #1112: Fix renaming files in add torrent dialog
|
||||
* #1247: Fix deluge-gtk from hanging on shutdown
|
||||
* #995: Rewrote tracker_icons
|
||||
* 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.
|
||||
* #378: Provide information about a torrent's pieces states
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
* Allow changing ownership of torrents
|
||||
* Host entries in the Connection Manager UI are now editable. They're
|
||||
now also migrated from the old format were automatic localhost logins were
|
||||
possible, which no longer is, this fixes #1814.
|
||||
* Implemented sequential downloads UI handling.
|
||||
* #378: Allow showing a pieces bar instead of a regular progress bar in a
|
||||
torrent's status tab.
|
||||
|
||||
==== WebUI ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid
|
||||
|
||||
==== Blocklist ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
|
||||
==== Web ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
|
5
DEPENDS
5
DEPENDS
@ -6,7 +6,9 @@
|
||||
* simplejson (if python < 2.6)
|
||||
* setuptools
|
||||
* gettext
|
||||
* intltool
|
||||
* pyxdg
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
@ -16,9 +18,6 @@
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== UIs ===
|
||||
* chardet
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
|
4
README
4
README
@ -15,9 +15,9 @@ For past developers and contributers see: http://dev.deluge-torrent.org/wiki/Abo
|
||||
License
|
||||
==========================
|
||||
Deluge is under the GNU GPLv3 license.
|
||||
Icon data/pixmaps/deluge.svg and derivatives in data/icons are copyright
|
||||
Icon ui/data/pixmaps/deluge.svg and derivatives in ui/data/icons are copyright
|
||||
Andrew Wedderburn and are under the GNU GPLv3.
|
||||
All other icons in data/pixmaps are copyright Andrew Resch and are under
|
||||
All other icons in ui/data/pixmaps are copyright Andrew Resch and are under
|
||||
the GNU GPLv3.
|
||||
|
||||
==========================
|
||||
|
21
check_glade.sh
Executable file
21
check_glade.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Fixes glade files which may have set gtk stock labels set to translatable
|
||||
for x in `find . -name '*.glade' |grep -v '.git\|build'` ; do \
|
||||
for y in gtk-add gtk-apply gtk-bold gtk-cancel gtk-cdrom gtk-clear \
|
||||
gtk-close gtk-color-picker gtk-connect gtk-convert gtk-copy gtk-cut \
|
||||
gtk-delete gtk-dialog-error gtk-dialog-info gtk-dialog-question \
|
||||
gtk-dialog-warning gtk-dnd gtk-dnd-multiple gtk-edit gtk-execute gtk-find \
|
||||
gtk-find-and-replace gtk-floppy gtk-goto-bottom gtk-goto-first \
|
||||
gtk-goto-last gtk-goto-top gtk-go-back gtk-go-down gtk-go-forward \
|
||||
gtk-go-up gtk-help gtk-home gtk-index gtk-italic gtk-jump-to \
|
||||
gtk-justify-center gtk-justify-fill gtk-justify-left gtk-missing-image \
|
||||
gtk-new gtk-no gtk-ok gtk-open gtk-paste gtk-preferences gtk-print \
|
||||
gtk-print-preview gtk-properties gtk-quit gtk-redo gtk-refresh \
|
||||
gtk-remove gtk-revert-to-saved gtk-save gtk-save-as gtk-select-color \
|
||||
gtk-select-font gtk-sort-descending gtk-spell-check gtk-stop \
|
||||
gtk-strikethrough gtk-undelete gtk-underline gtk-undo gtk-yes \
|
||||
gtk-zoom-100 gtk-zoom-fit gtk-zoom-in gtk-zoom-out; do \
|
||||
sed -i "s/<property\ name\=\"label\"\ translatable\=\"yes\">$y<\/property>/<property\ name\=\"label\"\ translatable\=\"no\">$y<\/property>/g" $x; \
|
||||
done;\
|
||||
done
|
17
create_potfiles_in.py
Normal file → Executable file
17
create_potfiles_in.py
Normal file → Executable file
@ -1,17 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
import re
|
||||
import sys
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts"
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.in"
|
||||
|
||||
print "Creating " + POTFILE_IN + " .."
|
||||
pattern = "deluge\/plugins\/.*\/build"
|
||||
compiled = re.compile(pattern)
|
||||
|
||||
sys.stdout.write("Creating " + POTFILE_IN + " ... ")
|
||||
sys.stdout.flush()
|
||||
to_translate = []
|
||||
for (dirpath, dirnames, filenames) in os.walk("deluge"):
|
||||
for filename in filenames:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade") and dirpath not in EXCLUSIONS:
|
||||
if os.path.splitext(filename)[1] in (".py", ".glade", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/data/\
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/ui/data/\
|
||||
icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
|
||||
-o deluge/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/data/icons/scalable/apps/; cp deluge/data/pixmaps/\
|
||||
deluge.svg deluge/data/icons/scalable/apps/deluge.svg; done
|
||||
-o deluge/ui/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/ui/data/pixmaps\
|
||||
/deluge.svg; mkdir -p deluge/ui/data/icons/scalable/apps/; cp deluge/ui/data/pixmaps/\
|
||||
deluge.svg deluge/ui/data/icons/scalable/apps/deluge.svg; done
|
||||
|
@ -1 +1,4 @@
|
||||
"""Deluge"""
|
||||
# this is a namespace package
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
|
209
deluge/common.py
209
deluge/common.py
@ -40,13 +40,16 @@ import os
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
import sys
|
||||
import chardet
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Do a little hack here just in case the user has json-py installed since it
|
||||
# has a different api
|
||||
if not hasattr(json, "dumps"):
|
||||
@ -63,26 +66,9 @@ if not hasattr(json, "dumps"):
|
||||
json.load = load
|
||||
|
||||
import pkg_resources
|
||||
import xdg, xdg.BaseDirectory
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
from deluge.log import LOG as log
|
||||
log.error("Unable to initialize gettext/locale!")
|
||||
log.exception(e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
LT_TORRENT_STATE = {
|
||||
@ -119,11 +105,12 @@ FILE_PRIORITY = {
|
||||
0: "Do Not Download",
|
||||
1: "Normal Priority",
|
||||
2: "High Priority",
|
||||
5: "Highest Priority",
|
||||
5: "High Priority",
|
||||
7: "Highest Priority",
|
||||
"Do Not Download": 0,
|
||||
"Normal Priority": 1,
|
||||
"High Priority": 2,
|
||||
"Highest Priority": 5
|
||||
"High Priority": 5,
|
||||
"Highest Priority": 7
|
||||
}
|
||||
|
||||
def get_version():
|
||||
@ -150,6 +137,7 @@ def get_default_config_dir(filename=None):
|
||||
else:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge")
|
||||
else:
|
||||
import xdg.BaseDirectory
|
||||
if filename:
|
||||
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
|
||||
else:
|
||||
@ -164,6 +152,18 @@ def get_default_download_dir():
|
||||
if windows_check():
|
||||
return os.path.expanduser("~")
|
||||
else:
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
userdir_file = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||
try:
|
||||
for line in open(userdir_file, 'r'):
|
||||
if not line.startswith('#') and 'XDG_DOWNLOAD_DIR' in line:
|
||||
download_dir = os.path.expandvars(\
|
||||
line.partition("=")[2].rstrip().strip('"'))
|
||||
if os.path.isdir(download_dir):
|
||||
return download_dir
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return os.environ.get("HOME")
|
||||
|
||||
def windows_check():
|
||||
@ -198,7 +198,7 @@ def osx_check():
|
||||
|
||||
def get_pixmap(fname):
|
||||
"""
|
||||
Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg
|
||||
Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg
|
||||
|
||||
:param fname: the filename to look for
|
||||
:type fname: string
|
||||
@ -206,8 +206,18 @@ def get_pixmap(fname):
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
"pixmaps", fname))
|
||||
return resource_filename("deluge", os.path.join("ui", "data", "pixmaps", 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.
|
||||
return pkg_resources.require("Deluge>=%s" % get_version())[0].get_resource_filename(
|
||||
pkg_resources._manager, os.path.join(*(module.split('.')+[path]))
|
||||
)
|
||||
|
||||
def open_file(path):
|
||||
"""
|
||||
@ -291,7 +301,14 @@ def fspeed(bps):
|
||||
'42.1 KiB/s'
|
||||
|
||||
"""
|
||||
return '%s/s' % (fsize(bps))
|
||||
fspeed_kb = bps / 1024.0
|
||||
if fspeed_kb < 1024:
|
||||
return "%.1f %s" % (fspeed_kb, _("KiB/s"))
|
||||
fspeed_mb = fspeed_kb / 1024.0
|
||||
if fspeed_mb < 1024:
|
||||
return "%.1f %s" % (fspeed_mb, _("MiB/s"))
|
||||
fspeed_gb = fspeed_mb / 1024.0
|
||||
return "%.1f %s" % (fspeed_gb, _("GiB/s"))
|
||||
|
||||
def fpeer(num_peers, total_peers):
|
||||
"""
|
||||
@ -474,7 +491,7 @@ def free_space(path):
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
else:
|
||||
disk_data = os.statvfs(path)
|
||||
disk_data = os.statvfs(path.encode("utf8"))
|
||||
block_size = disk_data.f_bsize
|
||||
return disk_data.f_bavail * block_size
|
||||
|
||||
@ -526,7 +543,7 @@ def path_join(*parts):
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
XML_ESCAPES = (
|
||||
XML_ESCAPES = (
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
@ -535,9 +552,9 @@ XML_ESCAPES = (
|
||||
)
|
||||
|
||||
def xml_decode(string):
|
||||
"""
|
||||
"""
|
||||
Unescape a string that was previously encoded for use within xml.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: The unescaped version of the string.
|
||||
@ -548,9 +565,9 @@ def xml_decode(string):
|
||||
return string
|
||||
|
||||
def xml_encode(string):
|
||||
"""
|
||||
"""
|
||||
Escape a string for use within an xml element or attribute.
|
||||
|
||||
|
||||
:param string: The string to escape
|
||||
:type string: string
|
||||
:returns: An escaped version of the string.
|
||||
@ -560,6 +577,41 @@ def xml_encode(string):
|
||||
string = string.replace(char, escape)
|
||||
return string
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and re-encodes it in utf8. If it cannot decode using
|
||||
`:param:encoding` then it will try to detect the string encoding and
|
||||
decode it.
|
||||
|
||||
:param s: string to decode
|
||||
:type s: string
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
s = s.decode(encoding).encode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
|
||||
return s
|
||||
|
||||
def utf8_encoded(s):
|
||||
"""
|
||||
Returns a utf8 encoded string of s
|
||||
|
||||
:param s: (unicode) string to (re-)encode
|
||||
:type s: basestring
|
||||
:returns: a utf8 encoded string of s
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s)
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8", "ignore")
|
||||
return s
|
||||
|
||||
class VersionSplit(object):
|
||||
"""
|
||||
Used for comparing version numbers.
|
||||
@ -570,13 +622,15 @@ class VersionSplit(object):
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.split("_") if "_" in ver else ver.split("-")
|
||||
vs = ver.replace("_", "-").split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".")]
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
if len(vs) > 1:
|
||||
for s in ("rc", "alpha", "beta", "dev"):
|
||||
if s in vs[1][:len(s)]:
|
||||
self.suffix = vs[1]
|
||||
if vs[1].startswith(("rc", "alpha", "beta")):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1] == 'dev':
|
||||
self.dev = True
|
||||
|
||||
def __cmp__(self, ver):
|
||||
"""
|
||||
@ -587,19 +641,74 @@ class VersionSplit(object):
|
||||
|
||||
"""
|
||||
|
||||
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
|
||||
return 1
|
||||
if self.version < ver.version:
|
||||
return -1
|
||||
# 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]
|
||||
v2 = [ver.version, ver.suffix or 'z', ver.dev]
|
||||
return cmp(v1, v2)
|
||||
|
||||
if self.version == ver.version:
|
||||
if self.suffix == ver.suffix:
|
||||
return 0
|
||||
if self.suffix is None:
|
||||
return 1
|
||||
if ver.suffix is None:
|
||||
return -1
|
||||
if self.suffix < ver.suffix:
|
||||
return -1
|
||||
if self.suffix > ver.suffix:
|
||||
return 1
|
||||
|
||||
# Common AUTH stuff
|
||||
AUTH_LEVEL_NONE = 0
|
||||
AUTH_LEVEL_READONLY = 1
|
||||
AUTH_LEVEL_NORMAL = 5
|
||||
AUTH_LEVEL_ADMIN = 10
|
||||
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
|
||||
def create_auth_file():
|
||||
import stat, configmanager
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
fd = open(auth_file, "w")
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|
||||
|
||||
def create_localclient_account(append=False):
|
||||
import configmanager, random
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
if not os.path.exists(auth_file):
|
||||
create_auth_file()
|
||||
|
||||
try:
|
||||
from hashlib import sha1 as sha_hash
|
||||
except ImportError:
|
||||
from sha import new as sha_hash
|
||||
fd = open(auth_file, "a" if append else "w")
|
||||
fd.write(":".join([
|
||||
"localclient",
|
||||
sha_hash(str(random.random())).hexdigest(),
|
||||
str(AUTH_LEVEL_ADMIN)
|
||||
]) + '\n')
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
|
||||
|
||||
# Initialize gettext
|
||||
def setup_translations(setup_pygtk=False):
|
||||
translations_path = resource_filename("deluge", "i18n")
|
||||
log.info("Setting up translations from %s", translations_path)
|
||||
|
||||
try:
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", translations_path)
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.install("deluge", translations_path, unicode=True)
|
||||
if setup_pygtk:
|
||||
# Even though we're not using glade anymore, let's set it up so that
|
||||
# plugins still using it get properly translated.
|
||||
log.info("Setting up GTK translations from %s", translations_path)
|
||||
import gtk
|
||||
import gtk.glade
|
||||
gtk.glade.bindtextdomain("deluge", translations_path)
|
||||
gtk.glade.textdomain("deluge")
|
||||
except Exception, e:
|
||||
log.error("Unable to initialize gettext/locale!")
|
||||
log.exception(e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
|
@ -33,9 +33,11 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
|
||||
from twisted.internet.task import LoopingCall
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
@ -96,6 +98,10 @@ class Component(object):
|
||||
self._component_stopping_deferred = None
|
||||
_ComponentRegistry.register(self)
|
||||
|
||||
def __del__(self):
|
||||
if _ComponentRegistry:
|
||||
_ComponentRegistry.deregister(self)
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, "update"):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
@ -139,11 +145,18 @@ class Component(object):
|
||||
self._component_timer.stop()
|
||||
return True
|
||||
|
||||
def on_stop_fail(result):
|
||||
self._component_state = "Started"
|
||||
self._component_stopping_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state != "Stopped" and self._component_state != "Stopping":
|
||||
if hasattr(self, "stop"):
|
||||
self._component_state = "Stopping"
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
@ -219,22 +232,22 @@ class ComponentRegistry(object):
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
|
||||
def deregister(self, name):
|
||||
def deregister(self, obj):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
:param name: the name of the component
|
||||
:type name: string
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
"""
|
||||
|
||||
if name in self.components:
|
||||
log.debug("Deregistering Component: %s", name)
|
||||
d = self.stop([name])
|
||||
if obj in self.components.values():
|
||||
log.debug("Deregistering Component: %s", obj._component_name)
|
||||
d = self.stop([obj._component_name])
|
||||
def on_stop(result, name):
|
||||
del self.components[name]
|
||||
return d.addCallback(on_stop, name)
|
||||
return d.addCallback(on_stop, obj._component_name)
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
|
@ -45,9 +45,9 @@ The format of the config file is two json encoded dicts:
|
||||
<version dict>
|
||||
<content dict>
|
||||
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
The version dict contains two keys: file and format. The format version is
|
||||
controlled by the Config class. It should only be changed when anything below
|
||||
it is changed directly by the Config class. An example of this would be if we
|
||||
changed the serializer for the content to something different.
|
||||
|
||||
The config file version is changed by the 'owner' of the config file. This is
|
||||
@ -68,14 +68,16 @@ version as this will be done internally.
|
||||
"""
|
||||
|
||||
import cPickle as pickle
|
||||
import logging
|
||||
import shutil
|
||||
import os
|
||||
|
||||
import deluge.common
|
||||
from deluge.log import LOG as log
|
||||
|
||||
json = deluge.common.json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def prop(func):
|
||||
"""Function decorator for defining property attributes
|
||||
|
||||
@ -93,13 +95,13 @@ def prop(func):
|
||||
def find_json_objects(s):
|
||||
"""
|
||||
Find json objects in a string.
|
||||
|
||||
|
||||
:param s: the string to find json objects in
|
||||
:type s: string
|
||||
|
||||
|
||||
:returns: a list of tuples containing start and end locations of json objects in the string `s`
|
||||
:rtype: [(start, end), ...]
|
||||
|
||||
|
||||
"""
|
||||
objects = []
|
||||
opens = 0
|
||||
@ -119,8 +121,8 @@ def find_json_objects(s):
|
||||
start = index + offset + 1
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class is used to access/create/modify config files
|
||||
@ -146,7 +148,8 @@ class Config(object):
|
||||
self._save_timer = None
|
||||
|
||||
if defaults:
|
||||
self.__config = dict(defaults)
|
||||
for key, value in defaults.iteritems():
|
||||
self.set_item(key, value)
|
||||
|
||||
# Load the config from file in the config_dir
|
||||
if config_dir:
|
||||
@ -187,6 +190,10 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
if isinstance(value, basestring):
|
||||
value = deluge.common.utf8_encoded(value)
|
||||
|
||||
|
||||
if not self.__config.has_key(key):
|
||||
self.__config[key] = value
|
||||
log.debug("Setting '%s' to %s of %s", key, value, type(value))
|
||||
@ -200,7 +207,10 @@ what is currently in the config and it could not convert the value
|
||||
|
||||
if value is not None and oldtype != type(None) and oldtype != newtype:
|
||||
try:
|
||||
value = oldtype(value)
|
||||
if oldtype == unicode:
|
||||
value = oldtype(value, "utf8")
|
||||
else:
|
||||
value = oldtype(value)
|
||||
except ValueError:
|
||||
log.warning("Type '%s' invalid for '%s'", newtype, key)
|
||||
raise
|
||||
@ -250,7 +260,38 @@ what is currently in the config and it could not convert the value
|
||||
5
|
||||
|
||||
"""
|
||||
return self.__config[key]
|
||||
if isinstance(self.__config[key], str):
|
||||
try:
|
||||
return self.__config[key].decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
return self.__config[key]
|
||||
else:
|
||||
return self.__config[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""
|
||||
See
|
||||
:meth:`del_item`
|
||||
"""
|
||||
self.del_item(key)
|
||||
|
||||
def del_item(self, key):
|
||||
"""
|
||||
Deletes item with a specific key from the configuration.
|
||||
|
||||
:param key: the item which you wish to delete.
|
||||
:raises KeyError: if 'key' is not in the config dictionary
|
||||
|
||||
**Usage**
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> del config["test"]
|
||||
"""
|
||||
del self.__config[key]
|
||||
# We set the save_timer for 5 seconds if not already set
|
||||
from twisted.internet import reactor
|
||||
if not self._save_timer or not self._save_timer.active():
|
||||
self._save_timer = reactor.callLater(5, self.save)
|
||||
|
||||
|
||||
def register_change_callback(self, callback):
|
||||
"""
|
||||
@ -348,21 +389,21 @@ what is currently in the config and it could not convert the value
|
||||
return
|
||||
|
||||
objects = find_json_objects(data)
|
||||
|
||||
|
||||
if not len(objects):
|
||||
# No json objects found, try depickling it
|
||||
try:
|
||||
self.__config.update(pickle.loads(data))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 1:
|
||||
start, end = objects[0]
|
||||
try:
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
elif len(objects) == 2:
|
||||
try:
|
||||
start, end = objects[0]
|
||||
@ -371,8 +412,8 @@ what is currently in the config and it could not convert the value
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.warning("Unable to load config file: %s", filename)
|
||||
|
||||
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)
|
||||
|
||||
@ -396,26 +437,24 @@ what is currently in the config and it could not convert the value
|
||||
version = json.loads(data[start:end])
|
||||
start, end = objects[1]
|
||||
loaded_data = json.loads(data[start:end])
|
||||
|
||||
if self.__config == loaded_data and self.__version == version:
|
||||
# The config has not changed so lets just return
|
||||
self._save_timer.cancel()
|
||||
return
|
||||
except Exception, e:
|
||||
log.warning("Unable to open config file: %s", filename)
|
||||
|
||||
|
||||
if self._save_timer and self._save_timer.active():
|
||||
self._save_timer.cancel()
|
||||
return True
|
||||
except IOError, e:
|
||||
log.warning("Unable to open config file: %s because: %s", filename, e)
|
||||
|
||||
# Save the new config and make sure it's written to disk
|
||||
try:
|
||||
log.debug("Saving new config file %s", filename + ".new")
|
||||
f = open(filename + ".new", "wb")
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__version, f, indent=2)
|
||||
json.dump(self.__config, f, indent=2)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
f.close()
|
||||
except Exception, e:
|
||||
except IOError, e:
|
||||
log.error("Error writing new config file: %s", e)
|
||||
return False
|
||||
|
||||
|
@ -34,11 +34,14 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
from deluge.log import LOG as log
|
||||
import deluge.log
|
||||
from deluge.config import Config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class _ConfigManager:
|
||||
def __init__(self):
|
||||
log.debug("ConfigManager started..")
|
||||
@ -52,7 +55,6 @@ class _ConfigManager:
|
||||
return self.__config_directory
|
||||
|
||||
def __del__(self):
|
||||
log.debug("ConfigManager stopping..")
|
||||
del self.config_files
|
||||
|
||||
def set_config_dir(self, directory):
|
||||
@ -86,6 +88,7 @@ class _ConfigManager:
|
||||
# to reload based on the new config directory
|
||||
self.save()
|
||||
self.config_files = {}
|
||||
deluge.log.tweak_logging_levels()
|
||||
|
||||
return True
|
||||
|
||||
|
@ -41,12 +41,13 @@ This should typically only be used by the Core. Plugins should utilize the
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
|
||||
import deluge.component as component
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
from deluge.log import LOG as log
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
|
@ -2,6 +2,7 @@
|
||||
# authmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -36,27 +37,56 @@
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
import deluge.error
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
|
||||
create_localclient_account)
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
||||
|
||||
AUTH_LEVEL_NONE = 0
|
||||
AUTH_LEVEL_READONLY = 1
|
||||
AUTH_LEVEL_NORMAL = 5
|
||||
AUTH_LEVEL_ADMIN = 10
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
||||
AUTH_LEVELS_MAPPING = {
|
||||
'NONE': AUTH_LEVEL_NONE,
|
||||
'READONLY': AUTH_LEVEL_READONLY,
|
||||
'DEFAULT': AUTH_LEVEL_NORMAL,
|
||||
'NORMAL': AUTH_LEVEL_DEFAULT,
|
||||
'ADMIN': AUTH_LEVEL_ADMIN
|
||||
}
|
||||
|
||||
AUTH_LEVELS_MAPPING_REVERSE = {}
|
||||
for key, value in AUTH_LEVELS_MAPPING.iteritems():
|
||||
AUTH_LEVELS_MAPPING_REVERSE[value] = key
|
||||
|
||||
class Account(object):
|
||||
__slots__ = ('username', 'password', 'authlevel')
|
||||
def __init__(self, username, password, authlevel):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.authlevel = authlevel
|
||||
|
||||
def data(self):
|
||||
return {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
|
||||
'authlevel_int': self.authlevel
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
|
||||
self.__dict__)
|
||||
|
||||
class BadLoginError(deluge.error.DelugeError):
|
||||
pass
|
||||
|
||||
class AuthManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AuthManager")
|
||||
component.Component.__init__(self, "AuthManager", interval=10)
|
||||
self.__auth = {}
|
||||
self.__auth_modification_time = None
|
||||
|
||||
def start(self):
|
||||
self.__load_auth_file()
|
||||
@ -67,6 +97,19 @@ class AuthManager(component.Component):
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
log.info("Authfile not found, recreating it.")
|
||||
self.__load_auth_file()
|
||||
return
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time != auth_file_modification_time:
|
||||
log.info("Auth file changed, reloading it!")
|
||||
self.__load_auth_file()
|
||||
|
||||
def authorize(self, username, password):
|
||||
"""
|
||||
Authorizes users based on username and password
|
||||
@ -76,49 +119,121 @@ class AuthManager(component.Component):
|
||||
:returns: int, the auth level for this user
|
||||
:rtype: int
|
||||
|
||||
:raises BadLoginError: if the username does not exist or password does not match
|
||||
:raises AuthenticationRequired: if aditional details are required to
|
||||
authenticate.
|
||||
:raises BadLoginError: if the username does not exist or password does
|
||||
not match.
|
||||
|
||||
"""
|
||||
if not username:
|
||||
raise AuthenticationRequired(
|
||||
"Username and Password are required.", username
|
||||
)
|
||||
|
||||
if username not in self.__auth:
|
||||
# Let's try to re-load the file.. Maybe it's been updated
|
||||
self.__load_auth_file()
|
||||
if username not in self.__auth:
|
||||
raise BadLoginError("Username does not exist")
|
||||
raise BadLoginError("Username does not exist", username)
|
||||
|
||||
if self.__auth[username][0] == password:
|
||||
if self.__auth[username].password == password:
|
||||
# Return the users auth level
|
||||
return int(self.__auth[username][1])
|
||||
return self.__auth[username].authlevel
|
||||
elif not password and self.__auth[username].password:
|
||||
raise AuthenticationRequired("Password is required", username)
|
||||
else:
|
||||
raise BadLoginError("Password does not match")
|
||||
raise BadLoginError("Password does not match", username)
|
||||
|
||||
def __create_localclient_account(self):
|
||||
def has_account(self, username):
|
||||
return username in self.__auth
|
||||
|
||||
def get_known_accounts(self):
|
||||
"""
|
||||
Returns the string.
|
||||
Returns a list of known deluge usernames.
|
||||
"""
|
||||
# We create a 'localclient' account with a random password
|
||||
self.__load_auth_file()
|
||||
return [account.data() for account in self.__auth.values()]
|
||||
|
||||
def create_account(self, username, password, authlevel):
|
||||
if username in self.__auth:
|
||||
raise AuthManagerError("Username in use.", username)
|
||||
try:
|
||||
from hashlib import sha1 as sha_hash
|
||||
except ImportError:
|
||||
from sha import new as sha_hash
|
||||
return "localclient:" + sha_hash(str(random.random())).hexdigest() + ":" + str(AUTH_LEVEL_ADMIN) + "\n"
|
||||
self.__auth[username] = Account(username, password,
|
||||
AUTH_LEVELS_MAPPING[authlevel])
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def __load_auth_file(self):
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
localclient = self.__create_localclient_account()
|
||||
fd = open(auth_file, "w")
|
||||
fd.write(localclient)
|
||||
def update_account(self, username, password, authlevel):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
try:
|
||||
self.__auth[username].username = username
|
||||
self.__auth[username].password = password
|
||||
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
raise err
|
||||
|
||||
def remove_account(self, username):
|
||||
if username not in self.__auth:
|
||||
raise AuthManagerError("Username not known", username)
|
||||
elif username == component.get("RPCServer").get_session_user():
|
||||
raise AuthManagerError(
|
||||
"You cannot delete your own account while logged in!", username
|
||||
)
|
||||
|
||||
del self.__auth[username]
|
||||
self.write_auth_file()
|
||||
return True
|
||||
|
||||
def write_auth_file(self):
|
||||
old_auth_file = configmanager.get_config_dir("auth")
|
||||
new_auth_file = old_auth_file + '.new'
|
||||
bak_auth_file = old_auth_file + '.bak'
|
||||
# Let's first create a backup
|
||||
if os.path.exists(old_auth_file):
|
||||
shutil.copy2(old_auth_file, bak_auth_file)
|
||||
|
||||
try:
|
||||
fd = open(new_auth_file, "w")
|
||||
for account in self.__auth.values():
|
||||
fd.write(
|
||||
"%(username)s:%(password)s:%(authlevel_int)s\n" %
|
||||
account.data()
|
||||
)
|
||||
fd.flush()
|
||||
os.fsync(fd.fileno())
|
||||
fd.close()
|
||||
# Change the permissions on the file so only this user can read/write it
|
||||
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
|
||||
f = [localclient]
|
||||
else:
|
||||
# Load the auth file into a dictionary: {username: password, ...}
|
||||
f = open(auth_file, "r").readlines()
|
||||
os.rename(new_auth_file, old_auth_file)
|
||||
except:
|
||||
# Something failed, let's restore the previous file
|
||||
if os.path.exists(bak_auth_file):
|
||||
os.rename(bak_auth_file, old_auth_file)
|
||||
|
||||
self.__load_auth_file()
|
||||
|
||||
def __load_auth_file(self):
|
||||
save_and_reload = False
|
||||
auth_file = configmanager.get_config_dir("auth")
|
||||
# Check for auth file and create if necessary
|
||||
if not os.path.exists(auth_file):
|
||||
create_localclient_account()
|
||||
return self.__load_auth_file()
|
||||
|
||||
auth_file_modification_time = os.stat(auth_file).st_mtime
|
||||
if self.__auth_modification_time is None:
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
elif self.__auth_modification_time == auth_file_modification_time:
|
||||
# File didn't change, no need for re-parsing's
|
||||
return
|
||||
|
||||
# Load the auth file into a dictionary: {username: Account(...)}
|
||||
f = open(auth_file, "r").readlines()
|
||||
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
@ -132,15 +247,43 @@ class AuthManager(component.Component):
|
||||
continue
|
||||
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)
|
||||
level = 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:
|
||||
authlevel = AUTH_LEVEL_DEFAULT
|
||||
# This is probably an old auth file
|
||||
save_and_reload = True
|
||||
elif len(lsplit) == 3:
|
||||
username, password, level = lsplit
|
||||
username, password, authlevel = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: Incorrect number of fields!")
|
||||
log.error("Your auth file is malformed: "
|
||||
"Incorrect number of fields!")
|
||||
continue
|
||||
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
try:
|
||||
authlevel = int(authlevel)
|
||||
except ValueError:
|
||||
try:
|
||||
authlevel = AUTH_LEVELS_MAPPING[authlevel]
|
||||
except KeyError:
|
||||
log.error("Your auth file is malformed: %r is not a valid auth "
|
||||
"level" % authlevel)
|
||||
continue
|
||||
|
||||
self.__auth[username] = Account(username, password, authlevel)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
create_localclient_account(True)
|
||||
return self.__load_auth_file()
|
||||
|
||||
|
||||
if save_and_reload:
|
||||
log.info("Re-writing auth file (upgrade)")
|
||||
self.write_auth_file()
|
||||
self.__auth_modification_time = auth_file_modification_time
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
#
|
||||
# autoadd.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.log import LOG as log
|
||||
|
||||
MAX_NUM_ATTEMPTS = 10
|
||||
|
||||
class AutoAdd(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AutoAdd", depend=["TorrentManager"], interval=5)
|
||||
# Get the core config
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
# A list of filenames
|
||||
self.invalid_torrents = []
|
||||
# Filename:Attempts
|
||||
self.attempts = {}
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("autoadd_enable",
|
||||
self._on_autoadd_enable, apply_now=True)
|
||||
self.config.register_set_function("autoadd_location",
|
||||
self._on_autoadd_location)
|
||||
|
||||
def update(self):
|
||||
if not self.config["autoadd_enable"]:
|
||||
# We shouldn't be updating because autoadd is not enabled
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
# Check the auto add folder for new torrents to add
|
||||
if not os.path.isdir(self.config["autoadd_location"]):
|
||||
log.warning("Invalid AutoAdd folder: %s", self.config["autoadd_location"])
|
||||
component.pause("AutoAdd")
|
||||
return
|
||||
|
||||
for filename in os.listdir(self.config["autoadd_location"]):
|
||||
if filename.split(".")[-1] == "torrent":
|
||||
try:
|
||||
filepath = os.path.join(self.config["autoadd_location"], filename)
|
||||
except UnicodeDecodeError, e:
|
||||
log.error("Unable to auto add torrent due to inproper filename encoding: %s", e)
|
||||
continue
|
||||
try:
|
||||
filedump = self.load_torrent(filepath)
|
||||
except (RuntimeError, Exception), e:
|
||||
# If the torrent is invalid, we keep track of it so that we
|
||||
# can try again on the next pass. This is because some
|
||||
# torrents may not be fully saved during the pass.
|
||||
log.debug("Torrent is invalid: %s", e)
|
||||
if filename in self.invalid_torrents:
|
||||
self.attempts[filename] += 1
|
||||
if self.attempts[filename] >= MAX_NUM_ATTEMPTS:
|
||||
os.rename(filepath, filepath + ".invalid")
|
||||
del self.attempts[filename]
|
||||
self.invalid_torrents.remove(filename)
|
||||
else:
|
||||
self.invalid_torrents.append(filename)
|
||||
self.attempts[filename] = 1
|
||||
continue
|
||||
|
||||
# The torrent looks good, so lets add it to the session
|
||||
component.get("TorrentManager").add(filedump=filedump, filename=filename)
|
||||
|
||||
os.remove(filepath)
|
||||
|
||||
def load_torrent(self, filename):
|
||||
try:
|
||||
log.debug("Attempting to open %s for add.", filename)
|
||||
_file = open(filename, "rb")
|
||||
filedump = _file.read()
|
||||
if not filedump:
|
||||
raise RuntimeError, "Torrent is 0 bytes!"
|
||||
_file.close()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open %s: %s", filename, e)
|
||||
raise e
|
||||
|
||||
# Get the info to see if any exceptions are raised
|
||||
info = lt.torrent_info(lt.bdecode(filedump))
|
||||
|
||||
return filedump
|
||||
|
||||
def _on_autoadd_enable(self, key, value):
|
||||
log.debug("_on_autoadd_enable")
|
||||
if value:
|
||||
component.resume("AutoAdd")
|
||||
else:
|
||||
component.pause("AutoAdd")
|
||||
|
||||
def _on_autoadd_location(self, key, value):
|
||||
log.debug("_on_autoadd_location")
|
||||
# We need to resume the component just incase it was paused due to
|
||||
# an invalid autoadd location.
|
||||
if self.config["autoadd_enable"]:
|
||||
component.resume("AutoAdd")
|
@ -2,6 +2,7 @@
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -38,36 +39,34 @@ from deluge._libtorrent import lt
|
||||
import os
|
||||
import glob
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
import warnings
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
|
||||
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.task import LoopingCall
|
||||
import twisted.web.client
|
||||
import twisted.web.error
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
from deluge.log import LOG as log
|
||||
|
||||
|
||||
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.event import *
|
||||
from deluge.error import *
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE
|
||||
from deluge.core.authmanager import AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE
|
||||
from deluge.core.torrentmanager import TorrentManager
|
||||
from deluge.core.pluginmanager import PluginManager
|
||||
from deluge.core.alertmanager import AlertManager
|
||||
from deluge.core.filtermanager import FilterManager
|
||||
from deluge.core.preferencesmanager import PreferencesManager
|
||||
from deluge.core.autoadd import AutoAdd
|
||||
from deluge.core.authmanager import AuthManager
|
||||
from deluge.core.eventmanager import EventManager
|
||||
from deluge.core.rpcserver import export
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Core(component.Component):
|
||||
def __init__(self, listen_interface=None):
|
||||
log.debug("Core init..")
|
||||
@ -77,7 +76,8 @@ class Core(component.Component):
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
version = [int(value.split("-")[0]) for value in
|
||||
deluge.common.get_version().split(".")]
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
@ -88,10 +88,17 @@ class Core(component.Component):
|
||||
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \
|
||||
{ 'deluge_version': deluge.common.get_version(),
|
||||
'lt_version': self.get_libtorrent_version().rpartition(".")[0] }
|
||||
|
||||
# Set session settings
|
||||
self.settings.send_redundant_have = True
|
||||
if deluge.common.windows_check():
|
||||
self.settings.disk_io_write_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.settings.disk_io_read_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
# Load metadata extension
|
||||
@ -106,7 +113,6 @@ class Core(component.Component):
|
||||
self.pluginmanager = PluginManager(self)
|
||||
self.torrentmanager = TorrentManager()
|
||||
self.filtermanager = FilterManager(self)
|
||||
self.autoadd = AutoAdd()
|
||||
self.authmanager = AuthManager()
|
||||
|
||||
# New release check information
|
||||
@ -114,6 +120,8 @@ class Core(component.Component):
|
||||
|
||||
# Get the core config
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf")
|
||||
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
|
||||
self.config.save()
|
||||
|
||||
# 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
|
||||
@ -147,19 +155,25 @@ class Core(component.Component):
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
|
||||
lt.bencode(self.session.state()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
open(session_state, "wb").write(lt.bencode(self.session.state()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to save lt state: %s", e)
|
||||
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
session_state = deluge.configmanager.get_config_dir("session.state")
|
||||
self.session.load_state(lt.bdecode(open(session_state, "rb").read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
|
||||
def __migrate_config_1_to_2(self, config):
|
||||
if 'sequential_download' not in config:
|
||||
config['sequential_download'] = False
|
||||
return config
|
||||
|
||||
def save_dht_state(self):
|
||||
"""Saves the dht state to a file"""
|
||||
try:
|
||||
@ -212,7 +226,9 @@ class Core(component.Component):
|
||||
log.exception(e)
|
||||
|
||||
try:
|
||||
torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename)
|
||||
torrent_id = self.torrentmanager.add(
|
||||
filedump=filedump, options=options, filename=filename
|
||||
)
|
||||
except Exception, e:
|
||||
log.error("There was an error adding the torrent file %s", filename)
|
||||
log.exception(e)
|
||||
@ -236,20 +252,44 @@ 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)
|
||||
def on_get_file(filename):
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
data = open(filename, "rb").read()
|
||||
return self.add_torrent_file(filename, base64.encodestring(data), options)
|
||||
f = open(filename, "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception, e:
|
||||
log.warning("Couldn't remove temp file: %s", e)
|
||||
return self.add_torrent_file(
|
||||
filename, base64.encodestring(data), options
|
||||
)
|
||||
|
||||
def on_get_file_error(failure):
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
return failure
|
||||
def on_download_fail(failure):
|
||||
if failure.check(twisted.web.error.PageRedirect):
|
||||
new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1])
|
||||
result = download_file(
|
||||
new_url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
elif failure.check(twisted.web.client.PartialDownloadError):
|
||||
result = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers,
|
||||
force_filename=True, allow_compression=False
|
||||
)
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
result = failure
|
||||
return result
|
||||
|
||||
d = download_file(url, url.split("/")[-1], headers=headers)
|
||||
d.addCallback(on_get_file)
|
||||
d.addErrback(on_get_file_error)
|
||||
d = download_file(
|
||||
url, tempfile.mkstemp()[1], headers=headers, force_filename=True
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
||||
@export
|
||||
@ -387,7 +427,11 @@ class Core(component.Component):
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
try:
|
||||
status = self.torrentmanager[torrent_id].get_status(keys, diff)
|
||||
except KeyError:
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
@ -454,7 +498,7 @@ class Core(component.Component):
|
||||
"""Set the config with values from dictionary"""
|
||||
# Load all the values into the configuration
|
||||
for key in config.keys():
|
||||
if isinstance(config[key], unicode) or isinstance(config[key], str):
|
||||
if isinstance(config[key], basestring):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
|
||||
@ -535,6 +579,11 @@ class Core(component.Component):
|
||||
"""Sets a higher priority to the first and last pieces"""
|
||||
return self.torrentmanager[torrent_id].set_prioritize_first_last(value)
|
||||
|
||||
@export
|
||||
def set_torrent_sequential_download(self, torrent_id, value):
|
||||
"""Toggle sequencial pieces download"""
|
||||
return self.torrentmanager[torrent_id].set_sequential_download(value)
|
||||
|
||||
@export
|
||||
def set_torrent_auto_managed(self, torrent_id, value):
|
||||
"""Sets the auto managed flag for queueing purposes"""
|
||||
@ -565,6 +614,32 @@ class Core(component.Component):
|
||||
"""Sets the path for the torrent to be moved when completed"""
|
||||
return self.torrentmanager[torrent_id].set_move_completed_path(value)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def set_torrents_owner(self, torrent_ids, username):
|
||||
"""Set's the torrent owner.
|
||||
|
||||
:param torrent_id: the torrent_id of the torrent to remove
|
||||
:type torrent_id: string
|
||||
:param username: the new owner username
|
||||
:type username: string
|
||||
|
||||
:raises DelugeError: if the username is not known
|
||||
"""
|
||||
if not self.authmanager.has_account(username):
|
||||
raise DelugeError("Username \"%s\" is not known." % username)
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_owner(username)
|
||||
return None
|
||||
|
||||
@export
|
||||
def set_torrents_shared(self, torrent_ids, shared):
|
||||
if isinstance(torrent_ids, basestring):
|
||||
torrent_ids = [torrent_ids]
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_options({"shared": shared})
|
||||
|
||||
@export
|
||||
def get_path_size(self, path):
|
||||
"""Returns the size of the file or folder 'path' and -1 if the path is
|
||||
@ -747,7 +822,11 @@ class Core(component.Component):
|
||||
def on_get_page(result):
|
||||
return bool(int(result))
|
||||
|
||||
def logError(failure):
|
||||
log.warning("Error testing listen port: %s", failure)
|
||||
|
||||
d.addCallback(on_get_page)
|
||||
d.addErrback(logError)
|
||||
|
||||
return d
|
||||
|
||||
@ -768,7 +847,10 @@ class Core(component.Component):
|
||||
"""
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
return deluge.common.free_space(path)
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
@ -780,3 +862,23 @@ class Core(component.Component):
|
||||
|
||||
"""
|
||||
return lt.version
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def get_known_accounts(self):
|
||||
return self.authmanager.get_known_accounts()
|
||||
|
||||
@export(AUTH_LEVEL_NONE)
|
||||
def get_auth_levels_mappings(self):
|
||||
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def create_account(self, username, password, authlevel):
|
||||
return self.authmanager.create_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def update_account(self, username, password, authlevel):
|
||||
return self.authmanager.update_account(username, password, authlevel)
|
||||
|
||||
@export(AUTH_LEVEL_ADMIN)
|
||||
def remove_account(self, username):
|
||||
return self.authmanager.remove_account(username)
|
||||
|
@ -33,9 +33,7 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import gettext
|
||||
import locale
|
||||
import pkg_resources
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
import twisted.internet.error
|
||||
|
||||
@ -43,16 +41,19 @@ import deluge.component as component
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
from deluge.core.rpcserver import RPCServer, export
|
||||
from deluge.log import LOG as log
|
||||
import deluge.error
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Daemon(object):
|
||||
def __init__(self, options=None, args=None, classic=False):
|
||||
# Check for another running instance of the daemon
|
||||
if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
# Get the PID and the port of the supposedly running daemon
|
||||
try:
|
||||
(pid, port) = open(deluge.configmanager.get_config_dir("deluged.pid")).read().strip().split(";")
|
||||
(pid, port) = open(
|
||||
deluge.configmanager.get_config_dir("deluged.pid")
|
||||
).read().strip().split(";")
|
||||
pid = int(pid)
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
@ -91,22 +92,10 @@ class Daemon(object):
|
||||
else:
|
||||
# This is a deluged!
|
||||
s.close()
|
||||
raise deluge.error.DaemonRunningError("There is a deluge daemon running with this config directory!")
|
||||
|
||||
# Initialize gettext
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
if hasattr(locale, "bindtextdomain"):
|
||||
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
if hasattr(locale, "textdomain"):
|
||||
locale.textdomain("deluge")
|
||||
gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
gettext.textdomain("deluge")
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
log.error("Unable to initialize gettext/locale: %s", e)
|
||||
import __builtin__
|
||||
__builtin__.__dict__["_"] = lambda x: x
|
||||
raise deluge.error.DaemonRunningError(
|
||||
"There is a deluge daemon running with this config "
|
||||
"directory!"
|
||||
)
|
||||
|
||||
# Twisted catches signals to terminate, so just have it call the shutdown
|
||||
# method.
|
||||
@ -120,7 +109,7 @@ class Daemon(object):
|
||||
def win_handler(ctrl_type):
|
||||
log.debug("ctrl_type: %s", ctrl_type)
|
||||
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
|
||||
self.__shutdown()
|
||||
self._shutdown()
|
||||
return 1
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
@ -189,18 +178,24 @@ class Daemon(object):
|
||||
except twisted.internet.error.ReactorNotRunning:
|
||||
log.debug("Tried to stop the reactor but it is not running..")
|
||||
|
||||
@export()
|
||||
def info(self):
|
||||
"""
|
||||
Returns some info from the daemon.
|
||||
|
||||
:returns: str, the version number
|
||||
"""
|
||||
return deluge.common.get_version()
|
||||
|
||||
@export()
|
||||
def get_method_list(self):
|
||||
"""
|
||||
Returns a list of the exported methods.
|
||||
"""
|
||||
return self.rpcserver.get_method_list()
|
||||
|
||||
@export(1)
|
||||
def authorized_call(self, rpc):
|
||||
"""
|
||||
Returns True if authorized to call rpc.
|
||||
|
||||
:param rpc: a rpc, eg, "core.get_torrents_status"
|
||||
:type rpc: string
|
||||
|
||||
"""
|
||||
if not rpc in self.get_method_list():
|
||||
return False
|
||||
|
||||
auth_level = self.rpcserver.get_session_auth_level()
|
||||
return auth_level >= self.rpcserver.get_rpc_auth_level()
|
||||
|
@ -33,8 +33,10 @@
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventManager(component.Component):
|
||||
def __init__(self):
|
||||
@ -53,7 +55,10 @@ class EventManager(component.Component):
|
||||
if event.name in self.handlers:
|
||||
for handler in self.handlers[event.name]:
|
||||
#log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args)
|
||||
handler(*event.args)
|
||||
try:
|
||||
handler(*event.args)
|
||||
except:
|
||||
log.error("Event handler %s failed in %s", event.name, handler)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
|
@ -33,12 +33,13 @@
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
STATE_SORT = ["All", "Downloading", "Seeding", "Active", "Paused", "Queued"]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#special purpose filters:
|
||||
def filter_keywords(torrent_ids, values):
|
||||
#cleanup.
|
||||
@ -77,6 +78,27 @@ def filter_one_keyword(torrent_ids, keyword):
|
||||
yield torrent_id
|
||||
break
|
||||
|
||||
def filter_by_name(torrent_ids, search_string):
|
||||
all_torrents = component.get("TorrentManager").torrents
|
||||
try:
|
||||
search_string, match_case = search_string[0].split('::match')
|
||||
except ValueError:
|
||||
search_string = search_string[0]
|
||||
match_case = False
|
||||
|
||||
if match_case is False:
|
||||
search_string = search_string.lower()
|
||||
|
||||
for torrent_id in torrent_ids:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
if match_case is False:
|
||||
torrent_name = all_torrents[torrent_id].get_name().lower()
|
||||
else:
|
||||
torrent_name = all_torrents[torrent_id].get_name()
|
||||
|
||||
if search_string in torrent_name:
|
||||
yield torrent_id
|
||||
|
||||
def tracker_error_filter(torrent_ids, values):
|
||||
filtered_torrent_ids = []
|
||||
tm = component.get("TorrentManager")
|
||||
@ -91,7 +113,7 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
|
||||
# that have this substring in their tracker_status
|
||||
for torrent_id in torrent_ids:
|
||||
if "Error:" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
@ -107,6 +129,7 @@ class FilterManager(component.Component):
|
||||
self.torrents = core.torrentmanager
|
||||
self.registered_filters = {}
|
||||
self.register_filter("keyword", filter_keywords)
|
||||
self.register_filter("name", filter_by_name)
|
||||
self.tree_fields = {}
|
||||
|
||||
self.register_tree_field("state", self._init_state_tree)
|
||||
@ -116,6 +139,10 @@ class FilterManager(component.Component):
|
||||
|
||||
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):
|
||||
"""
|
||||
returns a list of torrent_id's matching filter_dict.
|
||||
@ -131,7 +158,7 @@ class FilterManager(component.Component):
|
||||
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = filter_dict["id"]
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
@ -196,9 +223,8 @@ class FilterManager(component.Component):
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
|
||||
if "tracker_host" in items:
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
|
||||
|
||||
if "state" in tree_keys and not show_zero_hits:
|
||||
|
@ -39,12 +39,14 @@ import os.path
|
||||
import pickle
|
||||
import cPickle
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
import deluge.core.torrentmanager
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#start : http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
|
||||
def makeFakeClass(module, name):
|
||||
|
@ -36,13 +36,13 @@
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
import logging
|
||||
|
||||
from deluge.event import PluginEnabledEvent, PluginDisabledEvent
|
||||
import deluge.pluginmanagerbase
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
component.Component):
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# preferencesmanager.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -34,10 +34,9 @@
|
||||
#
|
||||
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
import pkg_resources
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
@ -46,7 +45,8 @@ from deluge.event import *
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
@ -62,6 +62,7 @@ DEFAULT_PREFS = {
|
||||
"torrentfiles_location": deluge.common.get_default_download_dir(),
|
||||
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
|
||||
"prioritize_first_last_pieces": False,
|
||||
"sequential_download": False,
|
||||
"random_port": True,
|
||||
"dht": True,
|
||||
"upnp": True,
|
||||
@ -85,8 +86,6 @@ DEFAULT_PREFS = {
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"enabled_plugins": [],
|
||||
"autoadd_location": deluge.common.get_default_download_dir(),
|
||||
"autoadd_enable": False,
|
||||
"add_paused": False,
|
||||
"max_active_seeding": 5,
|
||||
"max_active_downloading": 3,
|
||||
@ -140,7 +139,9 @@ DEFAULT_PREFS = {
|
||||
"rate_limit_ip_overhead": True,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"cache_size": 512,
|
||||
"cache_expiry": 60
|
||||
"cache_expiry": 60,
|
||||
"auto_manage_prefer_seeds": False,
|
||||
"shared": False
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
@ -148,92 +149,40 @@ class PreferencesManager(component.Component):
|
||||
component.Component.__init__(self, "PreferencesManager")
|
||||
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf", DEFAULT_PREFS)
|
||||
if 'public' in self.config:
|
||||
log.debug("Updating configuration file: Renamed torrent's public "
|
||||
"attribute to shared.")
|
||||
self.config["shared"] = self.config["public"]
|
||||
del self.config["public"]
|
||||
|
||||
def start(self):
|
||||
self.core = component.get("Core")
|
||||
self.session = component.get("Core").session
|
||||
self.settings = component.get("Core").settings
|
||||
|
||||
# Register set functions in the Config
|
||||
self.config.register_set_function("torrentfiles_location",
|
||||
self._on_set_torrentfiles_location)
|
||||
self.config.register_set_function("listen_ports",
|
||||
self._on_set_listen_ports)
|
||||
self.config.register_set_function("listen_interface",
|
||||
self._on_set_listen_interface)
|
||||
self.config.register_set_function("random_port",
|
||||
self._on_set_random_port)
|
||||
self.config.register_set_function("outgoing_ports",
|
||||
self._on_set_outgoing_ports)
|
||||
self.config.register_set_function("random_outgoing_ports",
|
||||
self._on_set_random_outgoing_ports)
|
||||
self.config.register_set_function("peer_tos",
|
||||
self._on_set_peer_tos)
|
||||
self.config.register_set_function("dht", self._on_set_dht)
|
||||
self.config.register_set_function("upnp", self._on_set_upnp)
|
||||
self.config.register_set_function("natpmp", self._on_set_natpmp)
|
||||
self.config.register_set_function("utpex", self._on_set_utpex)
|
||||
self.config.register_set_function("lsd", self._on_set_lsd)
|
||||
self.config.register_set_function("enc_in_policy",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_out_policy",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_level",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("enc_prefer_rc4",
|
||||
self._on_set_encryption)
|
||||
self.config.register_set_function("max_connections_global",
|
||||
self._on_set_max_connections_global)
|
||||
self.config.register_set_function("max_upload_speed",
|
||||
self._on_set_max_upload_speed)
|
||||
self.config.register_set_function("max_download_speed",
|
||||
self._on_set_max_download_speed)
|
||||
self.config.register_set_function("max_upload_slots_global",
|
||||
self._on_set_max_upload_slots_global)
|
||||
self.config.register_set_function("max_half_open_connections",
|
||||
self._on_set_max_half_open_connections)
|
||||
self.config.register_set_function("max_connections_per_second",
|
||||
self._on_set_max_connections_per_second)
|
||||
self.config.register_set_function("ignore_limits_on_local_network",
|
||||
self._on_ignore_limits_on_local_network)
|
||||
self.config.register_set_function("share_ratio_limit",
|
||||
self._on_set_share_ratio_limit)
|
||||
self.config.register_set_function("seed_time_ratio_limit",
|
||||
self._on_set_seed_time_ratio_limit)
|
||||
self.config.register_set_function("seed_time_limit",
|
||||
self._on_set_seed_time_limit)
|
||||
self.config.register_set_function("max_active_downloading",
|
||||
self._on_set_max_active_downloading)
|
||||
self.config.register_set_function("max_active_seeding",
|
||||
self._on_set_max_active_seeding)
|
||||
self.config.register_set_function("max_active_limit",
|
||||
self._on_set_max_active_limit)
|
||||
self.config.register_set_function("dont_count_slow_torrents",
|
||||
self._on_set_dont_count_slow_torrents)
|
||||
self.config.register_set_function("send_info",
|
||||
self._on_send_info)
|
||||
self.config.register_set_function("proxies",
|
||||
self._on_set_proxies)
|
||||
self.new_release_timer = None
|
||||
self.config.register_set_function("new_release_check",
|
||||
self._on_new_release_check)
|
||||
self.config.register_set_function("rate_limit_ip_overhead",
|
||||
self._on_rate_limit_ip_overhead)
|
||||
self.config.register_set_function("geoip_db_location",
|
||||
self._on_geoip_db_location)
|
||||
self.config.register_set_function("cache_size",
|
||||
self._on_cache_size)
|
||||
self.config.register_set_function("cache_expiry",
|
||||
self._on_cache_expiry)
|
||||
|
||||
# Set the initial preferences on start-up
|
||||
for key in DEFAULT_PREFS:
|
||||
self.do_config_set_func(key, self.config[key])
|
||||
|
||||
self.config.register_change_callback(self._on_config_value_change)
|
||||
|
||||
def stop(self):
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
# Config set functions
|
||||
def do_config_set_func(self, key, value):
|
||||
on_set_func = getattr(self, "_on_set_" + key, None)
|
||||
if on_set_func:
|
||||
on_set_func(key, value)
|
||||
|
||||
def session_set_setting(self, key, value):
|
||||
settings = self.session.settings()
|
||||
setattr(settings, key, value)
|
||||
self.session.set_settings(settings)
|
||||
|
||||
def _on_config_value_change(self, key, value):
|
||||
self.do_config_set_func(key, value)
|
||||
component.get("EventManager").emit(ConfigValueChangedEvent(key, value))
|
||||
|
||||
def _on_set_torrentfiles_location(self, key, value):
|
||||
@ -247,7 +196,9 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(
|
||||
value[0], value[1], str(self.config["listen_interface"])
|
||||
)
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@ -269,13 +220,15 @@ class PreferencesManager(component.Component):
|
||||
# Set the listen ports
|
||||
log.debug("listen port range set to %s-%s", listen_ports[0],
|
||||
listen_ports[1])
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(
|
||||
listen_ports[0], listen_ports[1],
|
||||
str(self.config["listen_interface"])
|
||||
)
|
||||
|
||||
def _on_set_outgoing_ports(self, key, value):
|
||||
if not self.config["random_outgoing_ports"]:
|
||||
log.debug("outgoing port range set to %s-%s", value[0], value[1])
|
||||
self.settings.outgoing_ports = value[0], value[1]
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("outgoing_ports", (value[0], value[1]))
|
||||
|
||||
def _on_set_random_outgoing_ports(self, key, value):
|
||||
if value:
|
||||
@ -284,13 +237,11 @@ class PreferencesManager(component.Component):
|
||||
def _on_set_peer_tos(self, key, value):
|
||||
log.debug("setting peer_tos to: %s", value)
|
||||
try:
|
||||
self.settings.peer_tos = chr(int(value, 16))
|
||||
self.session_set_setting("peer_tos", chr(int(value, 16)))
|
||||
except ValueError, e:
|
||||
log.debug("Invalid tos byte: %s", e)
|
||||
return
|
||||
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
def _on_set_dht(self, key, value):
|
||||
log.debug("dht value set to %s", value)
|
||||
state_file = deluge.configmanager.get_config_dir("dht.state")
|
||||
@ -339,6 +290,18 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
|
||||
def _on_set_enc_in_policy(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_out_policy(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_level(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_enc_prefer_rc4(self, key, value):
|
||||
self._on_set_encryption(key, value)
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
log.debug("encryption value %s set to %s..", key, value)
|
||||
pe_settings = lt.pe_settings()
|
||||
@ -387,53 +350,41 @@ class PreferencesManager(component.Component):
|
||||
self.session.set_max_half_open_connections(value)
|
||||
|
||||
def _on_set_max_connections_per_second(self, key, value):
|
||||
self.settings.connection_speed = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("connection_speed", value)
|
||||
|
||||
def _on_ignore_limits_on_local_network(self, key, value):
|
||||
self.settings.ignore_limits_on_local_network = value
|
||||
self.session.set_settings(self.settings)
|
||||
def _on_set_ignore_limits_on_local_network(self, key, value):
|
||||
self.session_set_setting("ignore_limits_on_local_network", value)
|
||||
|
||||
def _on_set_share_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.share_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("share_ratio_limit", value)
|
||||
|
||||
def _on_set_seed_time_ratio_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.seed_time_ratio_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("seed_time_ratio_limit", value)
|
||||
|
||||
def _on_set_seed_time_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
# This value is stored in minutes in deluge, but libtorrent wants seconds
|
||||
self.settings.seed_time_limit = int(value * 60)
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("seed_time_limit", int(value * 60))
|
||||
|
||||
def _on_set_max_active_downloading(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_downloads: %s", self.settings.active_downloads)
|
||||
self.settings.active_downloads = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_downloads", value)
|
||||
|
||||
def _on_set_max_active_seeding(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_seeds: %s", self.settings.active_seeds)
|
||||
self.settings.active_seeds = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_seeds", value)
|
||||
|
||||
def _on_set_max_active_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
log.debug("active_limit: %s", self.settings.active_limit)
|
||||
self.settings.active_limit = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("active_limit", value)
|
||||
|
||||
def _on_set_dont_count_slow_torrents(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.settings.dont_count_slow_torrents = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("dont_count_slow_torrents", value)
|
||||
|
||||
def _on_send_info(self, key, value):
|
||||
def _on_set_send_info(self, key, value):
|
||||
log.debug("Sending anonymous stats..")
|
||||
"""sends anonymous stats home"""
|
||||
class Send_Info_Thread(threading.Thread):
|
||||
@ -463,18 +414,18 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
Send_Info_Thread(self.config).start()
|
||||
|
||||
def _on_new_release_check(self, key, value):
|
||||
def _on_set_new_release_check(self, key, value):
|
||||
if value:
|
||||
log.debug("Checking for new release..")
|
||||
threading.Thread(target=self.core.get_new_release).start()
|
||||
if self.new_release_timer:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
# Set a timer to check for a new release every 3 days
|
||||
self.new_release_timer = LoopingCall(
|
||||
self._on_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:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
@ -489,19 +440,20 @@ class PreferencesManager(component.Component):
|
||||
log.debug("setting %s proxy settings", k)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_rate_limit_ip_overhead(self, key, value):
|
||||
def _on_set_rate_limit_ip_overhead(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.rate_limit_ip_overhead = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("rate_limit_ip_overhead", value)
|
||||
|
||||
def _on_geoip_db_location(self, key, value):
|
||||
def _on_set_geoip_db_location(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
# Load the GeoIP DB for country look-ups if available
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))
|
||||
elif os.path.exists(deluge.common.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = deluge.common.resource_filename(
|
||||
"deluge", os.path.join("data", "GeoIP.dat")
|
||||
)
|
||||
else:
|
||||
log.warning("Unable to find GeoIP database file!")
|
||||
|
||||
@ -512,12 +464,14 @@ class PreferencesManager(component.Component):
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_cache_size(self, key, value):
|
||||
def _on_set_cache_size(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.cache_size = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("cache_size", value)
|
||||
|
||||
def _on_cache_expiry(self, key, value):
|
||||
def _on_set_cache_expiry(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.settings.cache_expiry = value
|
||||
self.session.set_settings(self.settings)
|
||||
self.session_set_setting("cache_expiry", value)
|
||||
|
||||
def _on_auto_manage_prefer_seeds(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("auto_manage_prefer_seeds", value)
|
||||
|
@ -39,25 +39,33 @@ import sys
|
||||
import zlib
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import ssl, reactor, defer
|
||||
from twisted.internet import reactor, defer
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
|
||||
import deluge.rencode as rencode
|
||||
from deluge.log import LOG as log
|
||||
try:
|
||||
import rencode
|
||||
except ImportError:
|
||||
import deluge.rencode as rencode
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT
|
||||
from deluge.core.authmanager import (AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT,
|
||||
AUTH_LEVEL_ADMIN)
|
||||
from deluge.error import (DelugeError, NotAuthorizedError, WrappedException,
|
||||
_ClientSideRecreateError, IncompatibleClient)
|
||||
|
||||
RPC_RESPONSE = 1
|
||||
RPC_ERROR = 2
|
||||
RPC_EVENT = 3
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
"""
|
||||
Decorator function to register an object's method as an RPC. The object
|
||||
@ -90,13 +98,13 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
def format_request(call):
|
||||
"""
|
||||
Format the RPCRequest message for debug printing
|
||||
|
||||
|
||||
:param call: the request
|
||||
:type call: a RPCRequest
|
||||
|
||||
|
||||
:returns: a formatted string for printing
|
||||
:rtype: str
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
s = call[1] + "("
|
||||
@ -111,12 +119,6 @@ def format_request(call):
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
|
||||
class NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
@ -139,7 +141,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
"""
|
||||
This method is called whenever data is received from a client. The
|
||||
only message that a client sends to the server is a RPC Request message.
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
If the RPC Request message is valid, then the method is called in
|
||||
:meth:`dispatch`.
|
||||
|
||||
:param data: the data from the client. It should be a zlib compressed
|
||||
@ -175,7 +177,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
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)
|
||||
@ -187,7 +190,7 @@ class DelugeRPCProtocol(Protocol):
|
||||
:param data: the object that is to be sent to the client. This should
|
||||
be one of the RPC message types.
|
||||
:type data: object
|
||||
|
||||
|
||||
"""
|
||||
self.transport.write(zlib.compress(rencode.dumps(data)))
|
||||
|
||||
@ -196,7 +199,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
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] = AUTH_LEVEL_NONE
|
||||
|
||||
@ -218,6 +222,9 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
log.info("Deluge client disconnected: %s", reason.value)
|
||||
|
||||
def valid_session(self):
|
||||
return self.transport.sessionno in self.factory.authorized_sessions
|
||||
|
||||
def dispatch(self, request_id, method, args, kwargs):
|
||||
"""
|
||||
This method is run when a RPC Request is made. It will run the local method
|
||||
@ -239,33 +246,55 @@ class DelugeRPCProtocol(Protocol):
|
||||
Sends an error response with the contents of the exception that was raised.
|
||||
"""
|
||||
exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
|
||||
formated_tb = "".join(traceback.format_tb(exceptionTraceback))
|
||||
try:
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
exceptionType.__name__,
|
||||
exceptionValue._args,
|
||||
exceptionValue._kwargs,
|
||||
formated_tb
|
||||
))
|
||||
except Exception, err:
|
||||
# This most likely not a deluge exception, let's wrap it
|
||||
log.error("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.exception(err)
|
||||
try:
|
||||
raise WrappedException(str(exceptionValue), exceptionType.__name__, formated_tb)
|
||||
except:
|
||||
sendError()
|
||||
|
||||
self.sendData((
|
||||
RPC_ERROR,
|
||||
request_id,
|
||||
(exceptionType.__name__,
|
||||
exceptionValue.args[0] if len(exceptionValue.args) == 1 else "",
|
||||
"".join(traceback.format_tb(exceptionTraceback)))
|
||||
))
|
||||
|
||||
if method == "daemon.login":
|
||||
if method == "daemon.info":
|
||||
# This is a special case and used in the initial connection process
|
||||
self.sendData((RPC_RESPONSE, request_id, deluge.common.get_version()))
|
||||
return
|
||||
elif method == "daemon.login":
|
||||
# This is a special case and used in the initial connection process
|
||||
# We need to authenticate the user here
|
||||
log.debug("RPC dispatch daemon.login")
|
||||
try:
|
||||
client_version = kwargs.pop('client_version', None)
|
||||
if client_version is None:
|
||||
raise IncompatibleClient(deluge.common.get_version())
|
||||
ret = component.get("AuthManager").authorize(*args, **kwargs)
|
||||
if ret:
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = ret
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception, e:
|
||||
sendError()
|
||||
log.exception(e)
|
||||
if not isinstance(e, _ClientSideRecreateError):
|
||||
log.exception(e)
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, (ret)))
|
||||
if not ret:
|
||||
self.transport.loseConnection()
|
||||
finally:
|
||||
return
|
||||
elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
elif method == "daemon.set_event_interest" and self.valid_session():
|
||||
log.debug("RPC dispatch daemon.set_event_interest")
|
||||
# This special case is to allow clients to set which events they are
|
||||
# interested in receiving.
|
||||
# We are expecting a sequence from the client.
|
||||
@ -280,21 +309,24 @@ class DelugeRPCProtocol(Protocol):
|
||||
finally:
|
||||
return
|
||||
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
if method in self.factory.methods and self.valid_session():
|
||||
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 = self.factory.authorized_sessions[self.transport.sessionno][0]
|
||||
if auth_level < method_auth_requirement:
|
||||
# This session is not allowed to call this method
|
||||
log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement))
|
||||
log.debug("Session %s is trying to call a method it is not "
|
||||
"authorized to 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.
|
||||
self.factory.session_id = self.transport.sessionno
|
||||
ret = self.factory.methods[method](*args, **kwargs)
|
||||
except Exception, e:
|
||||
sendError()
|
||||
# Don't bother printing out DelugeErrors, because they are just for the client
|
||||
# Don't bother printing out DelugeErrors, because they are just
|
||||
# for the client
|
||||
if not isinstance(e, DelugeError):
|
||||
log.exception("Exception calling RPC request: %s", e)
|
||||
else:
|
||||
@ -338,7 +370,7 @@ class RPCServer(component.Component):
|
||||
self.factory = Factory()
|
||||
self.factory.protocol = DelugeRPCProtocol
|
||||
self.factory.session_id = -1
|
||||
|
||||
|
||||
# Holds the registered methods
|
||||
self.factory.methods = {}
|
||||
# Holds the session_ids and auth levels
|
||||
@ -348,6 +380,7 @@ class RPCServer(component.Component):
|
||||
# Holds the interested event list for the sessions
|
||||
self.factory.interested_events = {}
|
||||
|
||||
self.listen = listen
|
||||
if not listen:
|
||||
return
|
||||
|
||||
@ -391,6 +424,17 @@ class RPCServer(component.Component):
|
||||
log.debug("Registering method: %s", name + "." + d)
|
||||
self.factory.methods[name + "." + d] = getattr(obj, d)
|
||||
|
||||
def deregister_object(self, obj):
|
||||
"""
|
||||
Deregisters an objects exported rpc methods.
|
||||
|
||||
:param obj: the object that was previously registered
|
||||
|
||||
"""
|
||||
for key, value in self.factory.methods.items():
|
||||
if value.im_self == obj:
|
||||
del self.factory.methods[key]
|
||||
|
||||
def get_object_method(self, name):
|
||||
"""
|
||||
Returns a registered method.
|
||||
@ -417,26 +461,63 @@ class RPCServer(component.Component):
|
||||
def get_session_id(self):
|
||||
"""
|
||||
Returns the session id of the current RPC.
|
||||
|
||||
|
||||
:returns: the session id, this will be -1 if no connections have been made
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
return self.factory.session_id
|
||||
|
||||
|
||||
def get_session_user(self):
|
||||
"""
|
||||
Returns the username calling the current RPC.
|
||||
|
||||
:returns: the username of the user calling the current RPC
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if not self.listen:
|
||||
return "localclient"
|
||||
session_id = self.get_session_id()
|
||||
if session_id > -1 and session_id in self.factory.authorized_sessions:
|
||||
return self.factory.authorized_sessions[session_id][1]
|
||||
else:
|
||||
# No connections made yet
|
||||
return ""
|
||||
|
||||
def get_session_auth_level(self):
|
||||
"""
|
||||
Returns the auth level of the user calling the current RPC.
|
||||
|
||||
:returns: the auth level
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.listen:
|
||||
return AUTH_LEVEL_ADMIN
|
||||
return self.factory.authorized_sessions[self.get_session_id()][0]
|
||||
|
||||
def get_rpc_auth_level(self, rpc):
|
||||
"""
|
||||
Returns the auth level requirement for an exported rpc.
|
||||
|
||||
:returns: the auth level
|
||||
:rtype: int
|
||||
"""
|
||||
self.factory.methods[rpc]._rpcserver_auth_level
|
||||
|
||||
def is_session_valid(self, session_id):
|
||||
"""
|
||||
Checks if the session is still valid, eg, if the client is still connected.
|
||||
|
||||
|
||||
:param session_id: the session id
|
||||
:type session_id: int
|
||||
|
||||
|
||||
:returns: True if the session is valid
|
||||
:rtype: bool
|
||||
|
||||
|
||||
"""
|
||||
return session_id in self.factory.authorized_sessions
|
||||
|
||||
|
||||
def emit_event(self, event):
|
||||
"""
|
||||
Emits the event to interested clients.
|
||||
@ -454,6 +535,29 @@ class RPCServer(component.Component):
|
||||
(RPC_EVENT, event.name, event.args)
|
||||
)
|
||||
|
||||
def emit_event_for_session_id(self, session_id, event):
|
||||
"""
|
||||
Emits the event to specified session_id.
|
||||
|
||||
:param session_id: the event to emit
|
||||
:type session_id: int
|
||||
:param event: the event to emit
|
||||
: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)
|
||||
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)
|
||||
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)
|
||||
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))
|
||||
|
||||
|
||||
def check_ssl_keys():
|
||||
"""
|
||||
Check for SSL cert/key and create them if necessary
|
||||
@ -497,8 +601,12 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(
|
||||
crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
)
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").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)
|
||||
|
@ -36,6 +36,7 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from urllib import unquote
|
||||
from urlparse import urlparse
|
||||
|
||||
@ -44,30 +45,58 @@ from deluge._libtorrent import lt
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.log import LOG as log
|
||||
from deluge.event import *
|
||||
|
||||
TORRENT_STATE = deluge.common.TORRENT_STATE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def sanitize_filepath(filepath, folder=False):
|
||||
"""
|
||||
Returns a sanitized filepath to pass to libotorrent rename_file().
|
||||
The filepath will have backslashes substituted along with whitespace
|
||||
padding and duplicate slashes stripped. If `folder` is True a trailing
|
||||
slash is appended to the returned filepath.
|
||||
"""
|
||||
def clean_filename(filename):
|
||||
filename = filename.strip()
|
||||
if filename.replace('.', '') == '':
|
||||
return ''
|
||||
return filename
|
||||
|
||||
if '\\' in filepath or '/' in filepath:
|
||||
folderpath = filepath.replace('\\', '/').split('/')
|
||||
folderpath = [clean_filename(x) for x in folderpath]
|
||||
newfilepath = '/'.join(filter(None, folderpath))
|
||||
else:
|
||||
newfilepath = clean_filename(filepath)
|
||||
|
||||
if folder is True:
|
||||
return newfilepath + '/'
|
||||
else:
|
||||
return newfilepath
|
||||
|
||||
class TorrentOptions(dict):
|
||||
def __init__(self):
|
||||
config = ConfigManager("core.conf").config
|
||||
options_conf_map = {
|
||||
"max_connections": "max_connections_per_torrent",
|
||||
"max_upload_slots": "max_upload_slots_per_torrent",
|
||||
"max_upload_speed": "max_upload_speed_per_torrent",
|
||||
"max_download_speed": "max_download_speed_per_torrent",
|
||||
"prioritize_first_last_pieces": "prioritize_first_last_pieces",
|
||||
"compact_allocation": "compact_allocation",
|
||||
"download_location": "download_location",
|
||||
"auto_managed": "auto_managed",
|
||||
"stop_at_ratio": "stop_seed_at_ratio",
|
||||
"stop_ratio": "stop_seed_ratio",
|
||||
"remove_at_ratio": "remove_seed_at_ratio",
|
||||
"move_completed": "move_completed",
|
||||
"move_completed_path": "move_completed_path",
|
||||
"add_paused": "add_paused",
|
||||
}
|
||||
"max_connections": "max_connections_per_torrent",
|
||||
"max_upload_slots": "max_upload_slots_per_torrent",
|
||||
"max_upload_speed": "max_upload_speed_per_torrent",
|
||||
"max_download_speed": "max_download_speed_per_torrent",
|
||||
"prioritize_first_last_pieces": "prioritize_first_last_pieces",
|
||||
"sequential_download": "sequential_download",
|
||||
"compact_allocation": "compact_allocation",
|
||||
"download_location": "download_location",
|
||||
"auto_managed": "auto_managed",
|
||||
"stop_at_ratio": "stop_seed_at_ratio",
|
||||
"stop_ratio": "stop_seed_ratio",
|
||||
"remove_at_ratio": "remove_seed_at_ratio",
|
||||
"move_completed": "move_completed",
|
||||
"move_completed_path": "move_completed_path",
|
||||
"add_paused": "add_paused",
|
||||
"shared": "shared"
|
||||
}
|
||||
for opt_k, conf_k in options_conf_map.iteritems():
|
||||
self[opt_k] = config[conf_k]
|
||||
self["file_priorities"] = []
|
||||
@ -76,13 +105,13 @@ class TorrentOptions(dict):
|
||||
class Torrent(object):
|
||||
"""Torrent holds information about torrents added to the libtorrent session.
|
||||
"""
|
||||
def __init__(self, handle, options, state=None, filename=None, magnet=None):
|
||||
def __init__(self, handle, options, state=None, filename=None, magnet=None, owner=None):
|
||||
log.debug("Creating torrent object %s", str(handle.info_hash()))
|
||||
# Get the core config
|
||||
self.config = ConfigManager("core.conf")
|
||||
|
||||
self.rpcserver = component.get("RPCServer")
|
||||
|
||||
|
||||
# This dict holds previous status dicts returned for this torrent
|
||||
# We use this to return dicts that only contain changes from the previous
|
||||
# {session_id: status_dict, ...}
|
||||
@ -90,7 +119,7 @@ class Torrent(object):
|
||||
from twisted.internet.task import LoopingCall
|
||||
self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status)
|
||||
self.prev_status_cleanup_loop.start(10)
|
||||
|
||||
|
||||
# Set the libtorrent handle
|
||||
self.handle = handle
|
||||
# Set the torrent_id for this torrent
|
||||
@ -179,6 +208,23 @@ class Torrent(object):
|
||||
else:
|
||||
self.time_added = time.time()
|
||||
|
||||
# Keep track of the owner
|
||||
if state:
|
||||
self.owner = state.owner
|
||||
else:
|
||||
self.owner = owner
|
||||
|
||||
# Keep track of last seen complete
|
||||
if state:
|
||||
self._last_seen_complete = state.last_seen_complete or 0.0
|
||||
else:
|
||||
self._last_seen_complete = 0.0
|
||||
|
||||
# Keep track if we're forcing a recheck of the torrent so that we can
|
||||
# re-pause it after its done if necessary
|
||||
self.forcing_recheck = False
|
||||
self.forcing_recheck_paused = False
|
||||
|
||||
log.debug("Torrent object created.")
|
||||
|
||||
## Options methods ##
|
||||
@ -192,17 +238,44 @@ class Torrent(object):
|
||||
"max_download_speed": self.set_max_download_speed,
|
||||
"max_upload_slots": self.handle.set_max_uploads,
|
||||
"max_upload_speed": self.set_max_upload_speed,
|
||||
"prioritize_first_last_pieces": self.set_prioritize_first_last
|
||||
"prioritize_first_last_pieces": self.set_prioritize_first_last,
|
||||
"sequential_download": self.set_sequential_download
|
||||
|
||||
}
|
||||
for (key, value) in options.items():
|
||||
if OPTIONS_FUNCS.has_key(key):
|
||||
OPTIONS_FUNCS[key](value)
|
||||
|
||||
self.options.update(options)
|
||||
|
||||
def get_options(self):
|
||||
return self.options
|
||||
|
||||
def get_name(self):
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
elif self.magnet:
|
||||
try:
|
||||
keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')])
|
||||
name = keys.get('dn')
|
||||
if not name:
|
||||
return self.torrent_id
|
||||
name = unquote(name).replace('+', ' ')
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
except:
|
||||
pass
|
||||
return self.torrent_id
|
||||
|
||||
def set_owner(self, account):
|
||||
self.owner = account
|
||||
|
||||
def set_max_connections(self, max_connections):
|
||||
self.options["max_connections"] = int(max_connections)
|
||||
@ -231,14 +304,30 @@ class Torrent(object):
|
||||
|
||||
def set_prioritize_first_last(self, prioritize):
|
||||
self.options["prioritize_first_last_pieces"] = prioritize
|
||||
if prioritize:
|
||||
if self.handle.has_metadata():
|
||||
if self.handle.get_torrent_info().num_files() == 1:
|
||||
# We only do this if one file is in the torrent
|
||||
priorities = [1] * self.handle.get_torrent_info().num_pieces()
|
||||
priorities[0] = 7
|
||||
priorities[-1] = 7
|
||||
self.handle.prioritize_pieces(priorities)
|
||||
if self.handle.has_metadata():
|
||||
if self.options["compact_allocation"]:
|
||||
log.debug("Setting first/last priority with compact "
|
||||
"allocation does not work!")
|
||||
return
|
||||
|
||||
paths = {}
|
||||
ti = self.handle.get_torrent_info()
|
||||
for n in range(ti.num_pieces()):
|
||||
slices = ti.map_block(n, 0, ti.piece_size(n))
|
||||
for slice in slices:
|
||||
fe = ti.file_at(slice.file_index)
|
||||
paths.setdefault(fe.path, []).append(n)
|
||||
|
||||
priorities = self.handle.piece_priorities()
|
||||
for pieces in paths.itervalues():
|
||||
two_percent = 2*100/len(pieces)
|
||||
for piece in pieces[:two_percent]+pieces[-two_percent:]:
|
||||
priorities[piece] = prioritize and 7 or 1
|
||||
self.handle.prioritize_pieces(priorities)
|
||||
|
||||
def set_sequential_download(self, set_sequencial):
|
||||
self.options["sequential_download"] = set_sequencial
|
||||
self.handle.set_sequential_download(set_sequencial)
|
||||
|
||||
def set_auto_managed(self, auto_managed):
|
||||
self.options["auto_managed"] = auto_managed
|
||||
@ -308,7 +397,7 @@ class Torrent(object):
|
||||
tracker_list = []
|
||||
|
||||
for tracker in trackers:
|
||||
new_entry = lt.announce_entry(tracker["url"])
|
||||
new_entry = lt.announce_entry(str(tracker["url"]))
|
||||
new_entry.tier = tracker["tier"]
|
||||
tracker_list.append(new_entry)
|
||||
self.handle.replace_trackers(tracker_list)
|
||||
@ -319,7 +408,7 @@ class Torrent(object):
|
||||
# Set the tracker list in the torrent object
|
||||
self.trackers = trackers
|
||||
if len(trackers) > 0:
|
||||
# Force a reannounce if there is at least 1 tracker
|
||||
# Force a re-announce if there is at least 1 tracker
|
||||
self.force_reannounce()
|
||||
|
||||
self.tracker_host = None
|
||||
@ -341,7 +430,10 @@ class Torrent(object):
|
||||
|
||||
# Set self.state to the ltstate right away just incase we don't hit some
|
||||
# of the logic below
|
||||
self.state = str(ltstate)
|
||||
if ltstate in LTSTATE:
|
||||
self.state = LTSTATE[ltstate]
|
||||
else:
|
||||
self.state = str(ltstate)
|
||||
|
||||
log.debug("set_state_based_on_ltstate: %s", deluge.common.LT_TORRENT_STATE[ltstate])
|
||||
log.debug("session.is_paused: %s", component.get("Core").session.is_paused())
|
||||
@ -393,15 +485,14 @@ class Torrent(object):
|
||||
else:
|
||||
status = self.status
|
||||
|
||||
if self.is_finished and (self.options["stop_at_ratio"] or self.config["stop_seed_at_ratio"]):
|
||||
if self.is_finished and self.options["stop_at_ratio"]:
|
||||
# We're a seed, so calculate the time to the 'stop_share_ratio'
|
||||
if not status.upload_payload_rate:
|
||||
return 0
|
||||
stop_ratio = self.config["stop_seed_ratio"] if self.config["stop_seed_at_ratio"] else self.options["stop_ratio"]
|
||||
|
||||
stop_ratio = self.options["stop_ratio"]
|
||||
return ((status.all_time_download * stop_ratio) - status.all_time_upload) / status.upload_payload_rate
|
||||
|
||||
left = status.total_wanted - status.total_done
|
||||
left = status.total_wanted - status.total_wanted_done
|
||||
|
||||
if left <= 0 or status.download_payload_rate == 0:
|
||||
return 0
|
||||
@ -475,11 +566,11 @@ class Torrent(object):
|
||||
ret.append({
|
||||
"client": client,
|
||||
"country": country,
|
||||
"down_speed": peer.down_speed,
|
||||
"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.up_speed,
|
||||
"up_speed": peer.payload_up_speed,
|
||||
})
|
||||
|
||||
return ret
|
||||
@ -540,21 +631,31 @@ class Torrent(object):
|
||||
return host
|
||||
return ""
|
||||
|
||||
def get_last_seen_complete(self):
|
||||
"""
|
||||
Returns the time a torrent was last seen complete, ie, with all pieces
|
||||
available.
|
||||
"""
|
||||
if lt.version_minor > 15:
|
||||
return self.status.last_seen_complete
|
||||
self.calculate_last_seen_complete()
|
||||
return self._last_seen_complete
|
||||
|
||||
def get_status(self, keys, diff=False):
|
||||
"""
|
||||
Returns the status of the torrent based on the keys provided
|
||||
|
||||
|
||||
:param keys: the keys to get the status on
|
||||
:type keys: list of str
|
||||
:param diff: if True, will return a diff of the changes since the last
|
||||
call to get_status based on the session_id
|
||||
:type diff: bool
|
||||
|
||||
|
||||
:returns: a dictionary of the status keys and their values
|
||||
:rtype: dict
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Create the full dictionary
|
||||
self.status = self.handle.status()
|
||||
if self.handle.has_metadata():
|
||||
@ -568,7 +669,13 @@ class Torrent(object):
|
||||
if distributed_copies < 0:
|
||||
distributed_copies = 0.0
|
||||
|
||||
#if you add a key here->add it to core.py STATUS_KEYS too.
|
||||
# Calculate the seeds:peers ratio
|
||||
if self.status.num_incomplete == 0:
|
||||
# Use -1.0 to signify infinity
|
||||
seeds_peers_ratio = -1.0
|
||||
else:
|
||||
seeds_peers_ratio = self.status.num_complete / float(self.status.num_incomplete)
|
||||
|
||||
full_status = {
|
||||
"active_time": self.status.active_time,
|
||||
"all_time_download": self.status.all_time_download,
|
||||
@ -586,15 +693,21 @@ class Torrent(object):
|
||||
"message": self.statusmsg,
|
||||
"move_on_completed_path": self.options["move_completed_path"],
|
||||
"move_on_completed": self.options["move_completed"],
|
||||
"move_completed_path": self.options["move_completed_path"],
|
||||
"move_completed": self.options["move_completed"],
|
||||
"next_announce": self.status.next_announce.seconds,
|
||||
"num_peers": self.status.num_peers - self.status.num_seeds,
|
||||
"num_seeds": self.status.num_seeds,
|
||||
"owner": self.owner,
|
||||
"paused": self.status.paused,
|
||||
"prioritize_first_last": self.options["prioritize_first_last_pieces"],
|
||||
"sequential_download": self.options["sequential_download"],
|
||||
"progress": progress,
|
||||
"shared": self.options["shared"],
|
||||
"remove_at_ratio": self.options["remove_at_ratio"],
|
||||
"save_path": self.options["download_location"],
|
||||
"seeding_time": self.status.seeding_time,
|
||||
"seeds_peers_ratio": seeds_peers_ratio,
|
||||
"seed_rank": self.status.seed_rank,
|
||||
"state": self.state,
|
||||
"stop_at_ratio": self.options["stop_at_ratio"],
|
||||
@ -603,8 +716,8 @@ class Torrent(object):
|
||||
"total_done": self.status.total_done,
|
||||
"total_payload_download": self.status.total_payload_download,
|
||||
"total_payload_upload": self.status.total_payload_upload,
|
||||
"total_peers": self.status.num_incomplete,
|
||||
"total_seeds": self.status.num_complete,
|
||||
"total_peers": self.status.list_peers - self.status.list_seeds,
|
||||
"total_seeds": self.status.list_seeds,
|
||||
"total_uploaded": self.status.all_time_upload,
|
||||
"total_wanted": self.status.total_wanted,
|
||||
"tracker": self.status.current_tracker,
|
||||
@ -621,32 +734,6 @@ class Torrent(object):
|
||||
return self.torrent_info.comment()
|
||||
return ""
|
||||
|
||||
def ti_name():
|
||||
if self.handle.has_metadata():
|
||||
name = self.torrent_info.file_at(0).path.split("/", 1)[0]
|
||||
if not name:
|
||||
name = self.torrent_info.name()
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
|
||||
elif self.magnet:
|
||||
try:
|
||||
keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')])
|
||||
name = keys.get('dn')
|
||||
if not name:
|
||||
return self.torrent_id
|
||||
name = unquote(name).replace('+', ' ')
|
||||
try:
|
||||
return name.decode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
return name
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.torrent_id
|
||||
|
||||
def ti_priv():
|
||||
if self.handle.has_metadata():
|
||||
return self.torrent_info.priv()
|
||||
@ -667,6 +754,10 @@ class Torrent(object):
|
||||
if self.handle.has_metadata():
|
||||
return self.torrent_info.piece_length()
|
||||
return 0
|
||||
def ti_pieces_info():
|
||||
if self.handle.has_metadata():
|
||||
return self.get_pieces_info()
|
||||
return None
|
||||
|
||||
fns = {
|
||||
"comment": ti_comment,
|
||||
@ -674,9 +765,10 @@ class Torrent(object):
|
||||
"file_progress": self.get_file_progress,
|
||||
"files": self.get_files,
|
||||
"is_seed": self.handle.is_seed,
|
||||
"name": ti_name,
|
||||
"name": self.get_name,
|
||||
"num_files": ti_num_files,
|
||||
"num_pieces": ti_num_pieces,
|
||||
"pieces": ti_pieces_info,
|
||||
"peers": self.get_peers,
|
||||
"piece_length": ti_piece_length,
|
||||
"private": ti_priv,
|
||||
@ -684,6 +776,7 @@ class Torrent(object):
|
||||
"ratio": self.get_ratio,
|
||||
"total_size": ti_total_size,
|
||||
"tracker_host": self.get_tracker_host,
|
||||
"last_seen_complete": self.get_last_seen_complete
|
||||
}
|
||||
|
||||
# Create the desired status dictionary and return it
|
||||
@ -699,7 +792,7 @@ class Torrent(object):
|
||||
status_dict[key] = full_status[key]
|
||||
elif key in fns:
|
||||
status_dict[key] = fns[key]()
|
||||
|
||||
|
||||
session_id = self.rpcserver.get_session_id()
|
||||
if diff:
|
||||
if session_id in self.prev_status:
|
||||
@ -711,7 +804,7 @@ class Torrent(object):
|
||||
status_diff[key] = value
|
||||
else:
|
||||
status_diff[key] = value
|
||||
|
||||
|
||||
self.prev_status[session_id] = status_dict
|
||||
return status_diff
|
||||
|
||||
@ -727,6 +820,7 @@ class Torrent(object):
|
||||
self.handle.set_upload_limit(int(self.max_upload_speed * 1024))
|
||||
self.handle.set_download_limit(int(self.max_download_speed * 1024))
|
||||
self.handle.prioritize_files(self.file_priorities)
|
||||
self.handle.set_sequential_download(self.options["sequential_download"])
|
||||
self.handle.resolve_countries(True)
|
||||
|
||||
def pause(self):
|
||||
@ -761,13 +855,8 @@ class Torrent(object):
|
||||
|
||||
if self.handle.is_finished():
|
||||
# If the torrent has already reached it's 'stop_seed_ratio' then do not do anything
|
||||
if self.config["stop_seed_at_ratio"] or self.options["stop_at_ratio"]:
|
||||
if self.options["stop_at_ratio"]:
|
||||
ratio = self.options["stop_ratio"]
|
||||
else:
|
||||
ratio = self.config["stop_seed_ratio"]
|
||||
|
||||
if self.get_ratio() >= ratio:
|
||||
if self.options["stop_at_ratio"]:
|
||||
if self.get_ratio() >= self.options["stop_ratio"]:
|
||||
#XXX: This should just be returned in the RPC Response, no event
|
||||
#self.signals.emit_event("torrent_resume_at_stop_ratio")
|
||||
return
|
||||
@ -794,8 +883,27 @@ class Torrent(object):
|
||||
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
|
||||
# Attempt to convert utf8 path to unicode
|
||||
# Note: Inconsistent encoding for 'dest', needs future investigation
|
||||
try:
|
||||
self.handle.move_storage(dest.encode("utf8"))
|
||||
dest_u = unicode(dest, "utf-8")
|
||||
except TypeError:
|
||||
# String is already unicode
|
||||
dest_u = dest
|
||||
|
||||
if not os.path.exists(dest_u):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest_u)
|
||||
except IOError, e:
|
||||
log.exception(e)
|
||||
log.error("Could not move storage for torrent %s since %s does "
|
||||
"not exist and could not create the directory.",
|
||||
self.torrent_id, dest_u)
|
||||
return False
|
||||
try:
|
||||
self.handle.move_storage(dest_u)
|
||||
except:
|
||||
return False
|
||||
|
||||
@ -857,18 +965,22 @@ class Torrent(object):
|
||||
|
||||
def force_recheck(self):
|
||||
"""Forces a recheck of the torrents pieces"""
|
||||
paused = self.handle.is_paused()
|
||||
try:
|
||||
self.handle.force_recheck()
|
||||
self.handle.resume()
|
||||
except Exception, e:
|
||||
log.debug("Unable to force recheck: %s", e)
|
||||
return False
|
||||
self.forcing_recheck = True
|
||||
self.forcing_recheck_paused = paused
|
||||
return True
|
||||
|
||||
def rename_files(self, filenames):
|
||||
"""Renames files in the torrent. 'filenames' should be a list of
|
||||
(index, filename) pairs."""
|
||||
for index, filename in filenames:
|
||||
filename = sanitize_filepath(filename)
|
||||
self.handle.rename_file(index, filename.encode("utf-8"))
|
||||
|
||||
def rename_folder(self, folder, new_folder):
|
||||
@ -879,8 +991,7 @@ class Torrent(object):
|
||||
log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder)
|
||||
return
|
||||
|
||||
if new_folder[-1:] != "/":
|
||||
new_folder += "/"
|
||||
new_folder = sanitize_filepath(new_folder, folder=True)
|
||||
|
||||
wait_on_folder = (folder, new_folder, [])
|
||||
for f in self.get_files():
|
||||
@ -889,14 +1000,62 @@ class Torrent(object):
|
||||
wait_on_folder[2].append(f["index"])
|
||||
self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).encode("utf-8"))
|
||||
self.waiting_on_folder_rename.append(wait_on_folder)
|
||||
|
||||
|
||||
def cleanup_prev_status(self):
|
||||
"""
|
||||
This method gets called to check the validity of the keys in the prev_status
|
||||
dict. If the key is no longer valid, the dict will be deleted.
|
||||
|
||||
|
||||
"""
|
||||
for key in self.prev_status.keys():
|
||||
if not self.rpcserver.is_session_valid(key):
|
||||
del self.prev_status[key]
|
||||
|
||||
def calculate_last_seen_complete(self):
|
||||
if self._last_seen_complete+60 > time.time():
|
||||
# Simple caching. Only calculate every 1 min at minimum
|
||||
return self._last_seen_complete
|
||||
|
||||
availability = self.handle.piece_availability()
|
||||
if filter(lambda x: x<1, availability):
|
||||
# Torrent does not have all the pieces
|
||||
return
|
||||
log.trace("Torrent %s has all the pieces. Setting last seen complete.",
|
||||
self.torrent_id)
|
||||
self._last_seen_complete = time.time()
|
||||
|
||||
def get_pieces_info(self):
|
||||
pieces = {}
|
||||
# First get the pieces availability.
|
||||
availability = self.handle.piece_availability()
|
||||
# Pieces from connected peers
|
||||
for peer_info in self.handle.get_peer_info():
|
||||
if peer_info.downloading_piece_index < 0:
|
||||
# No piece index, then we're not downloading anything from
|
||||
# this peer
|
||||
continue
|
||||
pieces[peer_info.downloading_piece_index] = 2
|
||||
|
||||
# Now, the rest of the pieces
|
||||
for idx, piece in enumerate(self.handle.status().pieces):
|
||||
if idx in pieces:
|
||||
# Piece beeing downloaded, handled above
|
||||
continue
|
||||
elif piece:
|
||||
# Completed Piece
|
||||
pieces[idx] = 3
|
||||
continue
|
||||
elif availability[idx] > 0:
|
||||
# Piece not downloaded nor beeing downloaded but available
|
||||
pieces[idx] = 1
|
||||
continue
|
||||
# If we reached here, it means the piece is missing, ie, there's
|
||||
# no known peer with this piece, or this piece has not been asked
|
||||
# for so far.
|
||||
pieces[idx] = 0
|
||||
|
||||
sorted_indexes = pieces.keys()
|
||||
sorted_indexes.sort()
|
||||
# Return only the piece states, no need for the piece index
|
||||
# Keep the order
|
||||
return [pieces[idx] for idx in sorted_indexes]
|
||||
|
@ -41,24 +41,24 @@ import os
|
||||
import time
|
||||
import shutil
|
||||
import operator
|
||||
import logging
|
||||
import re
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
|
||||
from deluge.event import *
|
||||
from deluge.error import *
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
from deluge.ui.common import utf8_encoded
|
||||
from deluge.common import utf8_encoded
|
||||
|
||||
from deluge.log import LOG as log
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class TorrentState:
|
||||
def __init__(self,
|
||||
@ -74,6 +74,7 @@ class TorrentState:
|
||||
max_upload_speed=-1.0,
|
||||
max_download_speed=-1.0,
|
||||
prioritize_first_last=False,
|
||||
sequential_download=False,
|
||||
file_priorities=None,
|
||||
queue=None,
|
||||
auto_managed=True,
|
||||
@ -84,7 +85,10 @@ class TorrentState:
|
||||
move_completed=False,
|
||||
move_completed_path=None,
|
||||
magnet=None,
|
||||
time_added=-1
|
||||
time_added=-1,
|
||||
last_seen_complete=0.0, # 0 is the default returned when the info
|
||||
owner="", # does not exist on lt >= .16
|
||||
shared=False
|
||||
):
|
||||
self.torrent_id = torrent_id
|
||||
self.filename = filename
|
||||
@ -94,6 +98,8 @@ class TorrentState:
|
||||
self.is_finished = is_finished
|
||||
self.magnet = magnet
|
||||
self.time_added = time_added
|
||||
self.last_seen_complete = last_seen_complete
|
||||
self.owner = owner
|
||||
|
||||
# Options
|
||||
self.compact = compact
|
||||
@ -104,6 +110,7 @@ class TorrentState:
|
||||
self.max_upload_speed = max_upload_speed
|
||||
self.max_download_speed = max_download_speed
|
||||
self.prioritize_first_last = prioritize_first_last
|
||||
self.sequential_download = sequential_download
|
||||
self.file_priorities = file_priorities
|
||||
self.auto_managed = auto_managed
|
||||
self.stop_ratio = stop_ratio
|
||||
@ -111,6 +118,7 @@ class TorrentState:
|
||||
self.remove_at_ratio = remove_at_ratio
|
||||
self.move_completed = move_completed
|
||||
self.move_completed_path = move_completed_path
|
||||
self.shared = shared
|
||||
|
||||
class TorrentManagerState:
|
||||
def __init__(self):
|
||||
@ -124,7 +132,8 @@ class TorrentManager(component.Component):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "TorrentManager", interval=5, depend=["CorePluginManager"])
|
||||
component.Component.__init__(self, "TorrentManager", interval=5,
|
||||
depend=["CorePluginManager"])
|
||||
log.debug("TorrentManager init..")
|
||||
# Set the libtorrent session
|
||||
self.session = component.get("Core").session
|
||||
@ -139,6 +148,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Create the torrents dict { torrent_id: Torrent }
|
||||
self.torrents = {}
|
||||
self.last_seen_complete_loop = None
|
||||
|
||||
# This is a list of torrent_id when we shutdown the torrentmanager.
|
||||
# We use this list to determine if all active torrents have been paused
|
||||
@ -192,6 +202,8 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_metadata_received)
|
||||
self.alerts.register_handler("file_error_alert",
|
||||
self.on_alert_file_error)
|
||||
self.alerts.register_handler("file_completed_alert",
|
||||
self.on_alert_file_completed)
|
||||
|
||||
def start(self):
|
||||
# Get the pluginmanager reference
|
||||
@ -209,6 +221,9 @@ class TorrentManager(component.Component):
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
|
||||
if self.last_seen_complete_loop:
|
||||
self.last_seen_complete_loop.start(60)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
if self.save_state_timer.running:
|
||||
@ -217,6 +232,9 @@ class TorrentManager(component.Component):
|
||||
if self.save_resume_data_timer.running:
|
||||
self.save_resume_data_timer.stop()
|
||||
|
||||
if self.last_seen_complete_loop:
|
||||
self.last_seen_complete_loop.stop()
|
||||
|
||||
# Save state on shutdown
|
||||
self.save_state()
|
||||
|
||||
@ -256,16 +274,16 @@ class TorrentManager(component.Component):
|
||||
|
||||
def update(self):
|
||||
for torrent_id, torrent in self.torrents.items():
|
||||
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent
|
||||
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
|
||||
if self.config["stop_seed_at_ratio"] and not torrent.options["stop_at_ratio"]:
|
||||
if torrent.options["stop_at_ratio"] and torrent.state not in (
|
||||
"Checking", "Allocating", "Paused", "Queued"):
|
||||
# If the global setting is set, but the per-torrent isn't..
|
||||
# Just skip to the next torrent.
|
||||
# This is so that a user can turn-off the stop at ratio option
|
||||
# on a per-torrent basis
|
||||
if not torrent.options["stop_at_ratio"]:
|
||||
continue
|
||||
stop_ratio = self.config["stop_seed_ratio"]
|
||||
if torrent.options["stop_at_ratio"]:
|
||||
stop_ratio = torrent.options["stop_ratio"]
|
||||
if torrent.get_ratio() >= stop_ratio and torrent.is_finished:
|
||||
if self.config["remove_seed_at_ratio"] or torrent.options["remove_at_ratio"]:
|
||||
if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished:
|
||||
if torrent.options["remove_at_ratio"]:
|
||||
self.remove(torrent_id)
|
||||
break
|
||||
if not torrent.handle.is_paused():
|
||||
@ -277,7 +295,16 @@ class TorrentManager(component.Component):
|
||||
|
||||
def get_torrent_list(self):
|
||||
"""Returns a list of torrent_ids"""
|
||||
return self.torrents.keys()
|
||||
torrent_ids = self.torrents.keys()
|
||||
if component.get("RPCServer").get_session_auth_level() == AUTH_LEVEL_ADMIN:
|
||||
return torrent_ids
|
||||
|
||||
current_user = component.get("RPCServer").get_session_user()
|
||||
for torrent_id in torrent_ids[:]:
|
||||
torrent_status = self[torrent_id].get_status(["owner", "shared"])
|
||||
if torrent_status["owner"] != current_user and torrent_status["shared"] == False:
|
||||
torrent_ids.pop(torrent_ids.index(torrent_id))
|
||||
return torrent_ids
|
||||
|
||||
def get_torrent_info_from_file(self, filepath):
|
||||
"""Returns a torrent_info for the file specified or None"""
|
||||
@ -317,9 +344,14 @@ class TorrentManager(component.Component):
|
||||
log.warning("Unable to delete the fastresume file: %s", e)
|
||||
|
||||
def add(self, torrent_info=None, state=None, options=None, save_state=True,
|
||||
filedump=None, filename=None, magnet=None, resume_data=None):
|
||||
filedump=None, filename=None, magnet=None, resume_data=None, owner=None):
|
||||
"""Add a torrent to the manager and returns it's torrent_id"""
|
||||
|
||||
if owner is None:
|
||||
owner = component.get("RPCServer").get_session_user()
|
||||
if not owner:
|
||||
owner = "localclient"
|
||||
|
||||
if torrent_info is None and state is None and filedump is None and magnet is None:
|
||||
log.debug("You must specify a valid torrent_info, torrent state or magnet.")
|
||||
return
|
||||
@ -346,6 +378,7 @@ class TorrentManager(component.Component):
|
||||
options["max_upload_speed"] = state.max_upload_speed
|
||||
options["max_download_speed"] = state.max_download_speed
|
||||
options["prioritize_first_last_pieces"] = state.prioritize_first_last
|
||||
options["sequential_download"] = state.sequential_download
|
||||
options["file_priorities"] = state.file_priorities
|
||||
options["compact_allocation"] = state.compact
|
||||
options["download_location"] = state.save_path
|
||||
@ -356,6 +389,7 @@ class TorrentManager(component.Component):
|
||||
options["move_completed"] = state.move_completed
|
||||
options["move_completed_path"] = state.move_completed_path
|
||||
options["add_paused"] = state.paused
|
||||
options["shared"] = state.shared
|
||||
|
||||
ti = self.get_torrent_info_from_file(
|
||||
os.path.join(get_config_dir(),
|
||||
@ -376,7 +410,35 @@ class TorrentManager(component.Component):
|
||||
|
||||
add_torrent_params["resume_data"] = resume_data
|
||||
else:
|
||||
# We have a torrent_info object so we're not loading from state.
|
||||
# We have a torrent_info object or magnet uri so we're not loading from state.
|
||||
if torrent_info:
|
||||
add_torrent_id = str(torrent_info.info_hash())
|
||||
if add_torrent_id in self.get_torrent_list():
|
||||
# Torrent already exists just append any extra trackers.
|
||||
log.debug("Torrent (%s) exists, checking for trackers to add...", add_torrent_id)
|
||||
add_torrent_trackers = []
|
||||
for value in torrent_info.trackers():
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
add_torrent_trackers.append(tracker)
|
||||
|
||||
torrent_trackers = {}
|
||||
tracker_list = []
|
||||
for tracker in self[add_torrent_id].get_status(["trackers"])["trackers"]:
|
||||
torrent_trackers[(tracker["url"])] = tracker
|
||||
tracker_list.append(tracker)
|
||||
|
||||
added_tracker = False
|
||||
for tracker in add_torrent_trackers:
|
||||
if tracker['url'] not in torrent_trackers:
|
||||
tracker_list.append(tracker)
|
||||
added_tracker = True
|
||||
|
||||
if added_tracker:
|
||||
self[add_torrent_id].set_trackers(tracker_list)
|
||||
return
|
||||
|
||||
# Check if options is None and load defaults
|
||||
if options == None:
|
||||
options = TorrentOptions()
|
||||
@ -393,7 +455,6 @@ class TorrentManager(component.Component):
|
||||
torrent_info.rename_file(index, utf8_encoded(name))
|
||||
|
||||
add_torrent_params["ti"] = torrent_info
|
||||
add_torrent_params["resume_data"] = ""
|
||||
|
||||
#log.info("Adding torrent: %s", filename)
|
||||
log.debug("options: %s", options)
|
||||
@ -434,7 +495,13 @@ class TorrentManager(component.Component):
|
||||
# Set auto_managed to False because the torrent is paused
|
||||
handle.auto_managed(False)
|
||||
# Create a Torrent object
|
||||
torrent = Torrent(handle, options, state, filename, magnet)
|
||||
owner = state.owner if state else (
|
||||
owner if owner else component.get("RPCServer").get_session_user()
|
||||
)
|
||||
account_exists = component.get("AuthManager").has_account(owner)
|
||||
if not account_exists:
|
||||
owner = 'localclient'
|
||||
torrent = Torrent(handle, options, state, filename, magnet, owner)
|
||||
# Add the torrent object to the dictionary
|
||||
self.torrents[torrent.torrent_id] = torrent
|
||||
if self.config["queue_new_to_top"]:
|
||||
@ -473,9 +540,15 @@ class TorrentManager(component.Component):
|
||||
# Save the session state
|
||||
self.save_state()
|
||||
|
||||
# Emit the torrent_added signal
|
||||
component.get("EventManager").emit(TorrentAddedEvent(torrent.torrent_id))
|
||||
|
||||
# Emit torrent_added signal
|
||||
from_state = state is not None
|
||||
component.get("EventManager").emit(
|
||||
TorrentAddedEvent(torrent.torrent_id, from_state)
|
||||
)
|
||||
log.info("Torrent %s from user \"%s\" %s",
|
||||
torrent.get_status(["name"])["name"],
|
||||
torrent.get_status(["owner"])["owner"],
|
||||
(from_state and "loaded" or "added"))
|
||||
return torrent.torrent_id
|
||||
|
||||
def load_torrent(self, torrent_id):
|
||||
@ -515,6 +588,8 @@ class TorrentManager(component.Component):
|
||||
if torrent_id not in self.torrents:
|
||||
raise InvalidTorrentError("torrent_id not in session")
|
||||
|
||||
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
|
||||
|
||||
# Emit the signal to the clients
|
||||
component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id))
|
||||
|
||||
@ -554,7 +629,7 @@ class TorrentManager(component.Component):
|
||||
# Remove the torrent from deluge's session
|
||||
try:
|
||||
del self.torrents[torrent_id]
|
||||
except KeyError, ValueError:
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
# Save the session state
|
||||
@ -562,7 +637,8 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Emit the signal to the clients
|
||||
component.get("EventManager").emit(TorrentRemovedEvent(torrent_id))
|
||||
|
||||
log.info("Torrent %s removed by user: %s", torrent_name,
|
||||
component.get("RPCServer").get_session_user())
|
||||
return True
|
||||
|
||||
def load_state(self):
|
||||
@ -602,6 +678,17 @@ class TorrentManager(component.Component):
|
||||
log.error("Torrent state file is either corrupt or incompatible! %s", e)
|
||||
break
|
||||
|
||||
|
||||
if lt.version_minor < 16:
|
||||
log.debug("libtorrent version is lower than 0.16. Start looping "
|
||||
"callback to calculate last_seen_complete info.")
|
||||
def calculate_last_seen_complete():
|
||||
for torrent in self.torrents.values():
|
||||
torrent.calculate_last_seen_complete()
|
||||
self.last_seen_complete_loop = LoopingCall(
|
||||
calculate_last_seen_complete
|
||||
)
|
||||
|
||||
component.get("EventManager").emit(SessionStartedEvent())
|
||||
|
||||
def save_state(self):
|
||||
@ -626,6 +713,7 @@ class TorrentManager(component.Component):
|
||||
torrent.options["max_upload_speed"],
|
||||
torrent.options["max_download_speed"],
|
||||
torrent.options["prioritize_first_last_pieces"],
|
||||
torrent.options["sequential_download"],
|
||||
torrent.options["file_priorities"],
|
||||
torrent.get_queue_position(),
|
||||
torrent.options["auto_managed"],
|
||||
@ -636,7 +724,10 @@ class TorrentManager(component.Component):
|
||||
torrent.options["move_completed"],
|
||||
torrent.options["move_completed_path"],
|
||||
torrent.magnet,
|
||||
torrent.time_added
|
||||
torrent.time_added,
|
||||
torrent.get_last_seen_complete(),
|
||||
torrent.owner,
|
||||
torrent.options["shared"]
|
||||
)
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
@ -731,6 +822,38 @@ class TorrentManager(component.Component):
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
|
||||
def remove_empty_folders(self, torrent_id, folder):
|
||||
"""
|
||||
Recursively removes folders but only if they are empty.
|
||||
Cleans up after libtorrent folder renames.
|
||||
|
||||
"""
|
||||
if torrent_id not in self.torrents:
|
||||
raise InvalidTorrentError("torrent_id is not in session")
|
||||
|
||||
info = self.torrents[torrent_id].get_status(['save_path'])
|
||||
# Regex removes leading slashes that causes join function to ignore save_path
|
||||
folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder))
|
||||
folder_full_path = os.path.normpath(folder_full_path)
|
||||
|
||||
try:
|
||||
if not os.listdir(folder_full_path):
|
||||
os.removedirs(folder_full_path)
|
||||
log.debug("Removed Empty Folder %s", folder_full_path)
|
||||
else:
|
||||
for root, dirs, files in os.walk(folder_full_path, topdown=False):
|
||||
for name in dirs:
|
||||
try:
|
||||
os.removedirs(os.path.join(root, name))
|
||||
log.debug("Removed Empty Folder %s", os.path.join(root, name))
|
||||
except OSError as (errno, strerror):
|
||||
if errno == 39:
|
||||
# Error raised if folder is not empty
|
||||
log.debug("%s", strerror)
|
||||
|
||||
except OSError as (errno, strerror):
|
||||
log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno)
|
||||
|
||||
def queue_top(self, torrent_id):
|
||||
"""Queue torrent to top"""
|
||||
if self.torrents[torrent_id].get_queue_position() == 0:
|
||||
@ -850,6 +973,13 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
|
||||
# Check to see if we're forcing a recheck and set it back to paused
|
||||
# if necessary
|
||||
if torrent.forcing_recheck:
|
||||
torrent.forcing_recheck = False
|
||||
if torrent.forcing_recheck_paused:
|
||||
torrent.handle.pause()
|
||||
|
||||
# Set the torrent state
|
||||
torrent.update_state()
|
||||
|
||||
@ -905,7 +1035,7 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
torrent.set_save_path(alert.handle.save_path())
|
||||
torrent.set_save_path(os.path.normpath(alert.handle.save_path()))
|
||||
torrent.set_move_completed(False)
|
||||
|
||||
def on_alert_torrent_resumed(self, alert):
|
||||
@ -985,6 +1115,8 @@ class TorrentManager(component.Component):
|
||||
if len(wait_on_folder[2]) == 1:
|
||||
# This is the last alert we were waiting for, time to send signal
|
||||
component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1]))
|
||||
# Empty folders are removed after libtorrent folder renames
|
||||
self.remove_empty_folders(torrent_id, wait_on_folder[0])
|
||||
del torrent.waiting_on_folder_rename[i]
|
||||
self.save_resume_data((torrent_id,))
|
||||
break
|
||||
@ -1012,3 +1144,9 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", alert.message())
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
component.get("EventManager").emit(
|
||||
TorrentFileCompletedEvent(torrent_id, alert.index))
|
||||
|
@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Deluge BitTorrent Client
|
||||
GenericName=Bittorrent Client
|
||||
Comment=Transfer files using the Bittorrent protocol
|
||||
Exec=deluge-gtk
|
||||
Icon=deluge
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
@ -2,6 +2,7 @@
|
||||
# error.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -35,7 +36,21 @@
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
pass
|
||||
def _get_message(self):
|
||||
return self._message
|
||||
def _set_message(self, message):
|
||||
self._message = message
|
||||
message = property(_get_message, _set_message)
|
||||
del _get_message, _set_message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
|
||||
inst._args = args
|
||||
inst._kwargs = kwargs
|
||||
return inst
|
||||
|
||||
class NoCoreError(DelugeError):
|
||||
pass
|
||||
@ -48,3 +63,69 @@ class InvalidTorrentError(DelugeError):
|
||||
|
||||
class InvalidPathError(DelugeError):
|
||||
pass
|
||||
|
||||
class WrappedException(DelugeError):
|
||||
def _get_traceback(self):
|
||||
return self._traceback
|
||||
def _set_traceback(self, traceback):
|
||||
self._traceback = traceback
|
||||
traceback = property(_get_traceback, _set_traceback)
|
||||
del _get_traceback, _set_traceback
|
||||
|
||||
def _get_type(self):
|
||||
return self._type
|
||||
def _set_type(self, type):
|
||||
self._type = type
|
||||
type = property(_get_type, _set_type)
|
||||
del _get_type, _set_type
|
||||
|
||||
def __init__(self, message, exception_type, traceback):
|
||||
self.message = message
|
||||
self.type = exception_type
|
||||
self.traceback = traceback
|
||||
|
||||
class _ClientSideRecreateError(DelugeError):
|
||||
pass
|
||||
|
||||
class IncompatibleClient(_ClientSideRecreateError):
|
||||
def __init__(self, daemon_version):
|
||||
self.daemon_version = daemon_version
|
||||
self.message = _(
|
||||
"Your deluge client is not compatible with the daemon. "
|
||||
"Please upgrade your client to %(daemon_version)s"
|
||||
) % dict(daemon_version=self.daemon_version)
|
||||
|
||||
class NotAuthorizedError(_ClientSideRecreateError):
|
||||
|
||||
def __init__(self, current_level, required_level):
|
||||
self.message = _(
|
||||
"Auth level too low: %(current_level)s < %(required_level)s" %
|
||||
dict(current_level=current_level, required_level=required_level)
|
||||
)
|
||||
self.current_level = current_level
|
||||
self.required_level = required_level
|
||||
|
||||
|
||||
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
|
||||
|
||||
def _get_username(self):
|
||||
return self._username
|
||||
def _set_username(self, username):
|
||||
self._username = username
|
||||
username = property(_get_username, _set_username)
|
||||
del _get_username, _set_username
|
||||
|
||||
def __init__(self, message, username):
|
||||
super(_UsernameBasedPasstroughError, self).__init__(message)
|
||||
self.message = message
|
||||
self.username = username
|
||||
|
||||
|
||||
class BadLoginError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthenticationRequired(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthManagerError(_UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
@ -57,7 +57,9 @@ class DelugeEvent(object):
|
||||
The base class for all events.
|
||||
|
||||
:prop name: this is the name of the class which is in-turn the event name
|
||||
:type name: string
|
||||
:prop args: a list of the attribute values
|
||||
:type args: list
|
||||
|
||||
"""
|
||||
__metaclass__ = DelugeEventMetaClass
|
||||
@ -77,11 +79,14 @@ class TorrentAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new torrent is successfully added to the session.
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
def __init__(self, torrent_id, from_state):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id of the torrent that was added
|
||||
:param torrent_id: the torrent_id of the torrent that was added
|
||||
:type torrent_id: string
|
||||
:param from_state: was the torrent loaded from state? Or is it a new torrent.
|
||||
:type from_state: bool
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
self._args = [torrent_id, from_state]
|
||||
|
||||
class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
@ -89,7 +94,8 @@ class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -99,7 +105,8 @@ class PreTorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -109,8 +116,10 @@ class TorrentStateChangedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, state):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param state: str, the new state
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param state: the new state
|
||||
:type state: string
|
||||
"""
|
||||
self._args = [torrent_id, state]
|
||||
|
||||
@ -126,9 +135,12 @@ class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, old, new):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param old: str, the old folder name
|
||||
:param new: str, the new folder name
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param old: the old folder name
|
||||
:type old: string
|
||||
:param new: the new folder name
|
||||
:type new: string
|
||||
"""
|
||||
self._args = [torrent_id, old, new]
|
||||
|
||||
@ -138,9 +150,12 @@ class TorrentFileRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id, index, name):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param index: int, the index of the file
|
||||
:param name: str, the new filename
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param index: the index of the file
|
||||
:type index: int
|
||||
:param name: the new filename
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [torrent_id, index, name]
|
||||
|
||||
@ -150,7 +165,8 @@ class TorrentFinishedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
@ -160,17 +176,42 @@ class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: str, the torrent_id
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentFileCompletedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file completes.
|
||||
|
||||
This will only work with libtorrent 0.15 or greater.
|
||||
|
||||
"""
|
||||
def __init__(self, torrent_id, index):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param index: the file index
|
||||
:type index: int
|
||||
"""
|
||||
self._args = [torrent_id, index]
|
||||
|
||||
class CreateTorrentProgressEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when creating a torrent file remotely.
|
||||
"""
|
||||
def __init__(self, piece_count, num_pieces):
|
||||
self._args = [piece_count, num_pieces]
|
||||
|
||||
class NewVersionAvailableEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a more recent version of Deluge is available.
|
||||
"""
|
||||
def __init__(self, new_release):
|
||||
"""
|
||||
:param new_release: str, the new version that is available
|
||||
:param new_release: the new version that is available
|
||||
:type new_release: string
|
||||
"""
|
||||
self._args = [new_release]
|
||||
|
||||
@ -199,7 +240,8 @@ class ConfigValueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
def __init__(self, key, value):
|
||||
"""
|
||||
:param key: str, the key that changed
|
||||
:param key: the key that changed
|
||||
:type key: string
|
||||
:param value: the new value of the `:param:key`
|
||||
"""
|
||||
self._args = [key, value]
|
||||
@ -208,20 +250,13 @@ class PluginEnabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is enabled in the Core.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
"""
|
||||
:param name: the plugin name
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [name]
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
class PluginDisabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is disabled in the Core.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
"""
|
||||
:param name: the plugin name
|
||||
:type name: string
|
||||
"""
|
||||
self._args = [name]
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
|
@ -36,21 +36,26 @@ from twisted.web import client, http
|
||||
from twisted.web.error import PageRedirect
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet import reactor
|
||||
from deluge.log import setupLogger, LOG as log
|
||||
from common import get_version
|
||||
import logging
|
||||
import os.path
|
||||
import zlib
|
||||
|
||||
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):
|
||||
def __init__(self, url, filename, part_callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
"""
|
||||
: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
|
||||
@ -84,15 +89,20 @@ class HTTPDownloader(client.HTTPDownloader):
|
||||
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
|
||||
|
||||
if "content-disposition" in headers and not self.force_filename:
|
||||
try:
|
||||
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.fileName)[0], new_file_name)
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
else:
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
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.fileName)[0], 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
|
||||
|
||||
self.fileName = new_file_name
|
||||
self.value = new_file_name
|
||||
|
||||
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
|
||||
location = headers["location"][0]
|
||||
@ -129,8 +139,6 @@ def sanitise_filename(filename):
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
|
||||
:raises IOError: when the filename exists
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
@ -141,18 +149,16 @@ def sanitise_filename(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)
|
||||
|
||||
if os.path.exists(filename):
|
||||
raise IOError, "File '%s' already exists!" % filename
|
||||
|
||||
return filename
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None, force_filename=False, allow_compression=True):
|
||||
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. You can also
|
||||
specify a callback function to be called as parts are received.
|
||||
|
@ -1,290 +0,0 @@
|
||||
deluge/configmanager.py
|
||||
deluge/httpdownloader.py
|
||||
deluge/error.py
|
||||
deluge/component.py
|
||||
deluge/log.py
|
||||
deluge/metafile.py
|
||||
deluge/config.py
|
||||
deluge/main.py
|
||||
deluge/__init__.py
|
||||
deluge/common.py
|
||||
deluge/bencode.py
|
||||
deluge/pluginmanagerbase.py
|
||||
deluge/event.py
|
||||
deluge/rencode.py
|
||||
deluge/decorators.py
|
||||
deluge/_libtorrent.py
|
||||
deluge/__rpcapi.py
|
||||
deluge/maketorrent.py
|
||||
deluge/plugins/__init__.py
|
||||
deluge/plugins/pluginbase.py
|
||||
deluge/plugins/init.py
|
||||
deluge/plugins/feeder/setup.py
|
||||
deluge/plugins/feeder/feeder/__init__.py
|
||||
deluge/plugins/feeder/feeder/core.py
|
||||
deluge/plugins/feeder/feeder/webui.py
|
||||
deluge/plugins/feeder/build/lib/feeder/__init__.py
|
||||
deluge/plugins/feeder/build/lib/feeder/core.py
|
||||
deluge/plugins/feeder/build/lib/feeder/webui.py
|
||||
deluge/plugins/label/setup.py
|
||||
deluge/plugins/label/label/test.py
|
||||
deluge/plugins/label/label/__init__.py
|
||||
deluge/plugins/label/label/core.py
|
||||
deluge/plugins/label/label/webui.py
|
||||
deluge/plugins/label/label/gtkui/__init__.py
|
||||
deluge/plugins/label/label/gtkui/label_config.py
|
||||
deluge/plugins/label/label/gtkui/submenu.py
|
||||
deluge/plugins/label/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/label/data/label_pref.glade
|
||||
deluge/plugins/label/label/data/label_options.glade
|
||||
deluge/plugins/label/build/lib/label/test.py
|
||||
deluge/plugins/label/build/lib/label/__init__.py
|
||||
deluge/plugins/label/build/lib/label/core.py
|
||||
deluge/plugins/label/build/lib/label/webui.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/__init__.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/label_config.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/submenu.py
|
||||
deluge/plugins/label/build/lib/label/gtkui/sidebar_menu.py
|
||||
deluge/plugins/label/build/lib/label/data/label_pref.glade
|
||||
deluge/plugins/label/build/lib/label/data/label_options.glade
|
||||
deluge/plugins/autoadd/setup.py
|
||||
deluge/plugins/autoadd/autoadd/__init__.py
|
||||
deluge/plugins/autoadd/autoadd/common.py
|
||||
deluge/plugins/autoadd/autoadd/core.py
|
||||
deluge/plugins/autoadd/autoadd/webui.py
|
||||
deluge/plugins/autoadd/autoadd/gtkui.py
|
||||
deluge/plugins/autoadd/autoadd/data/config.glade
|
||||
deluge/plugins/autoadd/autoadd/data/autoadd_options.glade
|
||||
deluge/plugins/autoadd/build/lib/autoadd/__init__.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/common.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/core.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/webui.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/gtkui.py
|
||||
deluge/plugins/autoadd/build/lib/autoadd/data/config.glade
|
||||
deluge/plugins/autoadd/build/lib/autoadd/data/autoadd_options.glade
|
||||
deluge/plugins/scheduler/setup.py
|
||||
deluge/plugins/scheduler/scheduler/__init__.py
|
||||
deluge/plugins/scheduler/scheduler/common.py
|
||||
deluge/plugins/scheduler/scheduler/core.py
|
||||
deluge/plugins/scheduler/scheduler/webui.py
|
||||
deluge/plugins/scheduler/scheduler/gtkui.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/__init__.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/common.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/core.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/webui.py
|
||||
deluge/plugins/scheduler/build/lib/scheduler/gtkui.py
|
||||
deluge/plugins/notifications/setup.py
|
||||
deluge/plugins/notifications/notifications/test.py
|
||||
deluge/plugins/notifications/notifications/__init__.py
|
||||
deluge/plugins/notifications/notifications/common.py
|
||||
deluge/plugins/notifications/notifications/core.py
|
||||
deluge/plugins/notifications/notifications/webui.py
|
||||
deluge/plugins/notifications/notifications/gtkui.py
|
||||
deluge/plugins/notifications/notifications/data/config.glade
|
||||
deluge/plugins/notifications/build/lib/notifications/test.py
|
||||
deluge/plugins/notifications/build/lib/notifications/__init__.py
|
||||
deluge/plugins/notifications/build/lib/notifications/common.py
|
||||
deluge/plugins/notifications/build/lib/notifications/core.py
|
||||
deluge/plugins/notifications/build/lib/notifications/webui.py
|
||||
deluge/plugins/notifications/build/lib/notifications/gtkui.py
|
||||
deluge/plugins/notifications/build/lib/notifications/data/config.glade
|
||||
deluge/plugins/stats/setup.py
|
||||
deluge/plugins/stats/stats/test_total.py
|
||||
deluge/plugins/stats/stats/test.py
|
||||
deluge/plugins/stats/stats/__init__.py
|
||||
deluge/plugins/stats/stats/graph.py
|
||||
deluge/plugins/stats/stats/common.py
|
||||
deluge/plugins/stats/stats/core.py
|
||||
deluge/plugins/stats/stats/webui.py
|
||||
deluge/plugins/stats/stats/gtkui.py
|
||||
deluge/plugins/stats/stats/data/tabs.glade
|
||||
deluge/plugins/stats/stats/data/config.glade
|
||||
deluge/plugins/stats/build/lib/stats/test_total.py
|
||||
deluge/plugins/stats/build/lib/stats/test.py
|
||||
deluge/plugins/stats/build/lib/stats/__init__.py
|
||||
deluge/plugins/stats/build/lib/stats/graph.py
|
||||
deluge/plugins/stats/build/lib/stats/common.py
|
||||
deluge/plugins/stats/build/lib/stats/core.py
|
||||
deluge/plugins/stats/build/lib/stats/webui.py
|
||||
deluge/plugins/stats/build/lib/stats/gtkui.py
|
||||
deluge/plugins/stats/build/lib/stats/data/tabs.glade
|
||||
deluge/plugins/stats/build/lib/stats/data/config.glade
|
||||
deluge/plugins/webui/setup.py
|
||||
deluge/plugins/webui/webui/__init__.py
|
||||
deluge/plugins/webui/webui/common.py
|
||||
deluge/plugins/webui/webui/core.py
|
||||
deluge/plugins/webui/webui/gtkui.py
|
||||
deluge/plugins/webui/webui/data/config.glade
|
||||
deluge/plugins/webui/build/lib/webui/__init__.py
|
||||
deluge/plugins/webui/build/lib/webui/common.py
|
||||
deluge/plugins/webui/build/lib/webui/core.py
|
||||
deluge/plugins/webui/build/lib/webui/gtkui.py
|
||||
deluge/plugins/webui/build/lib/webui/data/config.glade
|
||||
deluge/plugins/extractor/setup.py
|
||||
deluge/plugins/extractor/build/lib/extractor/__init__.py
|
||||
deluge/plugins/extractor/build/lib/extractor/common.py
|
||||
deluge/plugins/extractor/build/lib/extractor/core.py
|
||||
deluge/plugins/extractor/build/lib/extractor/webui.py
|
||||
deluge/plugins/extractor/build/lib/extractor/gtkui.py
|
||||
deluge/plugins/extractor/build/lib/extractor/data/extractor_prefs.glade
|
||||
deluge/plugins/extractor/extractor/__init__.py
|
||||
deluge/plugins/extractor/extractor/common.py
|
||||
deluge/plugins/extractor/extractor/core.py
|
||||
deluge/plugins/extractor/extractor/webui.py
|
||||
deluge/plugins/extractor/extractor/gtkui.py
|
||||
deluge/plugins/extractor/extractor/data/extractor_prefs.glade
|
||||
deluge/plugins/execute/setup.py
|
||||
deluge/plugins/execute/build/lib/execute/__init__.py
|
||||
deluge/plugins/execute/build/lib/execute/common.py
|
||||
deluge/plugins/execute/build/lib/execute/core.py
|
||||
deluge/plugins/execute/build/lib/execute/webui.py
|
||||
deluge/plugins/execute/build/lib/execute/gtkui.py
|
||||
deluge/plugins/execute/build/lib/execute/data/execute_prefs.glade
|
||||
deluge/plugins/execute/execute/__init__.py
|
||||
deluge/plugins/execute/execute/common.py
|
||||
deluge/plugins/execute/execute/core.py
|
||||
deluge/plugins/execute/execute/webui.py
|
||||
deluge/plugins/execute/execute/gtkui.py
|
||||
deluge/plugins/execute/execute/data/execute_prefs.glade
|
||||
deluge/plugins/example/setup.py
|
||||
deluge/plugins/example/build/lib/example/__init__.py
|
||||
deluge/plugins/example/build/lib/example/common.py
|
||||
deluge/plugins/example/build/lib/example/core.py
|
||||
deluge/plugins/example/build/lib/example/webui.py
|
||||
deluge/plugins/example/build/lib/example/gtkui.py
|
||||
deluge/plugins/example/example/__init__.py
|
||||
deluge/plugins/example/example/common.py
|
||||
deluge/plugins/example/example/core.py
|
||||
deluge/plugins/example/example/webui.py
|
||||
deluge/plugins/example/example/gtkui.py
|
||||
deluge/plugins/freespace/setup.py
|
||||
deluge/plugins/freespace/build/lib/freespace/__init__.py
|
||||
deluge/plugins/freespace/build/lib/freespace/common.py
|
||||
deluge/plugins/freespace/build/lib/freespace/core.py
|
||||
deluge/plugins/freespace/build/lib/freespace/webui.py
|
||||
deluge/plugins/freespace/build/lib/freespace/gtkui.py
|
||||
deluge/plugins/freespace/build/lib/freespace/data/config.glade
|
||||
deluge/plugins/freespace/freespace/__init__.py
|
||||
deluge/plugins/freespace/freespace/common.py
|
||||
deluge/plugins/freespace/freespace/core.py
|
||||
deluge/plugins/freespace/freespace/webui.py
|
||||
deluge/plugins/freespace/freespace/gtkui.py
|
||||
deluge/plugins/freespace/freespace/data/config.glade
|
||||
deluge/plugins/blocklist/setup.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/decompressers.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/detect.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/readers.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/common.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/core.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/webui.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/build/lib/blocklist/data/blocklist_pref.glade
|
||||
deluge/plugins/blocklist/blocklist/peerguardian.py
|
||||
deluge/plugins/blocklist/blocklist/decompressers.py
|
||||
deluge/plugins/blocklist/blocklist/detect.py
|
||||
deluge/plugins/blocklist/blocklist/readers.py
|
||||
deluge/plugins/blocklist/blocklist/__init__.py
|
||||
deluge/plugins/blocklist/blocklist/common.py
|
||||
deluge/plugins/blocklist/blocklist/core.py
|
||||
deluge/plugins/blocklist/blocklist/webui.py
|
||||
deluge/plugins/blocklist/blocklist/gtkui.py
|
||||
deluge/plugins/blocklist/blocklist/data/blocklist_pref.glade
|
||||
deluge/core/eventmanager.py
|
||||
deluge/core/autoadd.py
|
||||
deluge/core/authmanager.py
|
||||
deluge/core/rpcserver.py
|
||||
deluge/core/torrentmanager.py
|
||||
deluge/core/oldstateupgrader.py
|
||||
deluge/core/__init__.py
|
||||
deluge/core/torrent.py
|
||||
deluge/core/pluginmanager.py
|
||||
deluge/core/core.py
|
||||
deluge/core/daemon.py
|
||||
deluge/core/alertmanager.py
|
||||
deluge/core/preferencesmanager.py
|
||||
deluge/core/filtermanager.py
|
||||
deluge/ui/sessionproxy.py
|
||||
deluge/ui/ui.py
|
||||
deluge/ui/session.py
|
||||
deluge/ui/tracker_icons.py
|
||||
deluge/ui/__init__.py
|
||||
deluge/ui/common.py
|
||||
deluge/ui/Win32IconImagePlugin.py
|
||||
deluge/ui/client.py
|
||||
deluge/ui/countries.py
|
||||
deluge/ui/coreconfig.py
|
||||
deluge/ui/web/server.py
|
||||
deluge/ui/web/web.py
|
||||
deluge/ui/web/__init__.py
|
||||
deluge/ui/web/common.py
|
||||
deluge/ui/web/pluginmanager.py
|
||||
deluge/ui/web/gen_gettext.py
|
||||
deluge/ui/web/auth.py
|
||||
deluge/ui/web/json_api.py
|
||||
deluge/ui/gtkui/connectionmanager.py
|
||||
deluge/ui/gtkui/torrentdetails.py
|
||||
deluge/ui/gtkui/queuedtorrents.py
|
||||
deluge/ui/gtkui/addtorrentdialog.py
|
||||
deluge/ui/gtkui/__init__.py
|
||||
deluge/ui/gtkui/status_tab.py
|
||||
deluge/ui/gtkui/preferences.py
|
||||
deluge/ui/gtkui/mainwindow.py
|
||||
deluge/ui/gtkui/notification.py
|
||||
deluge/ui/gtkui/ipcinterface.py
|
||||
deluge/ui/gtkui/createtorrentdialog.py
|
||||
deluge/ui/gtkui/torrentview.py
|
||||
deluge/ui/gtkui/listview.py
|
||||
deluge/ui/gtkui/systemtray.py
|
||||
deluge/ui/gtkui/common.py
|
||||
deluge/ui/gtkui/pluginmanager.py
|
||||
deluge/ui/gtkui/menubar.py
|
||||
deluge/ui/gtkui/sidebar.py
|
||||
deluge/ui/gtkui/statusbar.py
|
||||
deluge/ui/gtkui/filtertreeview.py
|
||||
deluge/ui/gtkui/new_release_dialog.py
|
||||
deluge/ui/gtkui/options_tab.py
|
||||
deluge/ui/gtkui/peers_tab.py
|
||||
deluge/ui/gtkui/details_tab.py
|
||||
deluge/ui/gtkui/files_tab.py
|
||||
deluge/ui/gtkui/gtkui.py
|
||||
deluge/ui/gtkui/edittrackersdialog.py
|
||||
deluge/ui/gtkui/removetorrentdialog.py
|
||||
deluge/ui/gtkui/toolbar.py
|
||||
deluge/ui/gtkui/dialogs.py
|
||||
deluge/ui/gtkui/aboutdialog.py
|
||||
deluge/ui/gtkui/glade/filtertree_menu.glade
|
||||
deluge/ui/gtkui/glade/main_window.glade
|
||||
deluge/ui/gtkui/glade/remove_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/create_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/connection_manager.glade
|
||||
deluge/ui/gtkui/glade/preferences_dialog.glade
|
||||
deluge/ui/gtkui/glade/torrent_menu.glade
|
||||
deluge/ui/gtkui/glade/queuedtorrents.glade
|
||||
deluge/ui/gtkui/glade/move_storage_dialog.glade
|
||||
deluge/ui/gtkui/glade/add_torrent_dialog.glade
|
||||
deluge/ui/gtkui/glade/dgtkpopups.glade
|
||||
deluge/ui/gtkui/glade/tray_menu.glade
|
||||
deluge/ui/gtkui/glade/edit_trackers.glade
|
||||
deluge/ui/console/colors.py
|
||||
deluge/ui/console/eventlog.py
|
||||
deluge/ui/console/main.py
|
||||
deluge/ui/console/__init__.py
|
||||
deluge/ui/console/statusbars.py
|
||||
deluge/ui/console/screen.py
|
||||
deluge/ui/console/commands/plugin.py
|
||||
deluge/ui/console/commands/info.py
|
||||
deluge/ui/console/commands/recheck.py
|
||||
deluge/ui/console/commands/quit.py
|
||||
deluge/ui/console/commands/connect.py
|
||||
deluge/ui/console/commands/help.py
|
||||
deluge/ui/console/commands/add.py
|
||||
deluge/ui/console/commands/config.py
|
||||
deluge/ui/console/commands/__init__.py
|
||||
deluge/ui/console/commands/cache.py
|
||||
deluge/ui/console/commands/debug.py
|
||||
deluge/ui/console/commands/pause.py
|
||||
deluge/ui/console/commands/rm.py
|
||||
deluge/ui/console/commands/halt.py
|
||||
deluge/ui/console/commands/resume.py
|
4750
deluge/i18n/ar.po
4750
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
7003
deluge/i18n/ast.po
7003
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
6177
deluge/i18n/be.po
6177
deluge/i18n/be.po
File diff suppressed because it is too large
Load Diff
4895
deluge/i18n/bg.po
4895
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
6026
deluge/i18n/bn.po
6026
deluge/i18n/bn.po
File diff suppressed because it is too large
Load Diff
6097
deluge/i18n/bs.po
6097
deluge/i18n/bs.po
File diff suppressed because it is too large
Load Diff
3796
deluge/i18n/ca.po
3796
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
4777
deluge/i18n/cs.po
4777
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
6069
deluge/i18n/cy.po
6069
deluge/i18n/cy.po
File diff suppressed because it is too large
Load Diff
5057
deluge/i18n/da.po
5057
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
3936
deluge/i18n/de.po
3936
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4528
deluge/i18n/el.po
4528
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
4641
deluge/i18n/en_AU.po
4641
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
4969
deluge/i18n/en_CA.po
4969
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
5135
deluge/i18n/en_GB.po
5135
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
6088
deluge/i18n/eo.po
6088
deluge/i18n/eo.po
File diff suppressed because it is too large
Load Diff
4757
deluge/i18n/es.po
4757
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
4412
deluge/i18n/et.po
4412
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
3626
deluge/i18n/eu.po
3626
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
6218
deluge/i18n/fa.po
6218
deluge/i18n/fa.po
File diff suppressed because it is too large
Load Diff
4663
deluge/i18n/fi.po
4663
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
4981
deluge/i18n/fr.po
4981
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
7069
deluge/i18n/fy.po
7069
deluge/i18n/fy.po
File diff suppressed because it is too large
Load Diff
4161
deluge/i18n/gl.po
4161
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
4531
deluge/i18n/he.po
4531
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
4248
deluge/i18n/hi.po
4248
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
6508
deluge/i18n/hr.po
6508
deluge/i18n/hr.po
File diff suppressed because it is too large
Load Diff
5012
deluge/i18n/hu.po
5012
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
3889
deluge/i18n/id.po
3889
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
4512
deluge/i18n/is.po
4512
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
5469
deluge/i18n/it.po
5469
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/iu.po
6013
deluge/i18n/iu.po
File diff suppressed because it is too large
Load Diff
4634
deluge/i18n/ja.po
4634
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
6203
deluge/i18n/ka.po
6203
deluge/i18n/ka.po
File diff suppressed because it is too large
Load Diff
4857
deluge/i18n/kk.po
4857
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
6143
deluge/i18n/kn.po
6143
deluge/i18n/kn.po
File diff suppressed because it is too large
Load Diff
4936
deluge/i18n/ko.po
4936
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
6041
deluge/i18n/ku.po
6041
deluge/i18n/ku.po
File diff suppressed because it is too large
Load Diff
6020
deluge/i18n/la.po
6020
deluge/i18n/la.po
File diff suppressed because it is too large
Load Diff
5138
deluge/i18n/lt.po
5138
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
5190
deluge/i18n/lv.po
5190
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
6115
deluge/i18n/mk.po
6115
deluge/i18n/mk.po
File diff suppressed because it is too large
Load Diff
5453
deluge/i18n/ms.po
5453
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
4992
deluge/i18n/nb.po
4992
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
6024
deluge/i18n/nds.po
6024
deluge/i18n/nds.po
File diff suppressed because it is too large
Load Diff
5318
deluge/i18n/nl.po
5318
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
5225
deluge/i18n/pl.po
5225
deluge/i18n/pl.po
File diff suppressed because it is too large
Load Diff
6014
deluge/i18n/pms.po
6014
deluge/i18n/pms.po
File diff suppressed because it is too large
Load Diff
4628
deluge/i18n/pt.po
4628
deluge/i18n/pt.po
File diff suppressed because it is too large
Load Diff
4496
deluge/i18n/pt_BR.po
4496
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load Diff
4671
deluge/i18n/ro.po
4671
deluge/i18n/ro.po
File diff suppressed because it is too large
Load Diff
4948
deluge/i18n/ru.po
4948
deluge/i18n/ru.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/si.po
6013
deluge/i18n/si.po
File diff suppressed because it is too large
Load Diff
4154
deluge/i18n/sk.po
4154
deluge/i18n/sk.po
File diff suppressed because it is too large
Load Diff
4630
deluge/i18n/sl.po
4630
deluge/i18n/sl.po
File diff suppressed because it is too large
Load Diff
4435
deluge/i18n/sr.po
4435
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
5552
deluge/i18n/sv.po
5552
deluge/i18n/sv.po
File diff suppressed because it is too large
Load Diff
6018
deluge/i18n/ta.po
6018
deluge/i18n/ta.po
File diff suppressed because it is too large
Load Diff
3453
deluge/i18n/th.po
3453
deluge/i18n/th.po
File diff suppressed because it is too large
Load Diff
6014
deluge/i18n/tl.po
6014
deluge/i18n/tl.po
File diff suppressed because it is too large
Load Diff
6013
deluge/i18n/tlh.po
6013
deluge/i18n/tlh.po
File diff suppressed because it is too large
Load Diff
4963
deluge/i18n/tr.po
4963
deluge/i18n/tr.po
File diff suppressed because it is too large
Load Diff
5340
deluge/i18n/uk.po
5340
deluge/i18n/uk.po
File diff suppressed because it is too large
Load Diff
6068
deluge/i18n/vi.po
6068
deluge/i18n/vi.po
File diff suppressed because it is too large
Load Diff
5116
deluge/i18n/zh_CN.po
5116
deluge/i18n/zh_CN.po
File diff suppressed because it is too large
Load Diff
6027
deluge/i18n/zh_HK.po
6027
deluge/i18n/zh_HK.po
File diff suppressed because it is too large
Load Diff
4361
deluge/i18n/zh_TW.po
4361
deluge/i18n/zh_TW.po
File diff suppressed because it is too large
Load Diff
256
deluge/log.py
256
deluge/log.py
@ -2,6 +2,7 @@
|
||||
# log.py
|
||||
#
|
||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
@ -33,18 +34,101 @@
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
"""Logging functions"""
|
||||
|
||||
import os
|
||||
import inspect
|
||||
import logging
|
||||
from deluge import common
|
||||
from twisted.internet import defer
|
||||
from twisted.python.log import PythonLoggingObserver
|
||||
|
||||
__all__ = ["setupLogger", "setLoggerLevel", "getPluginLogger", "LOG"]
|
||||
|
||||
LoggingLoggerClass = logging.getLoggerClass()
|
||||
|
||||
if 'dev' in common.get_version():
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s"
|
||||
else:
|
||||
DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(levelname)-8s][%%(name)-%ds] %%(message)s"
|
||||
MAX_LOGGER_NAME_LENGTH = 10
|
||||
|
||||
class Logging(LoggingLoggerClass):
|
||||
def __init__(self, logger_name):
|
||||
LoggingLoggerClass.__init__(self, logger_name)
|
||||
|
||||
# This makes module name padding increase to the biggest module name
|
||||
# so that logs keep readability.
|
||||
global MAX_LOGGER_NAME_LENGTH
|
||||
if len(logger_name) > MAX_LOGGER_NAME_LENGTH:
|
||||
MAX_LOGGER_NAME_LENGTH = len(logger_name)
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.setFormatter(logging.Formatter(
|
||||
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
|
||||
datefmt="%H:%M:%S"
|
||||
))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def garbage(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.log(self, 1, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def trace(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.log(self, 5, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def debug(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.debug(self, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def info(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.info(self, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def warning(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.warning(self, msg, *args, **kwargs)
|
||||
|
||||
warn = warning
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def error(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.error(self, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def critical(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.critical(self, msg, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def exception(self, msg, *args, **kwargs):
|
||||
yield LoggingLoggerClass.exception(self, msg, *args, **kwargs)
|
||||
|
||||
def findCaller(self):
|
||||
f = logging.currentframe().f_back
|
||||
rv = "(unknown file)", 0, "(unknown function)"
|
||||
while hasattr(f, "f_code"):
|
||||
co = f.f_code
|
||||
filename = os.path.normcase(co.co_filename)
|
||||
if filename in (__file__.replace('.pyc', '.py'),
|
||||
defer.__file__.replace('.pyc', '.py')):
|
||||
f = f.f_back
|
||||
continue
|
||||
rv = (filename, f.f_lineno, co.co_name)
|
||||
break
|
||||
return rv
|
||||
|
||||
levels = {
|
||||
"none": logging.NOTSET,
|
||||
"info": logging.INFO,
|
||||
"warn": logging.WARNING,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"none": logging.CRITICAL,
|
||||
"debug": logging.DEBUG
|
||||
"debug": logging.DEBUG,
|
||||
"trace": 5,
|
||||
"garbage": 1
|
||||
}
|
||||
|
||||
|
||||
def setupLogger(level="error", filename=None, filemode="w"):
|
||||
"""
|
||||
Sets up the basic logger and if `:param:filename` is set, then it will log
|
||||
@ -53,30 +137,168 @@ def setupLogger(level="error", filename=None, filemode="w"):
|
||||
:param level: str, the level to log
|
||||
:param filename: str, the file to log to
|
||||
"""
|
||||
import logging
|
||||
|
||||
if not level or level not in levels:
|
||||
level = "error"
|
||||
if logging.getLoggerClass() is not Logging:
|
||||
logging.setLoggerClass(Logging)
|
||||
logging.addLevelName(5, 'TRACE')
|
||||
logging.addLevelName(1, 'GARBAGE')
|
||||
|
||||
logging.basicConfig(
|
||||
level=levels[level],
|
||||
format="[%(levelname)-8s] %(asctime)s %(module)s:%(lineno)d %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
filename=filename,
|
||||
filemode=filemode
|
||||
level = levels.get(level, logging.ERROR)
|
||||
|
||||
rootLogger = logging.getLogger()
|
||||
|
||||
if filename and filemode=='a':
|
||||
import logging.handlers
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
filename, filemode,
|
||||
maxBytes=50*1024*1024, # 50 Mb
|
||||
backupCount=3,
|
||||
encoding='utf-8',
|
||||
delay=0
|
||||
)
|
||||
elif filename and filemode=='w':
|
||||
import logging.handlers
|
||||
handler = getattr(
|
||||
logging.handlers, 'WatchedFileHandler', logging.FileHandler)(
|
||||
filename, filemode, 'utf-8', delay=0
|
||||
)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
|
||||
handler.setLevel(level)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
|
||||
datefmt="%H:%M:%S"
|
||||
)
|
||||
|
||||
def setLoggerLevel(level):
|
||||
handler.setFormatter(formatter)
|
||||
rootLogger.addHandler(handler)
|
||||
rootLogger.setLevel(level)
|
||||
|
||||
twisted_logging = PythonLoggingObserver('twisted')
|
||||
twisted_logging.start()
|
||||
logging.getLogger("twisted").setLevel(level)
|
||||
|
||||
def tweak_logging_levels():
|
||||
"""This function allows tweaking the logging levels for all or some loggers.
|
||||
This is mostly usefull for developing purposes hence the contents of the
|
||||
file are NOT like regular deluge config file's.
|
||||
|
||||
To use is, create a file named "logging.conf" on your Deluge's config dir
|
||||
with contents like for example:
|
||||
deluge:warn
|
||||
deluge.core:debug
|
||||
deluge.plugin:error
|
||||
|
||||
What the above mean is the logger "deluge" will be set to the WARN level,
|
||||
the "deluge.core" logger will be set to the DEBUG level and the
|
||||
"deluge.plugin" will be set to the ERROR level.
|
||||
|
||||
Remember, one rule per line and this WILL override the setting passed from
|
||||
the command line.
|
||||
"""
|
||||
from deluge import configmanager
|
||||
logging_config_file = os.path.join(configmanager.get_config_dir(),
|
||||
'logging.conf')
|
||||
if not os.path.isfile(logging_config_file):
|
||||
return
|
||||
log = logging.getLogger(__name__)
|
||||
log.warn("logging.conf found! tweaking logging levels from %s",
|
||||
logging_config_file)
|
||||
for line in open(logging_config_file, 'r').readlines():
|
||||
if line.strip().startswith("#"):
|
||||
continue
|
||||
name, level = line.strip().split(':')
|
||||
if level not in levels:
|
||||
continue
|
||||
|
||||
log.warn("Setting logger \"%s\" to logging level \"%s\"", name, level)
|
||||
setLoggerLevel(level, name)
|
||||
|
||||
|
||||
def setLoggerLevel(level, logger_name=None):
|
||||
"""
|
||||
Sets the logger level.
|
||||
|
||||
:param level: str, a string representing the desired level
|
||||
:param logger_name: str, a string representing desired logger name for which
|
||||
the level should change. The default is "None" will will
|
||||
tweak the root logger level.
|
||||
|
||||
"""
|
||||
if level not in levels:
|
||||
return
|
||||
logging.getLogger(logger_name).setLevel(levels.get(level, "error"))
|
||||
|
||||
global LOG
|
||||
LOG.setLevel(levels[level])
|
||||
|
||||
# Get the logger
|
||||
LOG = logging.getLogger("deluge")
|
||||
def getPluginLogger(logger_name):
|
||||
import warnings
|
||||
stack = inspect.stack()
|
||||
stack.pop(0) # The logging call from this module
|
||||
module_stack = stack.pop(0) # The module that called the log function
|
||||
caller_module = inspect.getmodule(module_stack[0])
|
||||
# In some weird cases caller_module might be None, try to continue
|
||||
caller_module_name = getattr(caller_module, '__name__', '')
|
||||
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
|
||||
module_stack[1], module_stack[2],
|
||||
caller_module_name)
|
||||
|
||||
if 'deluge.plugins.' in logger_name:
|
||||
return logging.getLogger(logger_name)
|
||||
return logging.getLogger("deluge.plugin.%s" % logger_name)
|
||||
|
||||
|
||||
DEPRECATION_WARNING = """You seem to be using old style logging on your code, ie:
|
||||
from deluge.log import LOG as log
|
||||
|
||||
or:
|
||||
from deluge.log import getPluginLogger
|
||||
|
||||
This has been deprecated in favour of an enhanced logging system and both "LOG"
|
||||
and "getPluginLogger" will be removed on the next major version release of Deluge,
|
||||
meaning, code will break, specially plugins.
|
||||
If you're seeing this message and you're not the developer of the plugin which
|
||||
triggered this warning, please report to it's author.
|
||||
If you're the developer, please stop using the above code and instead use:
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
The above will result in, regarding the "Label" plugin for example a log message similar to:
|
||||
15:33:54 [deluge.plugins.label.core:78 ][INFO ] *** Start Label plugin ***
|
||||
|
||||
Triggering code:"""
|
||||
|
||||
class __BackwardsCompatibleLOG(object):
|
||||
def __getattribute__(self, name):
|
||||
import warnings
|
||||
logger_name = 'deluge'
|
||||
stack = inspect.stack()
|
||||
stack.pop(0) # The logging call from this module
|
||||
module_stack = stack.pop(0) # The module that called the log function
|
||||
caller_module = inspect.getmodule(module_stack[0])
|
||||
# In some weird cases caller_module might be None, try to continue
|
||||
caller_module_name = getattr(caller_module, '__name__', '')
|
||||
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
|
||||
module_stack[1], module_stack[2],
|
||||
caller_module_name)
|
||||
if caller_module:
|
||||
for member in stack:
|
||||
module = inspect.getmodule(member[0])
|
||||
if not module:
|
||||
continue
|
||||
if module.__name__ in ('deluge.plugins.pluginbase',
|
||||
'deluge.plugins.init'):
|
||||
logger_name += '.plugin.%s' % caller_module_name
|
||||
# Monkey Patch The Plugin Module
|
||||
caller_module.log = logging.getLogger(logger_name)
|
||||
break
|
||||
else:
|
||||
logging.getLogger(logger_name).warning(
|
||||
"Unable to monkey-patch the calling module's `log` attribute! "
|
||||
"You should really update and rebuild your plugins..."
|
||||
)
|
||||
return getattr(logging.getLogger(logger_name), name)
|
||||
|
||||
LOG = __BackwardsCompatibleLOG()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user