notificationstorage.py 4.91 KB
Newer Older
1
import glob
2
import os
3
import logging
Martin Matějek's avatar
Martin Matějek committed
4

Martin Matějek's avatar
Martin Matějek committed
5
from datetime import datetime
6
from functools import lru_cache
7 8 9

from .notification import Notification

10 11
logger = logging.getLogger(__name__)

12

Martin Matějek's avatar
Martin Matějek committed
13 14
class NotificationStorage:
    """In-memory notification storage that serialize and deserialize them"""
15
    SHORTID_LENGTH = 8
16

17
    def __init__(self, volatile_dir, persistent_dir, plugin_storage):
18 19 20 21
        self.storage_dirs = {
            'persistent': persistent_dir,
            'volatile': volatile_dir,
        }
22
        self.plugin_storage = plugin_storage
23

24
        self.notifications = {}
25
        self.shortid_map = {}
Martin Matějek's avatar
Martin Matějek committed
26

27 28 29
        self.load(volatile_dir)
        self.load(persistent_dir)

Martin Matějek's avatar
Martin Matějek committed
30
    def store(self, n):
31 32
        """
        Store in memory
33

34 35
        Serializate to disk
        Render fallback in default languages
36
        """
37
        self.notifications[n.notif_id] = n
38
        self.shortid_map[n.notif_id[:self.SHORTID_LENGTH]] = n.notif_id
Martin Matějek's avatar
Martin Matějek committed
39

40 41 42 43 44 45
        if n.persistent:
            storage_dir = self.storage_dirs['persistent']
        else:
            storage_dir = self.storage_dirs['volatile']

        # save to disk
46
        file_path = os.path.join(storage_dir, "{}.json".format(n.notif_id))
47 48 49

        try:
            with open(file_path, 'w') as f:
50
                f.write(n.serialize())
51 52
        except OSError:
            logger.error("Error during writing notification to disk!")
53 54 55
            return False

        return True
Martin Matějek's avatar
Martin Matějek committed
56 57

    def load(self, storage_dir):
58
        """Deserialize all notifications from FS"""
59
        logger.debug("Deserializing notifications from '%s'", storage_dir)
60
        for filepath in glob.glob(os.path.join(storage_dir, '*.json')):
61
            n = Notification.from_file(filepath, self.plugin_storage)
62 63 64 65

            if n:
                self.notifications[n.notif_id] = n
                self.shortid_map[n.notif_id[:self.SHORTID_LENGTH]] = n.notif_id
66

67 68 69
    def valid_id(self, msgid):
        """Check if msgid is valid and message with that id exists"""
        if msgid not in self.notifications and msgid not in self.shortid_map:
Martin Matějek's avatar
Martin Matějek committed
70
            logger.debug("Notification ID '%s' does not exist", msgid)
71 72 73 74 75 76
            return False

        return True

    def _full_id(self, msgid):
        """Get full id of notification based on short id"""
77
        if len(msgid) == self.SHORTID_LENGTH:
78 79 80
            return self.shortid_map[msgid]

        return msgid
Martin Matějek's avatar
Martin Matějek committed
81

82 83
    def get(self, msgid):
        """Return single notification instance"""
84 85 86
        if self.valid_id(msgid):
            msgid = self._full_id(msgid)

87
            return self.notifications[msgid]
88 89

        return None
90

91
    @lru_cache(maxsize=256)
92
    def get_rendered(self, msgid, media_type, lang, force_media_type=False):
Martin Matějek's avatar
Martin Matějek committed
93
        """Return notification either cached or if missing, cache it and return"""
94 95
        msgid = self._full_id(msgid)
        n = self.notifications[msgid]
96

97 98 99
        mt = n.has_media_type(media_type)
        if not mt and force_media_type:
            return None
100

101
        return n.render(media_type, lang)
Martin Matějek's avatar
Martin Matějek committed
102

103
    def get_all(self):
104
        """Get all stored notification objects"""
105
        return self.notifications
106

107 108
    def get_all_rendered(self, media_type, lang):
        """Get all notifications rendered in lang and in given media_type"""
109
        notifications = {}
110 111

        for msgid in self.notifications.keys():
112
            notifications[msgid] = self.get_rendered(msgid, media_type, lang)
113

114
        return notifications
115

116
    def delete_invalid_messages(self):
Martin Matějek's avatar
Martin Matějek committed
117 118
        """Delete messages based on their timeout"""
        to_delete = []
Martin Matějek's avatar
Martin Matějek committed
119
        now = datetime.utcnow()
Martin Matějek's avatar
Martin Matějek committed
120 121

        for n in self.notifications.values():
122
            if not n.is_valid(now):
Martin Matějek's avatar
Martin Matějek committed
123 124 125
                to_delete.append(n)

        for n in to_delete:
126
            logger.debug("Deleting notification '%s' due to timeout", n.notif_id)
127
            self.remove(n.notif_id)
Martin Matějek's avatar
Martin Matějek committed
128

129 130
    def remove(self, msgid):
        """Remove single notification"""
131 132 133 134 135
        if self.valid_id(msgid):
            msgid = self._full_id(msgid)
            n = self.notifications[msgid]

            del self.notifications[msgid]
136
            del self.shortid_map[msgid[:self.SHORTID_LENGTH]]
137 138 139 140 141 142 143

            logger.debug("Dismissing notification '%s'", msgid)
            if n.persistent:
                storage_dir = self.storage_dirs['persistent']
            else:
                storage_dir = self.storage_dirs['volatile']

144 145
            filepath = os.path.join(storage_dir, "{}.json".format(msgid))
            tmp_filepath = "{}.tmp".format(filepath)
146

147
            try:
148
                os.rename(filepath, tmp_filepath)
149 150 151
            except FileNotFoundError as e:
                logger.error(e)
                return
152
            except IsADirectoryError:
153 154
                logger.error("Cannot rename file. There already is a directory with the same name!")
                return
155 156

            try:
157
                os.unlink(tmp_filepath)
158
            except OSError as e:
159
                logger.error("Cannot remove tempfile '%s'. Reason: %s", tmp_filepath, e)
160
                return