Remove global state about configuration and logs

Many things have been changed to make this possible, probably leading to
many hidden bugs waiting to be found.

Related to #122.
This commit is contained in:
Guillaume Ayoub
2016-04-22 11:37:02 +09:00
parent 8ac19ae0fc
commit 2f97d7d1e1
15 changed files with 576 additions and 488 deletions

View File

@ -29,31 +29,29 @@ import json
import os
import posixpath
import shutil
import sys
import time
from contextlib import contextmanager
from hashlib import md5
from importlib import import_module
from uuid import uuid4
import vobject
from . import config, log
def _load():
def load(configuration, logger):
"""Load the storage manager chosen in configuration."""
storage_type = config.get("storage", "type")
storage_type = configuration.get("storage", "type")
if storage_type == "multifilesystem":
module = sys.modules[__name__]
collection_class = Collection
else:
__import__(storage_type)
module = sys.modules[storage_type]
sys.modules[__name__].Collection = module.Collection
collection_class = import_module(storage_type).Collection
class CollectionCopy(collection_class):
"""Collection copy, avoids overriding the original class attributes."""
CollectionCopy.configuration = configuration
CollectionCopy.logger = logger
return CollectionCopy
FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
FILESYSTEM_ENCODING = sys.getfilesystemencoding()
STORAGE_ENCODING = config.get("encoding", "stock")
MIMETYPES = {"VADDRESSBOOK": "text/vcard", "VCALENDAR": "text/calendar"}
@ -106,15 +104,14 @@ def path_to_filesystem(root, *paths):
continue
for part in path.split("/"):
if not is_safe_filesystem_path_component(part):
log.LOGGER.debug(
"Can't translate path safely to filesystem: %s", path)
raise ValueError("Unsafe path")
safe_path = os.path.join(safe_path, part)
return safe_path
class Item:
def __init__(self, item, href, last_modified=None):
def __init__(self, collection, item, href, last_modified=None):
self.collection = collection
self.item = item
self.href = href
self.last_modified = last_modified
@ -122,17 +119,17 @@ class Item:
def __getattr__(self, attr):
return getattr(self.item, attr)
@property
def content_length(self):
return len(self.serialize().encode(config.get("encoding", "request")))
@property
def etag(self):
return get_etag(self.serialize())
class Collection:
"""Collection stored in several files per calendar."""
class BaseCollection:
# Overriden on copy by the "load" function
configuration = None
logger = None
def __init__(self, path, principal=False):
"""Initialize the collection.
@ -140,17 +137,7 @@ class Collection:
the slash as the folder delimiter, with no leading nor trailing slash.
"""
self.encoding = "utf-8"
# path should already be sanitized
self.path = sanitize_path(path).strip("/")
self._filesystem_path = path_to_filesystem(FOLDER, self.path)
split_path = self.path.split("/")
if len(split_path) > 1:
# URL with at least one folder
self.owner = split_path[0]
else:
self.owner = None
self.is_principal = principal
raise NotImplementedError
@classmethod
def discover(cls, path, depth="1"):
@ -167,6 +154,117 @@ class Collection:
The ``path`` is relative.
"""
raise NotImplementedError
@property
def etag(self):
return get_etag(self.serialize())
@classmethod
def create_collection(cls, href, collection=None, tag=None):
"""Create a collection.
``collection`` is a list of vobject components.
``tag`` is the type of collection (VCALENDAR or VADDRESSBOOK). If
``tag`` is not given, it is guessed from the collection.
"""
raise NotImplementedError
def list(self):
"""List collection items."""
raise NotImplementedError
def get(self, href):
"""Fetch a single item."""
raise NotImplementedError
def get_multi(self, hrefs):
"""Fetch multiple items. Duplicate hrefs must be ignored.
Functionally similar to ``get``, but might bring performance benefits
on some storages when used cleverly.
"""
for href in set(hrefs):
yield self.get(href)
def has(self, href):
"""Check if an item exists by its href.
Functionally similar to ``get``, but might bring performance benefits
on some storages when used cleverly.
"""
return self.get(href) is not None
def upload(self, href, vobject_item):
"""Upload a new item."""
raise NotImplementedError
def update(self, href, vobject_item, etag=None):
"""Update an item.
Functionally similar to ``delete`` plus ``upload``, but might bring
performance benefits on some storages when used cleverly.
"""
self.delete(href, etag)
self.upload(href, vobject_item)
def delete(self, href=None, etag=None):
"""Delete an item.
When ``href`` is ``None``, delete the collection.
"""
raise NotImplementedError
@contextmanager
def at_once(self):
"""Set a context manager buffering the reads and writes."""
# TODO: use in code
yield
def get_meta(self, key):
"""Get metadata value for collection."""
raise NotImplementedError
def set_meta(self, key, value):
"""Set metadata value for collection."""
raise NotImplementedError
@property
def last_modified(self):
"""Get the HTTP-datetime of when the collection was modified."""
raise NotImplementedError
def serialize(self):
"""Get the unicode string representing the whole collection."""
raise NotImplementedError
class Collection(BaseCollection):
"""Collection stored in several files per calendar."""
def __init__(self, path, principal=False):
folder = os.path.expanduser(
self.configuration.get("storage", "filesystem_folder"))
# path should already be sanitized
self.path = sanitize_path(path).strip("/")
self.storage_encoding = self.configuration.get("encoding", "stock")
self._filesystem_path = path_to_filesystem(folder, self.path)
split_path = self.path.split("/")
if len(split_path) > 1:
# URL with at least one folder
self.owner = split_path[0]
else:
self.owner = None
self.is_principal = principal
@classmethod
def discover(cls, path, depth="1"):
# path == None means wrong URL
if path is None:
return
@ -178,12 +276,14 @@ class Collection:
return
# Try to guess if the path leads to a collection or an item
if not os.path.isdir(path_to_filesystem(FOLDER, sane_path)):
folder = os.path.expanduser(
cls.configuration.get("storage", "filesystem_folder"))
if not os.path.isdir(path_to_filesystem(folder, sane_path)):
# path is not a collection
if os.path.isfile(path_to_filesystem(FOLDER, sane_path)):
if os.path.isfile(path_to_filesystem(folder, sane_path)):
# path is an item
attributes.pop()
elif os.path.isdir(path_to_filesystem(FOLDER, *attributes[:-1])):
elif os.path.isdir(path_to_filesystem(folder, *attributes[:-1])):
# path parent is a collection
attributes.pop()
# TODO: else: return?
@ -207,15 +307,9 @@ class Collection:
@classmethod
def create_collection(cls, href, collection=None, tag=None):
"""Create a collection.
``collection`` is a list of vobject components.
``tag`` is the type of collection (VCALENDAR or VADDRESSBOOK). If
``tag`` is not given, it is guessed from the collection.
"""
path = path_to_filesystem(FOLDER, href)
folder = os.path.expanduser(
cls.configuration.get("storage", "filesystem_folder"))
path = path_to_filesystem(folder, href)
if not os.path.exists(path):
os.makedirs(path)
if not tag and collection:
@ -239,7 +333,6 @@ class Collection:
return self
def list(self):
"""List collection items."""
try:
hrefs = os.listdir(self._filesystem_path)
except IOError:
@ -248,82 +341,63 @@ class Collection:
for href in hrefs:
path = os.path.join(self._filesystem_path, href)
if not href.endswith(".props") and os.path.isfile(path):
with open(path, encoding=STORAGE_ENCODING) as fd:
with open(path, encoding=self.storage_encoding) as fd:
yield href, get_etag(fd.read())
def get(self, href):
"""Fetch a single item."""
if not href:
return
href = href.strip("{}").replace("/", "_")
if is_safe_filesystem_path_component(href):
path = os.path.join(self._filesystem_path, href)
if os.path.isfile(path):
with open(path, encoding=STORAGE_ENCODING) as fd:
with open(path, encoding=self.storage_encoding) as fd:
text = fd.read()
last_modified = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT",
time.gmtime(os.path.getmtime(path)))
return Item(vobject.readOne(text), href, last_modified)
return Item(self, vobject.readOne(text), href, last_modified)
else:
log.LOGGER.debug(
self.logger.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", href)
def get_multi(self, hrefs):
"""Fetch multiple items. Duplicate hrefs must be ignored.
Functionally similar to ``get``, but might bring performance benefits
on some storages when used cleverly.
"""
for href in set(hrefs):
yield self.get(href)
def has(self, href):
"""Check if an item exists by its href."""
return self.get(href) is not None
def upload(self, href, vobject_item):
"""Upload a new item."""
# TODO: use returned object in code
if is_safe_filesystem_path_component(href):
path = path_to_filesystem(self._filesystem_path, href)
if not os.path.exists(path):
item = Item(vobject_item, href)
with open(path, "w", encoding=STORAGE_ENCODING) as fd:
item = Item(self, vobject_item, href)
with open(path, "w", encoding=self.storage_encoding) as fd:
fd.write(item.serialize())
return item
else:
log.LOGGER.debug(
self.logger.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", href)
def update(self, href, vobject_item, etag=None):
"""Update an item."""
# TODO: use etag in code and test it here
# TODO: use returned object in code
if is_safe_filesystem_path_component(href):
path = path_to_filesystem(self._filesystem_path, href)
if os.path.exists(path):
with open(path, encoding=STORAGE_ENCODING) as fd:
with open(path, encoding=self.storage_encoding) as fd:
text = fd.read()
if not etag or etag == get_etag(text):
item = Item(vobject_item, href)
with open(path, "w", encoding=STORAGE_ENCODING) as fd:
item = Item(self, vobject_item, href)
with open(path, "w", encoding=self.storage_encoding) as fd:
fd.write(item.serialize())
return item
else:
log.LOGGER.debug(
self.logger.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", href)
def delete(self, href=None, etag=None):
"""Delete an item.
When ``href`` is ``None``, delete the collection.
"""
# TODO: use etag in code and test it here
# TODO: use returned object in code
if href is None:
@ -338,49 +412,44 @@ class Collection:
# Delete an item
path = path_to_filesystem(self._filesystem_path, href)
if os.path.isfile(path):
with open(path, encoding=STORAGE_ENCODING) as fd:
with open(path, encoding=self.storage_encoding) as fd:
text = fd.read()
if not etag or etag == get_etag(text):
os.remove(path)
return
else:
log.LOGGER.debug(
self.logger.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", href)
@contextmanager
def at_once(self):
"""Set a context manager buffering the reads and writes."""
# TODO: use in code
# TODO: use a file locker
yield
def get_meta(self, key):
"""Get metadata value for collection."""
props_path = self._filesystem_path + ".props"
if os.path.exists(props_path):
with open(props_path, encoding=STORAGE_ENCODING) as prop_file:
return json.load(prop_file).get(key)
with open(props_path, encoding=self.storage_encoding) as prop:
return json.load(prop).get(key)
def set_meta(self, key, value):
"""Get metadata value for collection."""
props_path = self._filesystem_path + ".props"
properties = {}
if os.path.exists(props_path):
with open(props_path, encoding=STORAGE_ENCODING) as prop_file:
properties.update(json.load(prop_file))
with open(props_path, encoding=self.storage_encoding) as prop:
properties.update(json.load(prop))
if value:
properties[key] = value
else:
properties.pop(key, None)
with open(props_path, "w+", encoding=STORAGE_ENCODING) as prop_file:
json.dump(properties, prop_file)
with open(props_path, "w+", encoding=self.storage_encoding) as prop:
json.dump(properties, prop)
@property
def last_modified(self):
"""Get the HTTP-datetime of when the collection was modified."""
last = max([os.path.getmtime(self._filesystem_path)] + [
os.path.getmtime(os.path.join(self._filesystem_path, filename))
for filename in os.listdir(self._filesystem_path)] or [0])
@ -391,7 +460,7 @@ class Collection:
for href in os.listdir(self._filesystem_path):
path = os.path.join(self._filesystem_path, href)
if os.path.isfile(path) and not path.endswith(".props"):
with open(path, encoding=STORAGE_ENCODING) as fd:
with open(path, encoding=self.storage_encoding) as fd:
items.append(vobject.readOne(fd.read()))
if self.get_meta("tag") == "VCALENDAR":
collection = vobject.iCalendar()
@ -404,7 +473,3 @@ class Collection:
elif self.get_meta("tag") == "VADDRESSBOOK":
return "".join([item.serialize() for item in items])
return ""
@property
def etag(self):
return get_etag(self.serialize())