|
|
@@ -23,75 +23,57 @@ Uses one folder per collection and one file per collection entry.
|
|
|
|
|
|
"""
|
|
|
|
|
|
-import contextlib
|
|
|
import os
|
|
|
import time
|
|
|
-from itertools import chain
|
|
|
-from tempfile import TemporaryDirectory
|
|
|
+from typing import Iterator, Optional
|
|
|
|
|
|
-from radicale import pathutils, storage
|
|
|
-from radicale.storage.multifilesystem.cache import CollectionCacheMixin
|
|
|
+from radicale import config
|
|
|
+from radicale.storage.multifilesystem.base import CollectionBase, StorageBase
|
|
|
+from radicale.storage.multifilesystem.cache import CollectionPartCache
|
|
|
from radicale.storage.multifilesystem.create_collection import \
|
|
|
- StorageCreateCollectionMixin
|
|
|
-from radicale.storage.multifilesystem.delete import CollectionDeleteMixin
|
|
|
-from radicale.storage.multifilesystem.discover import StorageDiscoverMixin
|
|
|
-from radicale.storage.multifilesystem.get import CollectionGetMixin
|
|
|
-from radicale.storage.multifilesystem.history import CollectionHistoryMixin
|
|
|
-from radicale.storage.multifilesystem.lock import (CollectionLockMixin,
|
|
|
- StorageLockMixin)
|
|
|
-from radicale.storage.multifilesystem.meta import CollectionMetaMixin
|
|
|
-from radicale.storage.multifilesystem.move import StorageMoveMixin
|
|
|
-from radicale.storage.multifilesystem.sync import CollectionSyncMixin
|
|
|
-from radicale.storage.multifilesystem.upload import CollectionUploadMixin
|
|
|
-from radicale.storage.multifilesystem.verify import StorageVerifyMixin
|
|
|
+ StoragePartCreateCollection
|
|
|
+from radicale.storage.multifilesystem.delete import CollectionPartDelete
|
|
|
+from radicale.storage.multifilesystem.discover import StoragePartDiscover
|
|
|
+from radicale.storage.multifilesystem.get import CollectionPartGet
|
|
|
+from radicale.storage.multifilesystem.history import CollectionPartHistory
|
|
|
+from radicale.storage.multifilesystem.lock import (CollectionPartLock,
|
|
|
+ StoragePartLock)
|
|
|
+from radicale.storage.multifilesystem.meta import CollectionPartMeta
|
|
|
+from radicale.storage.multifilesystem.move import StoragePartMove
|
|
|
+from radicale.storage.multifilesystem.sync import CollectionPartSync
|
|
|
+from radicale.storage.multifilesystem.upload import CollectionPartUpload
|
|
|
+from radicale.storage.multifilesystem.verify import StoragePartVerify
|
|
|
|
|
|
|
|
|
class Collection(
|
|
|
- CollectionCacheMixin, CollectionDeleteMixin, CollectionGetMixin,
|
|
|
- CollectionHistoryMixin, CollectionLockMixin, CollectionMetaMixin,
|
|
|
- CollectionSyncMixin, CollectionUploadMixin, storage.BaseCollection):
|
|
|
-
|
|
|
- def __init__(self, storage_, path, filesystem_path=None):
|
|
|
- self._storage = storage_
|
|
|
- folder = self._storage._get_collection_root_folder()
|
|
|
- # Path should already be sanitized
|
|
|
- self._path = pathutils.strip_path(path)
|
|
|
- self._encoding = self._storage.configuration.get("encoding", "stock")
|
|
|
- if filesystem_path is None:
|
|
|
- filesystem_path = pathutils.path_to_filesystem(folder, self.path)
|
|
|
- self._filesystem_path = filesystem_path
|
|
|
+ CollectionPartDelete, CollectionPartMeta, CollectionPartSync,
|
|
|
+ CollectionPartUpload, CollectionPartGet, CollectionPartCache,
|
|
|
+ CollectionPartLock, CollectionPartHistory, CollectionBase):
|
|
|
+
|
|
|
+ _etag_cache: Optional[str]
|
|
|
+
|
|
|
+ def __init__(self, storage_: "Storage", path: str,
|
|
|
+ filesystem_path: Optional[str] = None) -> None:
|
|
|
+ super().__init__(storage_, path, filesystem_path)
|
|
|
self._etag_cache = None
|
|
|
- super().__init__()
|
|
|
|
|
|
@property
|
|
|
- def path(self):
|
|
|
+ def path(self) -> str:
|
|
|
return self._path
|
|
|
|
|
|
- @contextlib.contextmanager
|
|
|
- def _atomic_write(self, path, mode="w", newline=None):
|
|
|
- parent_dir, name = os.path.split(path)
|
|
|
- # Do not use mkstemp because it creates with permissions 0o600
|
|
|
- with TemporaryDirectory(
|
|
|
- prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir:
|
|
|
- with open(os.path.join(tmp_dir, name), mode, newline=newline,
|
|
|
- encoding=None if "b" in mode else self._encoding) as tmp:
|
|
|
- yield tmp
|
|
|
- tmp.flush()
|
|
|
- self._storage._fsync(tmp)
|
|
|
- os.replace(os.path.join(tmp_dir, name), path)
|
|
|
- self._storage._sync_directory(parent_dir)
|
|
|
-
|
|
|
@property
|
|
|
- def last_modified(self):
|
|
|
- relevant_files = chain(
|
|
|
- (self._filesystem_path,),
|
|
|
- (self._props_path,) if os.path.exists(self._props_path) else (),
|
|
|
- (os.path.join(self._filesystem_path, h) for h in self._list()))
|
|
|
- last = max(map(os.path.getmtime, relevant_files))
|
|
|
+ def last_modified(self) -> str:
|
|
|
+ def relevant_files_iter() -> Iterator[str]:
|
|
|
+ yield self._filesystem_path
|
|
|
+ if os.path.exists(self._props_path):
|
|
|
+ yield self._props_path
|
|
|
+ for href in self._list():
|
|
|
+ yield os.path.join(self._filesystem_path, href)
|
|
|
+ last = max(map(os.path.getmtime, relevant_files_iter()))
|
|
|
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))
|
|
|
|
|
|
@property
|
|
|
- def etag(self):
|
|
|
+ def etag(self) -> str:
|
|
|
# reuse cached value if the storage is read-only
|
|
|
if self._storage._lock.locked == "w" or self._etag_cache is None:
|
|
|
self._etag_cache = super().etag
|
|
|
@@ -99,61 +81,11 @@ class Collection(
|
|
|
|
|
|
|
|
|
class Storage(
|
|
|
- StorageCreateCollectionMixin, StorageDiscoverMixin, StorageLockMixin,
|
|
|
- StorageMoveMixin, StorageVerifyMixin, storage.BaseStorage):
|
|
|
+ StoragePartCreateCollection, StoragePartLock, StoragePartMove,
|
|
|
+ StoragePartVerify, StoragePartDiscover, StorageBase):
|
|
|
|
|
|
_collection_class = Collection
|
|
|
|
|
|
- def __init__(self, configuration):
|
|
|
+ def __init__(self, configuration: config.Configuration) -> None:
|
|
|
super().__init__(configuration)
|
|
|
- folder = configuration.get("storage", "filesystem_folder")
|
|
|
- self._makedirs_synced(folder)
|
|
|
-
|
|
|
- def _get_collection_root_folder(self):
|
|
|
- filesystem_folder = self.configuration.get(
|
|
|
- "storage", "filesystem_folder")
|
|
|
- return os.path.join(filesystem_folder, "collection-root")
|
|
|
-
|
|
|
- def _fsync(self, f):
|
|
|
- if self.configuration.get("storage", "_filesystem_fsync"):
|
|
|
- try:
|
|
|
- pathutils.fsync(f.fileno())
|
|
|
- except OSError as e:
|
|
|
- raise RuntimeError("Fsync'ing file %r failed: %s" %
|
|
|
- (f.name, e)) from e
|
|
|
-
|
|
|
- def _sync_directory(self, path):
|
|
|
- """Sync directory to disk.
|
|
|
-
|
|
|
- This only works on POSIX and does nothing on other systems.
|
|
|
-
|
|
|
- """
|
|
|
- if not self.configuration.get("storage", "_filesystem_fsync"):
|
|
|
- return
|
|
|
- if os.name == "posix":
|
|
|
- try:
|
|
|
- fd = os.open(path, 0)
|
|
|
- try:
|
|
|
- pathutils.fsync(fd)
|
|
|
- finally:
|
|
|
- os.close(fd)
|
|
|
- except OSError as e:
|
|
|
- raise RuntimeError("Fsync'ing directory %r failed: %s" %
|
|
|
- (path, e)) from e
|
|
|
-
|
|
|
- def _makedirs_synced(self, filesystem_path):
|
|
|
- """Recursively create a directory and its parents in a sync'ed way.
|
|
|
-
|
|
|
- This method acts silently when the folder already exists.
|
|
|
-
|
|
|
- """
|
|
|
- if os.path.isdir(filesystem_path):
|
|
|
- return
|
|
|
- parent_filesystem_path = os.path.dirname(filesystem_path)
|
|
|
- # Prevent infinite loop
|
|
|
- if filesystem_path != parent_filesystem_path:
|
|
|
- # Create parent dirs recursively
|
|
|
- self._makedirs_synced(parent_filesystem_path)
|
|
|
- # Possible race!
|
|
|
- os.makedirs(filesystem_path, exist_ok=True)
|
|
|
- self._sync_directory(parent_filesystem_path)
|
|
|
+ self._makedirs_synced(self._filesystem_folder)
|