|
|
@@ -18,15 +18,13 @@
|
|
|
"""
|
|
|
Storage backends.
|
|
|
|
|
|
-This module loads the storage backend, according to the storage
|
|
|
-configuration.
|
|
|
+This module loads the storage backend, according to the storage configuration.
|
|
|
|
|
|
Default storage uses one folder per collection and one file per collection
|
|
|
entry.
|
|
|
|
|
|
"""
|
|
|
|
|
|
-import hashlib
|
|
|
import json
|
|
|
import os
|
|
|
import posixpath
|
|
|
@@ -34,6 +32,8 @@ import shutil
|
|
|
import sys
|
|
|
import time
|
|
|
from contextlib import contextmanager
|
|
|
+from hashlib import md5
|
|
|
+from random import randint
|
|
|
from uuid import uuid4
|
|
|
|
|
|
import vobject
|
|
|
@@ -54,6 +54,8 @@ def _load():
|
|
|
|
|
|
FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
|
|
|
FILESYSTEM_ENCODING = sys.getfilesystemencoding()
|
|
|
+STORAGE_ENCODING = config.get("encoding", "stock")
|
|
|
+
|
|
|
|
|
|
def serialize(tag, headers=(), items=()):
|
|
|
"""Return a text corresponding to given collection ``tag``.
|
|
|
@@ -92,47 +94,15 @@ def sanitize_path(path):
|
|
|
return new_path + trailing_slash
|
|
|
|
|
|
|
|
|
-def clean_name(name):
|
|
|
- """Clean an item name by removing slashes and leading/ending brackets."""
|
|
|
- # Remove leading and ending brackets that may have been put by Outlook
|
|
|
- name = name.strip("{}")
|
|
|
- # Remove slashes, mostly unwanted when saving on filesystems
|
|
|
- name = name.replace("/", "_")
|
|
|
- return name
|
|
|
-
|
|
|
-
|
|
|
-def is_safe_path_component(path):
|
|
|
- """Check if path is a single component of a POSIX path.
|
|
|
-
|
|
|
- Check that the path is safe to join too.
|
|
|
-
|
|
|
- """
|
|
|
- if not path:
|
|
|
- return False
|
|
|
- if posixpath.split(path)[0]:
|
|
|
- return False
|
|
|
- if path in (".", ".."):
|
|
|
- return False
|
|
|
- return True
|
|
|
-
|
|
|
-
|
|
|
def is_safe_filesystem_path_component(path):
|
|
|
"""Check if path is a single component of a filesystem path.
|
|
|
|
|
|
Check that the path is safe to join too.
|
|
|
|
|
|
"""
|
|
|
- if not path:
|
|
|
- return False
|
|
|
- drive, _ = os.path.splitdrive(path)
|
|
|
- if drive:
|
|
|
- return False
|
|
|
- head, _ = os.path.split(path)
|
|
|
- if head:
|
|
|
- return False
|
|
|
- if path in (os.curdir, os.pardir):
|
|
|
- return False
|
|
|
- return True
|
|
|
+ return (
|
|
|
+ path and not os.path.splitdrive(path)[0] and
|
|
|
+ not os.path.split(path)[0] and path not in (os.curdir, os.pardir))
|
|
|
|
|
|
|
|
|
def path_to_filesystem(path):
|
|
|
@@ -154,14 +124,6 @@ def path_to_filesystem(path):
|
|
|
return safe_path
|
|
|
|
|
|
|
|
|
-@contextmanager
|
|
|
-def _open(path, mode="r"):
|
|
|
- """Open a file at ``path`` with encoding set in the configuration."""
|
|
|
- abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
|
|
|
- with open(abs_path, mode, encoding=config.get("encoding", "stock")) as fd:
|
|
|
- yield fd
|
|
|
-
|
|
|
-
|
|
|
class Item(object):
|
|
|
"""Internal iCal item."""
|
|
|
def __init__(self, text, name=None):
|
|
|
@@ -189,7 +151,9 @@ class Item(object):
|
|
|
break
|
|
|
|
|
|
if self._name:
|
|
|
- self._name = clean_name(self._name)
|
|
|
+ # Leading and ending brackets that may have been put by Outlook.
|
|
|
+ # Slashes are mostly unwanted when saving collections on disk.
|
|
|
+ self._name = self._name.strip("{}").replace("/", "_")
|
|
|
else:
|
|
|
self._name = uuid4().hex
|
|
|
|
|
|
@@ -210,9 +174,9 @@ class Item(object):
|
|
|
Etag is mainly used to know if an item has changed.
|
|
|
|
|
|
"""
|
|
|
- md5 = hashlib.md5()
|
|
|
- md5.update(self.text.encode("utf-8"))
|
|
|
- return '"%s"' % md5.hexdigest()
|
|
|
+ etag = md5()
|
|
|
+ etag.update(self.text.encode("utf-8"))
|
|
|
+ return '"%s"' % etag.hexdigest()
|
|
|
|
|
|
@property
|
|
|
def name(self):
|
|
|
@@ -407,7 +371,7 @@ class Collection:
|
|
|
"skipping component: %s", name)
|
|
|
continue
|
|
|
filename = os.path.join(self._filesystem_path, name)
|
|
|
- with _open(filename, "w") as fd:
|
|
|
+ with open(filename, "w", encoding=STORAGE_ENCODING) as fd:
|
|
|
fd.write(component.text)
|
|
|
|
|
|
@property
|
|
|
@@ -447,7 +411,7 @@ class Collection:
|
|
|
for filename in filenames:
|
|
|
path = os.path.join(self._filesystem_path, filename)
|
|
|
try:
|
|
|
- with _open(path) as fd:
|
|
|
+ with open(path, encoding=STORAGE_ENCODING) as fd:
|
|
|
items.update(self._parse(fd.read(), components))
|
|
|
except (OSError, IOError) as e:
|
|
|
log.LOGGER.warning(
|
|
|
@@ -460,15 +424,14 @@ class Collection:
|
|
|
def children(cls, path):
|
|
|
filesystem_path = path_to_filesystem(path)
|
|
|
_, directories, files = next(os.walk(filesystem_path))
|
|
|
- for filename in directories + files:
|
|
|
- # make sure that the local filename can be translated
|
|
|
- # into an internal path
|
|
|
- if not is_safe_path_component(filename):
|
|
|
- log.LOGGER.debug("Skipping unsupported filename: %s", filename)
|
|
|
+ for path in directories + files:
|
|
|
+ # Check that the local path can be translated into an internal path
|
|
|
+ if not path or posixpath.split(path)[0] or path in (".", ".."):
|
|
|
+ log.LOGGER.debug("Skipping unsupported filename: %s", path)
|
|
|
continue
|
|
|
- rel_filename = posixpath.join(path, filename)
|
|
|
- if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
|
|
|
- yield cls(rel_filename)
|
|
|
+ relative_path = posixpath.join(path, path)
|
|
|
+ if cls.is_node(relative_path) or cls.is_leaf(relative_path):
|
|
|
+ yield cls(relative_path)
|
|
|
|
|
|
@classmethod
|
|
|
def is_node(cls, path):
|
|
|
@@ -567,9 +530,9 @@ class Collection:
|
|
|
@property
|
|
|
def etag(self):
|
|
|
"""Etag from collection."""
|
|
|
- md5 = hashlib.md5()
|
|
|
- md5.update(self.text.encode("utf-8"))
|
|
|
- return '"%s"' % md5.hexdigest()
|
|
|
+ etag = md5()
|
|
|
+ etag.update(self.text.encode("utf-8"))
|
|
|
+ return '"%s"' % etag.hexdigest()
|
|
|
|
|
|
@property
|
|
|
def name(self):
|