Completed
Push — file-actions ( 3ab188...6cd94b )
by Felipe A.
28s
created

Clipboard.__getstate__()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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