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:
@ -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())
|
||||
|
Reference in New Issue
Block a user