You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
11 KiB
289 lines
11 KiB
''' Filesystem base classes and registry '''
|
|
|
|
from typing import Any, Type as TypingType, TypeVar, Tuple, Set, \
|
|
MutableSet, Mapping, MutableMapping, Callable, \
|
|
AsyncGenerator, Generic
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from enum import Flag
|
|
from functools import total_ordering
|
|
from datetime import datetime
|
|
|
|
from asyncio import Queue
|
|
|
|
# {k: disordered[k] for k in sorted(disordered)}
|
|
|
|
FilesystemT = TypeVar('FilesystemT', bound='FilesystemBase')
|
|
SnapshotKeyT = TypeVar('SnapshotKeyT', bound='FilesystemBase.SnapshotKey')
|
|
EventT = TypeVar('EventT', bound='FilesystemBase.Event')
|
|
|
|
@total_ordering # type: ignore
|
|
class FilesystemBase(ABC):
|
|
''' Base filesystem class inherited by filesystem implementations. '''
|
|
|
|
class Event(dict):
|
|
''' Filesystem event class '''
|
|
|
|
class Type(Flag):
|
|
''' Filesystem event type enumerator. '''
|
|
|
|
GENERIC = 0x00000000
|
|
|
|
MOUNT = 0x00010000
|
|
MOUNTED = MOUNT | 0x0001
|
|
UNMOUNTED = MOUNT | 0x0002
|
|
|
|
FILE = 0x00020000 # Mandatory
|
|
FILE_CREATED = FILE | 0x0001
|
|
FILE_MODIFIED = FILE | 0x0002
|
|
FILE_MOVED = FILE | 0x0004
|
|
FILE_DELETED = FILE | 0x0008
|
|
|
|
SNAPSHOT = 0x00040000
|
|
SNAPSHOT_CREATED = SNAPSHOT | 0x0001
|
|
SNAPSHOT_DELETED = SNAPSHOT | 0x0002
|
|
|
|
def __init__(self, filesystem: FilesystemT,
|
|
event_type: Type, **properties):
|
|
|
|
''' Construct a filesystem event. Properties are assigned to the
|
|
event object, which doubles as a dictionary. '''
|
|
|
|
# Optional standard properties:
|
|
#
|
|
# description: human-readable description of the event
|
|
# path: Path the event originated from
|
|
# target: Path the event was targeted at
|
|
# snapshot: The snapshot the event pertained to
|
|
|
|
super().__init__(properties)
|
|
|
|
self._filesystem = filesystem
|
|
self._type = event_type
|
|
|
|
def filesystem(self) -> FilesystemT:
|
|
''' Get the filesystem the event occurred on. '''
|
|
return self._filesystem
|
|
|
|
def type(self) -> Type:
|
|
''' Get the event type. '''
|
|
return self._type
|
|
|
|
@total_ordering # type: ignore
|
|
class SnapshotKey(ABC):
|
|
''' Base snapshot key class inherited by filesystem implementations. '''
|
|
|
|
@abstractmethod
|
|
def filesystem(self) -> FilesystemT:
|
|
''' Get the parent filesystem for the snapshot. '''
|
|
|
|
@abstractmethod
|
|
def tag(self) -> str:
|
|
''' Get the tag name for the snapshot. Each user-configured schedule has
|
|
a unique tag name. '''
|
|
|
|
@abstractmethod
|
|
def timestamp(self) -> datetime:
|
|
''' Get the datetime the snapshot was taken at. '''
|
|
|
|
def __hash__(self) -> int:
|
|
''' Snapshot keys are hashable. Snapshots having equal filesystems,
|
|
tags and timestamps will have equal hash values. '''
|
|
return hash((self.filesystem(), self.tag(), self.timestamp()))
|
|
|
|
def __lt__(self: Any, other) -> bool:
|
|
''' Snapshot keys are sortable, and are ordered by filesystem first,
|
|
tag second and timestamp last. '''
|
|
return (isinstance(other, self.__class__) and
|
|
(self.filesystem() < other.filesystem() or
|
|
self.filesystem() == other.filesystem() and
|
|
(self.tag() < other.tag() or
|
|
self.tag() == other.tag() and
|
|
self.timestamp() < other.timestamp())))
|
|
|
|
def __eq__(self, other) -> bool:
|
|
''' Snapshot keys are comparable and are equal if they have equal hash
|
|
values. '''
|
|
return hash(self) == hash(other)
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def key(cls) -> str:
|
|
''' Retrieve the registry key for this filesystem class. '''
|
|
|
|
# Filesystem classes must add themselves to the filesystem registry as
|
|
# soon as they are imported.
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
async def filesystems(cls) -> Mapping[str, FilesystemT]:
|
|
''' Retrieve a dictionary of labels to filesystems managed by this
|
|
filesystem class. '''
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def configure(cls) -> Tuple[Mapping[str, Any], Callable[[Any], None]]:
|
|
''' Retrieve tuple for configuring the filesystem. '''
|
|
|
|
# This method returns a tuple (global_scope, callback), consisting of
|
|
# a dictionary and a callback function. It is called each time a
|
|
# configuration file has loaded, before it is executed.
|
|
#
|
|
# The dictionary consists of variables to be deep copied and exposed
|
|
# on the global execution scope of the configuration file. This may be
|
|
# used by the Filesystem class to provide constants, functions and
|
|
# variables that the user will need in order to configure filesystems
|
|
# of this class.
|
|
|
|
# The callback is invoked as callback(options) once execution has
|
|
# completed. Options are retrived from a variable Filesystem.key() on
|
|
# the local execution scope of the configuration file and will equal
|
|
# None if no such variable was assigned by the user. The variable is
|
|
# typically a dictionary, but may be of any type, and may be used by
|
|
# the Filesystem class to retrieve configuration options the user has
|
|
# set in order to configure filesystems of this class. '''
|
|
|
|
@abstractmethod
|
|
def label(self) -> str:
|
|
''' Retrieve the filesystem label.'''
|
|
|
|
# Labels must allow users to easily identify the underlying
|
|
# filesystems. Device names, drive letters, volume labels and
|
|
# mountpoints should be avoided, since these are not persistent and do
|
|
# not uniquely identify a filesystem. If the underlying filesystem
|
|
# does not provide labels that satisfy these criteria, a means for the
|
|
# user to map unique labels to unique filesystem identifiers must be
|
|
# provided in the configuration file.
|
|
|
|
@abstractmethod
|
|
async def children(self) -> Mapping[str, FilesystemT]:
|
|
''' Get a dictionary of labels to filesystems that are children of this
|
|
filesystem. '''
|
|
|
|
# Some filesystem classes allow hierarchical filesystem labels,
|
|
# permitting recursive snapshots to be taken on a filesystem and its
|
|
# children. An empty dictionary may be returned if this feature is not
|
|
# supported.
|
|
|
|
@abstractmethod
|
|
async def snapshots(self) -> Set[SnapshotKey]:
|
|
''' Get a set of keys for snapshots managed by this filesystem
|
|
object. Snapshots taken on any children of the filesystem,
|
|
recursively or otherwise, are not included in the set. '''
|
|
|
|
@abstractmethod
|
|
async def take(self, tag: str, recursive: bool=False) -> SnapshotKey:
|
|
''' Take a snapshot on this filesystem and return the key. For recursive
|
|
snapshots, return the key of the filesystem the snapshot was
|
|
requested on. '''
|
|
|
|
@abstractmethod
|
|
async def delete(self, key: SnapshotKey, recursive: bool=False) -> None:
|
|
''' Delete a snapshot having a given key from this filesystem. For
|
|
recursive deletions, snapshots on children of the filesystem having
|
|
a tag and a timestamp equal to that of the parent snapshot are
|
|
deleted recursively. '''
|
|
|
|
@abstractmethod
|
|
def watch(self) -> AsyncGenerator[Event, None]:
|
|
''' Factory function that returns an asynchronous generator that
|
|
yields dispatched events from an internal event queue. The
|
|
generator ends when the filesystem is unmounted. '''
|
|
|
|
def __hash__(self) -> int:
|
|
''' Filesystems are hashable. Filesystems having equal labels will have
|
|
equal hash values. '''
|
|
return hash(self.label())
|
|
|
|
def __lt__(self, other) -> bool:
|
|
''' Filesystems are sortable and are ordered by label. '''
|
|
return (isinstance(other, self.__class__) and
|
|
(self == other or self.label() < other.label()))
|
|
|
|
def __eq__(self, other):
|
|
''' Filesystems are comparable and are equal if they have equal hash
|
|
values. '''
|
|
return hash(self) == hash(other)
|
|
|
|
class DispatcherMixin(Generic[EventT]):
|
|
''' Class for dispatching filesystem events to asynchronous generators
|
|
produced by a factory function. Events are consumed by iterating
|
|
over the generators. '''
|
|
|
|
def __init__(self):
|
|
self._event_queues: MutableSet[Queue] = set()
|
|
|
|
async def dispatch(self, *events: EventT):
|
|
''' Dispatch events to generators. '''
|
|
for event in events:
|
|
for queue in self._event_queues:
|
|
await queue.put(event)
|
|
|
|
def watch(self) -> AsyncGenerator[EventT, None]:
|
|
''' Factory function that returns an asynchronous generator that
|
|
yields dispatched events from an internal event queue. The
|
|
generator ends when the filesystem is unmounted. '''
|
|
|
|
async def generator(queue):
|
|
while True:
|
|
event: EventT = await queue.get()
|
|
yield event
|
|
if FilesystemBase.Event.Type.UNMOUNTED in event.type():
|
|
break
|
|
|
|
self._event_queues.remove(queue)
|
|
|
|
queue: Queue = Queue()
|
|
self._event_queues.add(queue)
|
|
return generator(queue)
|
|
|
|
class FilesystemRegistry:
|
|
''' Class that keeps a global registry of filesystem classes and labels '''
|
|
|
|
_filesystems: MutableMapping[str, FilesystemBase] = {}
|
|
_labels: MutableSet[str] = set()
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def filesystems(cls) -> Mapping[str, TypingType[FilesystemBase]]:
|
|
''' Retrieve a dictionary of keys to registered filesystem classes. '''
|
|
return cls._filesystems
|
|
|
|
@classmethod
|
|
def has(cls, key: str) -> bool:
|
|
''' Returns True if the filesystem key exists in the registry. '''
|
|
return key in cls._filesystems
|
|
|
|
@classmethod
|
|
def register(cls, klass: TypingType[FilesystemBase]) -> None:
|
|
''' Register a filesystem class. '''
|
|
assert not klass.key() in cls._filesystems
|
|
cls._filesystems[klass.key()] = klass
|
|
|
|
@classmethod
|
|
def labels(cls) -> Set[str]:
|
|
''' Retrieve the set of the labels in the registry. '''
|
|
return set(cls._labels)
|
|
|
|
@classmethod
|
|
def mounted(cls, label: str) -> bool:
|
|
''' Returns True if the filesystem label exists in the registry. '''
|
|
return label in cls._labels
|
|
|
|
@classmethod
|
|
def mount(cls, label: str) -> None:
|
|
''' Add a new filesystem label to the registry. Must be called once a
|
|
filesystem becomes available. '''
|
|
assert not label in cls._labels
|
|
cls._labels.add(label)
|
|
|
|
@classmethod
|
|
def unmount(cls, label: str) -> None:
|
|
''' Remove a filesystem label from the registry. Must be called if a
|
|
filesystem becomes unavailable, or the configuration file was
|
|
reloaded and a filesystem has disappeared from it. '''
|
|
assert label in cls._labels
|
|
cls._labels.remove(label) |