Completed
Push — file-actions ( 6acf28...68b947 )
by Felipe A.
25s
created

Clipboard.copy()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 1
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import os
5
import json
6
import base64
7
import logging
8
import hashlib
9
import functools
10
11
try:
12
    import lzma
13
    LZMA_OPTIONS = {
14
        'format': lzma.FORMAT_RAW,
15
        'filters': [
16
            {'id': lzma.FILTER_DELTA, 'dist': 5},
17
            {'id': lzma.FILTER_LZMA2, 'preset': lzma.PRESET_DEFAULT},
18
            ]
19
        }
20
    compress = functools.partial(lzma.compress, **LZMA_OPTIONS)
21
    decompress = functools.partial(lzma.decompress, **LZMA_OPTIONS)
22
except ImportError:
23
    from zlib import compress, decompress
24
25
26
from flask import request
27
from browsepy.compat import range
28
29
from .exceptions import InvalidClipboardSizeError
30
31
logger = logging.getLogger(__name__)
32
33
34
class Clipboard(set):
35
    '''
36
    Clipboard (set) with convenience methods to pick its state from request
37
    cookies and save it to response cookies.
38
    '''
39
    cookie_secret = os.urandom(256)
40
    cookie_name = 'clipboard-{:x}'
41
    cookie_path = '/'
42
    request_cache_field = '_browsepy_file_actions_clipboard_cache'
43
    max_pages = 20
44
45
    @classmethod
46
    def count(cls, request=request):
47
        '''
48
        Get how many clipboard items are stores on request cookies.
49
50
        :param request: optional request, defaults to current flask request
51
        :type request: werkzeug.Request
52
        :return: number of clipboard items on request's cookies
53
        :rtype: int
54
        '''
55
        return len(cls.from_request(request))
56
57
    @classmethod
58
    def from_request(cls, request=request):
59
        '''
60
        Create clipboard object from request cookies.
61
        Uses request itself for cache.
62
63
        :param request: optional request, defaults to current flask request
64
        :type request: werkzeug.Request
65
        :returns: clipboard instance
66
        :rtype: Clipboard
67
        '''
68
        cached = getattr(request, cls.request_cache_field, None)
69
        if cached is not None:
70
            return cached
71
        self = cls()
72
        setattr(request, cls.request_cache_field, self)
73
        try:
74
            self.__setstate__(cls._read_paginated_cookie(request))
75
        except BaseException:
76
            pass
77
        return self
78
79
    @classmethod
80
    def _paginated_cookie_length(cls, page=0):
81
        name_fnc = cls.cookie_name.format
82
        return 3990 - len(name_fnc(page) + cls.cookie_path)
83
84
    @classmethod
85
    def _read_paginated_cookie(cls, request=request):
86
        chunks = []
87
        if request:
88
            name_fnc = cls.cookie_name.format
89
            for i in range(cls.max_pages):  # 2 ** 32 - 1
90
                cookie = request.cookies.get(name_fnc(i), '').encode('ascii')
91
                chunks.append(cookie)
92
                if len(cookie) < cls._paginated_cookie_length(i):
93
                    break
94
        serialized = decompress(base64.b64decode(b''.join(chunks)))
95
        return json.loads(serialized.decode('utf-8'))
96
97
    @classmethod
98
    def _write_paginated_cookie(cls, data, response):
99
        serialized = compress(json.dumps(data).encode('utf-8'))
100
        data = base64.b64encode(serialized)
101
        name_fnc = cls.cookie_name.format
102
        start = 0
103
        size = len(data)
104
        for i in range(cls.max_pages):
105
            end = cls._paginated_cookie_length(i)
106
            response.set_cookie(name_fnc(i), data[start:end].decode('ascii'))
107
            start = end
108
            if start > size:  # we need an empty page after start == size
109
                return i
110
        raise InvalidClipboardSizeError(max_cookies=cls.max_pages)
111
112
    @classmethod
113
    def _delete_paginated_cookie(cls, response, start=0, request=request):
114
        name_fnc = cls.cookie_name.format
115
        for i in range(start, cls.max_pages):
116
            name = name_fnc(i)
117
            if name not in request.cookies:
118
                break
119
            response.set_cookie(name, '', expires=0)
120
121
    @classmethod
122
    def _signature(cls, items, method):
123
        serialized = json.dumps(items).encode('utf-8')
124
        data = cls.cookie_secret + method.encode('utf-8') + serialized
125
        return base64.b64encode(hashlib.sha512(data).digest()).decode('ascii')
126
127
    def __init__(self, iterable=(), mode='copy'):
128
        self.mode = mode
129
        super(Clipboard, self).__init__(iterable)
130
131
    def __getstate__(self):
132
        items = list(self)
133
        return {
134
            'mode': self.mode,
135
            'items': items,
136
            'signature': self._signature(items, self.mode),
137
            }
138
139
    def __setstate__(self, data):
140
        if data['signature'] == self._signature(data['items'], data['mode']):
141
            self.update(data['items'])
142
            self.mode = data['mode']
143
144
    def to_response(self, response, request=request):
145
        '''
146
        Save clipboard state to response taking care of disposing old clipboard
147
        cookies from request.
148
149
        :param response: response object to write cookies on
150
        :type response: werkzeug.Response
151
        :param request: optional request, defaults to current flask request
152
        :type request: werkzeug.Request
153
        '''
154
        start = 0
155
        if self:
156
            data = self.__getstate__()
157
            start = self._write_paginated_cookie(data, response) + 1
158
        self._delete_paginated_cookie(response, start, request)
159
160
    def copy(self):
161
        '''
162
        Create another instance of this class with same items and mode.
163
164
        :returns: clipboard instance
165
        :rtype: Clipboard
166
        '''
167
        return self.___class__(self, mode=self.mode)
168