Ticket #1504: common.py

File common.py, 15.6 KB (added by mindthemonkey, 12 years ago)

Patched common.py

Line 
1#
2# common.py
3#
4# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
5#
6# Deluge is free software.
7#
8# You may redistribute it and/or modify it under the terms of the
9# GNU General Public License, as published by the Free Software
10# Foundation; either version 3 of the License, or (at your option)
11# any later version.
12#
13# deluge is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16# See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with deluge.    If not, write to:
20#       The Free Software Foundation, Inc.,
21#       51 Franklin Street, Fifth Floor
22#       Boston, MA  02110-1301, USA.
23#
24#    In addition, as a special exception, the copyright holders give
25#    permission to link the code of portions of this program with the OpenSSL
26#    library.
27#    You must obey the GNU General Public License in all respects for all of
28#    the code used other than OpenSSL. If you modify file(s) with this
29#    exception, you may extend this exception to your version of the file(s),
30#    but you are not obligated to do so. If you do not wish to do so, delete
31#    this exception statement from your version. If you delete this exception
32#    statement from all source files in the program, then also delete it here.
33#
34#
35
36
37
38"""Common functions for various parts of Deluge to use."""
39
40import os
41import time
42import subprocess
43import platform
44import sys
45import chardet
46
47try:
48    import json
49except ImportError:
50    import simplejson as json
51
52# Do a little hack here just in case the user has json-py installed since it
53# has a different api
54if not hasattr(json, "dumps"):
55    json.dumps = json.write
56    json.loads = json.read
57
58    def dump(obj, fp, **kw):
59        fp.write(json.dumps(obj))
60
61    def load(fp, **kw):
62        return json.loads(fp.read())
63
64    json.dump = dump
65    json.load = load
66
67import pkg_resources
68import gettext
69import locale
70
71# Initialize gettext
72try:
73    if hasattr(locale, "bindtextdomain"):
74        locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
75    if hasattr(locale, "textdomain"):
76        locale.textdomain("deluge")
77    gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
78    gettext.textdomain("deluge")
79    gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
80except Exception, e:
81    from deluge.log import LOG as log
82    log.error("Unable to initialize gettext/locale!")
83    log.exception(e)
84    import __builtin__
85    __builtin__.__dict__["_"] = lambda x: x
86
87from deluge.error import *
88
89LT_TORRENT_STATE = {
90    "Queued": 0,
91    "Checking": 1,
92    "Downloading Metadata": 2,
93    "Downloading": 3,
94    "Finished": 4,
95    "Seeding": 5,
96    "Allocating": 6,
97    "Checking Resume Data": 7,
98    0: "Queued",
99    1: "Checking",
100    2: "Downloading Metadata",
101    3: "Downloading",
102    4: "Finished",
103    5: "Seeding",
104    6: "Allocating",
105    7: "Checking Resume Data"
106}
107
108
109TORRENT_STATE = [
110    "Allocating",
111    "Checking",
112    "Downloading",
113    "Seeding",
114    "Paused",
115    "Error",
116    "Queued"
117]
118
119FILE_PRIORITY = {
120    0: "Do Not Download",
121    1: "Normal Priority",
122    2: "High Priority",
123    5: "Highest Priority",
124    "Do Not Download": 0,
125    "Normal Priority": 1,
126    "High Priority": 2,
127    "Highest Priority": 5
128}
129
130def get_version():
131    """
132    Returns the program version from the egg metadata
133
134    :returns: the version of Deluge
135    :rtype: string
136
137    """
138    return pkg_resources.require("Deluge")[0].version
139
140def get_default_config_dir(filename=None):
141    """
142    :param filename: if None, only the config path is returned, if provided, a path including the filename will be returned
143    :type filename: string
144    :returns: a file path to the config directory and optional filename
145    :rtype: string
146
147    """
148    if windows_check():
149       
150        appDataPath = os.environ.get("APPDATA")
151        if not appDataPath:
152            import _winreg
153            hkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders")
154            appDataReg = _winreg.QueryValueEx(hkey, "AppData")
155            appDataPath = appDataReg[0]
156            _winreg.CloseKey(hkey)
157           
158        if filename:
159            return os.path.join(appDataPath, "deluge", filename)
160        else:
161            return os.path.join(appDataPath, "deluge")
162    else:
163        import xdg.BaseDirectory
164        if filename:
165            return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
166        else:
167            return xdg.BaseDirectory.save_config_path("deluge")
168
169def get_default_download_dir():
170    """
171    :returns: the default download directory
172    :rtype: string
173
174    """
175    if windows_check():
176        return os.path.expanduser("~")
177    else:
178        return os.environ.get("HOME")
179
180def windows_check():
181    """
182    Checks if the current platform is Windows
183
184    :returns: True or False
185    :rtype: bool
186
187    """
188    return platform.system() in ('Windows', 'Microsoft')
189
190def vista_check():
191    """
192    Checks if the current platform is Windows Vista
193
194    :returns: True or False
195    :rtype: bool
196
197    """
198    return platform.release() == "Vista"
199
200def osx_check():
201    """
202    Checks if the current platform is Mac OS X
203
204    :returns: True or False
205    :rtype: bool
206
207    """
208    return platform.system() == "Darwin"
209
210def get_pixmap(fname):
211    """
212    Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg
213
214    :param fname: the filename to look for
215    :type fname: string
216    :returns: a path to a pixmap file included with Deluge
217    :rtype: string
218
219    """
220    return pkg_resources.resource_filename("deluge", os.path.join("data", \
221                                           "pixmaps", fname))
222
223def open_file(path):
224    """
225    Opens a file or folder using the system configured program
226
227    :param path: the path to the file or folder to open
228    :type path: string
229
230    """
231    if windows_check():
232        os.startfile("%s" % path)
233    else:
234        subprocess.Popen(["xdg-open", "%s" % path])
235
236def open_url_in_browser(url):
237    """
238    Opens a url in the desktop's default browser
239
240    :param url: the url to open
241    :type url: string
242
243    """
244    import webbrowser
245    webbrowser.open(url)
246
247## Formatting text functions
248
249def fsize(fsize_b):
250    """
251    Formats the bytes value into a string with KiB, MiB or GiB units
252
253    :param fsize_b: the filesize in bytes
254    :type fsize_b: int
255    :returns: formatted string in KiB, MiB or GiB units
256    :rtype: string
257
258    **Usage**
259
260    >>> fsize(112245)
261    '109.6 KiB'
262
263    """
264    fsize_kb = fsize_b / 1024.0
265    if fsize_kb < 1024:
266        return "%.1f %s" % (fsize_kb, _("KiB"))
267    fsize_mb = fsize_kb / 1024.0
268    if fsize_mb < 1024:
269        return "%.1f %s" % (fsize_mb, _("MiB"))
270    fsize_gb = fsize_mb / 1024.0
271    return "%.1f %s" % (fsize_gb, _("GiB"))
272
273def fpcnt(dec):
274    """
275    Formats a string to display a percentage with two decimal places
276
277    :param dec: the ratio in the range [0.0, 1.0]
278    :type dec: float
279    :returns: a formatted string representing a percentage
280    :rtype: string
281
282    **Usage**
283
284    >>> fpcnt(0.9311)
285    '93.11%'
286
287    """
288    return '%.2f%%' % (dec * 100)
289
290def fspeed(bps):
291    """
292    Formats a string to display a transfer speed utilizing :func:`fsize`
293
294    :param bps: bytes per second
295    :type bps: int
296    :returns: a formatted string representing transfer speed
297    :rtype: string
298
299    **Usage**
300
301    >>> fspeed(43134)
302    '42.1 KiB/s'
303
304    """
305    return '%s/s' % (fsize(bps))
306
307def fpeer(num_peers, total_peers):
308    """
309    Formats a string to show 'num_peers' ('total_peers')
310
311    :param num_peers: the number of connected peers
312    :type num_peers: int
313    :param total_peers: the total number of peers
314    :type total_peers: int
315    :returns: a formatted string: num_peers (total_peers), if total_peers < 0, then it will not be shown
316    :rtype: string
317
318    **Usage**
319
320    >>> fpeer(10, 20)
321    '10 (20)'
322    >>> fpeer(10, -1)
323    '10'
324
325    """
326    if total_peers > -1:
327        return "%d (%d)" % (num_peers, total_peers)
328    else:
329        return "%d" % num_peers
330
331def ftime(seconds):
332    """
333    Formats a string to show time in a human readable form
334
335    :param seconds: the number of seconds
336    :type seconds: int
337    :returns: a formatted time string, will return '' if seconds == 0
338    :rtype: string
339
340    **Usage**
341
342    >>> ftime(23011)
343    '6h 23m'
344
345    """
346    if seconds == 0:
347        return ""
348    if seconds < 60:
349        return '%ds' % (seconds)
350    minutes = seconds / 60
351    if minutes < 60:
352        seconds = seconds % 60
353        return '%dm %ds' % (minutes, seconds)
354    hours = minutes / 60
355    if hours < 24:
356        minutes = minutes % 60
357        return '%dh %dm' % (hours, minutes)
358    days = hours / 24
359    if days < 7:
360        hours = hours % 24
361        return '%dd %dh' % (days, hours)
362    weeks = days / 7
363    if weeks < 52:
364        days = days % 7
365        return '%dw %dd' % (weeks, days)
366    years = weeks / 52
367    weeks = weeks % 52
368    return '%dy %dw' % (years, weeks)
369
370def fdate(seconds):
371    """
372    Formats a date time string in the locale's date representation based on the systems timezone
373
374    :param seconds: time in seconds since the Epoch
375    :type seconds: float
376    :returns: a string in the locale's datetime representation or "" if seconds < 0
377    :rtype: string
378
379    """
380    if seconds < 0:
381        return ""
382    return time.strftime("%x %X", time.localtime(seconds))
383
384def is_url(url):
385    """
386    A simple test to check if the URL is valid
387
388    :param url: the url to test
389    :type url: string
390    :returns: True or False
391    :rtype: bool
392
393    **Usage**
394
395    >>> is_url("http://deluge-torrent.org")
396    True
397
398    """
399    return url.partition('://')[0] in ("http", "https", "ftp", "udp")
400
401def is_magnet(uri):
402    """
403    A check to determine if a uri is a valid bittorrent magnet uri
404
405    :param uri: the uri to check
406    :type uri: string
407    :returns: True or False
408    :rtype: bool
409
410    **Usage**
411
412    >>> is_magnet("magnet:?xt=urn:btih:SU5225URMTUEQLDXQWRB2EQWN6KLTYKN")
413    True
414
415    """
416    if uri[:20] == "magnet:?xt=urn:btih:":
417        return True
418    return False
419
420def create_magnet_uri(infohash, name=None, trackers=[]):
421    """
422    Creates a magnet uri
423
424    :param infohash: the info-hash of the torrent
425    :type infohash: string
426    :param name: the name of the torrent (optional)
427    :type name: string
428    :param trackers: the trackers to announce to (optional)
429    :type trackers: list of strings
430
431    :returns: a magnet uri string
432    :rtype: string
433
434    """
435    from base64 import b32encode
436    uri = "magnet:?xt=urn:btih:" + b32encode(infohash.decode("hex"))
437    if name:
438        uri = uri + "&dn=" + name
439    if trackers:
440        for t in trackers:
441            uri = uri + "&tr=" + t
442
443    return uri
444
445def get_path_size(path):
446    """
447    Gets the size in bytes of 'path'
448
449    :param path: the path to check for size
450    :type path: string
451    :returns: the size in bytes of the path or -1 if the path does not exist
452    :rtype: int
453
454    """
455    if not os.path.exists(path):
456        return -1
457
458    if os.path.isfile(path):
459        return os.path.getsize(path)
460
461    dir_size = 0
462    for (p, dirs, files) in os.walk(path):
463        for file in files:
464            filename = os.path.join(p, file)
465            dir_size += os.path.getsize(filename)
466    return dir_size
467
468def free_space(path):
469    """
470    Gets the free space available at 'path'
471
472    :param path: the path to check
473    :type path: string
474    :returns: the free space at path in bytes
475    :rtype: int
476
477    :raises InvalidPathError: if the path is not valid
478
479    """
480    if not os.path.exists(path):
481        raise InvalidPathError("%s is not a valid path" % path)
482
483    if windows_check():
484        import win32file
485        sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
486        return (free * sectors * bytes)
487    else:
488        disk_data = os.statvfs(path.encode("utf8"))
489        block_size = disk_data.f_bsize
490        return disk_data.f_bavail * block_size
491
492def is_ip(ip):
493    """
494    A simple test to see if 'ip' is valid
495
496    :param ip: the ip to check
497    :type ip: string
498    :returns: True or False
499    :rtype: bool
500
501    ** Usage **
502
503    >>> is_ip("127.0.0.1")
504    True
505
506    """
507    import socket
508    #first we test ipv4
509    try:
510        if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
511            return True
512    except socket.error:
513        if not socket.has_ipv6:
514            return False
515    #now test ipv6
516    try:
517        if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
518            return True
519    except socket.error:
520        return False
521
522def path_join(*parts):
523    """
524    An implementation of os.path.join that always uses / for the separator
525    to ensure that the correct paths are produced when working with internal
526    paths on Windows.
527    """
528    path = ''
529    for part in parts:
530        if not part:
531            continue
532        elif part[0] == '/':
533            path = part
534        elif not path:
535            path = part
536        else:
537            path += '/' + part
538    return path
539
540XML_ESCAPES = (
541    ('&', '&amp;'),
542    ('<', '&lt;'),
543    ('>', '&gt;'),
544    ('"', '&quot;'),
545    ("'", '&apos;')
546)
547
548def xml_decode(string):
549    """
550    Unescape a string that was previously encoded for use within xml.
551   
552    :param string: The string to escape
553    :type string: string
554    :returns: The unescaped version of the string.
555    :rtype: string
556    """
557    for char, escape in XML_ESCAPES:
558        string = string.replace(escape, char)
559    return string
560
561def xml_encode(string):
562    """
563    Escape a string for use within an xml element or attribute.
564   
565    :param string: The string to escape
566    :type string: string
567    :returns: An escaped version of the string.
568    :rtype: string
569    """
570    for char, escape in XML_ESCAPES:
571        string = string.replace(char, escape)
572    return string
573
574def decode_string(s, encoding="utf8"):
575    """
576    Decodes a string and re-encodes it in utf8.  If it cannot decode using
577    `:param:encoding` then it will try to detect the string encoding and
578    decode it.
579
580    :param s: string to decode
581    :type s: string
582    :keyword encoding: the encoding to use in the decoding
583    :type encoding: string
584
585    """
586
587    try:
588        s = s.decode(encoding).encode("utf8", "ignore")
589    except UnicodeDecodeError:
590        s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
591    return s
592
593def utf8_encoded(s):
594    """
595    Returns a utf8 encoded string of s
596
597    :param s: (unicode) string to (re-)encode
598    :type s: basestring
599    :returns: a utf8 encoded string of s
600    :rtype: str
601
602    """
603    if isinstance(s, str):
604        s = decode_string(s, locale.getpreferredencoding())
605    elif isinstance(s, unicode):
606        s = s.encode("utf8", "ignore")
607    return s
608
609class VersionSplit(object):
610    """
611    Used for comparing version numbers.
612
613    :param ver: the version
614    :type ver: string
615
616    """
617    def __init__(self, ver):
618        ver = ver.lower()
619        vs = ver.replace("_", "-").split("-")
620        self.version = [int(x) for x in vs[0].split(".")]
621        self.suffix = None
622        self.dev = False
623        if len(vs) > 1:
624            if vs[1].startswith(("rc", "alpha", "beta")):
625                self.suffix = vs[1]
626            if vs[-1] == 'dev':
627                self.dev = True
628
629    def __cmp__(self, ver):
630        """
631        The comparison method.
632
633        :param ver: the version to compare with
634        :type ver: VersionSplit
635
636        """
637
638        # If there is no suffix we use z because we want final
639        # to appear after alpha, beta, and rc alphabetically.
640        v1 = [self.version, self.suffix or 'z', self.dev]
641        v2 = [ver.version, ver.suffix or 'z', ver.dev]
642        return cmp(v1, v2)