summaryrefslogtreecommitdiff
path: root/distixlib/ticket_store.py
blob: 82ebc1e7c7ce169628e9def674ae8dd0b4f18914 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Copyright 2014  Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# =*= License: GPL-3+ =*=


import email
import os

import distixlib


class TicketHasNoId(distixlib.StructuredError):

    msg = "Can't save ticket without an id"


class TicketAlreadyInStoreError(distixlib.StructuredError):

    msg = 'Ticket {ticket_id} already exists in store {dirname}'


class TicketNotInStoreError(distixlib.StructuredError):

    msg = 'Ticket {ticket_id} is not in store {dirname}'


class TicketStore(object):

    '''A ticket store in a repository.

    This is just one set of tickets in a repository. The design allows
    for many stores.

    Tickets are stored in a directory. The name of the directory is
    given to the initialiser.

    '''

    def __init__(self, dirname):
        self._dirname = dirname
        self._loader = distixlib.TicketLoader()
        self._saver = distixlib.TicketSaver()
        self._ticket_cache = _TicketCache()
        self._dirty = False

    def is_dirty(self):
        return self._dirty or self._any_ticket_is_dirty()

    def _any_ticket_is_dirty(self):
        return any(ticket.is_dirty() for ticket in self._get_cached_tickets())

    def _get_cached_tickets(self):
        return self._ticket_cache.get_all()

    def make_clean(self):
        for ticket in self._get_cached_tickets():
            ticket.make_clean()
        self._dirty = False

    def get_ticket_ids(self):
        if os.path.exists(self._dirname):
            return os.listdir(self._dirname)
        return []

    def get_ticket(self, ticket_id_or_prefix):
        '''Return a distixlib.Ticket for an existing ticket in the store.'''

        ticket_id = self._find_ticket_id_from_prefix(ticket_id_or_prefix)
        assert ticket_id in self._ticket_cache
        return self._ticket_cache.get(ticket_id)

    def _find_ticket_id_from_prefix(self, prefix):
        cached_ids = self._ticket_cache.keys()
        matches = self._find_ticket_ids_matching_prefix(cached_ids, prefix)
        if len(matches) == 1:
            return matches[0]

        tickets = self.get_tickets()
        ticket_ids = [t.get_ticket_id() for t in tickets]
        matches = self._find_ticket_ids_matching_prefix(ticket_ids, prefix)
        if len(matches) == 1:
            return matches[0]

        raise TicketNotInStoreError(ticket_id=prefix)

    def _find_ticket_ids_matching_prefix(self, ticket_ids, prefix):
        prefix = prefix.lower()
        return [tid for tid in ticket_ids if tid.lower().startswith(prefix)]

    def _load_ticket(self, ticket_dir):
        return self._loader.load_from_directory(ticket_dir)

    def get_tickets(self):
        '''Return all the tickets in a store.'''

        ticket_dirs = self._find_ticket_dirs()
        loaded_tickets = [self._load_ticket(x) for x in ticket_dirs]
        self._cache_tickets(loaded_tickets)
        ticket_ids = [ticket.get_ticket_id() for ticket in loaded_tickets]
        return self._load_tickets_from_ticket_cache(ticket_ids)

    def _find_ticket_dirs(self):
        return [
            os.path.join(self._dirname, ticket_id)
            for ticket_id in self.get_ticket_ids()
        ]

    def _cache_tickets(self, tickets):
        for ticket in tickets:
            ticket_id = ticket.get_ticket_id()
            if ticket_id not in self._ticket_cache:
                self._ticket_cache.put(ticket)

    def _load_tickets_from_ticket_cache(self, ticket_ids):
        return [self._ticket_cache.get(ticket_id) for ticket_id in ticket_ids]

    def add_ticket(self, ticket):
        '''Add ticket to the store.

        Return list of pathnames to all the files that got created.

        '''

        ticket_id = ticket.get_ticket_id()
        if ticket_id is None:
            raise TicketHasNoId()

        if ticket_id in self.get_ticket_ids():
            raise distixlib.TicketAlreadyInStoreError(
                ticket_id=ticket_id, dirname=self._dirname)

        self._dirty = True
        self._ticket_cache.put(ticket)
        self._create_store_directory()
        return self._create_ticket(ticket)

    def _create_store_directory(self):
        if not os.path.exists(self._dirname):
            os.mkdir(self._dirname)

    def _create_ticket(self, ticket):
        ticket_dir = self._get_dir_for_ticket(ticket.get_ticket_id())
        return self._saver.create_ticket_on_disk(ticket, ticket_dir)

    def _get_dir_for_ticket(self, ticket_id):
        return os.path.join(self._dirname, ticket_id)

    def _get_ticket_id(self, ticket):  # pragma: no cover
        ticket_id = ticket.get_ticket_id()
        if ticket_id is None:
            return '<no ticket-id>'
        return ticket_id

    def save_changes(self):
        filenames = []
        for ticket in self._get_cached_tickets():
            if ticket.is_dirty():
                filenames.extend(self._save_ticket(ticket))
        self.make_clean()
        return filenames

    def _save_ticket(self, ticket):
        ticket_dir = self._get_dir_for_ticket(ticket.get_ticket_id())
        return self._saver.save_changes_to_ticket(ticket, ticket_dir)

    def ticket_has_message_with_text(
            self, ticket_id, msg_text):  # pragma: no cover
        filenames = self.get_message_filenames(ticket_id)
        for filename in filenames:
            if self._file_contains(filename, msg_text):
                return True
        return False

    def get_message_filenames(self, ticket_id):  # pragma: no cover
        ticket_dir = self._get_dir_for_ticket(ticket_id)
        maildir_pathname = self._saver._get_maildir_pathname(ticket_dir)

        message_filenames = []

        for dirname, subdirs, filenames in os.walk(maildir_pathname):
            if '.empty' in subdirs:
                subdirs.remove('.empty')
            for filename in filenames:
                message_filenames.append(os.path.join(dirname, filename))

        return message_filenames

    def _file_contains(self, filename, data):  # pragma: no cover Note
        # that the email.Message class may reformat headers, so that
        # msg.as_string() doesn't return the pristine original message
        # text. Thus we can't just compare file content with data.
        # Instead we load the file content into a message, let any
        # reformatting happen, and then compare msg.as_string() with
        # data.
        with open(filename) as f:
            msg = email.message_from_file(f)
        return msg.as_string() == data


class _TicketCache(object):

    def __init__(self):
        self._known_tickets = {}

    def keys(self):
        return self._known_tickets.keys()

    def put(self, ticket):
        self._known_tickets[ticket.get_ticket_id()] = ticket

    def __contains__(self, ticket_id):
        return ticket_id in self._known_tickets

    def get(self, ticket_id):
        return self._known_tickets[ticket_id]

    def get_all(self):
        return self._known_tickets.values()