protocol.py 7.89 KB
Newer Older
1
from ws4py.websocket import WebSocket
2
3
import json
import logging
4
from os.path import abspath
5
6
from agkyra.syncer import syncer
from agkyra.config import AgkyraConfig
7
8
9


LOG = logging.getLogger(__name__)
10
11
12
13
14
15


class WebSocketProtocol(WebSocket):
    """Helper-side WebSocket protocol for communication with GUI:

    -- INTERRNAL HANDSAKE --
16
    GUI: {"method": "post", "gui_id": <GUI ID>}
17
18
    HELPER: {"ACCEPTED": 202, "method": "post"}" or
        "{"REJECTED": 401, "action": "post gui_id"}
19
20
21

    -- SHUT DOWN --
    GUI: {"method": "post", "path": "shutdown"}
22

23
24
    -- PAUSE --
    GUI: {"method": "post", "path": "pause"}
25
    HELPER: {"OK": 200, "action": "post pause"} or error
26
27
28

    -- start --
    GUI: {"method": "post", "path": "start"}
29
    HELPER: {"OK": 200, "action": "post start"} or error
30

31
32
33
34
    -- GET SETTINGS --
    GUI: {"method": "get", "path": "settings"}
    HELPER:
        {
35
            "action": "get settings",
36
37
38
39
40
            "token": <user token>,
            "url": <auth url>,
            "container": <container>,
            "directory": <local directory>,
            "exclude": <file path>
41
        } or {<ERROR>: <ERROR CODE>}
42
43
44
45
46
47
48
49

    -- PUT SETTINGS --
    GUI: {
            "method": "put", "path": "settings",
            "token": <user token>,
            "url": <auth url>,
            "container": <container>,
            "directory": <local directory>,
50
            "pithos_url": <pithos URL>,
51
52
            "exclude": <file path>
        }
53
54
    HELPER: {"CREATED": 201, "action": "put settings",} or
        {<ERROR>: <ERROR CODE>, "action": "get settings",}
55
56
57

    -- GET STATUS --
    GUI: {"method": "get", "path": "status"}
58
59
    HELPER: {"progress": <int>, "paused": <boolean>, "action": "get status"} or
        {<ERROR>: <ERROR CODE>, "action": "get status"}
60
61
    """

62
63
    gui_id = None
    accepted = False
64
    settings = dict(
65
66
        token=None, url=None,
        container=None, directory=None,
67
        exclude=None)
68
69
    status = dict(progress=0, paused=True)
    file_syncer = None
70
71
    cnf = AgkyraConfig()

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
    def _get_default_sync(self):
        """Get global.default_sync or pick the first sync as default
        If there are no syncs, create a 'default' sync.
        """
        sync = self.cnf.get('global', 'default_sync')
        if not sync:
            for sync in self.cnf.keys('sync'):
                break
            self.cnf.set('global', 'default_sync', sync or 'default')
        return sync or 'default'

    def _get_sync_cloud(self, sync):
        """Get the <sync>.cloud or pick the first cloud and use it
        In case of cloud picking, set the cloud as the <sync>.cloud for future
        sessions.
        If no clouds are found, create a 'default' cloud, with an empty url.
        """
        try:
            cloud = self.cnf.get_sync(sync, 'cloud')
        except KeyError:
            cloud = None
        if not cloud:
            for cloud in self.cnf.keys('cloud'):
                break
            self.cnf.set_sync(sync, 'cloud', cloud or 'default')
        return cloud or 'default'

99
    def _load_settings(self):
100
        LOG.debug('Start loading settings')
101
102
        sync = self._get_default_sync()
        cloud = self._get_sync_cloud(sync)
103
104

        try:
105
106
107
108
109
110
111
            self.settings['url'] = self.cnf.get_cloud(cloud, 'url')
        except Exception:
            self.settings['url'] = None
        try:
            self.settings['token'] = self.cnf.get_cloud(cloud, 'token')
        except Exception:
            self.settings['url'] = None
112
113

        for option in ('container', 'directory', 'exclude'):
114
115
116
117
            try:
                self.settings[option] = self.cnf.get_sync(sync, option)
            except KeyError:
                LOG.debug('No %s is set' % option)
118

119
120
        LOG.debug('Finished loading settings')

121
    def _dump_settings(self):
122
        LOG.debug('Saving settings')
