Ticket #1504: common.py

File common.py, 15.6 KB (added by mindthemonkey, 16 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)