|
8 | 8 | # under the terms of the GPLv2, the GNU General Public License version 2, as
|
9 | 9 | # published by the Free Software Foundation. http://gnu.org/licenses/gpl.html
|
10 | 10 |
|
| 11 | +import os |
11 | 12 | import sys
|
| 13 | +import errno |
| 14 | +import contextlib |
12 | 15 | import ConfigParser
|
13 | 16 | import getopt
|
14 | 17 | import iniparse
|
15 | 18 | import pipes
|
| 19 | +import shutil |
16 | 20 | import string
|
| 21 | +import tempfile |
17 | 22 | from cStringIO import StringIO
|
18 | 23 |
|
19 | 24 | try:
|
@@ -195,7 +200,7 @@ def _parse_file(filename, add_default=False):
|
195 | 200 | conf = iniparse.RawConfigParser()
|
196 | 201 | conf.readfp(fp)
|
197 | 202 | return conf
|
198 |
| - except IOError as e: |
| 203 | + except EnvironmentError as e: |
199 | 204 | error(str(e))
|
200 | 205 | sys.exit(1)
|
201 | 206 |
|
@@ -355,18 +360,84 @@ except ConfigParser.NoOptionError:
|
355 | 360 | error('Parameter not found: %s' % param)
|
356 | 361 | sys.exit(1)
|
357 | 362 |
|
| 363 | +def delete_if_exists(path): |
| 364 | + """Delete a file, but ignore file not found error. |
| 365 | + """ |
| 366 | + try: |
| 367 | + os.unlink(path) |
| 368 | + except EnvironmentError as e: |
| 369 | + if e.errno != errno.ENOENT: |
| 370 | + print str(e) |
| 371 | + raise |
| 372 | + |
| 373 | +@contextlib.contextmanager |
| 374 | +def remove_file_on_error(path): |
| 375 | + """Protect code that wants to operate on PATH atomically. |
| 376 | + Any exception will cause PATH to be removed. |
| 377 | + """ |
| 378 | + try: |
| 379 | + yield |
| 380 | + except Exception: |
| 381 | + t, v, tb = sys.exc_info() |
| 382 | + delete_if_exists(path) |
| 383 | + raise t, v, tb |
| 384 | + |
| 385 | +def file_replace(name, data): |
| 386 | + """Replace file as atomically as possible, |
| 387 | + fulfilling and AC properties of ACID. |
| 388 | + This is essentially using method 9 from: |
| 389 | + http://www.pixelbeat.org/docs/unix_file_replacement.html |
| 390 | +
|
| 391 | + Caveats: |
| 392 | + - Changes ownership of the file being edited |
| 393 | + by non root users (due to POSIX interface limitations). |
| 394 | + - Loses any extended attributes of the original file |
| 395 | + (due to the simplicity of this implementation). |
| 396 | + - Existing hardlinks will be separated from the |
| 397 | + newly replaced file. |
| 398 | + - Ignores the write permissions of the original file. |
| 399 | + - Requires write permission on the directory as well as the file. |
| 400 | + - With python2 on windows we don't fulfill the A ACID property. |
| 401 | +
|
| 402 | + To avoid the above caveats see the --inplace option. |
| 403 | + """ |
| 404 | + (f, tmp) = tempfile.mkstemp(".tmp", prefix=name+".", dir=".") |
| 405 | + |
| 406 | + with remove_file_on_error(tmp): |
| 407 | + shutil.copystat(name, tmp) |
| 408 | + |
| 409 | + if hasattr(os, 'fchown') and os.geteuid() == 0: |
| 410 | + st = os.stat(name) |
| 411 | + os.fchown(f, st.st_uid, st.st_gid) |
| 412 | + |
| 413 | + os.write(f, data) |
| 414 | + os.close(f) |
| 415 | + |
| 416 | + if hasattr(os,'replace'): # >= python 3.3 |
| 417 | + os.replace(tmp, name) # atomic even on windos |
| 418 | + elif os.name == 'posix': |
| 419 | + os.rename(tmp, name) # atomic on POSIX |
| 420 | + else: |
| 421 | + backup = tmp+'.backup' |
| 422 | + os.rename(name, backup) |
| 423 | + os.rename(tmp, name) |
| 424 | + delete_if_exists(backup) |
| 425 | + |
358 | 426 | if mode != '--get':
|
359 |
| - with open(cfgfile, 'w') as f: |
360 |
| - # XXX: Ideally we should just do conf.write(f) here, |
361 |
| - # but to avoid iniparse issues, we massage the data a little here |
362 |
| - str_data = str(conf.data) |
363 |
| - if len(str_data) and str_data[-1] != '\n': |
364 |
| - str_data += '\n' |
365 |
| - |
366 |
| - if ( |
367 |
| - (added_default_section and not (section_explicit_default and mode in ('--set', '--merge'))) |
368 |
| - or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None) |
369 |
| - ): |
370 |
| - str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1) |
371 |
| - |
372 |
| - f.write(str_data) |
| 427 | + # XXX: Ideally we should just do conf.write(f) here, |
| 428 | + # but to avoid iniparse issues, we massage the data a little here |
| 429 | + str_data = str(conf.data) |
| 430 | + if len(str_data) and str_data[-1] != '\n': |
| 431 | + str_data += '\n' |
| 432 | + |
| 433 | + if ( |
| 434 | + (added_default_section and not (section_explicit_default and mode in ('--set', '--merge'))) |
| 435 | + or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None) |
| 436 | + ): |
| 437 | + str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1) |
| 438 | + |
| 439 | + try: |
| 440 | + file_replace(cfgfile, str_data) |
| 441 | + except EnvironmentError as e: |
| 442 | + error(str(e)) |
| 443 | + sys.exit(1) |
0 commit comments