123
124
125
126
127
128
129
130
131
132
133
        if not self.settings.get('url', None):
            LOG.debug('No settings to save')
            return

        sync = self._get_default_sync()
        cloud = self._get_sync_cloud(sync)

        try:
            old_url = self.cnf.get_cloud(cloud, 'url') or ''
        except KeyError:
            old_url = self.settings['url']
134
135
136
137
138
139
140
141
142

        while old_url != self.settings['url']:
            cloud = '%s_%s' % (cloud, sync)
            try:
                self.cnf.get_cloud(cloud, 'url')
            except KeyError:
                break

        self.cnf.set_cloud(cloud, 'url', self.settings['url'])
143
        self.cnf.set_cloud(cloud, 'token', self.settings['token'] or '')
144
145
146
        self.cnf.set_sync(sync, 'cloud', cloud)

        for option in ('directory', 'container', 'exclude'):
147
            self.cnf.set_sync(sync, option, self.settings[option] or '')
148

149
150
151
        self.cnf.write()
        LOG.debug('Settings saved')

152
153
    # Syncer-related methods
    def get_status(self):
154
155
156
        from random import randint
        if self.status['progress'] < 100:
            self.status['progress'] += 0 if randint(0, 2) else 1
157
        return self.status
158
159

    def get_settings(self):
160
161
        return self.settings

162
163
    def set_settings(self, new_settings):
        self.settings = new_settings
164
        self._dump_settings()
165

166
167
168
169
170
    def pause_sync(self):
        self.status['paused'] = True

    def start_sync(self):
        self.status['paused'] = False
171

172
    # WebSocket connection methods
173
    def opened(self):
174
        LOG.debug('Helper: connection established')
175
176

    def closed(self, *args):
177
178
179
180
181
182
183
184
185
186
        LOG.debug('Helper: connection closed')

    def send_json(self, msg):
        LOG.debug('send: %s' % msg)
        self.send(json.dumps(msg))

    # Protocol handling methods
    def _post(self, r):
        """Handle POST requests"""
        if self.accepted:
187
188
            action = r['path']
            if action == 'shutdown':
189
                self.close()
190
191
192
193
194
                return
            {
                'start': self.start_sync,
                'pause': self.pause_sync
            }[action]()
195
            self.send_json({'OK': 200, 'action': 'post %s' % action})
196
        elif r['gui_id'] == self.gui_id:
197
            self._load_settings()
198
            self.accepted = True
199
            self.send_json({'ACCEPTED': 202, 'action': 'post gui_id'})
200
        else:
201
202
            action = r.get('path', 'gui_id')
            self.send_json({'REJECTED': 401, 'action': 'post %s' % action})
203
204
205
206
            self.terminate()

    def _put(self, r):
        """Handle PUT requests"""
207
        if self.accepted:
208
            LOG.debug('put %s' % r)
209
210
211
212
            action = r.pop('path')
            self.set_settings(r)
            r.update({'CREATED': 201, 'action': 'put %s' % action})
            self.send_json(r)
213
214
215
216
        else:
            action = r['path']
            self.send_json({'UNAUTHORIZED': 401, 'action': 'put %s' % action})
            self.terminate()
217
218
219

    def _get(self, r):
        """Handle GET requests"""
220
        action = r.pop('path')
221
        if not self.accepted:
222
            self.send_json({'UNAUTHORIZED': 401, 'action': 'get %s' % action})
223
224
225
226
227
            self.terminate()
        else:
            data = {
                'settings': self.get_settings,
                'status': self.get_status,
228
229
            }[action]()
            data['action'] = 'get %s' % action
230
            self.send_json(data)
231
232

    def received_message(self, message):
233
234
235
236
237
238
239
240
241
        """Route requests to corresponding handling methods"""
        LOG.debug('recv: %s' % message)
        try:
            r = json.loads('%s' % message)
        except ValueError as ve:
            self.send_json({'BAD REQUEST': 400})
            LOG.error('JSON ERROR: %s' % ve)
            return
        try:
242
            method = r.pop('method')
243
244
245
246
            {
                'post': self._post,
                'put': self._put,
                'get': self._get
247
            }[method](r)
248
249
250
251
252
253
254
        except KeyError as ke:
            self.send_json({'BAD REQUEST': 400})
            LOG.error('KEY ERROR: %s' % ke)
        except Exception as e:
            self.send_json({'INTERNAL ERROR': 500})
            LOG.error('EXCEPTION: %s' % e)
            self.terminate()