Compare commits
27 Commits
extjs4-por
...
deluge-0.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
062f138e0d | ||
![]() |
a700009391 | ||
![]() |
0ed95be971 | ||
![]() |
709c4fb81d | ||
![]() |
f143d52f66 | ||
![]() |
bc9cfe94d5 | ||
![]() |
c8f97a9421 | ||
![]() |
49c0e3462a | ||
![]() |
d7149278cc | ||
![]() |
c6779ab0f1 | ||
![]() |
2b29dc7366 | ||
![]() |
e2aa751822 | ||
![]() |
641ae96cee | ||
![]() |
1c1398cddb | ||
![]() |
6520d43f69 | ||
![]() |
d33923d77c | ||
![]() |
5b762efa86 | ||
![]() |
117aa75346 | ||
![]() |
0fea61bd96 | ||
![]() |
fc16de269e | ||
![]() |
4455e702f0 | ||
![]() |
63e4001faf | ||
![]() |
5197a99eab | ||
![]() |
97fd644d57 | ||
![]() |
a5f9812abf | ||
![]() |
69ab940a10 | ||
![]() |
5f2ff7b4f6 |
10
.gitattributes
vendored
10
.gitattributes
vendored
@@ -1,10 +0,0 @@
|
||||
/libtorrent export-ignore
|
||||
/win32 export-ignore
|
||||
docs/build export-ignore
|
||||
docs/source export-ignore
|
||||
/tests export-ignore
|
||||
deluge/scripts export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitmodules export-ignore
|
||||
.gitignore export-ignore
|
||||
*.py diff=python
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,11 +0,0 @@
|
||||
*~
|
||||
build
|
||||
dist
|
||||
*egg-info
|
||||
*.egg
|
||||
*.log
|
||||
*.pyc
|
||||
*.tar.*
|
||||
_trial_temp
|
||||
deluge/i18n/*/
|
||||
*.desktop
|
255
ChangeLog
255
ChangeLog
@@ -1,149 +1,120 @@
|
||||
=== 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.
|
||||
Deluge 0.5.6 (xx October 2007)
|
||||
* Web Interface Plugin
|
||||
* Hopefully fix "losing data" and having to re-download parts (for real this time :p)
|
||||
* Use new full allocation method which does not create files until one of its
|
||||
pieces is downloaded
|
||||
* Tray lock password is no longer stored in plain text
|
||||
* Update the Scheduler plugin and fix a bunch of bugs on it
|
||||
* Double-clicking on a torrent opens up its containing folder
|
||||
* Fix SpeedLimiter plugin when setting upload limits
|
||||
* Fix MoveTorrent plugin when moving actively downloading torrents
|
||||
* Pause torrents while importing blocklist and resume them when finished
|
||||
* Remove TorrentPieces and disable its use
|
||||
* A whole bunch of stuff for Win32
|
||||
* Add private flag to TorrentCreator plugin
|
||||
* Use SVG for internal logo usage (except on Win32)
|
||||
* Use theme for tray icon instead of hard-coded
|
||||
* Properly release port on shutdown
|
||||
* TorrentFiles plugin now has progress bars
|
||||
* Removing torrent files no longer deletes files that werent part of the torrent
|
||||
* New max half-open connections setting to deal with cheap/broken routers
|
||||
* Inherit UPnP fixes from libtorrent
|
||||
* Use threading for everything, instead of spawnning
|
||||
|
||||
==== Core ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
* Implement #457 progress bars for folders
|
||||
* Implement #1012 httpdownloader supports gzip decoding
|
||||
* #496: Remove deprecated functions in favour of get_session_status()
|
||||
* #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
|
||||
Deluge 0.5.5 (09 September 2007)
|
||||
* Editing a torrent's tracker list is now persistent between sessions
|
||||
* Persistence between sessions for Speed Limiter, Web Seed and Desired Ratio
|
||||
plugins
|
||||
* New wizard to aid first-time users with configuration
|
||||
* Reorderable tabs and remember order (with exception of details tab)
|
||||
* Fix losing data and having to re-download parts
|
||||
* Fix password lock showing when main window is not hidden
|
||||
* Get rid of the plugin manager and integrate it into preferences
|
||||
* New Move torrent plugin - takes over for "move completed downloads" feature
|
||||
and provides a "Move Torrent" option when right-clicking on a torrent
|
||||
* Save column widths
|
||||
* Queue order after restart fixes.
|
||||
* Use payload instead of including protocol overhead to ease user confusion
|
||||
of seeding torrents "downloading"
|
||||
* New Web Seed plugin for adding URLs to torrents for http seeding
|
||||
* Add FAST-extension (http://www.bittorrent.org/fast_extensions.html)
|
||||
|
||||
==== 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.
|
||||
Deluge 0.5.4.1 (10 August 2007)
|
||||
* Add "Open containing folder" and "Open File" to the torrent and file
|
||||
menu, respectively
|
||||
* Load Blocklist plugin last and have it not lock up the interface during
|
||||
import
|
||||
* Add full allocation to preferences for clarification
|
||||
* Catch SIGINT, SIGHUP, SIGTERM and Gnome logoff to quit properly
|
||||
* Add send local info to developers
|
||||
* Fix up pieces, peers and files plugins
|
||||
* UPnP fixes
|
||||
* Add ExtraStats plugin
|
||||
* FreeBSD full allocation fix
|
||||
* Added per torrent max upload slots and max connections preferences
|
||||
* A lot of other less visible improvements
|
||||
|
||||
Deluge 0.5.4 (06 August 2007)
|
||||
|
||||
==== WebUI ====
|
||||
* Migrate to ExtJS 3.1
|
||||
* Add gzip compression of HTTP data to the server
|
||||
* Improve the efficiency of the TorrentGrid
|
||||
* Tray message includes session totals
|
||||
* Ticket #198 - Display peers countries in the Peers tab.
|
||||
* Ticket #474 - Multiple password prompts displayed
|
||||
* Pause all/resume all in tray menu
|
||||
* Peers and Files tabs are now plugins
|
||||
* New Location plugin
|
||||
* Option to use a random port every time
|
||||
* Proxy system redone - you can now specify different information for each
|
||||
type of proxy (DHT, peer, tracker, web-seed)
|
||||
* TorrentPieces plugin to view piece updates and show pieces table per file
|
||||
* EventLogger plugin to view/log every activity
|
||||
* SpeedLimiter plugin, which allows you to set speed limits on a per-torrent
|
||||
basis
|
||||
* New release alerts - Client will inform user if their version of deluge is
|
||||
outdated
|
||||
|
||||
==== Blocklist ====
|
||||
* Implement local blocklist support
|
||||
* #861: Pause transfers until blocklist is imported
|
||||
Deluge 0.5.3 (25 July 2007)
|
||||
|
||||
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
|
||||
==== Core ====
|
||||
* Implement new RPC protocol DelugeRPC replacing XMLRPC
|
||||
* Move to a twisted framework
|
||||
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
|
||||
* Use system GeoIP database if available, this is now an optional dependency
|
||||
* Added ChangeLog
|
||||
* Ticket #53 - Added files priorities within torrent
|
||||
* Ticket #111 - Remember directory of last added torrent
|
||||
* Ticket #232 - Added Move completed downloads to feature
|
||||
* Ticket #245 - Added ability to select torrent files before starting
|
||||
* Ticked #368 - Added ability to prioritize first and last pieces of files
|
||||
in torrents
|
||||
* Ticket #371 - Proper full storage allocation of files on reiser4 and
|
||||
ntfs-3g filesystems
|
||||
* Ticket #420 - Show size of torrent minus size of unselected files as Total
|
||||
Size
|
||||
* Ticket #405 - Properly start in tray when run deluge --tray
|
||||
* Ticket #437 - Gracefully upgrade from old versions
|
||||
* Picking a file to not download now checks for compact_mode status to prevent
|
||||
all sorts of problems such as downloading pieces into the wrong file
|
||||
* RSS plugin inclusion
|
||||
* Added ability to queue new torrents above completed ones when
|
||||
seeds are set to queue at the bottom
|
||||
* Added availability and piece size display in details, availailability
|
||||
column
|
||||
* Added ability to automatically remove torrents when max share ratio is set
|
||||
* Show text from clipboard in Add URL dialog only if it looks like an URL
|
||||
* Added Torrent Notification plugin
|
||||
* Added event handling callbacks for plugins
|
||||
* Added ability to designate a torrent as private (in file selection dialog)
|
||||
* Added merging trackers of duplicate torrents
|
||||
* Details, Peers and Files tabs more responsible and their perfomance
|
||||
greatly improved especially on torrents with many files and peers
|
||||
* A lot of other less visible improvements
|
||||
|
||||
==== GtkUI ====
|
||||
* Remove SignalReceiver
|
||||
* Implemented a cross-platform IPC method thus removing the DBUS dependency
|
||||
* Implement a "True" Classic Mode where there is no longer a separate daemon process
|
||||
* Add preferences option "Add torrent in paused state"
|
||||
* Add tracker icons to the Tracker column
|
||||
* Implement #259 show tooltip with country name in the peers tab
|
||||
* Add an error category to the tracker sidebar list
|
||||
* Add Find More Plugins button to Plugins preference page
|
||||
* Fix #518 remove header in add torrent dialog to save vertical space
|
||||
* Add a Cache preferences page to adjust cache settings and examine cache status
|
||||
* Add ability to rename files prior to adding them
|
||||
* Fix shutdown handler with GNOME session manager
|
||||
* Allow 4 MiB piece sizes when creating a torrent
|
||||
Deluge 0.5.2 (05 July 2007)
|
||||
* ticket #6 - Torrent creation built into main client
|
||||
* ticket #315 - Plugins implemented as modules
|
||||
* ticket #310 - Configuration options for PEX and UPnP
|
||||
* ticket #390 - Individual file progress shown in File tab
|
||||
* The usual slew of improvements
|
||||
|
||||
==== ConsoleUI ====
|
||||
* Changed to use curses for a more interactive client
|
||||
|
||||
==== WebUI ====
|
||||
* Move over to using Twisted-Web for the webserver.
|
||||
* Move to only AJAX interface built upon Ext-JS.
|
||||
|
||||
==== Plugins ====
|
||||
* Add Scheduler plugin
|
||||
* Add Extractor plugin
|
||||
|
||||
==== Misc ====
|
||||
* PyGTK dependency bumped to => 2.12 to use new tooltip system
|
||||
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
|
||||
* Remove GeoIP database from the source tree
|
||||
|
||||
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
|
||||
==== Core ====
|
||||
* Implement #79 ability to change outgoing port range
|
||||
* Implement #296 ability to change peer TOS byte
|
||||
* Add per-torrent move on completed settings
|
||||
* Implement #414 use async save_resume_data method
|
||||
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
|
||||
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
|
||||
* Remove remaining gtk functions in common
|
||||
* Tracker icons.
|
||||
* Add ETA for torrents with stop at seed ratio set
|
||||
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
|
||||
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
|
||||
* Add -P, --pidfile option to deluged
|
||||
|
||||
==== GtkUI ====
|
||||
* Add peer progress to the peers tab
|
||||
* Add ability to manually add peers
|
||||
* Sorting # column will place downloaders above seeds
|
||||
* Remove dependency on libtorrent for add torrent dialog
|
||||
* Allow adding multiple trackers at once in the edit tracker dialog
|
||||
* Implement #28 Create Torrent Dialog
|
||||
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
|
||||
* Implement #428 the ability to rename files and directories
|
||||
* Implement #229 add date added column
|
||||
* Implement #596 show speeds in title
|
||||
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
|
||||
* Fix #624 do not allow changing file priorities when using compact allocation
|
||||
* Fix #602 re-did files/peers tab state saving/loading
|
||||
* Fix gtk warnings
|
||||
* Add protocol traffic statusbar item
|
||||
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
|
||||
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
|
||||
* Make active port test use internal graphic instead of launching browser
|
||||
|
||||
==== WebUI ====
|
||||
* Lots of smaller tweaks.
|
||||
* All details tabs have the same features as in gtk-ui 1.0.x
|
||||
* Persistent sessions #486
|
||||
* Plugin improvements for easy use of templates and images in eggs. #497
|
||||
* Classic template takes over some style elements from white template.
|
||||
* https (for users that know how to create certificates)
|
||||
* Easier apache mod_proxy use.
|
||||
* Redesigned sidebar
|
||||
|
||||
==== AjaxUI ====
|
||||
* Hosted in a webui template.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* New ConsoleUI written by Idoa01
|
||||
* Callable from command-line for scripts.
|
||||
|
||||
==== Plugins ====
|
||||
* Stats plugin for graphs.
|
||||
* Label plugin for grouping torrents and per torrent settings.
|
||||
|
||||
==== Misc ====
|
||||
* Implement #478 display UI options in usage help
|
||||
* Fix #547 add description to name field per HIG entry 2.1.1.1
|
||||
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
|
||||
Deluge 0.5.1 (11 June 2007)
|
||||
* Peer Exchange
|
||||
* ticket #254 - Encryption
|
||||
* ticket #142 - UPnP + NATPMP
|
||||
* Improved user interface
|
||||
* Redesigned preferences dialog
|
||||
* Proper startup and shutdown
|
||||
|
29
DEPENDS
29
DEPENDS
@@ -1,29 +0,0 @@
|
||||
=== Core ===
|
||||
* python >= 2.5
|
||||
* twisted >= 8.1
|
||||
* twisted-web >= 8.1
|
||||
* pyopenssl
|
||||
* simplejson (if python < 2.6)
|
||||
* setuptools
|
||||
* gettext
|
||||
* intltool
|
||||
* pyxdg
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
|
||||
* If building included libtorrent::
|
||||
* boost >= 1.34.1
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
* pygtk >= 2.12
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
|
||||
=== Web ===
|
||||
* mako
|
40
HACKING
Normal file
40
HACKING
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2006 Marcos Pinto ('markybob') <markybob@gmail.com>
|
||||
|
||||
This is pretty much taken straight out of PEP 8, the "Style Guide for Python
|
||||
Code" (http://www.python.org/dev/peps/pep-0008/)
|
||||
More or less, if you try to submit a patch that doesn't follow this guide, odds
|
||||
are your patch will be denied...unless it does some incredibly magnificient
|
||||
things, in which case I *might* edit it. Don't bet on it, though.
|
||||
|
||||
Here are the highlights:
|
||||
Indents are FOUR (4) spaces. Not 8, not 5 or 2 and definitely NOT tab.
|
||||
Limit all lines to a maximum of 80 characters.
|
||||
Use UTF-8 encoding
|
||||
Every single import should be on its own line
|
||||
Avoid extraneous whitespace in the following situations:
|
||||
Yes: spam(ham[1], {eggs: 2})
|
||||
No: spam( ham[ 1 ], { eggs: 2 } )
|
||||
Yes: spam(1)
|
||||
No: spam (1)
|
||||
Yes: if x == 4: print x, y; x, y = y, x
|
||||
No: if x == 4 : print x , y ; x , y = y , x
|
||||
Yes: dict['key'] = list[index]
|
||||
No: dict ['key'] = list [index]
|
||||
Yes:
|
||||
x = 1
|
||||
y = 2
|
||||
long_variable = 3
|
||||
No:
|
||||
x = 1
|
||||
y = 2
|
||||
long_variable = 3
|
||||
|
||||
Some more recommendations:
|
||||
* "Don't repeat yourself (DRY). Every distinct concept and/or piece of
|
||||
data should live in one, and only one, place. Redundancy is bad.
|
||||
Normalization is good." (taken straight from django's Design philosophies)
|
||||
* Try to use iterators/generators where applicable. The simplest change from
|
||||
range to xrange is also good.
|
||||
* In UI and deluge code for consistency we use notion of speed not rate.
|
||||
Libtorrent mixes this usage and so do we on deluge-libtorrent boundary,
|
||||
but all deluge only code should use speed.
|
908
LICENSE
908
LICENSE
@@ -1,634 +1,350 @@
|
||||
Deluge is licensed under the GNU General Public License version 3 with the
|
||||
addition of the following special exception:
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
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.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
NO WARRANTY
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
16. Limitation of Liability.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
|
||||
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.
|
||||
|
24
MANIFEST.in
24
MANIFEST.in
@@ -1,12 +1,12 @@
|
||||
recursive-include docs/man *
|
||||
recursive-include deluge *
|
||||
recursive-include win32 *
|
||||
|
||||
recursive-exclude deluge *.egg-link
|
||||
exclude deluge/ui/web/gen_gettext.py
|
||||
exclude deluge/ui/web/css/*-debug.css
|
||||
exclude deluge/ui/web/js/build.sh
|
||||
exclude deluge/ui/web/js/Deluge*.js
|
||||
exclude deluge/ui/web/js/*-debug.js
|
||||
prune deluge/ui/web/docs
|
||||
prune deluge/scripts
|
||||
include LICENSE
|
||||
include README
|
||||
include README.Packagers
|
||||
include Makefile
|
||||
include deluge.desktop
|
||||
include deluge.png
|
||||
include msgfmt.py
|
||||
recursive-include libtorrent/ *
|
||||
recursive-include glade/ *.glade
|
||||
recursive-include pixmaps/ *.png *.svg
|
||||
recursive-include plugins/ *
|
||||
recursive-include po/ *
|
||||
|
31
Makefile
Normal file
31
Makefile
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# Makefile for Deluge
|
||||
#
|
||||
PYVER=`python -c "import sys; print sys.version[:3]"`
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?= ./
|
||||
|
||||
all:
|
||||
python setup.py build
|
||||
|
||||
tarball:
|
||||
python setup.py sdist
|
||||
mv dist/deluge-*.tar.gz $(DESTDIR)
|
||||
|
||||
install:
|
||||
python setup.py install --prefix=$(PREFIX)
|
||||
|
||||
clean:
|
||||
python setup.py clean
|
||||
rm -rf ./build
|
||||
rm msgfmt.pyc
|
||||
find . -name *.pyc -exec rm {} \;
|
||||
|
||||
uninstall:
|
||||
-rm $(PREFIX)/bin/deluge
|
||||
-rm -r $(PREFIX)/lib/python${PYVER}/site-packages/deluge
|
||||
-rm -r $(PREFIX)/lib/python${PYVER}/site-packages/deluge-*.egg-info
|
||||
-rm -r $(PREFIX)/share/deluge
|
||||
-find ${PREFIX}/share/locale -name deluge.mo -exec rm {} \;
|
||||
-rm $(PREFIX)/share/applications/deluge.desktop
|
||||
-rm $(PREFIX)/share/pixmaps/deluge.png
|
10
PKG-INFO
Normal file
10
PKG-INFO
Normal file
@@ -0,0 +1,10 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: deluge
|
||||
Version: 0.5.2
|
||||
Summary: A bittorrent client written in PyGTK
|
||||
Home-page: http://deluge-torrent.org
|
||||
Author: Zach Tibbitts, Alon Zakai, Marcos Pinto, Alex Dedul, Andrew Resch
|
||||
Author-email: zach@collegegeek.org, kripkensteiner@gmail.com, marcospinto@dipconsultants.com, rotmer@gmail.com, alonzakai@gmail.com
|
||||
License: GPLv2
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
127
README
127
README
@@ -1,24 +1,15 @@
|
||||
|
||||
==========================
|
||||
Deluge BitTorrent Client
|
||||
==========================
|
||||
|
||||
Homepage: http://deluge-torrent.org
|
||||
|
||||
Authors:
|
||||
Andrew Resch
|
||||
Damien Churchill
|
||||
Zach Tibbitts, aka zachtib
|
||||
Alon Zakai, aka kripkenstein
|
||||
Marcos Pinto, aka markybob
|
||||
Andrew Resch, aka andar
|
||||
Alex Dedul, aka plisk
|
||||
|
||||
For past developers and contributers see: http://dev.deluge-torrent.org/wiki/About
|
||||
|
||||
==========================
|
||||
License
|
||||
==========================
|
||||
Deluge is under the GNU GPLv3 license.
|
||||
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 ui/data/pixmaps are copyright Andrew Resch and are under
|
||||
the GNU GPLv3.
|
||||
Homepage: http://deluge-torrent.org
|
||||
|
||||
==========================
|
||||
Contact/Support:
|
||||
@@ -26,69 +17,85 @@ Contact/Support:
|
||||
|
||||
We have two options available for support:
|
||||
|
||||
Our Forum, at: http://forum.deluge-torrent.org
|
||||
Our Forum, at http://forum.deluge-torrent.org
|
||||
or
|
||||
Our IRC Channel, at #deluge on Freenode: http://freenode.net
|
||||
Our IRC Channel, at #deluge on Freenode
|
||||
|
||||
==========================
|
||||
Installation Instructions:
|
||||
==========================
|
||||
|
||||
For more detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
|
||||
First, make sure you have the proper build dependencies installed. On a normal
|
||||
Debian or Ubuntu system, those dependencies are:
|
||||
|
||||
See: DEPENDS for a full list of dependencies.
|
||||
g++
|
||||
make
|
||||
python-all-dev
|
||||
python-all version >= 2.4
|
||||
python-dbus
|
||||
python-gtk2 version >= 2.9
|
||||
python-notify
|
||||
librsvg2-common
|
||||
python-xdg
|
||||
python-support
|
||||
libboost-dev >= 1.33.1
|
||||
libboost-thread-dev
|
||||
libboost-date-time-dev
|
||||
libboost-filesystem-dev
|
||||
libboost-serialization-dev
|
||||
libssl-dev
|
||||
zlib1g-dev
|
||||
|
||||
First, make sure you have the proper build dependencies installed. On a normal
|
||||
Debian or Ubuntu system:
|
||||
But the names of the packages may vary depending on your OS / distro.
|
||||
|
||||
sudo apt-get install g++ make python-all-dev python-all python-dbus \
|
||||
python-gtk2 python-notify librsvg2-common python-xdg python-support \
|
||||
subversion libboost-dev libboost-python-dev \
|
||||
libboost-thread-dev libboost-date-time-dev libboost-filesystem-dev \
|
||||
libssl-dev zlib1g-dev python-setuptools \
|
||||
python-mako python-twisted-web python-chardet python-simplejson
|
||||
Once you have the needed libraries installed, build Deluge by running:
|
||||
|
||||
The names of the packages may vary depending on your OS / distro.
|
||||
$ make
|
||||
|
||||
Once you have the needed libraries installed, build and install by running:
|
||||
You shouldn't get any errors. Then run, as root (or by using sudo):
|
||||
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
$ make install
|
||||
|
||||
and Deluge will be installed. By default, Deluge will be installed to the
|
||||
prefix /usr. If you wish, you can install Deluge to a different prefix by
|
||||
specifying it when you install it:
|
||||
|
||||
$ PREFIX=yourprefixhere make install
|
||||
|
||||
So, to install to /usr/local, run:
|
||||
|
||||
$ PREFIX=/usr/local make install
|
||||
|
||||
You can then run Deluge by executing:
|
||||
|
||||
$ deluge
|
||||
|
||||
==========================
|
||||
FAQ
|
||||
Uninstallation/Upgrading:
|
||||
==========================
|
||||
|
||||
How to start the various user-interfaces
|
||||
If you wish to upgrade from the older Deluge version please remove it first,
|
||||
then install the latest version as per "Installation Instructions". If you
|
||||
installed via the tarball, cd into the unpacked source tarball and then run,
|
||||
as root (or by using sudo):
|
||||
|
||||
Gtk:
|
||||
deluge-gtk
|
||||
Console:
|
||||
deluge-console
|
||||
Web:
|
||||
deluge-web
|
||||
Go to http://localhost:8112/ default-password = "deluge"
|
||||
$ make uninstall
|
||||
|
||||
Why is deluge still listed in my system tray even after I close it ?
|
||||
If you installed via the deb package, run as root (or by using sudo:)
|
||||
$ dpkg --purge remove deluge-torrent
|
||||
|
||||
You closed the gtk user-interface but you did not close the daemon. Choose "Quit & Shutdown Daemon" to close both Daemon and gtk-ui.
|
||||
Now install the latest version (and check out the additional notes).
|
||||
|
||||
How do I start the daemon?
|
||||
==========================
|
||||
Additional Notes:
|
||||
==========================
|
||||
|
||||
deluged
|
||||
|
||||
How do I start the daemon with logging to console?
|
||||
|
||||
deluged -d -L <log level>
|
||||
|
||||
I can't connect to the daemon from another machine
|
||||
|
||||
* Configure the daemon to allow remote connections
|
||||
* Add a user to the auth file located in the config folder: ~/.config/deluge/auth
|
||||
* Restart the daemon.
|
||||
|
||||
I upgraded from 0.5 and plugin x is missing
|
||||
|
||||
1.0 is a rewrite, all old 0.5 plugins have to be rewritten.
|
||||
|
||||
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
|
||||
1) On some distributions, boost libraries are renamed to have "-mt" at the end
|
||||
(boost_thread_mt instead of boost_thread, for example), the "mt" indicating
|
||||
"multithreaded". In some cases it appears the distros lack symlinks to connect
|
||||
things. The solution is to either add symlinks from the short names to those
|
||||
with "-mt", or to alter setup.py to look for the "-mt" versions.
|
||||
|
||||
2) After upgrading your Deluge installation, it may fail to start. If this
|
||||
happens to you, you need to remove your ~/.config/deluge directory to allow
|
||||
Deluge to rebuild it's configuration file.
|
||||
|
8
README.Packagers
Normal file
8
README.Packagers
Normal file
@@ -0,0 +1,8 @@
|
||||
NOTE: Deluge 0.5.1 and newer uses an svn build of libtorrent. This build
|
||||
differs from a clean libtorrent source checkout and has been hacked in order
|
||||
to get it to work properly with Deluge. As a result, Deluge will likely not
|
||||
build properly against a vanilla libtorrent 0.12 installation or a nightly
|
||||
build oflibtorrent 0.13. It is recommended that you build against our included
|
||||
libtorrent, as our build will not conflict with any installed libtorrent.
|
||||
|
||||
- zachtib
|
@@ -1,21 +0,0 @@
|
||||
#!/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
|
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
# Paths to exclude
|
||||
EXCLUSIONS = [
|
||||
"deluge/scripts",
|
||||
"deluge/i18n",
|
||||
]
|
||||
|
||||
POTFILE_IN = "deluge/i18n/POTFILES.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", ".in") \
|
||||
and dirpath not in EXCLUSIONS \
|
||||
and not compiled.match(dirpath):
|
||||
to_translate.append(os.path.join(dirpath, filename))
|
||||
|
||||
f = open(POTFILE_IN, "wb")
|
||||
for line in to_translate:
|
||||
f.write(line + "\n")
|
||||
|
||||
f.close()
|
||||
|
||||
print "Done"
|
@@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
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/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
|
||||
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p icons/hicolor/\
|
||||
${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
|
||||
-o icons/hicolor/${size}x${size}/apps/deluge.png pixmaps/deluge.svg; mkdir -p \
|
||||
icons/scalable/apps/; cp pixmaps/deluge.svg icons/scalable/apps/deluge.svg; done
|
||||
|
12
deluge.desktop
Normal file
12
deluge.desktop
Normal file
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Encoding=UTF-8
|
||||
Name=Deluge BitTorrent Client
|
||||
Comment=Bittorrent client written in PyGTK
|
||||
Exec=deluge
|
||||
Icon=deluge.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Application;Network
|
||||
StartupNotify=true
|
||||
MimeType=application/x-bittorrent;
|
BIN
deluge.png
Normal file
BIN
deluge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@@ -1,4 +0,0 @@
|
||||
"""Deluge"""
|
||||
# this is a namespace package
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
@@ -1,20 +0,0 @@
|
||||
from new import classobj
|
||||
from deluge.core.core import Core
|
||||
from deluge.core.daemon import Daemon
|
||||
|
||||
class RpcApi:
|
||||
pass
|
||||
|
||||
def scan_for_methods(obj):
|
||||
methods = {
|
||||
'__doc__': 'Methods available in %s' % obj.__name__.lower()
|
||||
}
|
||||
for d in dir(obj):
|
||||
if not hasattr(getattr(obj,d), '_rpcserver_export'):
|
||||
continue
|
||||
methods[d] = getattr(obj, d)
|
||||
cobj = classobj(obj.__name__.lower(), (object,), methods)
|
||||
setattr(RpcApi, obj.__name__.lower(), cobj)
|
||||
|
||||
scan_for_methods(Core)
|
||||
scan_for_methods(Daemon)
|
@@ -1,60 +0,0 @@
|
||||
#
|
||||
# _libtorrent.py
|
||||
#
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
This module is used to handle the importing of libtorrent.
|
||||
|
||||
We use this module to control what versions of libtorrent this version of Deluge
|
||||
supports.
|
||||
|
||||
** Usage **
|
||||
|
||||
>>> from deluge._libtorrent import lt
|
||||
|
||||
"""
|
||||
|
||||
REQUIRED_VERSION = "0.14.9.0"
|
||||
|
||||
def check_version(LT):
|
||||
from deluge.common import VersionSplit
|
||||
if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION):
|
||||
raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION)
|
||||
|
||||
try:
|
||||
import deluge.libtorrent as lt
|
||||
check_version(lt)
|
||||
except ImportError:
|
||||
import libtorrent as lt
|
||||
check_version(lt)
|
@@ -1,129 +0,0 @@
|
||||
# The contents of this file are subject to the Python Software Foundation
|
||||
# License Version 2.3 (the License). You may not copy or use this file, in
|
||||
# either source code or executable form, except in compliance with the License.
|
||||
# You may obtain a copy of the License at http://www.python.org/license.
|
||||
#
|
||||
# Software distributed under the License is distributed on an AS IS basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
# for the specific language governing rights and limitations under the
|
||||
# License.
|
||||
|
||||
# Written by Petru Paler
|
||||
|
||||
# Minor modifications made by Andrew Resch to replace the BTFailure errors with Exceptions
|
||||
|
||||
def decode_int(x, f):
|
||||
f += 1
|
||||
newf = x.index('e', f)
|
||||
n = int(x[f:newf])
|
||||
if x[f] == '-':
|
||||
if x[f + 1] == '0':
|
||||
raise ValueError
|
||||
elif x[f] == '0' and newf != f+1:
|
||||
raise ValueError
|
||||
return (n, newf+1)
|
||||
|
||||
def decode_string(x, f):
|
||||
colon = x.index(':', f)
|
||||
n = int(x[f:colon])
|
||||
if x[f] == '0' and colon != f+1:
|
||||
raise ValueError
|
||||
colon += 1
|
||||
return (x[colon:colon+n], colon+n)
|
||||
|
||||
def decode_list(x, f):
|
||||
r, f = [], f+1
|
||||
while x[f] != 'e':
|
||||
v, f = decode_func[x[f]](x, f)
|
||||
r.append(v)
|
||||
return (r, f + 1)
|
||||
|
||||
def decode_dict(x, f):
|
||||
r, f = {}, f+1
|
||||
while x[f] != 'e':
|
||||
k, f = decode_string(x, f)
|
||||
r[k], f = decode_func[x[f]](x, f)
|
||||
return (r, f + 1)
|
||||
|
||||
decode_func = {}
|
||||
decode_func['l'] = decode_list
|
||||
decode_func['d'] = decode_dict
|
||||
decode_func['i'] = decode_int
|
||||
decode_func['0'] = decode_string
|
||||
decode_func['1'] = decode_string
|
||||
decode_func['2'] = decode_string
|
||||
decode_func['3'] = decode_string
|
||||
decode_func['4'] = decode_string
|
||||
decode_func['5'] = decode_string
|
||||
decode_func['6'] = decode_string
|
||||
decode_func['7'] = decode_string
|
||||
decode_func['8'] = decode_string
|
||||
decode_func['9'] = decode_string
|
||||
|
||||
def bdecode(x):
|
||||
try:
|
||||
r, l = decode_func[x[0]](x, 0)
|
||||
except (IndexError, KeyError, ValueError):
|
||||
raise Exception("not a valid bencoded string")
|
||||
|
||||
return r
|
||||
|
||||
from types import StringType, IntType, LongType, DictType, ListType, TupleType
|
||||
|
||||
|
||||
class Bencached(object):
|
||||
|
||||
__slots__ = ['bencoded']
|
||||
|
||||
def __init__(self, s):
|
||||
self.bencoded = s
|
||||
|
||||
def encode_bencached(x,r):
|
||||
r.append(x.bencoded)
|
||||
|
||||
def encode_int(x, r):
|
||||
r.extend(('i', str(x), 'e'))
|
||||
|
||||
def encode_bool(x, r):
|
||||
if x:
|
||||
encode_int(1, r)
|
||||
else:
|
||||
encode_int(0, r)
|
||||
|
||||
def encode_string(x, r):
|
||||
r.extend((str(len(x)), ':', x))
|
||||
|
||||
def encode_list(x, r):
|
||||
r.append('l')
|
||||
for i in x:
|
||||
encode_func[type(i)](i, r)
|
||||
r.append('e')
|
||||
|
||||
def encode_dict(x,r):
|
||||
r.append('d')
|
||||
ilist = x.items()
|
||||
ilist.sort()
|
||||
for k, v in ilist:
|
||||
r.extend((str(len(k)), ':', k))
|
||||
encode_func[type(v)](v, r)
|
||||
r.append('e')
|
||||
|
||||
encode_func = {}
|
||||
encode_func[Bencached] = encode_bencached
|
||||
encode_func[IntType] = encode_int
|
||||
encode_func[LongType] = encode_int
|
||||
encode_func[StringType] = encode_string
|
||||
encode_func[ListType] = encode_list
|
||||
encode_func[TupleType] = encode_list
|
||||
encode_func[DictType] = encode_dict
|
||||
|
||||
try:
|
||||
from types import BooleanType
|
||||
encode_func[BooleanType] = encode_bool
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def bencode(x):
|
||||
r = []
|
||||
encode_func[type(x)](x, r)
|
||||
return ''.join(r)
|
714
deluge/common.py
714
deluge/common.py
@@ -1,714 +0,0 @@
|
||||
#
|
||||
# common.py
|
||||
#
|
||||
# Copyright (C) 2007, 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.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import platform
|
||||
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"):
|
||||
json.dumps = json.write
|
||||
json.loads = json.read
|
||||
|
||||
def dump(obj, fp, **kw):
|
||||
fp.write(json.dumps(obj))
|
||||
|
||||
def load(fp, **kw):
|
||||
return json.loads(fp.read())
|
||||
|
||||
json.dump = dump
|
||||
json.load = load
|
||||
|
||||
import pkg_resources
|
||||
import gettext
|
||||
import locale
|
||||
|
||||
from deluge.error import *
|
||||
|
||||
LT_TORRENT_STATE = {
|
||||
"Queued": 0,
|
||||
"Checking": 1,
|
||||
"Downloading Metadata": 2,
|
||||
"Downloading": 3,
|
||||
"Finished": 4,
|
||||
"Seeding": 5,
|
||||
"Allocating": 6,
|
||||
"Checking Resume Data": 7,
|
||||
0: "Queued",
|
||||
1: "Checking",
|
||||
2: "Downloading Metadata",
|
||||
3: "Downloading",
|
||||
4: "Finished",
|
||||
5: "Seeding",
|
||||
6: "Allocating",
|
||||
7: "Checking Resume Data"
|
||||
}
|
||||
|
||||
|
||||
TORRENT_STATE = [
|
||||
"Allocating",
|
||||
"Checking",
|
||||
"Downloading",
|
||||
"Seeding",
|
||||
"Paused",
|
||||
"Error",
|
||||
"Queued"
|
||||
]
|
||||
|
||||
FILE_PRIORITY = {
|
||||
0: "Do Not Download",
|
||||
1: "Normal Priority",
|
||||
2: "High Priority",
|
||||
5: "High Priority",
|
||||
7: "Highest Priority",
|
||||
"Do Not Download": 0,
|
||||
"Normal Priority": 1,
|
||||
"High Priority": 5,
|
||||
"Highest Priority": 7
|
||||
}
|
||||
|
||||
def get_version():
|
||||
"""
|
||||
Returns the program version from the egg metadata
|
||||
|
||||
:returns: the version of Deluge
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return pkg_resources.require("Deluge")[0].version
|
||||
|
||||
def get_default_config_dir(filename=None):
|
||||
"""
|
||||
:param filename: if None, only the config path is returned, if provided, a path including the filename will be returned
|
||||
:type filename: string
|
||||
:returns: a file path to the config directory and optional filename
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
if filename:
|
||||
return os.path.join(os.environ.get("APPDATA"), "deluge", filename)
|
||||
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:
|
||||
return xdg.BaseDirectory.save_config_path("deluge")
|
||||
|
||||
def get_default_download_dir():
|
||||
"""
|
||||
:returns: the default download directory
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
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():
|
||||
"""
|
||||
Checks if the current platform is Windows
|
||||
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return platform.system() in ('Windows', 'Microsoft')
|
||||
|
||||
def vista_check():
|
||||
"""
|
||||
Checks if the current platform is Windows Vista
|
||||
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return platform.release() == "Vista"
|
||||
|
||||
def osx_check():
|
||||
"""
|
||||
Checks if the current platform is Mac OS X
|
||||
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return platform.system() == "Darwin"
|
||||
|
||||
def get_pixmap(fname):
|
||||
"""
|
||||
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
|
||||
:returns: a path to a pixmap file included with Deluge
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Opens a file or folder using the system configured program
|
||||
|
||||
:param path: the path to the file or folder to open
|
||||
:type path: string
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
os.startfile("%s" % path)
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", "%s" % path])
|
||||
|
||||
def open_url_in_browser(url):
|
||||
"""
|
||||
Opens a url in the desktop's default browser
|
||||
|
||||
:param url: the url to open
|
||||
:type url: string
|
||||
|
||||
"""
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
|
||||
## Formatting text functions
|
||||
|
||||
def fsize(fsize_b):
|
||||
"""
|
||||
Formats the bytes value into a string with KiB, MiB or GiB units
|
||||
|
||||
:param fsize_b: the filesize in bytes
|
||||
:type fsize_b: int
|
||||
:returns: formatted string in KiB, MiB or GiB units
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fsize(112245)
|
||||
'109.6 KiB'
|
||||
|
||||
"""
|
||||
fsize_kb = fsize_b / 1024.0
|
||||
if fsize_kb < 1024:
|
||||
return "%.1f %s" % (fsize_kb, _("KiB"))
|
||||
fsize_mb = fsize_kb / 1024.0
|
||||
if fsize_mb < 1024:
|
||||
return "%.1f %s" % (fsize_mb, _("MiB"))
|
||||
fsize_gb = fsize_mb / 1024.0
|
||||
return "%.1f %s" % (fsize_gb, _("GiB"))
|
||||
|
||||
def fpcnt(dec):
|
||||
"""
|
||||
Formats a string to display a percentage with two decimal places
|
||||
|
||||
:param dec: the ratio in the range [0.0, 1.0]
|
||||
:type dec: float
|
||||
:returns: a formatted string representing a percentage
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fpcnt(0.9311)
|
||||
'93.11%'
|
||||
|
||||
"""
|
||||
return '%.2f%%' % (dec * 100)
|
||||
|
||||
def fspeed(bps):
|
||||
"""
|
||||
Formats a string to display a transfer speed utilizing :func:`fsize`
|
||||
|
||||
:param bps: bytes per second
|
||||
:type bps: int
|
||||
:returns: a formatted string representing transfer speed
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fspeed(43134)
|
||||
'42.1 KiB/s'
|
||||
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Formats a string to show 'num_peers' ('total_peers')
|
||||
|
||||
:param num_peers: the number of connected peers
|
||||
:type num_peers: int
|
||||
:param total_peers: the total number of peers
|
||||
:type total_peers: int
|
||||
:returns: a formatted string: num_peers (total_peers), if total_peers < 0, then it will not be shown
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> fpeer(10, 20)
|
||||
'10 (20)'
|
||||
>>> fpeer(10, -1)
|
||||
'10'
|
||||
|
||||
"""
|
||||
if total_peers > -1:
|
||||
return "%d (%d)" % (num_peers, total_peers)
|
||||
else:
|
||||
return "%d" % num_peers
|
||||
|
||||
def ftime(seconds):
|
||||
"""
|
||||
Formats a string to show time in a human readable form
|
||||
|
||||
:param seconds: the number of seconds
|
||||
:type seconds: int
|
||||
:returns: a formatted time string, will return '' if seconds == 0
|
||||
:rtype: string
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> ftime(23011)
|
||||
'6h 23m'
|
||||
|
||||
"""
|
||||
if seconds == 0:
|
||||
return ""
|
||||
if seconds < 60:
|
||||
return '%ds' % (seconds)
|
||||
minutes = seconds / 60
|
||||
if minutes < 60:
|
||||
seconds = seconds % 60
|
||||
return '%dm %ds' % (minutes, seconds)
|
||||
hours = minutes / 60
|
||||
if hours < 24:
|
||||
minutes = minutes % 60
|
||||
return '%dh %dm' % (hours, minutes)
|
||||
days = hours / 24
|
||||
if days < 7:
|
||||
hours = hours % 24
|
||||
return '%dd %dh' % (days, hours)
|
||||
weeks = days / 7
|
||||
if weeks < 52:
|
||||
days = days % 7
|
||||
return '%dw %dd' % (weeks, days)
|
||||
years = weeks / 52
|
||||
weeks = weeks % 52
|
||||
return '%dy %dw' % (years, weeks)
|
||||
|
||||
def fdate(seconds):
|
||||
"""
|
||||
Formats a date time string in the locale's date representation based on the systems timezone
|
||||
|
||||
:param seconds: time in seconds since the Epoch
|
||||
:type seconds: float
|
||||
:returns: a string in the locale's datetime representation or "" if seconds < 0
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if seconds < 0:
|
||||
return ""
|
||||
return time.strftime("%x %X", time.localtime(seconds))
|
||||
|
||||
def is_url(url):
|
||||
"""
|
||||
A simple test to check if the URL is valid
|
||||
|
||||
:param url: the url to test
|
||||
:type url: string
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> is_url("http://deluge-torrent.org")
|
||||
True
|
||||
|
||||
"""
|
||||
return url.partition('://')[0] in ("http", "https", "ftp", "udp")
|
||||
|
||||
def is_magnet(uri):
|
||||
"""
|
||||
A check to determine if a uri is a valid bittorrent magnet uri
|
||||
|
||||
:param uri: the uri to check
|
||||
:type uri: string
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> is_magnet("magnet:?xt=urn:btih:SU5225URMTUEQLDXQWRB2EQWN6KLTYKN")
|
||||
True
|
||||
|
||||
"""
|
||||
if uri[:20] == "magnet:?xt=urn:btih:":
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_magnet_uri(infohash, name=None, trackers=[]):
|
||||
"""
|
||||
Creates a magnet uri
|
||||
|
||||
:param infohash: the info-hash of the torrent
|
||||
:type infohash: string
|
||||
:param name: the name of the torrent (optional)
|
||||
:type name: string
|
||||
:param trackers: the trackers to announce to (optional)
|
||||
:type trackers: list of strings
|
||||
|
||||
:returns: a magnet uri string
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
from base64 import b32encode
|
||||
uri = "magnet:?xt=urn:btih:" + b32encode(infohash.decode("hex"))
|
||||
if name:
|
||||
uri = uri + "&dn=" + name
|
||||
if trackers:
|
||||
for t in trackers:
|
||||
uri = uri + "&tr=" + t
|
||||
|
||||
return uri
|
||||
|
||||
def get_path_size(path):
|
||||
"""
|
||||
Gets the size in bytes of 'path'
|
||||
|
||||
:param path: the path to check for size
|
||||
:type path: string
|
||||
:returns: the size in bytes of the path or -1 if the path does not exist
|
||||
:rtype: int
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return -1
|
||||
|
||||
if os.path.isfile(path):
|
||||
return os.path.getsize(path)
|
||||
|
||||
dir_size = 0
|
||||
for (p, dirs, files) in os.walk(path):
|
||||
for file in files:
|
||||
filename = os.path.join(p, file)
|
||||
dir_size += os.path.getsize(filename)
|
||||
return dir_size
|
||||
|
||||
def free_space(path):
|
||||
"""
|
||||
Gets the free space available at 'path'
|
||||
|
||||
:param path: the path to check
|
||||
:type path: string
|
||||
:returns: the free space at path in bytes
|
||||
:rtype: int
|
||||
|
||||
:raises InvalidPathError: if the path is not valid
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
if windows_check():
|
||||
import win32file
|
||||
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
|
||||
return (free * sectors * bytes)
|
||||
else:
|
||||
disk_data = os.statvfs(path.encode("utf8"))
|
||||
block_size = disk_data.f_bsize
|
||||
return disk_data.f_bavail * block_size
|
||||
|
||||
def is_ip(ip):
|
||||
"""
|
||||
A simple test to see if 'ip' is valid
|
||||
|
||||
:param ip: the ip to check
|
||||
:type ip: string
|
||||
:returns: True or False
|
||||
:rtype: bool
|
||||
|
||||
** Usage **
|
||||
|
||||
>>> is_ip("127.0.0.1")
|
||||
True
|
||||
|
||||
"""
|
||||
import socket
|
||||
#first we test ipv4
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
if not socket.has_ipv6:
|
||||
return False
|
||||
#now test ipv6
|
||||
try:
|
||||
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
def path_join(*parts):
|
||||
"""
|
||||
An implementation of os.path.join that always uses / for the separator
|
||||
to ensure that the correct paths are produced when working with internal
|
||||
paths on Windows.
|
||||
"""
|
||||
path = ''
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
elif part[0] == '/':
|
||||
path = part
|
||||
elif not path:
|
||||
path = part
|
||||
else:
|
||||
path += '/' + part
|
||||
return path
|
||||
|
||||
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.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
string = string.replace(escape, char)
|
||||
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.
|
||||
:rtype: string
|
||||
"""
|
||||
for char, escape in XML_ESCAPES:
|
||||
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.
|
||||
|
||||
:param ver: the version
|
||||
:type ver: string
|
||||
|
||||
"""
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.replace("_", "-").split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".")]
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
if len(vs) > 1:
|
||||
if vs[1].startswith(("rc", "alpha", "beta")):
|
||||
self.suffix = vs[1]
|
||||
if vs[-1] == 'dev':
|
||||
self.dev = True
|
||||
|
||||
def __cmp__(self, ver):
|
||||
"""
|
||||
The comparison method.
|
||||
|
||||
:param ver: the version to compare with
|
||||
:type ver: VersionSplit
|
||||
|
||||
"""
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# 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
|
@@ -1,417 +0,0 @@
|
||||
#
|
||||
# component.py
|
||||
#
|
||||
# Copyright (C) 2007-2010 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 logging
|
||||
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ComponentAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
class Component(object):
|
||||
"""
|
||||
Component objects are singletons managed by the :class:`ComponentRegistry`.
|
||||
When a new Component object is instantiated, it will be automatically
|
||||
registered with the :class:`ComponentRegistry`.
|
||||
|
||||
The ComponentRegistry has the ability to start, stop, pause and shutdown the
|
||||
components registered with it.
|
||||
|
||||
**Events:**
|
||||
|
||||
**start()** - This method is called when the client has connected to a
|
||||
Deluge core.
|
||||
|
||||
**stop()** - This method is called when the client has disconnected from a
|
||||
Deluge core.
|
||||
|
||||
**update()** - This method is called every 1 second by default while the
|
||||
Componented is in a *Started* state. The interval can be
|
||||
specified during instantiation. The update() timer can be
|
||||
paused by instructing the :class:`ComponentRegistry` to pause
|
||||
this Component.
|
||||
|
||||
**shutdown()** - This method is called when the client is exiting. If the
|
||||
Component is in a "Started" state when this is called, a
|
||||
call to stop() will be issued prior to shutdown().
|
||||
|
||||
**States:**
|
||||
|
||||
A Component can be in one of these 5 states.
|
||||
|
||||
**Started** - The Component has been started by the :class:`ComponentRegistry`
|
||||
and will have it's update timer started.
|
||||
|
||||
**Starting** - The Component has had it's start method called, but it hasn't
|
||||
fully started yet.
|
||||
|
||||
**Stopped** - The Component has either been stopped or has yet to be started.
|
||||
|
||||
**Stopping** - The Component has had it's stop method called, but it hasn't
|
||||
fully stopped yet.
|
||||
|
||||
**Paused** - The Component has had it's update timer stopped, but will
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
def __init__(self, name, interval=1, depend=None):
|
||||
self._component_name = name
|
||||
self._component_interval = interval
|
||||
self._component_depend = depend
|
||||
self._component_state = "Stopped"
|
||||
self._component_timer = None
|
||||
self._component_starting_deferred = None
|
||||
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)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
self._component_state = "Started"
|
||||
self._component_starting_deferred = None
|
||||
self._component_start_timer()
|
||||
return True
|
||||
|
||||
def on_start_fail(result):
|
||||
self._component_state = "Stopped"
|
||||
self._component_starting_deferred = None
|
||||
log.error(result)
|
||||
return result
|
||||
|
||||
if self._component_state == "Stopped":
|
||||
if hasattr(self, "start"):
|
||||
self._component_state = "Starting"
|
||||
d = maybeDeferred(self.start)
|
||||
d.addCallback(on_start)
|
||||
d.addErrback(on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_start, None)
|
||||
elif self._component_state == "Starting":
|
||||
return self._component_starting_deferred
|
||||
elif self._component_state == "Started":
|
||||
d = succeed(True)
|
||||
else:
|
||||
d = fail("Cannot start a component not in a Stopped state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_stop(self):
|
||||
def on_stop(result):
|
||||
self._component_state = "Stopped"
|
||||
if self._component_timer and self._component_timer.running:
|
||||
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)
|
||||
|
||||
if self._component_state == "Stopping":
|
||||
return self._component_stopping_deferred
|
||||
|
||||
return succeed(None)
|
||||
|
||||
def _component_pause(self):
|
||||
def on_pause(result):
|
||||
self._component_state = "Paused"
|
||||
|
||||
if self._component_state == "Started":
|
||||
if self._component_timer and self._component_timer.running:
|
||||
d = maybeDeferred(self._component_timer.stop)
|
||||
d.addCallback(on_pause)
|
||||
else:
|
||||
d = succeed(None)
|
||||
elif self._component_state == "Paused":
|
||||
d = succeed(None)
|
||||
else:
|
||||
d = fail("Cannot pause a component in a non-Started state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_resume(self):
|
||||
def on_resume(result):
|
||||
self._component_state = "Started"
|
||||
|
||||
if self._component_state == "Paused":
|
||||
d = maybeDeferred(self._component_start_timer)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail("Component cannot be resumed from a non-Paused state!")
|
||||
|
||||
return d
|
||||
|
||||
def _component_shutdown(self):
|
||||
def on_stop(result):
|
||||
if hasattr(self, "shutdown"):
|
||||
return maybeDeferred(self.shutdown)
|
||||
return succeed(None)
|
||||
|
||||
d = self._component_stop()
|
||||
d.addCallback(on_stop)
|
||||
return d
|
||||
|
||||
class ComponentRegistry(object):
|
||||
"""
|
||||
The ComponentRegistry holds a list of currently registered
|
||||
:class:`Component` objects. It is used to manage the Components by
|
||||
starting, stopping, pausing and shutting them down.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.components = {}
|
||||
|
||||
def register(self, obj):
|
||||
"""
|
||||
Registers a component object with the registry. This is done
|
||||
automatically when a Component object is instantiated.
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
|
||||
|
||||
"""
|
||||
name = obj._component_name
|
||||
if name in self.components:
|
||||
raise ComponentAlreadyRegistered(
|
||||
"Component already registered with name %s" % name)
|
||||
|
||||
self.components[obj._component_name] = obj
|
||||
|
||||
def deregister(self, obj):
|
||||
"""
|
||||
Deregisters a component from the registry. A stop will be
|
||||
issued to the component prior to deregistering it.
|
||||
|
||||
:param obj: the Component object
|
||||
:type obj: object
|
||||
|
||||
"""
|
||||
|
||||
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, obj._component_name)
|
||||
else:
|
||||
return succeed(None)
|
||||
|
||||
def start(self, names=[]):
|
||||
"""
|
||||
Starts Components that are currently in a Stopped state and their
|
||||
dependencies. If *names* is specified, will only start those
|
||||
Components and their dependencies and if not it will start all
|
||||
registered components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully started
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
# Start all the components if names is empty
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
def on_depends_started(result, name):
|
||||
return self.components[name]._component_start()
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_depend:
|
||||
# This component has depends, so we need to start them first.
|
||||
d = self.start(self.components[name]._component_depend)
|
||||
d.addCallback(on_depends_started, name)
|
||||
deferreds.append(d)
|
||||
else:
|
||||
deferreds.append(self.components[name]._component_start())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def stop(self, names=[]):
|
||||
"""
|
||||
Stops Components that are currently not in a Stopped state. If
|
||||
*names* is specified, then it will only stop those Components,
|
||||
and if not it will stop all the registered Components.
|
||||
|
||||
:param names: a list of Components to start
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully stopped
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if name in self.components:
|
||||
deferreds.append(self.components[name]._component_stop())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def pause(self, names=[]):
|
||||
"""
|
||||
Pauses Components that are currently in a Started state. If
|
||||
*names* is specified, then it will only pause those Components,
|
||||
and if not it will pause all the registered Components.
|
||||
|
||||
:param names: a list of Components to pause
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully paused
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Started":
|
||||
deferreds.append(self.components[name]._component_pause())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def resume(self, names=[]):
|
||||
"""
|
||||
Resumes Components that are currently in a Paused state. If
|
||||
*names* is specified, then it will only resume those Components,
|
||||
and if not it will resume all the registered Components.
|
||||
|
||||
:param names: a list of Components to resume
|
||||
:type names: list
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
if not names:
|
||||
names = self.components.keys()
|
||||
elif isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
deferreds = []
|
||||
|
||||
for name in names:
|
||||
if self.components[name]._component_state == "Paused":
|
||||
deferreds.append(self.components[name]._component_resume())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdowns all Components regardless of state. This will call
|
||||
:meth:`stop` on call the components prior to shutting down. This should
|
||||
be called when the program is exiting to ensure all Components have a
|
||||
chance to properly shutdown.
|
||||
|
||||
:returns: a Deferred object that will fire once all Components have been sucessfully resumed
|
||||
:rtype: twisted.internet.defer.Deferred
|
||||
|
||||
"""
|
||||
deferreds = []
|
||||
|
||||
for component in self.components.values():
|
||||
deferreds.append(component._component_shutdown())
|
||||
|
||||
return DeferredList(deferreds)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Updates all Components that are in a Started state.
|
||||
|
||||
"""
|
||||
for component in self.components.items():
|
||||
component.update()
|
||||
|
||||
_ComponentRegistry = ComponentRegistry()
|
||||
|
||||
deregister = _ComponentRegistry.deregister
|
||||
start = _ComponentRegistry.start
|
||||
stop = _ComponentRegistry.stop
|
||||
pause = _ComponentRegistry.pause
|
||||
resume = _ComponentRegistry.resume
|
||||
update = _ComponentRegistry.update
|
||||
shutdown = _ComponentRegistry.shutdown
|
||||
|
||||
def get(name):
|
||||
"""
|
||||
Return a reference to a component.
|
||||
|
||||
:param name: the Component name to get
|
||||
:type name: string
|
||||
|
||||
:returns: the Component object
|
||||
:rtype: object
|
||||
|
||||
:raises KeyError: if the Component does not exist
|
||||
|
||||
"""
|
||||
return _ComponentRegistry.components[name]
|
526
deluge/config.py
526
deluge/config.py
@@ -1,526 +0,0 @@
|
||||
#
|
||||
# config.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.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
Deluge Config Module
|
||||
|
||||
This module is used for loading and saving of configuration files.. or anything
|
||||
really.
|
||||
|
||||
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
|
||||
changed the serializer for the content to something different.
|
||||
|
||||
The config file version is changed by the 'owner' of the config file. This is
|
||||
to signify that there is a change in the naming of some config keys or something
|
||||
similar along those lines.
|
||||
|
||||
The content is simply the dict to be saved and will be serialized before being
|
||||
written.
|
||||
|
||||
Converting
|
||||
|
||||
Since the format of the config could change, there needs to be a way to have
|
||||
the Config object convert to newer formats. To do this, you will need to
|
||||
register conversion functions for various versions of the config file. Note that
|
||||
this can only be done for the 'config file version' and not for the 'format'
|
||||
version as this will be done internally.
|
||||
|
||||
"""
|
||||
|
||||
import cPickle as pickle
|
||||
import logging
|
||||
import shutil
|
||||
import os
|
||||
|
||||
import deluge.common
|
||||
|
||||
json = deluge.common.json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def prop(func):
|
||||
"""Function decorator for defining property attributes
|
||||
|
||||
The decorated function is expected to return a dictionary
|
||||
containing one or more of the following pairs:
|
||||
fget - function for getting attribute value
|
||||
fset - function for setting attribute value
|
||||
fdel - function for deleting attribute
|
||||
This can be conveniently constructed by the locals() builtin
|
||||
function; see:
|
||||
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
|
||||
"""
|
||||
return property(doc=func.__doc__, **func())
|
||||
|
||||
def find_json_objects(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
|
||||
start = s.find("{")
|
||||
offset = start
|
||||
|
||||
if start < 0:
|
||||
return []
|
||||
|
||||
for index, c in enumerate(s[offset:]):
|
||||
if c == "{":
|
||||
opens += 1
|
||||
elif c == "}":
|
||||
opens -= 1
|
||||
if opens == 0:
|
||||
objects.append((start, index+offset+1))
|
||||
start = index + offset + 1
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class is used to access/create/modify config files
|
||||
|
||||
:param filename: the name of the config file
|
||||
:param defaults: dictionary of default values
|
||||
:param config_dir: the path to the config directory
|
||||
|
||||
"""
|
||||
def __init__(self, filename, defaults=None, config_dir=None):
|
||||
self.__config = {}
|
||||
self.__set_functions = {}
|
||||
self.__change_callbacks = []
|
||||
|
||||
# These hold the version numbers and they will be set when loaded
|
||||
self.__version = {
|
||||
"format": 1,
|
||||
"file": 1
|
||||
}
|
||||
|
||||
# This will get set with a reactor.callLater whenever a config option
|
||||
# is set.
|
||||
self._save_timer = None
|
||||
|
||||
if defaults:
|
||||
for key, value in defaults.iteritems():
|
||||
self.set_item(key, value)
|
||||
|
||||
# Load the config from file in the config_dir
|
||||
if config_dir:
|
||||
self.__config_file = os.path.join(config_dir, filename)
|
||||
else:
|
||||
self.__config_file = deluge.common.get_default_config_dir(filename)
|
||||
|
||||
self.load()
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.__config
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
See
|
||||
:meth:`set_item`
|
||||
"""
|
||||
|
||||
return self.set_item(key, value)
|
||||
|
||||
def set_item(self, key, value):
|
||||
"""
|
||||
Sets item 'key' to 'value' in the config dictionary, but does not allow
|
||||
changing the item's type unless it is None. If the types do not match,
|
||||
it will attempt to convert it to the set type before raising a ValueError.
|
||||
|
||||
:param key: string, item to change to change
|
||||
:param value: the value to change item to, must be same type as what is currently in the config
|
||||
|
||||
:raises ValueError: raised when the type of value is not the same as\
|
||||
what is currently in the config and it could not convert the value
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> config = Config("test.conf")
|
||||
>>> config["test"] = 5
|
||||
>>> config["test"]
|
||||
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))
|
||||
return
|
||||
|
||||
if self.__config[key] == value:
|
||||
return
|
||||
|
||||
# Do not allow the type to change unless it is None
|
||||
oldtype, newtype = type(self.__config[key]), type(value)
|
||||
|
||||
if value is not None and oldtype != type(None) and oldtype != newtype:
|
||||
try:
|
||||
if oldtype == unicode:
|
||||
value = oldtype(value, "utf8")
|
||||
else:
|
||||
value = oldtype(value)
|
||||
except ValueError:
|
||||
log.warning("Type '%s' invalid for '%s'", newtype, key)
|
||||
raise
|
||||
|
||||
log.debug("Setting '%s' to %s of %s", key, value, type(value))
|
||||
|
||||
self.__config[key] = value
|
||||
# Run the set_function for this key if any
|
||||
from twisted.internet import reactor
|
||||
try:
|
||||
for func in self.__set_functions[key]:
|
||||
reactor.callLater(0, func, key, value)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
def do_change_callbacks(key, value):
|
||||
for func in self.__change_callbacks:
|
||||
func(key, value)
|
||||
reactor.callLater(0, do_change_callbacks, key, value)
|
||||
except:
|
||||
pass
|
||||
|
||||
# We set the save_timer for 5 seconds if not already set
|
||||
if not self._save_timer or not self._save_timer.active():
|
||||
self._save_timer = reactor.callLater(5, self.save)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
See
|
||||
:meth:`get_item`
|
||||
"""
|
||||
return self.get_item(key)
|
||||
|
||||
def get_item(self, key):
|
||||
"""
|
||||
Gets the value of item 'key'
|
||||
|
||||
:param key: the item for which you want it's value
|
||||
:return: the value of item 'key'
|
||||
|
||||
:raises KeyError: if 'key' is not in the config dictionary
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> config["test"]
|
||||
5
|
||||
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Registers a callback function that will be called when a value is changed in the config dictionary
|
||||
|
||||
:param callback: the function, callback(key, value)
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> def cb(key, value):
|
||||
... print key, value
|
||||
...
|
||||
>>> config.register_change_callback(cb)
|
||||
|
||||
"""
|
||||
self.__change_callbacks.append(callback)
|
||||
|
||||
def register_set_function(self, key, function, apply_now=True):
|
||||
"""
|
||||
Register a function to be called when a config value changes
|
||||
|
||||
:param key: the item to monitor for change
|
||||
:param function: the function to call when the value changes, f(key, value)
|
||||
:keyword apply_now: if True, the function will be called after it's registered
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> def cb(key, value):
|
||||
... print key, value
|
||||
...
|
||||
>>> config.register_set_function("test", cb, apply_now=True)
|
||||
test 5
|
||||
|
||||
"""
|
||||
log.debug("Registering function for %s key..", key)
|
||||
if key not in self.__set_functions:
|
||||
self.__set_functions[key] = []
|
||||
|
||||
self.__set_functions[key].append(function)
|
||||
|
||||
# Run the function now if apply_now is set
|
||||
if apply_now:
|
||||
function(key, self.__config[key])
|
||||
return
|
||||
|
||||
def apply_all(self):
|
||||
"""
|
||||
Calls all set functions
|
||||
|
||||
**Usage**
|
||||
|
||||
>>> config = Config("test.conf", defaults={"test": 5})
|
||||
>>> def cb(key, value):
|
||||
... print key, value
|
||||
...
|
||||
>>> config.register_set_function("test", cb, apply_now=False)
|
||||
>>> config.apply_all()
|
||||
test 5
|
||||
|
||||
"""
|
||||
log.debug("Calling all set functions..")
|
||||
for key, value in self.__set_functions.iteritems():
|
||||
for func in value:
|
||||
func(key, self.__config[key])
|
||||
|
||||
def apply_set_functions(self, key):
|
||||
"""
|
||||
Calls set functions for `:param:key`.
|
||||
|
||||
:param key: str, the config key
|
||||
|
||||
"""
|
||||
log.debug("Calling set functions for key %s..", key)
|
||||
if key in self.__set_functions:
|
||||
for func in self.__set_functions[key]:
|
||||
func(key, self.__config[key])
|
||||
|
||||
def load(self, filename=None):
|
||||
"""
|
||||
Load a config file
|
||||
|
||||
:param filename: if None, uses filename set in object initialization
|
||||
|
||||
|
||||
"""
|
||||
if not filename:
|
||||
filename = self.__config_file
|
||||
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open config file %s: %s", filename, e)
|
||||
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)
|
||||
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)
|
||||
elif len(objects) == 2:
|
||||
try:
|
||||
start, end = objects[0]
|
||||
self.__version.update(json.loads(data[start:end]))
|
||||
start, end = objects[1]
|
||||
self.__config.update(json.loads(data[start:end]))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
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)
|
||||
|
||||
def save(self, filename=None):
|
||||
"""
|
||||
Save configuration to disk
|
||||
|
||||
:param filename: if None, uses filename set in object initiliazation
|
||||
:rtype bool:
|
||||
:return: whether or not the save succeeded.
|
||||
|
||||
"""
|
||||
if not filename:
|
||||
filename = self.__config_file
|
||||
# Check to see if the current config differs from the one on disk
|
||||
# We will only write a new config file if there is a difference
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
objects = find_json_objects(data)
|
||||
start, end = objects[0]
|
||||
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
|
||||
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.__config, f, indent=2)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
f.close()
|
||||
except IOError, e:
|
||||
log.error("Error writing new config file: %s", e)
|
||||
return False
|
||||
|
||||
# Make a backup of the old config
|
||||
try:
|
||||
log.debug("Backing up old config file to %s~", filename)
|
||||
shutil.move(filename, filename + "~")
|
||||
except Exception, e:
|
||||
log.error("Error backing up old config..")
|
||||
|
||||
# The new config file has been written successfully, so let's move it over
|
||||
# the existing one.
|
||||
try:
|
||||
log.debug("Moving new config file %s to %s..", filename + ".new", filename)
|
||||
shutil.move(filename + ".new", filename)
|
||||
except Exception, e:
|
||||
log.error("Error moving new config file: %s", e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
finally:
|
||||
if self._save_timer and self._save_timer.active():
|
||||
self._save_timer.cancel()
|
||||
|
||||
def run_converter(self, input_range, output_version, func):
|
||||
"""
|
||||
Runs a function that will convert file versions in the `:param:input_range`
|
||||
to the `:param:output_version`.
|
||||
|
||||
:param input_range: tuple, (int, int) the range of input versions this
|
||||
function will accept
|
||||
:param output_version: int, the version this function will return
|
||||
:param func: func, the function that will do the conversion, it will take
|
||||
the config dict as an argument and return the augmented dict
|
||||
|
||||
:raises ValueError: if the output_version is less than the input_range
|
||||
|
||||
"""
|
||||
if output_version in input_range or output_version <= max(input_range):
|
||||
raise ValueError("output_version needs to be greater than input_range")
|
||||
|
||||
if self.__version["file"] not in input_range:
|
||||
log.debug("File version %s is not in input_range %s, ignoring converter function..",
|
||||
self.__version["file"], input_range)
|
||||
return
|
||||
|
||||
try:
|
||||
self.__config = func(self.__config)
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("There was an exception try to convert config file %s %s to %s",
|
||||
self.__config_file, self.__version["file"], output_version)
|
||||
raise e
|
||||
else:
|
||||
self.__version["file"] = output_version
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def config_file(self):
|
||||
return self.__config_file
|
||||
|
||||
@prop
|
||||
def config():
|
||||
"""The config dictionary"""
|
||||
def fget(self):
|
||||
return self.__config
|
||||
def fdel(self):
|
||||
return self.save()
|
||||
return locals()
|
@@ -1,138 +0,0 @@
|
||||
#
|
||||
# configmanager.py
|
||||
#
|
||||
# Copyright (C) 2007 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
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
import deluge.log
|
||||
from deluge.config import Config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class _ConfigManager:
|
||||
def __init__(self):
|
||||
log.debug("ConfigManager started..")
|
||||
self.config_files = {}
|
||||
self.__config_directory = None
|
||||
|
||||
@property
|
||||
def config_directory(self):
|
||||
if self.__config_directory is None:
|
||||
self.__config_directory = deluge.common.get_default_config_dir()
|
||||
return self.__config_directory
|
||||
|
||||
def __del__(self):
|
||||
del self.config_files
|
||||
|
||||
def set_config_dir(self, directory):
|
||||
"""
|
||||
Sets the config directory.
|
||||
|
||||
:param directory: str, the directory where the config info should be
|
||||
|
||||
:returns bool: True if successfully changed directory, False if not
|
||||
"""
|
||||
|
||||
if not directory:
|
||||
return False
|
||||
|
||||
log.info("Setting config directory to: %s", directory)
|
||||
if not os.path.exists(directory):
|
||||
# Try to create the config folder if it doesn't exist
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
except Exception, e:
|
||||
log.error("Unable to make config directory: %s", e)
|
||||
return False
|
||||
elif not os.path.isdir(directory):
|
||||
log.error("Config directory needs to be a directory!")
|
||||
return False
|
||||
|
||||
self.__config_directory = directory
|
||||
|
||||
# Reset the config_files so we don't get config from old config folder
|
||||
# XXX: Probably should have it go through the config_files dict and try
|
||||
# to reload based on the new config directory
|
||||
self.save()
|
||||
self.config_files = {}
|
||||
deluge.log.tweak_logging_levels()
|
||||
|
||||
return True
|
||||
|
||||
def get_config_dir(self):
|
||||
return self.config_directory
|
||||
|
||||
def close(self, config):
|
||||
"""Closes a config file."""
|
||||
try:
|
||||
del self.config_files[config]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
"""Saves all the configs to disk."""
|
||||
for value in self.config_files.values():
|
||||
value.save()
|
||||
# We need to return True to keep the timer active
|
||||
return True
|
||||
|
||||
def get_config(self, config_file, defaults=None):
|
||||
"""Get a reference to the Config object for this filename"""
|
||||
log.debug("Getting config '%s'", config_file)
|
||||
# Create the config object if not already created
|
||||
if config_file not in self.config_files.keys():
|
||||
self.config_files[config_file] = Config(config_file, defaults, self.config_directory)
|
||||
|
||||
return self.config_files[config_file]
|
||||
|
||||
# Singleton functions
|
||||
_configmanager = _ConfigManager()
|
||||
|
||||
def ConfigManager(config, defaults=None):
|
||||
return _configmanager.get_config(config, defaults)
|
||||
|
||||
def set_config_dir(directory):
|
||||
"""Sets the config directory, else just uses default"""
|
||||
return _configmanager.set_config_dir(directory)
|
||||
|
||||
def get_config_dir(filename=None):
|
||||
if filename != None:
|
||||
return os.path.join(_configmanager.get_config_dir(), filename)
|
||||
else:
|
||||
return _configmanager.get_config_dir()
|
||||
|
||||
def close(config):
|
||||
return _configmanager.close(config)
|
@@ -1,133 +0,0 @@
|
||||
#
|
||||
# alertmanager.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 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.
|
||||
#
|
||||
|
||||
"""
|
||||
|
||||
The AlertManager handles all the libtorrent alerts.
|
||||
|
||||
This should typically only be used by the Core. Plugins should utilize the
|
||||
`:mod:EventManager` for similar functionality.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
|
||||
import deluge.component as component
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
log.debug("AlertManager initialized..")
|
||||
component.Component.__init__(self, "AlertManager", interval=0.05)
|
||||
self.session = component.get("Core").session
|
||||
|
||||
self.session.set_alert_mask(
|
||||
lt.alert.category_t.error_notification |
|
||||
lt.alert.category_t.port_mapping_notification |
|
||||
lt.alert.category_t.storage_notification |
|
||||
lt.alert.category_t.tracker_notification |
|
||||
lt.alert.category_t.status_notification |
|
||||
lt.alert.category_t.ip_block_notification |
|
||||
lt.alert.category_t.performance_warning)
|
||||
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = {}
|
||||
|
||||
self.delayed_calls = []
|
||||
|
||||
def update(self):
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts()
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
"""
|
||||
Registers a function that will be called when 'alert_type' is pop'd
|
||||
in handle_alerts. The handler function should look like: handler(alert)
|
||||
Where 'alert' is the actual alert object from libtorrent.
|
||||
|
||||
:param alert_type: str, this is string representation of the alert name
|
||||
:param handler: func(alert), the function to be called when the alert is raised
|
||||
"""
|
||||
if alert_type not in self.handlers:
|
||||
# There is no entry for this alert type yet, so lets make it with an
|
||||
# empty list.
|
||||
self.handlers[alert_type] = []
|
||||
|
||||
# Append the handler to the list in the handlers dictionary
|
||||
self.handlers[alert_type].append(handler)
|
||||
log.debug("Registered handler for alert %s", alert_type)
|
||||
|
||||
def deregister_handler(self, handler):
|
||||
"""
|
||||
De-registers the `:param:handler` function from all alert types.
|
||||
|
||||
:param handler: func, the handler function to deregister
|
||||
"""
|
||||
# Iterate through all handlers and remove 'handler' where found
|
||||
for (key, value) in self.handlers.items():
|
||||
if handler in value:
|
||||
# Handler is in this alert type list
|
||||
value.remove(handler)
|
||||
|
||||
def handle_alerts(self, wait=False):
|
||||
"""
|
||||
Pops all libtorrent alerts in the session queue and handles them
|
||||
appropriately.
|
||||
|
||||
:param wait: bool, if True then the handler functions will be run right
|
||||
away and waited to return before processing the next alert
|
||||
"""
|
||||
alert = self.session.pop_alert()
|
||||
# Loop through all alerts in the queue
|
||||
while alert is not None:
|
||||
alert_type = type(alert).__name__
|
||||
# Display the alert message
|
||||
log.debug("%s: %s", alert_type, alert.message())
|
||||
# Call any handlers for this alert type
|
||||
if alert_type in self.handlers:
|
||||
for handler in self.handlers[alert_type]:
|
||||
if not wait:
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert))
|
||||
else:
|
||||
handler(alert)
|
||||
|
||||
alert = self.session.pop_alert()
|
@@ -1,289 +0,0 @@
|
||||
#
|
||||
# authmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# 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
|
||||
import random
|
||||
import stat
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager as configmanager
|
||||
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
|
||||
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
|
||||
create_localclient_account)
|
||||
|
||||
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
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 AuthManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "AuthManager", interval=10)
|
||||
self.__auth = {}
|
||||
self.__auth_modification_time = None
|
||||
|
||||
def start(self):
|
||||
self.__load_auth_file()
|
||||
|
||||
def stop(self):
|
||||
self.__auth = {}
|
||||
|
||||
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
|
||||
|
||||
:param username: str, username
|
||||
:param password: str, password
|
||||
:returns: int, the auth level for this user
|
||||
:rtype: int
|
||||
|
||||
: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", username)
|
||||
|
||||
if self.__auth[username].password == password:
|
||||
# Return the users auth level
|
||||
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", username)
|
||||
|
||||
def has_account(self, username):
|
||||
return username in self.__auth
|
||||
|
||||
def get_known_accounts(self):
|
||||
"""
|
||||
Returns a list of known deluge usernames.
|
||||
"""
|
||||
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:
|
||||
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 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()
|
||||
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("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
log.error("Your auth file is malformed: %s", e)
|
||||
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)
|
||||
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, authlevel = lsplit
|
||||
else:
|
||||
log.error("Your auth file is malformed: "
|
||||
"Incorrect number of fields!")
|
||||
continue
|
||||
|
||||
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:
|
||||
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,884 +0,0 @@
|
||||
#
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import os
|
||||
import glob
|
||||
import base64
|
||||
import logging
|
||||
import threading
|
||||
import tempfile
|
||||
from urlparse import urljoin
|
||||
|
||||
import twisted.web.client
|
||||
import twisted.web.error
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
|
||||
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.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..")
|
||||
component.Component.__init__(self, "Core")
|
||||
|
||||
# Start the libtorrent session
|
||||
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(".")]
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
|
||||
# Load the session state if available
|
||||
self.__load_session_state()
|
||||
|
||||
# Set the user agent
|
||||
self.settings = lt.session_settings()
|
||||
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
|
||||
self.session.add_extension(lt.create_metadata_plugin)
|
||||
self.session.add_extension(lt.create_ut_metadata_plugin)
|
||||
self.session.add_extension(lt.create_smart_ban_plugin)
|
||||
|
||||
# Create the components
|
||||
self.eventmanager = EventManager()
|
||||
self.preferencesmanager = PreferencesManager()
|
||||
self.alertmanager = AlertManager()
|
||||
self.pluginmanager = PluginManager(self)
|
||||
self.torrentmanager = TorrentManager()
|
||||
self.filtermanager = FilterManager(self)
|
||||
self.authmanager = AuthManager()
|
||||
|
||||
# New release check information
|
||||
self.new_release = None
|
||||
|
||||
# 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
|
||||
self.__old_interface = None
|
||||
if listen_interface:
|
||||
self.__old_interface = self.config["listen_interface"]
|
||||
self.config["listen_interface"] = listen_interface
|
||||
|
||||
def start(self):
|
||||
"""Starts the core"""
|
||||
# New release check information
|
||||
self.__new_release = None
|
||||
|
||||
def stop(self):
|
||||
# Save the DHT state if necessary
|
||||
if self.config["dht"]:
|
||||
self.save_dht_state()
|
||||
# Save the libtorrent session state
|
||||
self.__save_session_state()
|
||||
|
||||
# We stored a copy of the old interface value
|
||||
if self.__old_interface:
|
||||
self.config["listen_interface"] = self.__old_interface
|
||||
|
||||
# Make sure the config file has been saved
|
||||
self.config.save()
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def __save_session_state(self):
|
||||
"""Saves the libtorrent session state"""
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
dht_data = open(deluge.configmanager.get_config_dir("dht.state"), "wb")
|
||||
dht_data.write(lt.bencode(self.session.dht_state()))
|
||||
dht_data.close()
|
||||
except Exception, e:
|
||||
log.warning("Failed to save dht state: %s", e)
|
||||
|
||||
def get_new_release(self):
|
||||
log.debug("get_new_release")
|
||||
from urllib2 import urlopen
|
||||
try:
|
||||
self.new_release = urlopen(
|
||||
"http://download.deluge-torrent.org/version-1.0").read().strip()
|
||||
except Exception, e:
|
||||
log.debug("Unable to get release info from website: %s", e)
|
||||
return
|
||||
self.check_new_release()
|
||||
|
||||
def check_new_release(self):
|
||||
if self.new_release:
|
||||
log.debug("new_release: %s", self.new_release)
|
||||
if deluge.common.VersionSplit(self.new_release) > deluge.common.VersionSplit(deluge.common.get_version()):
|
||||
component.get("EventManager").emit(NewVersionAvailableEvent(self.new_release))
|
||||
return self.new_release
|
||||
return False
|
||||
|
||||
# Exported Methods
|
||||
@export
|
||||
def add_torrent_file(self, filename, filedump, options):
|
||||
"""
|
||||
Adds a torrent file to the session.
|
||||
|
||||
:param filename: the filename of the torrent
|
||||
:type filename: string
|
||||
:param filedump: a base64 encoded string of the torrent file contents
|
||||
:type filedump: string
|
||||
:param options: the options to apply to the torrent on add
|
||||
:type options: dict
|
||||
|
||||
:returns: the torrent_id as a str or None
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception, e:
|
||||
log.error("There was an error decoding the filedump string!")
|
||||
log.exception(e)
|
||||
|
||||
try:
|
||||
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)
|
||||
torrent_id = None
|
||||
|
||||
return torrent_id
|
||||
|
||||
@export
|
||||
def add_torrent_url(self, url, options, headers=None):
|
||||
"""
|
||||
Adds a torrent from a url. Deluge will attempt to fetch the torrent
|
||||
from url prior to adding it to the session.
|
||||
|
||||
:param url: the url pointing to the torrent file
|
||||
:type url: string
|
||||
:param options: the options to apply to the torrent on add
|
||||
:type options: dict
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dict
|
||||
|
||||
:returns: a Deferred which returns the torrent_id as a str or None
|
||||
"""
|
||||
log.info("Attempting to add url %s", url)
|
||||
def on_download_success(filename):
|
||||
# We got the file, so add it to the session
|
||||
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_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, tempfile.mkstemp()[1], headers=headers, force_filename=True
|
||||
)
|
||||
d.addCallbacks(on_download_success, on_download_fail)
|
||||
return d
|
||||
|
||||
@export
|
||||
def add_torrent_magnet(self, uri, options):
|
||||
"""
|
||||
Adds a torrent from a magnet link.
|
||||
|
||||
:param uri: the magnet link
|
||||
:type uri: string
|
||||
:param options: the options to apply to the torrent on add
|
||||
:type options: dict
|
||||
|
||||
:returns: the torrent_id
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
log.debug("Attempting to add by magnet uri: %s", uri)
|
||||
|
||||
return self.torrentmanager.add(magnet=uri, options=options)
|
||||
|
||||
@export
|
||||
def remove_torrent(self, torrent_id, remove_data):
|
||||
"""
|
||||
Removes a torrent from the session.
|
||||
|
||||
:param torrent_id: the torrent_id of the torrent to remove
|
||||
:type torrent_id: string
|
||||
:param remove_data: if True, remove the data associated with this torrent
|
||||
:type remove_data: boolean
|
||||
:returns: True if removed successfully
|
||||
:rtype: bool
|
||||
|
||||
:raises InvalidTorrentError: if the torrent_id does not exist in the session
|
||||
|
||||
"""
|
||||
log.debug("Removing torrent %s from the core.", torrent_id)
|
||||
return self.torrentmanager.remove(torrent_id, remove_data)
|
||||
|
||||
@export
|
||||
def get_session_status(self, keys):
|
||||
"""
|
||||
Gets the session status values for 'keys', these keys are taking
|
||||
from libtorrent's session status.
|
||||
|
||||
See: http://www.rasterbar.com/products/libtorrent/manual.html#status
|
||||
|
||||
:param keys: the keys for which we want values
|
||||
:type keys: list
|
||||
:returns: a dictionary of {key: value, ...}
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
status = {}
|
||||
session_status = self.session.status()
|
||||
for key in keys:
|
||||
status[key] = getattr(session_status, key)
|
||||
|
||||
return status
|
||||
|
||||
@export
|
||||
def get_cache_status(self):
|
||||
"""
|
||||
Returns a dictionary of the session's cache status.
|
||||
|
||||
:returns: the cache status
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
|
||||
status = self.session.get_cache_status()
|
||||
cache = {}
|
||||
for attr in dir(status):
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
cache[attr] = getattr(status, attr)
|
||||
|
||||
# Add in a couple ratios
|
||||
try:
|
||||
cache["write_hit_ratio"] = float((cache["blocks_written"] - cache["writes"])) / float(cache["blocks_written"])
|
||||
except ZeroDivisionError:
|
||||
cache["write_hit_ratio"] = 0.0
|
||||
|
||||
try:
|
||||
cache["read_hit_ratio"] = float(cache["blocks_read_hit"]) / float(cache["blocks_read"])
|
||||
except ZeroDivisionError:
|
||||
cache["read_hit_ratio"] = 0.0
|
||||
|
||||
return cache
|
||||
|
||||
@export
|
||||
def force_reannounce(self, torrent_ids):
|
||||
log.debug("Forcing reannouncment to: %s", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].force_reannounce()
|
||||
|
||||
@export
|
||||
def pause_torrent(self, torrent_ids):
|
||||
log.debug("Pausing: %s", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
if not self.torrentmanager[torrent_id].pause():
|
||||
log.warning("Error pausing torrent %s", torrent_id)
|
||||
|
||||
@export
|
||||
def connect_peer(self, torrent_id, ip, port):
|
||||
log.debug("adding peer %s to %s", ip, torrent_id)
|
||||
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
|
||||
log.warning("Error adding peer %s:%s to %s", ip, port, torrent_id)
|
||||
|
||||
@export
|
||||
def move_storage(self, torrent_ids, dest):
|
||||
log.debug("Moving storage %s to %s", torrent_ids, dest)
|
||||
for torrent_id in torrent_ids:
|
||||
if not self.torrentmanager[torrent_id].move_storage(dest):
|
||||
log.warning("Error moving torrent %s to %s", torrent_id, dest)
|
||||
|
||||
@export
|
||||
def pause_all_torrents(self):
|
||||
"""Pause all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.pause()
|
||||
|
||||
@export
|
||||
def resume_all_torrents(self):
|
||||
"""Resume all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.resume()
|
||||
component.get("EventManager").emit(SessionResumedEvent())
|
||||
|
||||
@export
|
||||
def resume_torrent(self, torrent_ids):
|
||||
log.debug("Resuming: %s", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].resume()
|
||||
|
||||
@export
|
||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
||||
# Build the status dictionary
|
||||
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()))
|
||||
if len(leftover_fields) > 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, leftover_fields))
|
||||
return status
|
||||
|
||||
@export
|
||||
def get_torrents_status(self, filter_dict, keys, diff=False):
|
||||
"""
|
||||
returns all torrents , optionally filtered by filter_dict.
|
||||
"""
|
||||
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
|
||||
status_dict = {}.fromkeys(torrent_ids)
|
||||
|
||||
# Get the torrent status for each torrent_id
|
||||
for torrent_id in torrent_ids:
|
||||
status_dict[torrent_id] = self.get_torrent_status(torrent_id, keys, diff)
|
||||
|
||||
return status_dict
|
||||
|
||||
@export
|
||||
def get_filter_tree(self , show_zero_hits=True, hide_cat=None):
|
||||
"""
|
||||
returns {field: [(value,count)] }
|
||||
for use in sidebar(s)
|
||||
"""
|
||||
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
|
||||
|
||||
@export
|
||||
def get_session_state(self):
|
||||
"""Returns a list of torrent_ids in the session."""
|
||||
# Get the torrent list from the TorrentManager
|
||||
return self.torrentmanager.get_torrent_list()
|
||||
|
||||
@export
|
||||
def get_config(self):
|
||||
"""Get all the preferences as a dictionary"""
|
||||
return self.config.config
|
||||
|
||||
@export
|
||||
def get_config_value(self, key):
|
||||
"""Get the config value for key"""
|
||||
try:
|
||||
value = self.config[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
@export
|
||||
def get_config_values(self, keys):
|
||||
"""Get the config values for the entered keys"""
|
||||
config = {}
|
||||
for key in keys:
|
||||
try:
|
||||
config[key] = self.config[key]
|
||||
except KeyError:
|
||||
pass
|
||||
return config
|
||||
|
||||
@export
|
||||
def set_config(self, config):
|
||||
"""Set the config with values from dictionary"""
|
||||
# Load all the values into the configuration
|
||||
for key in config.keys():
|
||||
if isinstance(config[key], basestring):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
|
||||
@export
|
||||
def get_listen_port(self):
|
||||
"""Returns the active listen port"""
|
||||
return self.session.listen_port()
|
||||
|
||||
@export
|
||||
def get_num_connections(self):
|
||||
"""Returns the current number of connections"""
|
||||
return self.session.num_connections()
|
||||
|
||||
@export
|
||||
def get_available_plugins(self):
|
||||
"""Returns a list of plugins available in the core"""
|
||||
return self.pluginmanager.get_available_plugins()
|
||||
|
||||
@export
|
||||
def get_enabled_plugins(self):
|
||||
"""Returns a list of enabled plugins in the core"""
|
||||
return self.pluginmanager.get_enabled_plugins()
|
||||
|
||||
@export
|
||||
def enable_plugin(self, plugin):
|
||||
self.pluginmanager.enable_plugin(plugin)
|
||||
return None
|
||||
|
||||
@export
|
||||
def disable_plugin(self, plugin):
|
||||
self.pluginmanager.disable_plugin(plugin)
|
||||
return None
|
||||
|
||||
@export
|
||||
def force_recheck(self, torrent_ids):
|
||||
"""Forces a data recheck on torrent_ids"""
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].force_recheck()
|
||||
|
||||
@export
|
||||
def set_torrent_options(self, torrent_ids, options):
|
||||
"""Sets the torrent options for torrent_ids"""
|
||||
for torrent_id in torrent_ids:
|
||||
self.torrentmanager[torrent_id].set_options(options)
|
||||
|
||||
@export
|
||||
def set_torrent_trackers(self, torrent_id, trackers):
|
||||
"""Sets a torrents tracker list. trackers will be [{"url", "tier"}]"""
|
||||
return self.torrentmanager[torrent_id].set_trackers(trackers)
|
||||
|
||||
@export
|
||||
def set_torrent_max_connections(self, torrent_id, value):
|
||||
"""Sets a torrents max number of connections"""
|
||||
return self.torrentmanager[torrent_id].set_max_connections(value)
|
||||
|
||||
@export
|
||||
def set_torrent_max_upload_slots(self, torrent_id, value):
|
||||
"""Sets a torrents max number of upload slots"""
|
||||
return self.torrentmanager[torrent_id].set_max_upload_slots(value)
|
||||
|
||||
@export
|
||||
def set_torrent_max_upload_speed(self, torrent_id, value):
|
||||
"""Sets a torrents max upload speed"""
|
||||
return self.torrentmanager[torrent_id].set_max_upload_speed(value)
|
||||
|
||||
@export
|
||||
def set_torrent_max_download_speed(self, torrent_id, value):
|
||||
"""Sets a torrents max download speed"""
|
||||
return self.torrentmanager[torrent_id].set_max_download_speed(value)
|
||||
|
||||
@export
|
||||
def set_torrent_file_priorities(self, torrent_id, priorities):
|
||||
"""Sets a torrents file priorities"""
|
||||
return self.torrentmanager[torrent_id].set_file_priorities(priorities)
|
||||
|
||||
@export
|
||||
def set_torrent_prioritize_first_last(self, torrent_id, value):
|
||||
"""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"""
|
||||
return self.torrentmanager[torrent_id].set_auto_managed(value)
|
||||
|
||||
@export
|
||||
def set_torrent_stop_at_ratio(self, torrent_id, value):
|
||||
"""Sets the torrent to stop at 'stop_ratio'"""
|
||||
return self.torrentmanager[torrent_id].set_stop_at_ratio(value)
|
||||
|
||||
@export
|
||||
def set_torrent_stop_ratio(self, torrent_id, value):
|
||||
"""Sets the ratio when to stop a torrent if 'stop_at_ratio' is set"""
|
||||
return self.torrentmanager[torrent_id].set_stop_ratio(value)
|
||||
|
||||
@export
|
||||
def set_torrent_remove_at_ratio(self, torrent_id, value):
|
||||
"""Sets the torrent to be removed at 'stop_ratio'"""
|
||||
return self.torrentmanager[torrent_id].set_remove_at_ratio(value)
|
||||
|
||||
@export
|
||||
def set_torrent_move_completed(self, torrent_id, value):
|
||||
"""Sets the torrent to be moved when completed"""
|
||||
return self.torrentmanager[torrent_id].set_move_completed(value)
|
||||
|
||||
@export
|
||||
def set_torrent_move_completed_path(self, torrent_id, value):
|
||||
"""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
|
||||
unaccessible (non-existent or insufficient privs)"""
|
||||
return deluge.common.get_path_size(path)
|
||||
|
||||
@export
|
||||
def create_torrent(self, path, tracker, piece_length, comment, target,
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
|
||||
log.debug("creating torrent..")
|
||||
threading.Thread(target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session)).start()
|
||||
|
||||
def _create_torrent_thread(self, path, tracker, piece_length, comment, target,
|
||||
webseeds, private, created_by, trackers, add_to_session):
|
||||
import deluge.metafile
|
||||
deluge.metafile.make_meta_file(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment=comment,
|
||||
target=target,
|
||||
webseeds=webseeds,
|
||||
private=private,
|
||||
created_by=created_by,
|
||||
trackers=trackers)
|
||||
log.debug("torrent created!")
|
||||
if add_to_session:
|
||||
options = {}
|
||||
options["download_location"] = os.path.split(path)[0]
|
||||
self.add_torrent_file(os.path.split(target)[1], open(target, "rb").read(), options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename, filedump):
|
||||
"""This method is used to upload new plugins to the daemon. It is used
|
||||
when connecting to the daemon remotely and installing a new plugin on
|
||||
the client side. 'plugin_data' is a xmlrpc.Binary object of the file data,
|
||||
ie, plugin_file.read()"""
|
||||
|
||||
try:
|
||||
filedump = base64.decodestring(filedump)
|
||||
except Exception, e:
|
||||
log.error("There was an error decoding the filedump string!")
|
||||
log.exception(e)
|
||||
return
|
||||
|
||||
f = open(os.path.join(deluge.configmanager.get_config_dir(), "plugins", filename), "wb")
|
||||
f.write(filedump)
|
||||
f.close()
|
||||
component.get("CorePluginManager").scan_for_plugins()
|
||||
|
||||
@export
|
||||
def rescan_plugins(self):
|
||||
"""
|
||||
Rescans the plugin folders for new plugins
|
||||
"""
|
||||
component.get("CorePluginManager").scan_for_plugins()
|
||||
|
||||
@export
|
||||
def rename_files(self, torrent_id, filenames):
|
||||
"""
|
||||
Rename files in torrent_id. Since this is an asynchronous operation by
|
||||
libtorrent, watch for the TorrentFileRenamedEvent to know when the
|
||||
files have been renamed.
|
||||
|
||||
:param torrent_id: the torrent_id to rename files
|
||||
:type torrent_id: string
|
||||
:param filenames: a list of index, filename pairs
|
||||
:type filenames: ((index, filename), ...)
|
||||
|
||||
:raises InvalidTorrentError: if torrent_id is invalid
|
||||
|
||||
"""
|
||||
if torrent_id not in self.torrentmanager.torrents:
|
||||
raise InvalidTorrentError("torrent_id is not in session")
|
||||
|
||||
self.torrentmanager[torrent_id].rename_files(filenames)
|
||||
|
||||
@export
|
||||
def rename_folder(self, torrent_id, folder, new_folder):
|
||||
"""
|
||||
Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
|
||||
TorrentFolderRenamedEvent which is emitted when the folder has been
|
||||
renamed successfully.
|
||||
|
||||
:param torrent_id: the torrent to rename folder in
|
||||
:type torrent_id: string
|
||||
:param folder: the folder to rename
|
||||
:type folder: string
|
||||
:param new_folder: the new folder name
|
||||
:type new_folder: string
|
||||
|
||||
:raises InvalidTorrentError: if the torrent_id is invalid
|
||||
|
||||
"""
|
||||
if torrent_id not in self.torrentmanager.torrents:
|
||||
raise InvalidTorrentError("torrent_id is not in session")
|
||||
|
||||
self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
|
||||
|
||||
@export
|
||||
def queue_top(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to top", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_top(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
|
||||
@export
|
||||
def queue_up(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to up", torrent_ids)
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_up(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
|
||||
@export
|
||||
def queue_down(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to down", torrent_ids)
|
||||
#torrent_ids must be sorted before moving.
|
||||
torrent_ids = list(torrent_ids)
|
||||
torrent_ids.sort(key = lambda id: -self.torrentmanager.torrents[id].get_queue_position())
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_down(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
|
||||
@export
|
||||
def queue_bottom(self, torrent_ids):
|
||||
log.debug("Attempting to queue %s to bottom", torrent_ids)
|
||||
for torrent_id in torrent_ids:
|
||||
try:
|
||||
# If the queue method returns True, then we should emit a signal
|
||||
if self.torrentmanager.queue_bottom(torrent_id):
|
||||
component.get("EventManager").emit(TorrentQueueChangedEvent())
|
||||
except KeyError:
|
||||
log.warning("torrent_id: %s does not exist in the queue", torrent_id)
|
||||
|
||||
@export
|
||||
def glob(self, path):
|
||||
return glob.glob(path)
|
||||
|
||||
@export
|
||||
def test_listen_port(self):
|
||||
"""
|
||||
Checks if the active port is open
|
||||
|
||||
:returns: True if the port is open, False if not
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
from twisted.web.client import getPage
|
||||
|
||||
d = getPage("http://deluge-torrent.org/test_port.php?port=%s" %
|
||||
self.get_listen_port(), timeout=30)
|
||||
|
||||
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
|
||||
|
||||
@export
|
||||
def get_free_space(self, path=None):
|
||||
"""
|
||||
Returns the number of free bytes at path
|
||||
|
||||
:param path: the path to check free space at, if None, use the default
|
||||
download location
|
||||
:type path: string
|
||||
|
||||
:returns: the number of free bytes at path
|
||||
:rtype: int
|
||||
|
||||
:raises InvalidPathError: if the path is invalid
|
||||
|
||||
"""
|
||||
if not path:
|
||||
path = self.config["download_location"]
|
||||
try:
|
||||
return deluge.common.free_space(path)
|
||||
except InvalidPathError:
|
||||
return 0
|
||||
|
||||
@export
|
||||
def get_libtorrent_version(self):
|
||||
"""
|
||||
Returns the libtorrent version.
|
||||
|
||||
:returns: the version
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
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)
|
@@ -1,201 +0,0 @@
|
||||
#
|
||||
# daemon.py
|
||||
#
|
||||
# Copyright (C) 2007-2009 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
|
||||
import logging
|
||||
from twisted.internet import reactor
|
||||
import twisted.internet.error
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
from deluge.core.rpcserver import RPCServer, export
|
||||
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 = int(pid)
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
pid = None
|
||||
port = None
|
||||
|
||||
|
||||
def process_running(pid):
|
||||
if deluge.common.windows_check():
|
||||
# Do some fancy WMI junk to see if the PID exists in Windows
|
||||
from win32com.client import GetObject
|
||||
def get_proclist():
|
||||
WMI = GetObject('winmgmts:')
|
||||
processes = WMI.InstancesOf('Win32_Process')
|
||||
return [process.Properties_('ProcessID').Value for process in processes]
|
||||
return pid in get_proclist()
|
||||
else:
|
||||
# We can just use os.kill on UNIX to test if the process is running
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
if pid is not None and process_running(pid):
|
||||
# Ok, so a process is running with this PID, let's make doubly-sure
|
||||
# it's a deluged process by trying to open a socket to it's port.
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect(("127.0.0.1", port))
|
||||
except socket.error:
|
||||
# Can't connect, so it must not be a deluged process..
|
||||
pass
|
||||
else:
|
||||
# This is a deluged!
|
||||
s.close()
|
||||
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.
|
||||
reactor.addSystemEventTrigger("after", "shutdown", self.shutdown)
|
||||
|
||||
# Catch some Windows specific signals
|
||||
if deluge.common.windows_check():
|
||||
from win32api import SetConsoleCtrlHandler
|
||||
from win32con import CTRL_CLOSE_EVENT
|
||||
from win32con import CTRL_SHUTDOWN_EVENT
|
||||
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()
|
||||
return 1
|
||||
SetConsoleCtrlHandler(win_handler)
|
||||
|
||||
version = deluge.common.get_version()
|
||||
|
||||
log.info("Deluge daemon %s", version)
|
||||
log.debug("options: %s", options)
|
||||
log.debug("args: %s", args)
|
||||
# Set the config directory
|
||||
if options and options.config:
|
||||
deluge.configmanager.set_config_dir(options.config)
|
||||
|
||||
from deluge.core.core import Core
|
||||
# Start the core as a thread and join it until it's done
|
||||
self.core = Core()
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
port = options.port
|
||||
if options and options.ui_interface:
|
||||
interface = options.ui_interface
|
||||
else:
|
||||
interface = ""
|
||||
|
||||
self.rpcserver = RPCServer(
|
||||
port=port,
|
||||
allow_remote=self.core.config["allow_remote"],
|
||||
listen=not classic,
|
||||
interface=interface
|
||||
)
|
||||
|
||||
# Register the daemon and the core RPCs
|
||||
self.rpcserver.register_object(self.core)
|
||||
self.rpcserver.register_object(self)
|
||||
|
||||
|
||||
# Make sure we start the PreferencesManager first
|
||||
component.start("PreferencesManager")
|
||||
|
||||
if not classic:
|
||||
# Write out a pid file all the time, we use this to see if a deluged is running
|
||||
# We also include the running port number to do an additional test
|
||||
open(deluge.configmanager.get_config_dir("deluged.pid"), "wb").write(
|
||||
"%s;%s\n" % (os.getpid(), port))
|
||||
|
||||
component.start()
|
||||
try:
|
||||
reactor.run()
|
||||
finally:
|
||||
self._shutdown()
|
||||
|
||||
@export()
|
||||
def shutdown(self, *args, **kwargs):
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
def _shutdown(self, *args, **kwargs):
|
||||
try:
|
||||
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
log.error("Error removing deluged.pid!")
|
||||
|
||||
component.shutdown()
|
||||
try:
|
||||
reactor.stop()
|
||||
except twisted.internet.error.ReactorNotRunning:
|
||||
log.debug("Tried to stop the reactor but it is not running..")
|
||||
|
||||
@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()
|
@@ -1,86 +0,0 @@
|
||||
#
|
||||
# eventmanager.py
|
||||
#
|
||||
# Copyright (C) 2009 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 logging
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EventManager(component.Component):
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "EventManager")
|
||||
self.handlers = {}
|
||||
|
||||
def emit(self, event):
|
||||
"""
|
||||
Emits the event to interested clients.
|
||||
|
||||
:param event: DelugeEvent
|
||||
"""
|
||||
# Emit the event to the interested clients
|
||||
component.get("RPCServer").emit_event(event)
|
||||
# Call any handlers for the event
|
||||
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)
|
||||
try:
|
||||
handler(*event.args)
|
||||
except:
|
||||
log.error("Event handler %s failed in %s", event.name, handler)
|
||||
|
||||
def register_event_handler(self, event, handler):
|
||||
"""
|
||||
Registers a function to be called when a `:param:event` is emitted.
|
||||
|
||||
:param event: str, the event name
|
||||
:param handler: function, to be called when `:param:event` is emitted
|
||||
|
||||
"""
|
||||
if event not in self.handlers:
|
||||
self.handlers[event] = []
|
||||
|
||||
if handler not in self.handlers[event]:
|
||||
self.handlers[event].append(handler)
|
||||
|
||||
def deregister_event_handler(self, event, handler):
|
||||
"""
|
||||
Deregisters an event handler function.
|
||||
|
||||
:param event: str, the event name
|
||||
:param handler: function, currently registered to handle `:param:event`
|
||||
|
||||
"""
|
||||
if event in self.handlers and handler in self.handlers[event]:
|
||||
self.handlers[event].remove(handler)
|
@@ -1,294 +0,0 @@
|
||||
#
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@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 logging
|
||||
import deluge.component as component
|
||||
|
||||
STATE_SORT = ["All", "Downloading", "Seeding", "Active", "Paused", "Queued"]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#special purpose filters:
|
||||
def filter_keywords(torrent_ids, values):
|
||||
#cleanup.
|
||||
keywords = ",".join([v.lower() for v in values])
|
||||
keywords = keywords.split(",")
|
||||
|
||||
|
||||
for keyword in keywords:
|
||||
torrent_ids = filter_one_keyword(torrent_ids, keyword)
|
||||
return torrent_ids
|
||||
|
||||
def filter_one_keyword(torrent_ids, keyword):
|
||||
"""
|
||||
search torrent on keyword.
|
||||
searches title,state,tracker-status,tracker,files
|
||||
"""
|
||||
all_torrents = component.get("TorrentManager").torrents
|
||||
#filter:
|
||||
found = False
|
||||
for torrent_id in torrent_ids:
|
||||
torrent = all_torrents[torrent_id]
|
||||
if keyword in torrent.filename.lower():
|
||||
yield torrent_id
|
||||
elif keyword in torrent.state.lower():
|
||||
yield torrent_id
|
||||
elif torrent.trackers and keyword in torrent.trackers[0]["url"]:
|
||||
yield torrent_id
|
||||
elif keyword in torrent_id:
|
||||
yield torrent_id
|
||||
#i want to find broken torrents (search on "error", or "unregistered")
|
||||
elif keyword in torrent.tracker_status.lower():
|
||||
yield torrent_id
|
||||
else:
|
||||
for t_file in torrent.get_files():
|
||||
if keyword in t_file["path"].lower():
|
||||
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")
|
||||
|
||||
# If this is a tracker_host, then we need to filter on it
|
||||
if values[0] != "Error":
|
||||
for torrent_id in torrent_ids:
|
||||
if values[0] == tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
return filtered_torrent_ids
|
||||
|
||||
# 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"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
|
||||
class FilterManager(component.Component):
|
||||
"""FilterManager
|
||||
|
||||
"""
|
||||
def __init__(self, core):
|
||||
component.Component.__init__(self, "FilterManager")
|
||||
log.debug("FilterManager init..")
|
||||
self.core = core
|
||||
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)
|
||||
def _init_tracker_tree():
|
||||
return {"Error": 0}
|
||||
self.register_tree_field("tracker_host", _init_tracker_tree)
|
||||
|
||||
self.register_filter("tracker_host", tracker_error_filter)
|
||||
|
||||
def _init_users_tree():
|
||||
return {"": 0}
|
||||
self.register_tree_field("owner", _init_users_tree)
|
||||
|
||||
def filter_torrent_ids(self, filter_dict):
|
||||
"""
|
||||
returns a list of torrent_id's matching filter_dict.
|
||||
core filter method
|
||||
"""
|
||||
if not filter_dict:
|
||||
return self.torrents.get_torrent_list()
|
||||
|
||||
#sanitize input: filter-value must be a list of strings
|
||||
for key, value in filter_dict.items():
|
||||
if isinstance(value, str):
|
||||
filter_dict[key] = [value]
|
||||
|
||||
|
||||
if "id"in filter_dict: #optimized filter for id:
|
||||
torrent_ids = list(filter_dict["id"])
|
||||
del filter_dict["id"]
|
||||
else:
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
|
||||
if not filter_dict: #return if there's nothing more to filter
|
||||
return torrent_ids
|
||||
|
||||
#special purpose: state=Active.
|
||||
if "state" in filter_dict:
|
||||
# We need to make sure this is a list for the logic below
|
||||
filter_dict["state"] = list(filter_dict["state"])
|
||||
|
||||
if "state" in filter_dict and "Active" in filter_dict["state"]:
|
||||
filter_dict["state"].remove("Active")
|
||||
if not filter_dict["state"]:
|
||||
del filter_dict["state"]
|
||||
torrent_ids = self.filter_state_active(torrent_ids)
|
||||
|
||||
if not filter_dict: #return if there's nothing more to filter
|
||||
return torrent_ids
|
||||
|
||||
#Registered filters:
|
||||
for field, values in filter_dict.items():
|
||||
if field in self.registered_filters:
|
||||
# a set filters out the doubles.
|
||||
torrent_ids = list(set(self.registered_filters[field](torrent_ids, values)))
|
||||
del filter_dict[field]
|
||||
|
||||
|
||||
if not filter_dict: #return if there's nothing more to filter
|
||||
return torrent_ids
|
||||
|
||||
#leftover filter arguments:
|
||||
#default filter on status fields.
|
||||
status_func = self.core.get_torrent_status #premature optimalisation..
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, filter_dict.keys()) #status={key:value}
|
||||
for field, values in filter_dict.iteritems():
|
||||
if (not status[field] in values) and torrent_id in torrent_ids:
|
||||
torrent_ids.remove(torrent_id)
|
||||
|
||||
return torrent_ids
|
||||
|
||||
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
|
||||
"""
|
||||
returns {field: [(value,count)] }
|
||||
for use in sidebar.
|
||||
"""
|
||||
torrent_ids = self.torrents.get_torrent_list()
|
||||
status_func = self.core.get_torrent_status #premature optimalisation..
|
||||
tree_keys = list(self.tree_fields.keys())
|
||||
if hide_cat:
|
||||
for cat in hide_cat:
|
||||
tree_keys.remove(cat)
|
||||
|
||||
items = dict( (field, self.tree_fields[field]()) for field in tree_keys)
|
||||
|
||||
#count status fields.
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, tree_keys) #status={key:value}
|
||||
for field in tree_keys:
|
||||
value = status[field]
|
||||
items[field][value] = items[field].get(value, 0) + 1
|
||||
|
||||
if "tracker_host" in items:
|
||||
items["tracker_host"]["All"] = len(torrent_ids)
|
||||
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
|
||||
|
||||
if "state" in tree_keys and not show_zero_hits:
|
||||
self._hide_state_items(items["state"])
|
||||
|
||||
#return a dict of tuples:
|
||||
sorted_items = {}
|
||||
for field in tree_keys:
|
||||
sorted_items[field] = sorted(items[field].iteritems())
|
||||
|
||||
if "state" in tree_keys:
|
||||
sorted_items["state"].sort(self._sort_state_items)
|
||||
|
||||
return sorted_items
|
||||
|
||||
def _init_state_tree(self):
|
||||
return {"All":len(self.torrents.get_torrent_list()),
|
||||
"Downloading":0,
|
||||
"Seeding":0,
|
||||
"Paused":0,
|
||||
"Checking":0,
|
||||
"Queued":0,
|
||||
"Error":0,
|
||||
"Active":len(self.filter_state_active(self.torrents.get_torrent_list()))
|
||||
}
|
||||
|
||||
def register_filter(self, id, filter_func, filter_value = None):
|
||||
self.registered_filters[id] = filter_func
|
||||
|
||||
def deregister_filter(self, id):
|
||||
del self.registered_filters[id]
|
||||
|
||||
def register_tree_field(self, field, init_func = lambda : {}):
|
||||
self.tree_fields[field] = init_func
|
||||
|
||||
def deregister_tree_field(self, field):
|
||||
if field in self.tree_fields:
|
||||
del self.tree_fields[field]
|
||||
|
||||
def filter_state_active(self, torrent_ids):
|
||||
get_status = self.core.get_torrent_status
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = get_status(torrent_id, ["download_payload_rate", "upload_payload_rate"])
|
||||
if status["download_payload_rate"] or status["upload_payload_rate"]:
|
||||
pass #ok
|
||||
else:
|
||||
torrent_ids.remove(torrent_id)
|
||||
return torrent_ids
|
||||
|
||||
def _hide_state_items(self, state_items):
|
||||
"for hide(show)-zero hits"
|
||||
for (value, count) in state_items.items():
|
||||
if value != "All" and count == 0:
|
||||
del state_items[value]
|
||||
|
||||
def _sort_state_items(self, x, y):
|
||||
""
|
||||
if x[0] in STATE_SORT:
|
||||
ix = STATE_SORT.index(x[0])
|
||||
else:
|
||||
ix = 99
|
||||
if y[0] in STATE_SORT:
|
||||
iy = STATE_SORT.index(y[0])
|
||||
else:
|
||||
iy = 99
|
||||
|
||||
return ix - iy
|
@@ -1,143 +0,0 @@
|
||||
#
|
||||
# oldstateupgrader.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
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#start : http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
|
||||
def makeFakeClass(module, name):
|
||||
class FakeThing(object):
|
||||
pass
|
||||
FakeThing.__name__ = name
|
||||
FakeThing.__module__ = '(fake)' + module
|
||||
return FakeThing
|
||||
|
||||
class PickleUpgrader(pickle.Unpickler):
|
||||
def find_class(self, module, cname):
|
||||
# Pickle tries to load a couple things like copy_reg and
|
||||
# __builtin__.object even though a pickle file doesn't
|
||||
# explicitly reference them (afaict): allow them to be loaded
|
||||
# normally.
|
||||
if module in ('copy_reg', '__builtin__'):
|
||||
thing = pickle.Unpickler.find_class(self, module, cname)
|
||||
return thing
|
||||
return makeFakeClass(module, cname)
|
||||
# end: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
|
||||
|
||||
class OldStateUpgrader:
|
||||
def __init__(self):
|
||||
self.config = ConfigManager("core.conf")
|
||||
self.state05_location = os.path.join(get_config_dir(), "persistent.state")
|
||||
self.state10_location = os.path.join(get_config_dir(), "state", "torrents.state")
|
||||
if os.path.exists(self.state05_location) and not os.path.exists(self.state10_location):
|
||||
# If the 0.5 state file exists and the 1.0 doesn't, then let's upgrade it
|
||||
self.upgrade05()
|
||||
|
||||
def upgrade05(self):
|
||||
try:
|
||||
state = PickleUpgrader(open(self.state05_location, "rb")).load()
|
||||
except Exception, e:
|
||||
log.debug("Unable to open 0.5 state file: %s", e)
|
||||
return
|
||||
|
||||
# Check to see if we can upgrade this file
|
||||
if type(state).__name__ == 'list':
|
||||
log.warning("0.5 state file is too old to upgrade")
|
||||
return
|
||||
|
||||
new_state = deluge.core.torrentmanager.TorrentManagerState()
|
||||
for ti, uid in state.torrents.items():
|
||||
torrent_path = os.path.join(get_config_dir(), "torrentfiles", ti.filename)
|
||||
try:
|
||||
torrent_info = None
|
||||
log.debug("Attempting to create torrent_info from %s", torrent_path)
|
||||
_file = open(torrent_path, "rb")
|
||||
torrent_info = lt.torrent_info(lt.bdecode(_file.read()))
|
||||
_file.close()
|
||||
except (IOError, RuntimeError), e:
|
||||
log.warning("Unable to open %s: %s", torrent_path, e)
|
||||
|
||||
# Copy the torrent file to the new location
|
||||
import shutil
|
||||
shutil.copyfile(torrent_path, os.path.join(get_config_dir(), "state", str(torrent_info.info_hash()) + ".torrent"))
|
||||
|
||||
# Set the file prioritiy property if not already there
|
||||
if not hasattr(ti, "priorities"):
|
||||
ti.priorities = [1] * torrent_info.num_files()
|
||||
|
||||
# Create the new TorrentState object
|
||||
new_torrent = deluge.core.torrentmanager.TorrentState(
|
||||
torrent_id=str(torrent_info.info_hash()),
|
||||
filename=ti.filename,
|
||||
save_path=ti.save_dir,
|
||||
compact=ti.compact,
|
||||
paused=ti.user_paused,
|
||||
total_uploaded=ti.uploaded_memory,
|
||||
max_upload_speed=ti.upload_rate_limit,
|
||||
max_download_speed=ti.download_rate_limit,
|
||||
file_priorities=ti.priorities,
|
||||
queue=state.queue.index(ti)
|
||||
)
|
||||
# Append the object to the state list
|
||||
new_state.torrents.append(new_torrent)
|
||||
|
||||
# Now we need to write out the new state file
|
||||
try:
|
||||
log.debug("Saving torrent state file.")
|
||||
state_file = open(
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"), "wb")
|
||||
cPickle.dump(new_state, state_file)
|
||||
state_file.close()
|
||||
except IOError, e:
|
||||
log.warning("Unable to save state file: %s", e)
|
||||
return
|
||||
|
||||
# Rename the persistent.state file
|
||||
try:
|
||||
os.rename(self.state05_location, self.state05_location + ".old")
|
||||
except Exception, e:
|
||||
log.debug("Unable to rename old persistent.state file! %s", e)
|
@@ -1,114 +0,0 @@
|
||||
#
|
||||
# pluginmanager.py
|
||||
#
|
||||
# Copyright (C) 2007 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.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.event import PluginEnabledEvent, PluginDisabledEvent
|
||||
import deluge.pluginmanagerbase
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
component.Component):
|
||||
"""PluginManager handles the loading of plugins and provides plugins with
|
||||
functions to access parts of the core."""
|
||||
|
||||
def __init__(self, core):
|
||||
component.Component.__init__(self, "CorePluginManager")
|
||||
|
||||
self.status_fields = {}
|
||||
|
||||
# Call the PluginManagerBase constructor
|
||||
deluge.pluginmanagerbase.PluginManagerBase.__init__(
|
||||
self, "core.conf", "deluge.plugin.core")
|
||||
|
||||
def start(self):
|
||||
# Enable plugins that are enabled in the config
|
||||
self.enable_plugins()
|
||||
|
||||
def stop(self):
|
||||
# Disable all enabled plugins
|
||||
self.disable_plugins()
|
||||
|
||||
def shutdown(self):
|
||||
self.stop()
|
||||
|
||||
def update_plugins(self):
|
||||
for plugin in self.plugins.keys():
|
||||
if hasattr(self.plugins[plugin], "update"):
|
||||
try:
|
||||
self.plugins[plugin].update()
|
||||
except Exception, e:
|
||||
log.exception(e)
|
||||
|
||||
def enable_plugin(self, name):
|
||||
if name not in self.plugins:
|
||||
super(PluginManager, self).enable_plugin(name)
|
||||
if name in self.plugins:
|
||||
component.get("EventManager").emit(PluginEnabledEvent(name))
|
||||
|
||||
def disable_plugin(self, name):
|
||||
if name in self.plugins:
|
||||
super(PluginManager, self).disable_plugin(name)
|
||||
if name not in self.plugins:
|
||||
component.get("EventManager").emit(PluginDisabledEvent(name))
|
||||
|
||||
def get_status(self, torrent_id, fields):
|
||||
"""Return the value of status fields for the selected torrent_id."""
|
||||
status = {}
|
||||
for field in fields:
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
except KeyError:
|
||||
pass
|
||||
return status
|
||||
|
||||
def register_status_field(self, field, function):
|
||||
"""Register a new status field. This can be used in the same way the
|
||||
client requests other status information from core."""
|
||||
log.debug("Registering status field %s with PluginManager", field)
|
||||
self.status_fields[field] = function
|
||||
|
||||
def deregister_status_field(self, field):
|
||||
"""Deregisters a status field"""
|
||||
log.debug("Deregistering status field %s with PluginManager", field)
|
||||
try:
|
||||
del self.status_fields[field]
|
||||
except:
|
||||
log.warning("Unable to deregister status field %s", field)
|
@@ -1,477 +0,0 @@
|
||||
#
|
||||
# preferencesmanager.py
|
||||
#
|
||||
# Copyright (C) 2008-2010 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
|
||||
import logging
|
||||
import threading
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
from deluge.event import *
|
||||
import deluge.configmanager
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
"info_sent": 0.0,
|
||||
"daemon_port": 58846,
|
||||
"allow_remote": False,
|
||||
"compact_allocation": False,
|
||||
"download_location": deluge.common.get_default_download_dir(),
|
||||
"listen_ports": [6881, 6891],
|
||||
"listen_interface": "",
|
||||
"copy_torrent_file": False,
|
||||
"del_copy_torrent_file": False,
|
||||
"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,
|
||||
"natpmp": True,
|
||||
"utpex": True,
|
||||
"lsd": True,
|
||||
"enc_in_policy": 1,
|
||||
"enc_out_policy": 1,
|
||||
"enc_level": 2,
|
||||
"enc_prefer_rc4": True,
|
||||
"max_connections_global": 200,
|
||||
"max_upload_speed": -1.0,
|
||||
"max_download_speed": -1.0,
|
||||
"max_upload_slots_global": 4,
|
||||
"max_half_open_connections": (lambda: deluge.common.windows_check() and
|
||||
(lambda: deluge.common.vista_check() and 4 or 8)() or 50)(),
|
||||
"max_connections_per_second": 20,
|
||||
"ignore_limits_on_local_network": True,
|
||||
"max_connections_per_torrent": -1,
|
||||
"max_upload_slots_per_torrent": -1,
|
||||
"max_upload_speed_per_torrent": -1,
|
||||
"max_download_speed_per_torrent": -1,
|
||||
"enabled_plugins": [],
|
||||
"add_paused": False,
|
||||
"max_active_seeding": 5,
|
||||
"max_active_downloading": 3,
|
||||
"max_active_limit": 8,
|
||||
"dont_count_slow_torrents": False,
|
||||
"queue_new_to_top": False,
|
||||
"stop_seed_at_ratio": False,
|
||||
"remove_seed_at_ratio": False,
|
||||
"stop_seed_ratio": 2.00,
|
||||
"share_ratio_limit": 2.00,
|
||||
"seed_time_ratio_limit": 7.00,
|
||||
"seed_time_limit": 180,
|
||||
"auto_managed": True,
|
||||
"move_completed": False,
|
||||
"move_completed_path": deluge.common.get_default_download_dir(),
|
||||
"new_release_check": True,
|
||||
"proxies": {
|
||||
"peer": {
|
||||
"type": 0,
|
||||
"hostname": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 8080
|
||||
},
|
||||
"web_seed": {
|
||||
"type": 0,
|
||||
"hostname": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 8080
|
||||
},
|
||||
"tracker": {
|
||||
"type": 0,
|
||||
"hostname": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 8080
|
||||
},
|
||||
"dht": {
|
||||
"type": 0,
|
||||
"hostname": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 8080
|
||||
},
|
||||
|
||||
},
|
||||
"outgoing_ports": [0, 0],
|
||||
"random_outgoing_ports": True,
|
||||
"peer_tos": "0x00",
|
||||
"rate_limit_ip_overhead": True,
|
||||
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
|
||||
"cache_size": 512,
|
||||
"cache_expiry": 60,
|
||||
"auto_manage_prefer_seeds": False,
|
||||
"shared": False
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
def __init__(self):
|
||||
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.new_release_timer = None
|
||||
|
||||
# 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 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):
|
||||
if self.config["copy_torrent_file"]:
|
||||
try:
|
||||
os.makedirs(value)
|
||||
except Exception, e:
|
||||
log.debug("Unable to make directory: %s", e)
|
||||
|
||||
def _on_set_listen_ports(self, key, value):
|
||||
# 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"])
|
||||
)
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
self._on_set_random_port("random_port", self.config["random_port"])
|
||||
|
||||
def _on_set_random_port(self, key, value):
|
||||
log.debug("random port value set to %s", value)
|
||||
# We need to check if the value has been changed to true and false
|
||||
# and then handle accordingly.
|
||||
if value:
|
||||
import random
|
||||
listen_ports = []
|
||||
randrange = lambda: random.randrange(49152, 65525)
|
||||
listen_ports.append(randrange())
|
||||
listen_ports.append(listen_ports[0]+10)
|
||||
else:
|
||||
listen_ports = self.config["listen_ports"]
|
||||
|
||||
# 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"])
|
||||
)
|
||||
|
||||
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.session_set_setting("outgoing_ports", (value[0], value[1]))
|
||||
|
||||
def _on_set_random_outgoing_ports(self, key, value):
|
||||
if value:
|
||||
self.session.outgoing_ports(0, 0)
|
||||
|
||||
def _on_set_peer_tos(self, key, value):
|
||||
log.debug("setting peer_tos to: %s", value)
|
||||
try:
|
||||
self.session_set_setting("peer_tos", chr(int(value, 16)))
|
||||
except ValueError, e:
|
||||
log.debug("Invalid tos byte: %s", e)
|
||||
return
|
||||
|
||||
def _on_set_dht(self, key, value):
|
||||
log.debug("dht value set to %s", value)
|
||||
state_file = deluge.configmanager.get_config_dir("dht.state")
|
||||
if value:
|
||||
state = None
|
||||
try:
|
||||
state = lt.bdecode(open(state_file, "rb").read())
|
||||
except Exception, e:
|
||||
log.warning("Unable to read DHT state file: %s", e)
|
||||
|
||||
try:
|
||||
self.session.start_dht(state)
|
||||
except Exception, e:
|
||||
log.warning("Restoring old DHT state failed: %s", e)
|
||||
self.session.start_dht(None)
|
||||
self.session.add_dht_router("router.bittorrent.com", 6881)
|
||||
self.session.add_dht_router("router.utorrent.com", 6881)
|
||||
self.session.add_dht_router("router.bitcomet.com", 6881)
|
||||
else:
|
||||
self.core.save_dht_state()
|
||||
self.session.stop_dht()
|
||||
|
||||
def _on_set_upnp(self, key, value):
|
||||
log.debug("upnp value set to %s", value)
|
||||
if value:
|
||||
self.session.start_upnp()
|
||||
else:
|
||||
self.session.stop_upnp()
|
||||
|
||||
def _on_set_natpmp(self, key, value):
|
||||
log.debug("natpmp value set to %s", value)
|
||||
if value:
|
||||
self.session.start_natpmp()
|
||||
else:
|
||||
self.session.stop_natpmp()
|
||||
|
||||
def _on_set_lsd(self, key, value):
|
||||
log.debug("lsd value set to %s", value)
|
||||
if value:
|
||||
self.session.start_lsd()
|
||||
else:
|
||||
self.session.stop_lsd()
|
||||
|
||||
def _on_set_utpex(self, key, value):
|
||||
log.debug("utpex value set to %s", value)
|
||||
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()
|
||||
pe_settings.out_enc_policy = \
|
||||
lt.enc_policy(self.config["enc_out_policy"])
|
||||
pe_settings.in_enc_policy = lt.enc_policy(self.config["enc_in_policy"])
|
||||
pe_settings.allowed_enc_level = lt.enc_level(self.config["enc_level"])
|
||||
pe_settings.prefer_rc4 = self.config["enc_prefer_rc4"]
|
||||
self.session.set_pe_settings(pe_settings)
|
||||
set = self.session.get_pe_settings()
|
||||
log.debug("encryption settings:\n\t\t\tout_policy: %s\n\t\t\
|
||||
in_policy: %s\n\t\t\tlevel: %s\n\t\t\tprefer_rc4: %s",
|
||||
set.out_enc_policy,
|
||||
set.in_enc_policy,
|
||||
set.allowed_enc_level,
|
||||
set.prefer_rc4)
|
||||
|
||||
def _on_set_max_connections_global(self, key, value):
|
||||
log.debug("max_connections_global set to %s..", value)
|
||||
self.session.set_max_connections(value)
|
||||
|
||||
def _on_set_max_upload_speed(self, key, value):
|
||||
log.debug("max_upload_speed set to %s..", value)
|
||||
# We need to convert Kb/s to B/s
|
||||
if value < 0:
|
||||
v = -1
|
||||
else:
|
||||
v = int(value * 1024)
|
||||
|
||||
self.session.set_upload_rate_limit(v)
|
||||
|
||||
def _on_set_max_download_speed(self, key, value):
|
||||
log.debug("max_download_speed set to %s..", value)
|
||||
# We need to convert Kb/s to B/s
|
||||
if value < 0:
|
||||
v = -1
|
||||
else:
|
||||
v = int(value * 1024)
|
||||
self.session.set_download_rate_limit(v)
|
||||
|
||||
def _on_set_max_upload_slots_global(self, key, value):
|
||||
log.debug("max_upload_slots_global set to %s..", value)
|
||||
self.session.set_max_uploads(value)
|
||||
|
||||
def _on_set_max_half_open_connections(self, key, value):
|
||||
self.session.set_max_half_open_connections(value)
|
||||
|
||||
def _on_set_max_connections_per_second(self, key, value):
|
||||
self.session_set_setting("connection_speed", value)
|
||||
|
||||
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.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.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.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)
|
||||
self.session_set_setting("active_downloads", value)
|
||||
|
||||
def _on_set_max_active_seeding(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
self.session_set_setting("active_seeds", value)
|
||||
|
||||
def _on_set_max_active_limit(self, key, value):
|
||||
log.debug("%s set to %s..", key, value)
|
||||
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.session_set_setting("dont_count_slow_torrents", value)
|
||||
|
||||
def _on_set_send_info(self, key, value):
|
||||
log.debug("Sending anonymous stats..")
|
||||
"""sends anonymous stats home"""
|
||||
class Send_Info_Thread(threading.Thread):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
threading.Thread.__init__(self)
|
||||
def run(self):
|
||||
import time
|
||||
now = time.time()
|
||||
# check if we've done this within the last week or never
|
||||
if (now - self.config["info_sent"]) >= (60 * 60 * 24 * 7):
|
||||
import deluge.common
|
||||
from urllib import quote_plus
|
||||
from urllib2 import urlopen
|
||||
import platform
|
||||
try:
|
||||
url = "http://deluge-torrent.org/stats_get.php?processor=" + \
|
||||
platform.machine() + "&python=" + platform.python_version() \
|
||||
+ "&deluge=" + deluge.common.get_version() \
|
||||
+ "&os=" + platform.system() \
|
||||
+ "&plugins=" + quote_plus(":".join(self.config["enabled_plugins"]))
|
||||
urlopen(url)
|
||||
except IOError, e:
|
||||
log.debug("Network error while trying to send info: %s", e)
|
||||
else:
|
||||
self.config["info_sent"] = now
|
||||
if value:
|
||||
Send_Info_Thread(self.config).start()
|
||||
|
||||
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 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_set_new_release_check, "new_release_check", True)
|
||||
self.new_release_timer.start(72 * 60 * 60, False)
|
||||
else:
|
||||
if self.new_release_timer and self.new_release_timer.running:
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
for k, v in value.items():
|
||||
if v["type"]:
|
||||
proxy_settings = lt.proxy_settings()
|
||||
proxy_settings.type = lt.proxy_type(v["type"])
|
||||
proxy_settings.username = str(v["username"])
|
||||
proxy_settings.password = str(v["password"])
|
||||
proxy_settings.hostname = str(v["hostname"])
|
||||
proxy_settings.port = v["port"]
|
||||
log.debug("setting %s proxy settings", k)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_set_rate_limit_ip_overhead(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.session_set_setting("rate_limit_ip_overhead", 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(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!")
|
||||
|
||||
if geoip_db:
|
||||
try:
|
||||
self.session.load_country_db(str(geoip_db))
|
||||
except Exception, e:
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_set_cache_size(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.session_set_setting("cache_size", value)
|
||||
|
||||
def _on_set_cache_expiry(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
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)
|
@@ -1,612 +0,0 @@
|
||||
#
|
||||
# rpcserver.py
|
||||
#
|
||||
# Copyright (C) 2008,2009 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.
|
||||
#
|
||||
#
|
||||
|
||||
"""RPCServer Module"""
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import reactor, defer
|
||||
|
||||
from OpenSSL import crypto, SSL
|
||||
from types import FunctionType
|
||||
|
||||
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,
|
||||
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
|
||||
will need to be registered with an :class:`RPCServer` to be effective.
|
||||
|
||||
:param func: the function to export
|
||||
:type func: function
|
||||
:param auth_level: the auth level required to call this method
|
||||
:type auth_level: int
|
||||
|
||||
"""
|
||||
def wrap(func, *args, **kwargs):
|
||||
func._rpcserver_export = True
|
||||
func._rpcserver_auth_level = auth_level
|
||||
doc = func.__doc__
|
||||
func.__doc__ = "**RPC Exported Function** (*Auth Level: %s*)\n\n" % auth_level
|
||||
if doc:
|
||||
func.__doc__ += doc
|
||||
|
||||
return func
|
||||
|
||||
if type(auth_level) is FunctionType:
|
||||
func = auth_level
|
||||
auth_level = AUTH_LEVEL_DEFAULT
|
||||
return wrap(func)
|
||||
else:
|
||||
return wrap
|
||||
|
||||
|
||||
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] + "("
|
||||
if call[2]:
|
||||
s += ", ".join([str(x) for x in call[2]])
|
||||
if call[3]:
|
||||
if call[2]:
|
||||
s += ", "
|
||||
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
|
||||
s += ")"
|
||||
except UnicodeEncodeError:
|
||||
return "UnicodeEncodeError, call: %s" % call
|
||||
else:
|
||||
return s
|
||||
|
||||
class ServerContextFactory(object):
|
||||
def getContext(self):
|
||||
"""
|
||||
Create an SSL context.
|
||||
|
||||
This loads the servers cert/private key SSL files for use with the
|
||||
SSL transport.
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
ctx = SSL.Context(SSL.SSLv3_METHOD)
|
||||
ctx.use_certificate_file(os.path.join(ssl_dir, "daemon.cert"))
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
|
||||
return ctx
|
||||
|
||||
class DelugeRPCProtocol(Protocol):
|
||||
__buffer = None
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
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
|
||||
:meth:`dispatch`.
|
||||
|
||||
:param data: the data from the client. It should be a zlib compressed
|
||||
rencoded string.
|
||||
:type data: str
|
||||
|
||||
"""
|
||||
if self.__buffer:
|
||||
# We have some data from the last dataReceived() so lets prepend it
|
||||
data = self.__buffer + data
|
||||
self.__buffer = None
|
||||
|
||||
while data:
|
||||
dobj = zlib.decompressobj()
|
||||
try:
|
||||
request = rencode.loads(dobj.decompress(data))
|
||||
except Exception, e:
|
||||
#log.debug("Received possible invalid message (%r): %s", data, e)
|
||||
# This could be cut-off data, so we'll save this in the buffer
|
||||
# and try to prepend it on the next dataReceived()
|
||||
self.__buffer = data
|
||||
return
|
||||
else:
|
||||
data = dobj.unused_data
|
||||
|
||||
if type(request) is not tuple:
|
||||
log.debug("Received invalid message: type is not tuple")
|
||||
return
|
||||
|
||||
if len(request) < 1:
|
||||
log.debug("Received invalid message: there are no items")
|
||||
return
|
||||
|
||||
for call in request:
|
||||
if len(call) != 4:
|
||||
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)
|
||||
|
||||
def sendData(self, data):
|
||||
"""
|
||||
Sends the data to the client.
|
||||
|
||||
: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)))
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
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)
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
This method is called when the client is disconnected.
|
||||
|
||||
:param reason: the reason the client disconnected.
|
||||
:type reason: str
|
||||
|
||||
"""
|
||||
|
||||
# We need to remove this session from various dicts
|
||||
del self.factory.authorized_sessions[self.transport.sessionno]
|
||||
if self.transport.sessionno in self.factory.session_protocols:
|
||||
del self.factory.session_protocols[self.transport.sessionno]
|
||||
if self.transport.sessionno in self.factory.interested_events:
|
||||
del self.factory.interested_events[self.transport.sessionno]
|
||||
|
||||
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
|
||||
and will send either a RPC Response or RPC Error back to the client.
|
||||
|
||||
:param request_id: the request_id from the client (sent in the RPC Request)
|
||||
:type request_id: int
|
||||
:param method: the local method to call. It must be registered with
|
||||
the :class:`RPCServer`.
|
||||
:type method: str
|
||||
:param args: the arguments to pass to `method`
|
||||
:type args: list
|
||||
:param kwargs: the keyword-arguments to pass to `method`
|
||||
:type kwargs: dict
|
||||
|
||||
"""
|
||||
def sendError():
|
||||
"""
|
||||
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()
|
||||
|
||||
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, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception, e:
|
||||
sendError()
|
||||
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.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.
|
||||
try:
|
||||
if self.transport.sessionno not in self.factory.interested_events:
|
||||
self.factory.interested_events[self.transport.sessionno] = []
|
||||
self.factory.interested_events[self.transport.sessionno].extend(args[0])
|
||||
except Exception, e:
|
||||
sendError()
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, (True)))
|
||||
finally:
|
||||
return
|
||||
|
||||
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][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, 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
|
||||
if not isinstance(e, DelugeError):
|
||||
log.exception("Exception calling RPC request: %s", e)
|
||||
else:
|
||||
# Check if the return value is a deferred, since we'll need to
|
||||
# wait for it to fire before sending the RPC_RESPONSE
|
||||
if isinstance(ret, defer.Deferred):
|
||||
def on_success(result):
|
||||
self.sendData((RPC_RESPONSE, request_id, result))
|
||||
return result
|
||||
|
||||
def on_fail(failure):
|
||||
try:
|
||||
failure.raiseException()
|
||||
except Exception, e:
|
||||
sendError()
|
||||
return failure
|
||||
|
||||
ret.addCallbacks(on_success, on_fail)
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, ret))
|
||||
|
||||
class RPCServer(component.Component):
|
||||
"""
|
||||
This class is used to handle rpc requests from the client. Objects are
|
||||
registered with this class and their methods are exported using the export
|
||||
decorator.
|
||||
|
||||
:param port: the port the RPCServer will listen on
|
||||
:type port: int
|
||||
:param interface: the interface to listen on, this may override the `allow_remote` setting
|
||||
:type interface: str
|
||||
:param allow_remote: set True if the server should allow remote connections
|
||||
:type allow_remote: bool
|
||||
:param listen: if False, will not start listening.. This is only useful in Classic Mode
|
||||
:type listen: bool
|
||||
"""
|
||||
|
||||
def __init__(self, port=58846, interface="", allow_remote=False, listen=True):
|
||||
component.Component.__init__(self, "RPCServer")
|
||||
|
||||
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
|
||||
self.factory.authorized_sessions = {}
|
||||
# Holds the protocol objects with the session_id as key
|
||||
self.factory.session_protocols = {}
|
||||
# Holds the interested event list for the sessions
|
||||
self.factory.interested_events = {}
|
||||
|
||||
self.listen = listen
|
||||
if not listen:
|
||||
return
|
||||
|
||||
if allow_remote:
|
||||
hostname = ""
|
||||
else:
|
||||
hostname = "localhost"
|
||||
|
||||
if interface:
|
||||
hostname = interface
|
||||
|
||||
log.info("Starting DelugeRPC server %s:%s", hostname, port)
|
||||
|
||||
# Check for SSL keys and generate some if needed
|
||||
check_ssl_keys()
|
||||
|
||||
try:
|
||||
reactor.listenSSL(port, self.factory, ServerContextFactory(), interface=hostname)
|
||||
except Exception, e:
|
||||
log.info("Daemon already running or port not available..")
|
||||
log.error(e)
|
||||
sys.exit(0)
|
||||
|
||||
def register_object(self, obj, name=None):
|
||||
"""
|
||||
Registers an object to export it's rpc methods. These methods should
|
||||
be exported with the export decorator prior to registering the object.
|
||||
|
||||
:param obj: the object that we want to export
|
||||
:type obj: object
|
||||
:param name: the name to use, if None, it will be the class name of the object
|
||||
:type name: str
|
||||
"""
|
||||
if not name:
|
||||
name = obj.__class__.__name__.lower()
|
||||
|
||||
for d in dir(obj):
|
||||
if d[0] == "_":
|
||||
continue
|
||||
if getattr(getattr(obj, d), '_rpcserver_export', False):
|
||||
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.
|
||||
|
||||
:param name: the name of the method, usually in the form of 'object.method'
|
||||
:type name: str
|
||||
|
||||
:returns: method
|
||||
|
||||
:raises KeyError: if `name` is not registered
|
||||
|
||||
"""
|
||||
return self.factory.methods[name]
|
||||
|
||||
def get_method_list(self):
|
||||
"""
|
||||
Returns a list of the exported methods.
|
||||
|
||||
:returns: the exported methods
|
||||
:rtype: list
|
||||
"""
|
||||
return self.factory.methods.keys()
|
||||
|
||||
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.
|
||||
|
||||
:param event: the event to emit
|
||||
:type event: :class:`deluge.event.DelugeEvent`
|
||||
"""
|
||||
log.debug("intevents: %s", self.factory.interested_events)
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.iteritems():
|
||||
if event.name in interest:
|
||||
log.debug("Emit Event: %s %s", event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
self.factory.session_protocols[session_id].sendData(
|
||||
(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
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
if not os.path.exists(ssl_dir):
|
||||
# The ssl folder doesn't exist so we need to create it
|
||||
os.makedirs(ssl_dir)
|
||||
generate_ssl_keys()
|
||||
else:
|
||||
for f in ("daemon.pkey", "daemon.cert"):
|
||||
if not os.path.exists(os.path.join(ssl_dir, f)):
|
||||
generate_ssl_keys()
|
||||
break
|
||||
|
||||
def generate_ssl_keys():
|
||||
"""
|
||||
This method generates a new SSL key/cert.
|
||||
"""
|
||||
digest = "md5"
|
||||
# Generate key pair
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 1024)
|
||||
|
||||
# Generate cert request
|
||||
req = crypto.X509Req()
|
||||
subj = req.get_subject()
|
||||
setattr(subj, "CN", "Deluge Daemon")
|
||||
req.set_pubkey(pkey)
|
||||
req.sign(pkey, digest)
|
||||
|
||||
# Generate certificate
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(0)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60*60*24*365*5) # Five Years
|
||||
cert.set_issuer(req.get_subject())
|
||||
cert.set_subject(req.get_subject())
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
cert.sign(pkey, digest)
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
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)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# decorators.py
|
||||
#
|
||||
# Copyright (C) 2010 John Garland <johnnybg+deluge@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.
|
||||
#
|
||||
#
|
||||
|
||||
from functools import wraps
|
||||
|
||||
def proxy(proxy_func):
|
||||
"""
|
||||
Factory class which returns a decorator that passes
|
||||
the decorated function to a proxy function
|
||||
|
||||
:param proxy_func: the proxy function
|
||||
:type proxy_func: function
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return proxy_func(func, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
131
deluge/error.py
131
deluge/error.py
@@ -1,131 +0,0 @@
|
||||
#
|
||||
# error.py
|
||||
#
|
||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class DelugeError(Exception):
|
||||
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
|
||||
|
||||
class DaemonRunningError(DelugeError):
|
||||
pass
|
||||
|
||||
class InvalidTorrentError(DelugeError):
|
||||
pass
|
||||
|
||||
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
|
262
deluge/event.py
262
deluge/event.py
@@ -1,262 +0,0 @@
|
||||
#
|
||||
# event.py
|
||||
#
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
Event module.
|
||||
|
||||
This module describes the types of events that can be generated by the daemon
|
||||
and subsequently emitted to the clients.
|
||||
|
||||
"""
|
||||
|
||||
known_events = {}
|
||||
|
||||
class DelugeEventMetaClass(type):
|
||||
"""
|
||||
This metaclass simply keeps a list of all events classes created.
|
||||
"""
|
||||
def __init__(cls, name, bases, dct):
|
||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
||||
if name != "DelugeEvent":
|
||||
known_events[name] = cls
|
||||
|
||||
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
|
||||
|
||||
def _get_name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def _get_args(self):
|
||||
if not hasattr(self, "_args"):
|
||||
return []
|
||||
return self._args
|
||||
|
||||
name = property(fget=_get_name)
|
||||
args = property(fget=_get_args)
|
||||
|
||||
class TorrentAddedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a new torrent is successfully added to the session.
|
||||
"""
|
||||
def __init__(self, torrent_id, from_state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id of the torrent that was added
|
||||
: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, from_state]
|
||||
|
||||
class TorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent has been removed from the session.
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
class PreTorrentRemovedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent is about to be removed from the session.
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentStateChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent changes state.
|
||||
"""
|
||||
def __init__(self, torrent_id, state):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
:param state: the new state
|
||||
:type state: string
|
||||
"""
|
||||
self._args = [torrent_id, state]
|
||||
|
||||
class TorrentQueueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the queue order has changed.
|
||||
"""
|
||||
pass
|
||||
|
||||
class TorrentFolderRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a folder within a torrent has been renamed.
|
||||
"""
|
||||
def __init__(self, torrent_id, old, new):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
: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]
|
||||
|
||||
class TorrentFileRenamedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a file within a torrent has been renamed.
|
||||
"""
|
||||
def __init__(self, torrent_id, index, name):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
: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]
|
||||
|
||||
class TorrentFinishedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent finishes downloading.
|
||||
"""
|
||||
def __init__(self, torrent_id):
|
||||
"""
|
||||
:param torrent_id: the torrent_id
|
||||
:type torrent_id: string
|
||||
"""
|
||||
self._args = [torrent_id]
|
||||
|
||||
class TorrentResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a torrent resumes from a paused state.
|
||||
"""
|
||||
def __init__(self, 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: the new version that is available
|
||||
:type new_release: string
|
||||
"""
|
||||
self._args = [new_release]
|
||||
|
||||
class SessionStartedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a session has started. This typically only happens once when
|
||||
the daemon is initially started.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SessionPausedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been paused.
|
||||
"""
|
||||
pass
|
||||
|
||||
class SessionResumedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when the session has been resumed.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ConfigValueChangedEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a config value changes in the Core.
|
||||
"""
|
||||
def __init__(self, key, value):
|
||||
"""
|
||||
:param key: the key that changed
|
||||
:type key: string
|
||||
:param value: the new value of the `:param:key`
|
||||
"""
|
||||
self._args = [key, value]
|
||||
|
||||
class PluginEnabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is enabled in the Core.
|
||||
"""
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
||||
class PluginDisabledEvent(DelugeEvent):
|
||||
"""
|
||||
Emitted when a plugin is disabled in the Core.
|
||||
"""
|
||||
def __init__(self, plugin_name):
|
||||
self._args = [plugin_name]
|
||||
|
@@ -1,207 +0,0 @@
|
||||
#
|
||||
# httpdownloader.py
|
||||
#
|
||||
# Copyright (C) 2009 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.
|
||||
#
|
||||
|
||||
from twisted.web import client, http
|
||||
from twisted.web.error import PageRedirect
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet import reactor
|
||||
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):
|
||||
"""
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param force_filename: forces use of the supplied filename, regardless of header content
|
||||
:type force_filename: bool
|
||||
:param part_callback: a function to be called when a part of data
|
||||
is received, it's signature should be: func(data, current_length, total_length)
|
||||
:type part_callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
"""
|
||||
self.part_callback = part_callback
|
||||
self.current_length = 0
|
||||
self.decoder = None
|
||||
self.value = filename
|
||||
self.force_filename = force_filename
|
||||
self.allow_compression = allow_compression
|
||||
agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
|
||||
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
|
||||
|
||||
def gotStatus(self, version, status, message):
|
||||
self.code = int(status)
|
||||
client.HTTPDownloader.gotStatus(self, version, status, message)
|
||||
|
||||
def gotHeaders(self, headers):
|
||||
if self.code == http.OK:
|
||||
if "content-length" in headers:
|
||||
self.total_length = int(headers["content-length"][0])
|
||||
else:
|
||||
self.total_length = 0
|
||||
|
||||
if self.allow_compression and "content-encoding" in headers and \
|
||||
headers["content-encoding"][0] in ("gzip", "x-gzip", "deflate"):
|
||||
# Adding 32 to the wbits enables gzip & zlib decoding (with automatic header detection)
|
||||
# Adding 16 just enables gzip decoding (no zlib)
|
||||
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
|
||||
|
||||
if "content-disposition" in headers and not self.force_filename:
|
||||
new_file_name = str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(os.path.split(self.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]
|
||||
error = PageRedirect(self.code, location=location)
|
||||
self.noPage(Failure(error))
|
||||
|
||||
return client.HTTPDownloader.gotHeaders(self, headers)
|
||||
|
||||
def pagePart(self, data):
|
||||
if self.code == http.OK:
|
||||
self.current_length += len(data)
|
||||
if self.decoder:
|
||||
data = self.decoder.decompress(data)
|
||||
if self.part_callback:
|
||||
self.part_callback(data, self.current_length, self.total_length)
|
||||
|
||||
return client.HTTPDownloader.pagePart(self, data)
|
||||
|
||||
def pageEnd(self):
|
||||
if self.decoder:
|
||||
data = self.decoder.flush()
|
||||
self.current_length -= len(data)
|
||||
self.decoder = None
|
||||
self.pagePart(data)
|
||||
|
||||
return client.HTTPDownloader.pageEnd(self)
|
||||
|
||||
def sanitise_filename(filename):
|
||||
"""
|
||||
Sanitises a filename to use as a download destination file.
|
||||
Logs any filenames that could be considered malicious.
|
||||
|
||||
:param filename: the filename to sanitise
|
||||
:type filename: string
|
||||
:returns: the sanitised filename
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
# Remove any quotes
|
||||
filename = filename.strip("'\"")
|
||||
|
||||
if os.path.basename(filename) != filename:
|
||||
# Dodgy server, log it
|
||||
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)
|
||||
|
||||
return filename
|
||||
|
||||
def download_file(url, filename, callback=None, headers=None,
|
||||
force_filename=False, allow_compression=True):
|
||||
"""
|
||||
Downloads a file from a specific URL and returns a Deferred. You can also
|
||||
specify a callback function to be called as parts are received.
|
||||
|
||||
:param url: the url to download from
|
||||
:type url: string
|
||||
:param filename: the filename to save the file as
|
||||
:type filename: string
|
||||
:param 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 callback: function
|
||||
:param headers: any optional headers to send
|
||||
:type headers: dictionary
|
||||
:param force_filename: force us to use the filename specified rather than
|
||||
one the server may suggest
|
||||
:type force_filename: boolean
|
||||
:param allow_compression: allows gzip & deflate decoding
|
||||
:type allow_compression: boolean
|
||||
|
||||
:returns: the filename of the downloaded file
|
||||
:rtype: Deferred
|
||||
|
||||
:raises t.w.e.PageRedirect: when server responds with a temporary redirect
|
||||
or permanently moved.
|
||||
:raises t.w.e.Error: for all other HTTP response errors (besides OK)
|
||||
"""
|
||||
url = str(url)
|
||||
filename = str(filename)
|
||||
if headers:
|
||||
for key, value in headers.items():
|
||||
headers[str(key)] = str(value)
|
||||
|
||||
if allow_compression:
|
||||
if not headers:
|
||||
headers = {}
|
||||
headers["accept-encoding"] = "deflate, gzip, x-gzip"
|
||||
|
||||
scheme, host, port, path = client._parse(url)
|
||||
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
|
||||
if scheme == "https":
|
||||
from twisted.internet import ssl
|
||||
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
|
||||
else:
|
||||
reactor.connectTCP(host, port, factory)
|
||||
|
||||
return factory.deferred
|
4191
deluge/i18n/ar.po
4191
deluge/i18n/ar.po
File diff suppressed because it is too large
Load Diff
4653
deluge/i18n/ast.po
4653
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
3922
deluge/i18n/be.po
3922
deluge/i18n/be.po
File diff suppressed because it is too large
Load Diff
4664
deluge/i18n/bg.po
4664
deluge/i18n/bg.po
File diff suppressed because it is too large
Load Diff
3725
deluge/i18n/bn.po
3725
deluge/i18n/bn.po
File diff suppressed because it is too large
Load Diff
3744
deluge/i18n/bs.po
3744
deluge/i18n/bs.po
File diff suppressed because it is too large
Load Diff
4115
deluge/i18n/ca.po
4115
deluge/i18n/ca.po
File diff suppressed because it is too large
Load Diff
4726
deluge/i18n/cs.po
4726
deluge/i18n/cs.po
File diff suppressed because it is too large
Load Diff
3820
deluge/i18n/cy.po
3820
deluge/i18n/cy.po
File diff suppressed because it is too large
Load Diff
4694
deluge/i18n/da.po
4694
deluge/i18n/da.po
File diff suppressed because it is too large
Load Diff
4777
deluge/i18n/de.po
4777
deluge/i18n/de.po
File diff suppressed because it is too large
Load Diff
4733
deluge/i18n/el.po
4733
deluge/i18n/el.po
File diff suppressed because it is too large
Load Diff
4734
deluge/i18n/en_AU.po
4734
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load Diff
4736
deluge/i18n/en_CA.po
4736
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load Diff
4736
deluge/i18n/en_GB.po
4736
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load Diff
3741
deluge/i18n/eo.po
3741
deluge/i18n/eo.po
File diff suppressed because it is too large
Load Diff
4753
deluge/i18n/es.po
4753
deluge/i18n/es.po
File diff suppressed because it is too large
Load Diff
4708
deluge/i18n/et.po
4708
deluge/i18n/et.po
File diff suppressed because it is too large
Load Diff
3760
deluge/i18n/eu.po
3760
deluge/i18n/eu.po
File diff suppressed because it is too large
Load Diff
3967
deluge/i18n/fa.po
3967
deluge/i18n/fa.po
File diff suppressed because it is too large
Load Diff
4737
deluge/i18n/fi.po
4737
deluge/i18n/fi.po
File diff suppressed because it is too large
Load Diff
4832
deluge/i18n/fr.po
4832
deluge/i18n/fr.po
File diff suppressed because it is too large
Load Diff
4460
deluge/i18n/fy.po
4460
deluge/i18n/fy.po
File diff suppressed because it is too large
Load Diff
4086
deluge/i18n/gl.po
4086
deluge/i18n/gl.po
File diff suppressed because it is too large
Load Diff
4579
deluge/i18n/he.po
4579
deluge/i18n/he.po
File diff suppressed because it is too large
Load Diff
4636
deluge/i18n/hi.po
4636
deluge/i18n/hi.po
File diff suppressed because it is too large
Load Diff
4198
deluge/i18n/hr.po
4198
deluge/i18n/hr.po
File diff suppressed because it is too large
Load Diff
4714
deluge/i18n/hu.po
4714
deluge/i18n/hu.po
File diff suppressed because it is too large
Load Diff
4098
deluge/i18n/id.po
4098
deluge/i18n/id.po
File diff suppressed because it is too large
Load Diff
4642
deluge/i18n/is.po
4642
deluge/i18n/is.po
File diff suppressed because it is too large
Load Diff
4825
deluge/i18n/it.po
4825
deluge/i18n/it.po
File diff suppressed because it is too large
Load Diff
3712
deluge/i18n/iu.po
3712
deluge/i18n/iu.po
File diff suppressed because it is too large
Load Diff
4652
deluge/i18n/ja.po
4652
deluge/i18n/ja.po
File diff suppressed because it is too large
Load Diff
3918
deluge/i18n/ka.po
3918
deluge/i18n/ka.po
File diff suppressed because it is too large
Load Diff
4737
deluge/i18n/kk.po
4737
deluge/i18n/kk.po
File diff suppressed because it is too large
Load Diff
3922
deluge/i18n/kn.po
3922
deluge/i18n/kn.po
File diff suppressed because it is too large
Load Diff
4645
deluge/i18n/ko.po
4645
deluge/i18n/ko.po
File diff suppressed because it is too large
Load Diff
3754
deluge/i18n/ku.po
3754
deluge/i18n/ku.po
File diff suppressed because it is too large
Load Diff
3727
deluge/i18n/la.po
3727
deluge/i18n/la.po
File diff suppressed because it is too large
Load Diff
4720
deluge/i18n/lt.po
4720
deluge/i18n/lt.po
File diff suppressed because it is too large
Load Diff
4742
deluge/i18n/lv.po
4742
deluge/i18n/lv.po
File diff suppressed because it is too large
Load Diff
4044
deluge/i18n/mk.po
4044
deluge/i18n/mk.po
File diff suppressed because it is too large
Load Diff
4688
deluge/i18n/ms.po
4688
deluge/i18n/ms.po
File diff suppressed because it is too large
Load Diff
4671
deluge/i18n/nb.po
4671
deluge/i18n/nb.po
File diff suppressed because it is too large
Load Diff
3739
deluge/i18n/nds.po
3739
deluge/i18n/nds.po
File diff suppressed because it is too large
Load Diff
4749
deluge/i18n/nl.po
4749
deluge/i18n/nl.po
File diff suppressed because it is too large
Load Diff
4725
deluge/i18n/pl.po
4725
deluge/i18n/pl.po
File diff suppressed because it is too large
Load Diff
3715
deluge/i18n/pms.po
3715
deluge/i18n/pms.po
File diff suppressed because it is too large
Load Diff
4746
deluge/i18n/pt.po
4746
deluge/i18n/pt.po
File diff suppressed because it is too large
Load Diff
4746
deluge/i18n/pt_BR.po
4746
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load Diff
4744
deluge/i18n/ro.po
4744
deluge/i18n/ro.po
File diff suppressed because it is too large
Load Diff
4799
deluge/i18n/ru.po
4799
deluge/i18n/ru.po
File diff suppressed because it is too large
Load Diff
3712
deluge/i18n/si.po
3712
deluge/i18n/si.po
File diff suppressed because it is too large
Load Diff
4627
deluge/i18n/sk.po
4627
deluge/i18n/sk.po
File diff suppressed because it is too large
Load Diff
4544
deluge/i18n/sl.po
4544
deluge/i18n/sl.po
File diff suppressed because it is too large
Load Diff
4388
deluge/i18n/sr.po
4388
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
4767
deluge/i18n/sv.po
4767
deluge/i18n/sv.po
File diff suppressed because it is too large
Load Diff
3733
deluge/i18n/ta.po
3733
deluge/i18n/ta.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user