Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

# -*- coding: utf-8 -*- 

# 

# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> 

# 

# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 

# the additional special exception to link portions of this program with the OpenSSL library. 

# See LICENSE for more details. 

# 

 

""" 

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 json 

import logging 

import os 

import shutil 

 

from deluge.common import get_default_config_dir, utf8_encoded 

 

log = logging.getLogger(__name__) 

callLater = None  # Necessary for the config tests 

 

 

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 

135        if config_dir: 

            self.__config_file = os.path.join(config_dir, filename) 

        else: 

            self.__config_file = 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 = utf8_encoded(value) 

 

        if key not in self.__config: 

            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 

        if value is not None and not isinstance( 

                self.__config[key], type(None)) and not isinstance(self.__config[key], type(value)): 

            try: 

                oldtype = type(self.__config[key]) 

187                if isinstance(self.__config[key], unicode): 

                    value = oldtype(value, "utf8") 

                else: 

                    value = oldtype(value) 

            except ValueError: 

                log.warning("Type '%s' invalid for '%s'", type(value), key) 

                raise 

 

        log.debug("Setting '%s' to %s of %s", key, value, type(value)) 

 

        self.__config[key] = value 

 

        global callLater 

        if callLater is None: 

            # Must import here and not at the top or it will throw ReactorAlreadyInstalledError 

            from twisted.internet.reactor import callLater 

        # Run the set_function for this key if any 

        try: 

205   208            for func in self.__set_functions[key]: 

                callLater(0, func, key, value) 

        except KeyError: 

            pass 

        try: 

            def do_change_callbacks(key, value): 

211                for func in self.__change_callbacks: 

                    func(key, value) 

            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 = 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 get(self, key, default=None): 

        """ 

        Gets the value of item 'key' if key is in the config, else default. 

        If default is not given, it defaults to None, so that this method 

        never raises a KeyError. 

 

        :param key: the item for which you want it's value 

        :param default: the default value if key is missing 

        :return: the value of item 'key' or default 

 

        **Usage** 

 

        >>> config = Config("test.conf", defaults={"test": 5}) 

        >>> config.get("test", 10) 

        5 

        >>> config.get("bad_key", 10) 

        10 

 

        """ 

        try: 

            return self.get_item(key) 

        except KeyError: 

            return default 

 

    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] 

 

        global callLater 

        if callLater is None: 

            # Must import here and not at the top or it will throw ReactorAlreadyInstalledError 

            from twisted.internet.reactor import callLater 

 

        # 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 = 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 

348        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 

 

 

        """ 

393        if not filename: 

            filename = self.__config_file 

 

        try: 

            data = open(filename, "rb").read() 

        except IOError as ex: 

            log.warning("Unable to open config file %s: %s", filename, ex) 

            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 as ex: 

                log.exception(ex) 

                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 as ex: 

                log.exception(ex) 

                log.warning("Unable to load config file: %s", filename) 

425        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 as ex: 

                log.exception(ex) 

                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. 

 

        """ 

441        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 

451                if self._save_timer and self._save_timer.active(): 

                    self._save_timer.cancel() 

                return True 

        except (IOError, IndexError) as ex: 

            log.warning("Unable to open config file: %s because: %s", filename, ex) 

 

        # 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 as ex: 

            log.error("Error writing new config file: %s", ex) 

            return False 

 

        # Make a backup of the old config 

        try: 

            log.debug("Backing up old config file to %s.bak", filename) 

            shutil.move(filename, filename + ".bak") 

        except IOError as ex: 

            log.warning("Unable to backup old config: %s", ex) 

 

        # 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 IOError as ex: 

            log.error("Error moving new config file: %s", ex) 

            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 as ex: 

            log.exception(ex) 

            log.error("There was an exception try to convert config file %s %s to %s", 

                      self.__config_file, self.__version["file"], output_version) 

            raise ex 

        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()