mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Rewrite safe_write
This commit is contained in:
parent
8a723febd3
commit
8ab9c429cc
5 changed files with 60 additions and 21 deletions
|
|
@ -227,3 +227,17 @@ def test_request_ssl(httpsserver):
|
||||||
utils.request('GET', httpsserver.url,
|
utils.request('GET', httpsserver.url,
|
||||||
verify_fingerprint=''.join(reversed(sha1)))
|
verify_fingerprint=''.join(reversed(sha1)))
|
||||||
assert 'Fingerprints did not match' in str(excinfo.value)
|
assert 'Fingerprints did not match' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_atomic_write(tmpdir):
|
||||||
|
x = utils.atomic_write
|
||||||
|
fname = tmpdir.join('ha')
|
||||||
|
for i in range(2):
|
||||||
|
with x(str(fname), binary=False, overwrite=True) as f:
|
||||||
|
f.write('hoho')
|
||||||
|
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
with x(str(fname), binary=False, overwrite=False) as f:
|
||||||
|
f.write('haha')
|
||||||
|
|
||||||
|
assert fname.read() == 'hoho'
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from .. import DOCS_HOME, PROJECT_HOME, exceptions, log
|
||||||
from ..doubleclick import click
|
from ..doubleclick import click
|
||||||
from ..storage import storage_names
|
from ..storage import storage_names
|
||||||
from ..sync import StorageEmpty, SyncConflict
|
from ..sync import StorageEmpty, SyncConflict
|
||||||
from ..utils import expand_path, get_class_init_args, safe_write
|
from ..utils import atomic_write, expand_path, get_class_init_args
|
||||||
from ..utils.compat import text_type
|
from ..utils.compat import text_type
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -345,7 +345,7 @@ def save_status(base_path, pair, collection=None, data_type=None, data=None):
|
||||||
if e.errno != errno.EEXIST:
|
if e.errno != errno.EEXIST:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with safe_write(path, 'w+') as f:
|
with atomic_write(path, binary=True, overwrite=True) as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import os
|
||||||
|
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
from .. import exceptions, log
|
from .. import exceptions, log
|
||||||
from ..utils import checkdir, expand_path, get_etag_from_file, safe_write
|
from ..utils import atomic_write, checkdir, expand_path, get_etag_from_file
|
||||||
from ..utils.compat import text_type
|
from ..utils.compat import text_type
|
||||||
|
|
||||||
logger = log.get(__name__)
|
logger = log.get(__name__)
|
||||||
|
|
@ -101,15 +101,21 @@ class FilesystemStorage(Storage):
|
||||||
def upload(self, item):
|
def upload(self, item):
|
||||||
href = self._get_href(item)
|
href = self._get_href(item)
|
||||||
fpath = self._get_filepath(href)
|
fpath = self._get_filepath(href)
|
||||||
if os.path.exists(fpath):
|
|
||||||
raise exceptions.AlreadyExistingError(item)
|
|
||||||
|
|
||||||
if not isinstance(item.raw, text_type):
|
if not isinstance(item.raw, text_type):
|
||||||
raise TypeError('item.raw must be a unicode string.')
|
raise TypeError('item.raw must be a unicode string.')
|
||||||
|
|
||||||
with safe_write(fpath, 'wb+') as f:
|
|
||||||
|
try:
|
||||||
|
with atomic_write(fpath, binary=True, overwrite=False) as f:
|
||||||
f.write(item.raw.encode(self.encoding))
|
f.write(item.raw.encode(self.encoding))
|
||||||
return href, f.get_etag()
|
return href, f.get_etag()
|
||||||
|
except OSError as e:
|
||||||
|
import errno
|
||||||
|
if e.errno == errno.EEXIST:
|
||||||
|
raise exceptions.AlreadyExistingError(item)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def update(self, href, item, etag):
|
def update(self, href, item, etag):
|
||||||
fpath = self._get_filepath(href)
|
fpath = self._get_filepath(href)
|
||||||
|
|
@ -125,7 +131,7 @@ class FilesystemStorage(Storage):
|
||||||
if not isinstance(item.raw, text_type):
|
if not isinstance(item.raw, text_type):
|
||||||
raise TypeError('item.raw must be a unicode string.')
|
raise TypeError('item.raw must be a unicode string.')
|
||||||
|
|
||||||
with safe_write(fpath, 'wb') as f:
|
with atomic_write(fpath, binary=True, overwrite=True) as f:
|
||||||
f.write(item.raw.encode(self.encoding))
|
f.write(item.raw.encode(self.encoding))
|
||||||
return f.get_etag()
|
return f.get_etag()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import os
|
||||||
|
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
from .. import exceptions, log
|
from .. import exceptions, log
|
||||||
from ..utils import checkfile, expand_path, safe_write
|
from ..utils import atomic_write, checkfile, expand_path
|
||||||
from ..utils.compat import iteritems, itervalues
|
from ..utils.compat import iteritems, itervalues
|
||||||
from ..utils.vobject import join_collection, split_collection
|
from ..utils.vobject import join_collection, split_collection
|
||||||
|
|
||||||
|
|
@ -166,7 +166,7 @@ class SingleFileStorage(Storage):
|
||||||
(item.raw for item, etag in itervalues(self._items)),
|
(item.raw for item, etag in itervalues(self._items)),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with safe_write(self.path, self._write_mode) as f:
|
with atomic_write(self.path, binary=True, overwrite=True) as f:
|
||||||
f.write(text.encode(self.encoding))
|
f.write(text.encode(self.encoding))
|
||||||
finally:
|
finally:
|
||||||
self._items = None
|
self._items = None
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.packages.urllib3.poolmanager import PoolManager
|
from requests.packages.urllib3.poolmanager import PoolManager
|
||||||
|
|
@ -241,15 +242,21 @@ def request(method, url, session=None, latin1_fallback=True,
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
class safe_write(object):
|
class atomic_write(object):
|
||||||
'''A helper class for performing atomic writes. Writes to a tempfile in
|
'''
|
||||||
the same directory and then renames. The tempfile location can be
|
A helper class for performing atomic writes.
|
||||||
overridden, but must reside on the same filesystem to be atomic.
|
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
with safe_write(fpath, 'w+') as f:
|
with safe_write(fpath, binary=False, overwrite=False) as f:
|
||||||
f.write('hohoho')
|
f.write('hohoho')
|
||||||
|
|
||||||
|
:param fpath: The destination filepath. May or may not exist.
|
||||||
|
:param binary: Whether binary write mode should be used.
|
||||||
|
:param overwrite: If set to false, an error is raised if ``fpath`` exists.
|
||||||
|
This should still be atomic.
|
||||||
|
:param tmppath: An alternative tmpfile location. Must reside on the same
|
||||||
|
filesystem to be atomic.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
f = None
|
f = None
|
||||||
|
|
@ -257,23 +264,35 @@ class safe_write(object):
|
||||||
fpath = None
|
fpath = None
|
||||||
mode = None
|
mode = None
|
||||||
|
|
||||||
def __init__(self, fpath, mode, tmppath=None):
|
def __init__(self, fpath, binary, overwrite, tmppath=None):
|
||||||
self.tmppath = tmppath or fpath + '.tmp'
|
if not tmppath:
|
||||||
|
base = os.path.dirname(fpath)
|
||||||
|
tmppath = os.path.join(base, str(uuid.uuid4()) + '.tmp')
|
||||||
|
|
||||||
self.fpath = fpath
|
self.fpath = fpath
|
||||||
self.mode = mode
|
self.binary = binary
|
||||||
|
self.overwrite = overwrite
|
||||||
|
self.tmppath = tmppath
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.f = f = open(self.tmppath, self.mode)
|
self.f = f = open(self.tmppath, 'wb' if self.binary else 'w')
|
||||||
self.write = f.write
|
self.write = f.write
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, cls, value, tb):
|
def __exit__(self, cls, value, tb):
|
||||||
self.f.close()
|
self.f.close()
|
||||||
if cls is None:
|
if cls is None:
|
||||||
os.rename(self.tmppath, self.fpath)
|
self._commit()
|
||||||
else:
|
else:
|
||||||
os.remove(self.tmppath)
|
os.remove(self.tmppath)
|
||||||
|
|
||||||
|
def _commit(self):
|
||||||
|
if self.overwrite:
|
||||||
|
os.rename(self.tmppath, self.fpath) # atomic
|
||||||
|
else:
|
||||||
|
os.link(self.tmppath, self.fpath) # atomic, fails if file exists
|
||||||
|
os.unlink(self.tmppath) # doesn't matter if atomic
|
||||||
|
|
||||||
def get_etag(self):
|
def get_etag(self):
|
||||||
self.f.flush()
|
self.f.flush()
|
||||||
return get_etag_from_file(self.tmppath)
|
return get_etag_from_file(self.tmppath)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue