configfile.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. # The contents of this file are subject to the BitTorrent Open Source License
  2. # Version 1.1 (the License). You may not copy or use this file, in either
  3. # source code or executable form, except in compliance with the License. You
  4. # may obtain a copy of the License at http://www.bittorrent.com/license/.
  5. #
  6. # Software distributed under the License is distributed on an AS IS basis,
  7. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  8. # for the specific language governing rights and limitations under the
  9. # License.
  10. # Written by Uoti Urpala and Matt Chisholm
  11. ###########
  12. # Needs redesign. Adding an app requires modifying a lot of if's. Blech.
  13. # --Dave
  14. ############
  15. import os
  16. import sys
  17. import traceback
  18. from BitTorrent.translation import _
  19. from ConfigParser import RawConfigParser
  20. from ConfigParser import MissingSectionHeaderError, ParsingError
  21. from BitTorrent import parseargs, version, BTFailure
  22. from BTL.platform import app_name
  23. from BTL.platform import encode_for_filesystem, decode_from_filesystem
  24. from BitTorrent.platform import get_save_dir, locale_root, is_frozen_exe
  25. from BitTorrent.platform import get_dot_dir, get_incomplete_data_dir
  26. from BitTorrent.platform import enforce_shortcut, enforce_association
  27. from BitTorrent.platform import smart_gettext_and_install
  28. from BitTorrent.platform import get_old_incomplete_data_dir
  29. from BitTorrent.platform import get_temp_subdir
  30. from BitTorrent.platform import old_broken_config_subencoding
  31. from BitTorrent.zurllib import bind_tracker_connection
  32. from BTL.exceptions import str_exc
  33. from BitTorrent.shortargs import convert_from_shortforms
  34. downloader_save_options = [
  35. # General
  36. 'confirm_quit' ,
  37. # Appearance
  38. 'progressbar_style' ,
  39. 'toolbar_text' ,
  40. 'toolbar_size' ,
  41. # Bandwidth
  42. 'max_upload_rate' ,
  43. 'max_download_rate' ,
  44. # Saving
  45. 'save_in' ,
  46. 'save_incomplete_in' ,
  47. 'ask_for_save' ,
  48. # Network
  49. 'minport' ,
  50. 'maxport' ,
  51. 'upnp' ,
  52. 'ip' ,
  53. 'resolve_hostnames' ,
  54. # Misc
  55. 'open_from' ,
  56. 'geometry' ,
  57. 'start_maximized' ,
  58. 'column_order' ,
  59. 'enabled_columns' ,
  60. 'column_widths' ,
  61. 'sort_column' ,
  62. 'sort_ascending' ,
  63. 'show_details' ,
  64. 'settings_tab' ,
  65. 'details_tab' ,
  66. 'splitter_height' ,
  67. 'theme' ,
  68. 'donated' ,
  69. 'notified' ,
  70. ]
  71. if os.name == 'nt':
  72. downloader_save_options.extend([
  73. # General
  74. 'enforce_association' ,
  75. 'launch_on_startup' ,
  76. 'minimize_to_tray' ,
  77. 'start_minimized' ,
  78. 'close_to_tray' ,
  79. # Bandwidth
  80. 'bandwidth_management',
  81. 'show_variance_line' ,
  82. ])
  83. MAIN_CONFIG_FILE = 'ui_config'
  84. TORRENT_CONFIG_FILE = 'torrent_config'
  85. alt_uiname = {'bittorrent':'btdownloadgui',
  86. 'maketorrent':'btmaketorrentgui',}
  87. def _read_config(filename):
  88. """Returns a RawConfigParser that has parsed the config file specified by
  89. the passed filename."""
  90. # check for bad config files
  91. p = RawConfigParser()
  92. fp = None
  93. try:
  94. fp = open(filename)
  95. except IOError:
  96. pass
  97. if fp is not None:
  98. try:
  99. p.readfp(fp, filename=filename)
  100. except MissingSectionHeaderError:
  101. fp.close()
  102. del fp
  103. bad_config(filename)
  104. except ParsingError:
  105. fp.close()
  106. del fp
  107. bad_config(filename)
  108. else:
  109. fp.close()
  110. return p
  111. def _write_config(error_callback, filename, p):
  112. if not p.has_section('format'):
  113. p.add_section('format')
  114. p.set('format', 'encoding', 'utf-8')
  115. try:
  116. f = file(filename, 'wb')
  117. p.write(f)
  118. f.close()
  119. except Exception, e:
  120. try:
  121. f.close()
  122. except:
  123. pass
  124. error_callback(_("Could not permanently save options: ")+str_exc(e))
  125. def bad_config(filename):
  126. base_bad_filename = filename + '.broken'
  127. bad_filename = base_bad_filename
  128. i = 0
  129. while os.access(bad_filename, os.F_OK):
  130. bad_filename = base_bad_filename + str(i)
  131. i+=1
  132. os.rename(filename, bad_filename)
  133. sys.stderr.write(_("Error reading config file. "
  134. "Old config file stored in \"%s\"") % bad_filename)
  135. def get_config(defaults, section):
  136. """This reads the key-value pairs from the specified section in the
  137. config file and from the common section. It then places those
  138. appearing in the defaults into a dict, which is then returned.
  139. @type defaults: dict
  140. @param defaults: dict of name-value pairs derived from the
  141. defaults list for this application (see defaultargs.py).
  142. Only the names in the name-value pairs are used. get_config
  143. only reads variables from the config file with matching names.
  144. @type section: str
  145. @param section: in the configuration from which to read options.
  146. So far, the sections have been named after applications, e.g.,
  147. bittorrent, bittorrent-console, etc.
  148. @return: a dict containing option-value pairs.
  149. """
  150. assert type(defaults)==dict
  151. assert type(section)==str
  152. configdir = get_dot_dir()
  153. if configdir is None:
  154. return {}
  155. if not os.path.isdir(configdir):
  156. try:
  157. os.mkdir(configdir, 0700)
  158. except:
  159. pass
  160. p = _read_config(os.path.join(configdir, 'config')) # returns parser.
  161. if p.has_section('format'):
  162. encoding = p.get('format', 'encoding')
  163. else:
  164. encoding = old_broken_config_subencoding
  165. values = {}
  166. if p.has_section(section):
  167. for name, value in p.items(section):
  168. if name in defaults:
  169. values[name] = value
  170. if p.has_section('common'):
  171. for name, value in p.items('common'):
  172. if name in defaults and name not in values:
  173. values[name] = value
  174. if defaults.get('data_dir') == '' and \
  175. 'data_dir' not in values and os.path.isdir(configdir):
  176. datadir = os.path.join(configdir, 'data')
  177. values['data_dir'] = decode_from_filesystem(datadir)
  178. parseargs.parse_options(defaults, values, encoding)
  179. return values
  180. def save_global_config(defaults, section, error_callback,
  181. save_options=downloader_save_options):
  182. filename = os.path.join(defaults['data_dir'], MAIN_CONFIG_FILE)
  183. p = _read_config(filename)
  184. p.remove_section(section)
  185. if p.has_section(alt_uiname[section]):
  186. p.remove_section(alt_uiname[section])
  187. p.add_section(section)
  188. for name in save_options:
  189. name.decode('ascii').encode('utf-8') # just to make sure we can
  190. if defaults.has_key(name):
  191. value = defaults[name]
  192. if isinstance(value, str):
  193. value = value.decode('ascii').encode('utf-8')
  194. elif isinstance(value, unicode):
  195. value = value.encode('utf-8')
  196. p.set(section, name, value)
  197. else:
  198. err_str = _("Configuration option mismatch: '%s'") % name
  199. if is_frozen_exe:
  200. err_str = _("You must quit %s and reinstall it. (%s)") % (app_name, err_str)
  201. error_callback(err_str)
  202. _write_config(error_callback, filename, p)
  203. def save_torrent_config(path, infohash, config, error_callback):
  204. section = infohash.encode('hex')
  205. filename = os.path.join(path, TORRENT_CONFIG_FILE)
  206. p = _read_config(filename)
  207. p.remove_section(section)
  208. p.add_section(section)
  209. for key, value in config.items():
  210. p.set(section, key, value)
  211. _write_config(error_callback, filename, p)
  212. def read_torrent_config(global_config, path, infohash, error_callback):
  213. section = infohash.encode('hex')
  214. filename = os.path.join(path, TORRENT_CONFIG_FILE)
  215. p = _read_config(filename)
  216. if not p.has_section(section):
  217. return {}
  218. else:
  219. c = {}
  220. for name, value in p.items(section):
  221. if global_config.has_key(name):
  222. t = type(global_config[name])
  223. if t == bool:
  224. c[name] = value in ('1', 'True', True)
  225. else:
  226. try:
  227. c[name] = type(global_config[name])(value)
  228. except ValueError, e:
  229. error_callback('%s (name:%s value:%s type:%s global:%s)' %
  230. (str_exc(e), name, repr(value),
  231. type(global_config[name]), global_config[name]))
  232. # is this reasonable?
  233. c[name] = global_config[name]
  234. elif name == 'save_as':
  235. # Backwards compatibility for BitTorrent 4.4 torrent_config file
  236. c[name] = value
  237. try:
  238. c[name] = c[name].decode('utf-8')
  239. except:
  240. pass
  241. return c
  242. def remove_torrent_config(path, infohash, error_callback):
  243. section = infohash.encode('hex')
  244. filename = os.path.join(path, TORRENT_CONFIG_FILE)
  245. p = _read_config(filename)
  246. if p.has_section(section):
  247. p.remove_section(section)
  248. _write_config(error_callback, filename, p)
  249. def parse_configuration_and_args(defaults, uiname, arglist=[], minargs=None,
  250. maxargs=None):
  251. """Given the default option settings and overrides these defaults
  252. from values read from the config file, and again overrides the
  253. config file with the arguments that appear in the arglist.
  254. 'defaults' is a list of tuples of the form (optname, value,
  255. desc) where 'optname' is a string containing the option's name,
  256. value is the option's default value, and desc is the option's
  257. description.
  258. 'uiname' is a string specifying the user interface that has been
  259. created by the caller. Ex: bittorrent, maketorrent.
  260. arglist is usually argv[1:], i.e., excluding the name used to
  261. execute the program.
  262. minargs specifies the minimum number of arguments that must appear in
  263. arglist. If the number of arguments is less than the minimum then
  264. a BTFailure exception is raised.
  265. maxargs specifies the maximum number of arguments that can appear
  266. in arglist. If the number of arguments exceeds the maximum then
  267. a BTFailure exception is raised.
  268. This returns the tuple (config,args) where config is
  269. a dictionary of (option, value) pairs, and args is the list
  270. of arguments in arglist after the command-line arguments have
  271. been removed.
  272. For example:
  273. bittorrent-curses.py --save_as lx-2.6.rpm lx-2.6.rpm.torrent --max_upload_rate 0
  274. returns a (config,args) pair where the
  275. config dictionary contains many defaults plus
  276. the mappings
  277. 'save_as': 'linux-2.6.15.tar.gz'
  278. and
  279. 'max_upload_rate': 0
  280. The args in the returned pair is
  281. args= ['linux-2.6.15.tar.gz.torrent']
  282. """
  283. assert type(defaults)==list
  284. assert type(uiname)==str
  285. assert type(arglist)==list
  286. assert minargs is None or type(minargs) in (int,long) and minargs>=0
  287. assert maxargs is None or type(maxargs) in (int,long) and maxargs>=minargs
  288. # remap shortform arguments to their long-forms.
  289. arglist = convert_from_shortforms(arglist)
  290. defconfig = dict([(name, value) for (name, value, doc) in defaults])
  291. if arglist[0:] == ['--version']:
  292. print version
  293. sys.exit(0)
  294. if arglist[0:] == '--help':
  295. parseargs.printHelp(uiname, defaults)
  296. sys.exit(0)
  297. if "--use_factory_defaults" not in arglist:
  298. presets = get_config(defconfig, uiname) # read from .bittorrent dir.
  299. # run as if fresh install using temporary directories.
  300. else:
  301. presets = {}
  302. temp_dir = get_temp_subdir()
  303. #set_config_dir(temp_dir) # is already set in platform.py.
  304. save_in = encode_for_filesystem( u"save_in" )[0]
  305. presets["save_in"] = \
  306. decode_from_filesystem(os.path.join(temp_dir,save_in))
  307. data = encode_for_filesystem( u"data" )[0]
  308. presets["data_dir"] = \
  309. decode_from_filesystem(os.path.join(temp_dir,data))
  310. incomplete = encode_for_filesystem( u"incomplete" )[0]
  311. presets["save_incomplete_in"] = \
  312. decode_from_filesystem(os.path.join(temp_dir,incomplete))
  313. presets["one_connection_per_ip"] = False
  314. config = args = None
  315. try:
  316. config, args = parseargs.parseargs(arglist, defaults, minargs, maxargs,
  317. presets)
  318. except parseargs.UsageException, e:
  319. print e
  320. parseargs.printHelp(uiname, defaults)
  321. sys.exit(0)
  322. datadir = config.get('data_dir')
  323. found_4x_config = False
  324. if datadir:
  325. datadir,bad = encode_for_filesystem(datadir)
  326. if bad:
  327. raise BTFailure(_("Invalid path encoding."))
  328. if not os.path.exists(datadir):
  329. os.mkdir(datadir)
  330. if uiname in ('bittorrent', 'maketorrent'):
  331. values = {}
  332. p = _read_config(os.path.join(datadir, MAIN_CONFIG_FILE))
  333. if p.has_section('format'):
  334. encoding = p.get('format', 'encoding')
  335. else:
  336. encoding = old_broken_config_subencoding
  337. if not p.has_section(uiname) and p.has_section(alt_uiname[uiname]):
  338. uiname = alt_uiname[uiname]
  339. if p.has_section(uiname):
  340. for name, value in p.items(uiname):
  341. if name in defconfig:
  342. values[name] = value
  343. elif not found_4x_config:
  344. # identify 4.x version config file
  345. if name in ('start_torrent_behavior',
  346. 'seed_forever',
  347. 'progressbar_hack',
  348. 'seed_last_forever',
  349. 'next_torrent_ratio',
  350. 'next_torrent_time',
  351. 'last_torrent_ratio',
  352. ):
  353. found_4x_config = True
  354. parseargs.parse_options(defconfig, values, encoding)
  355. presets.update(values)
  356. config, args = parseargs.parseargs(arglist, defaults, minargs,
  357. maxargs, presets)
  358. for d in ('', 'resume', 'metainfo', 'torrents'):
  359. ddir = os.path.join(datadir, d)
  360. if not os.path.exists(ddir):
  361. os.mkdir(ddir, 0700)
  362. else:
  363. assert(os.path.isdir(ddir))
  364. if found_4x_config:
  365. # version 4.x stored KB/s, < version 4.x stores B/s
  366. config['max_upload_rate'] *= 1024
  367. if config.get('language'):
  368. # this is non-blocking if the language does not exist
  369. smart_gettext_and_install('bittorrent', locale_root,
  370. languages=[config['language']])
  371. if config.has_key('bind') and config['bind'] != '':
  372. bind_tracker_connection(config['bind'])
  373. if config.has_key('launch_on_startup'):
  374. enforce_shortcut(config, log_func=sys.stderr.write)
  375. if os.name == 'nt' and config.has_key('enforce_association'):
  376. enforce_association()
  377. if config.has_key('save_in') and config['save_in'] == '' and \
  378. (not config.has_key("save_as") or config['save_as'] == '' ) \
  379. and uiname != 'bittorrent':
  380. config['save_in'] = decode_from_filesystem(get_save_dir())
  381. incomplete = decode_from_filesystem(get_incomplete_data_dir())
  382. if config.get('save_incomplete_in') == '':
  383. config['save_incomplete_in'] = incomplete
  384. if config.get('save_incomplete_in') == get_old_incomplete_data_dir():
  385. config['save_incomplete_in'] = incomplete
  386. if uiname == "test-client" or (uiname.startswith("bittorrent")
  387. and uiname != 'bittorrent-tracker'):
  388. if not config.get('ask_for_save'):
  389. # we check for existance, so things like "D:\" don't trip us up.
  390. if (config['save_in'] and
  391. not os.path.exists(config['save_in'])):
  392. try:
  393. os.makedirs(config['save_in'])
  394. except OSError, e:
  395. if (e.errno == 2 or # no such file or directory
  396. e.errno == 13): # permission denied
  397. traceback.print_exc()
  398. print >> sys.stderr, "save_in could not be created. Falling back to prompting."
  399. config['ask_for_save'] = True
  400. elif e.errno != 17: # path already exists
  401. raise
  402. if (config['save_incomplete_in'] and
  403. not os.path.exists(config['save_incomplete_in'])):
  404. try:
  405. os.makedirs(config['save_incomplete_in'])
  406. except OSError, e:
  407. if e.errno != 17: # path already exists
  408. traceback.print_exc()
  409. print >> sys.stderr, "save_incomplete_in could not be created. Falling back to default incomplete path."
  410. config['save_incomplete_in'] = incomplete
  411. return config, args