vdirsyncer/vdirsyncer/storage/singlefile.py
2015-01-21 13:02:30 +01:00

166 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
import collections
import os
from .base import Item, Storage
from .. import exceptions, log
from ..utils import atomic_write, checkfile, expand_path
from ..utils.compat import iteritems, itervalues
from ..utils.vobject import join_collection, split_collection
logger = log.get(__name__)
class SingleFileStorage(Storage):
'''Save data in single local ``.vcf`` or ``.ics`` file.
The storage basically guesses how items should be joined in the file.
.. versionadded:: 0.1.6
.. note::
This storage has many raceconditions, which basically means that you
should avoid changing the file with another program (or even running
any programs which might modify the file) while vdirsyncer is running.
Also it is currently very slow, so you should consider limiting the
amount of items you synchronize. In combination with
:py:class:`vdirsyncer.storage.CaldavStorage` this can be achieved via
the ``start_date`` and ``end_date`` parameters.
:param path: The filepath to the file to be written to.
:param encoding: Which encoding the file should use. Defaults to UTF-8.
Example for syncing with :py:class:`vdirsyncer.storage.CaldavStorage`::
[pair my_calendar]
a = my_calendar_local
b = my_calendar_remote
[storage my_calendar_local]
type = singlefile
path = ~/my_calendar.ics
[storage my_calendar_remote]
type = caldav
url = https://caldav.example.org/username/my_calendar/
#username =
#password =
'''
storage_name = 'singlefile'
_repr_attributes = ('path',)
_write_mode = 'wb'
_append_mode = 'ab'
_read_mode = 'rb'
_items = None
_last_mtime = None
def __init__(self, path, encoding='utf-8', **kwargs):
super(SingleFileStorage, self).__init__(**kwargs)
path = expand_path(path)
collection = kwargs.get('collection')
if collection is not None:
raise ValueError('collection is not a valid argument for {}'
.format(type(self).__name__))
checkfile(path, create=False)
self.path = path
self.encoding = encoding
@classmethod
def create_collection(cls, collection, **kwargs):
if collection is not None:
raise ValueError('collection is not a valid argument for {}'
.format(cls.__name__))
checkfile(kwargs['path'], create=True)
return kwargs
def list(self):
self._items = collections.OrderedDict()
try:
self._last_mtime = os.path.getmtime(self.path)
with open(self.path, self._read_mode) as f:
text = f.read().decode(self.encoding)
except OSError as e:
import errno
if e.errno != errno.ENOENT: # file not found
raise IOError(e)
text = None
if not text:
return ()
for item in split_collection(text):
item = Item(item)
etag = item.hash
self._items[item.ident] = item, etag
return ((href, etag) for href, (item, etag) in iteritems(self._items))
def get(self, href):
if self._items is None:
self.list()
try:
return self._items[href]
except KeyError:
raise exceptions.NotFoundError(href)
def upload(self, item):
href = item.ident
self.list()
if href in self._items:
raise exceptions.AlreadyExistingError(href)
self._items[href] = item, item.hash
self._write()
return href, item.hash
def update(self, href, item, etag):
self.list()
if href not in self._items:
raise exceptions.NotFoundError(href)
_, actual_etag = self._items[href]
if etag != actual_etag:
raise exceptions.WrongEtagError(etag, actual_etag)
self._items[href] = item, item.hash
self._write()
return item.hash
def delete(self, href, etag):
self.list()
if href not in self._items:
raise exceptions.NotFoundError(href)
_, actual_etag = self._items[href]
if etag != actual_etag:
raise exceptions.WrongEtagError(etag, actual_etag)
del self._items[href]
self._write()
def _write(self):
if self._last_mtime is not None and \
self._last_mtime != os.path.getmtime(self.path):
raise exceptions.PreconditionFailed(
'Some other program modified the file {r!}'.format(self.path))
text = join_collection(
(item.raw for item, etag in itervalues(self._items)),
)
try:
with atomic_write(self.path, binary=True, overwrite=True) as f:
f.write(text.encode(self.encoding))
finally:
self._items = None
self._last_mtime = None