notificationstorage.py 4.89 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):
18 19 20 21 22
        self.storage_dirs = {
            'persistent': persistent_dir,
            'volatile': volatile_dir,
        }

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

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

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

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

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

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

        try:
            with open(file_path, 'w') as f:
49
                f.write(n.serialize())
50 51
        except OSError:
            logger.error("Error during writing notification to disk!")
Martin Matějek's avatar
Martin Matějek committed
52 53

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

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

63 64 65 66 67 68 69 70 71 72
    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:
            logger.warning("Notification id is invalid - notification does not exist")
            return False

        return True

    def _full_id(self, msgid):
        """Get full id of notification based on short id"""
73
        if len(msgid) == self.SHORTID_LENGTH:
74 75 76
            return self.shortid_map[msgid]

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

78 79
    def get(self, msgid):
        """Return single notification instance"""
80 81 82
        if self.valid_id(msgid):
            msgid = self._full_id(msgid)

83
            return self.notifications[msgid]
84 85

        return None
86

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

93 94 95
        mt = n.has_media_type(media_type)
        if not mt and force_media_type:
            return None
96

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

99
    def get_all(self):
100
        """Get all stored notification objects"""
101
        return self.notifications
102

103 104
    def get_all_rendered(self, media_type, lang):
        """Get all notifications rendered in lang and in given media_type"""
105
        notifications = {}
106 107

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

110
        return notifications
111

112
    def delete_invalid_messages(self):
Martin Matějek's avatar
Martin Matějek committed
113 114
        """Delete messages based on their timeout"""
        to_delete = []
Martin Matějek's avatar
Martin Matějek committed
115
        now = datetime.utcnow()
Martin Matějek's avatar
Martin Matějek committed
116 117

        for n in self.notifications.values():
118
            if not n.is_valid(now):
Martin Matějek's avatar
Martin Matějek committed
119 120 121
                to_delete.append(n)

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

125 126
    def remove(self, msgid):
        """Remove single notification"""
127 128 129 130 131
        if self.valid_id(msgid):
            msgid = self._full_id(msgid)
            n = self.notifications[msgid]

            del self.notifications[msgid]
132
            del self.shortid_map[msgid[:self.SHORTID_LENGTH]]
133 134 135 136 137 138 139 140 141 142

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

            filename = os.path.join(storage_dir, "{}.json".format(msgid))
            tmp_filename = os.path.join("/tmp", "{}.json".format(msgid))

143 144
            # TODO: figure out how to pass what exactly failed to user

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

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