filesystem.py 17.4 KB
Newer Older
1
2
# -*- coding: utf-8 -*-

Marcel's avatar
Marcel committed
3
# imports
4
import StringIO
murban's avatar
murban committed
5
import json
6
7
import stat
import logging
Gero Müller's avatar
Gero Müller committed
8

9
from cherrypy.lib import file_generator, cptools, httputil
10
import cherrypy
Martin Urban's avatar
Martin Urban committed
11
import rpyc
12

13
import vispa
14
from vispa import AjaxException
15
from vispa.controller import AbstractController
16
17
18


logger = logging.getLogger(__name__)
19

Marcel's avatar
Marcel committed
20

21
class FSController(AbstractController):
22

Gero Müller's avatar
Gero Müller committed
23
24
25
    def __init__(self):
        AbstractController.__init__(self, mount_static=False)

26
    @staticmethod
27
    def _stream_remote_file(fs, path, callback=None):
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        offset = 0
        buffer_size = 2 ** 20
        while True:
            try:
                data = fs.get_file_content(path, offset, buffer_size)
            except:
                logger.exception("get content")
                raise

            l = len(data)
            if l <= 0:
                break
            else:
                offset += l

            yield data

45
46
47
        if hasattr(callback, "__call__"):
            callback()

48
    @cherrypy.expose
Gero Müller's avatar
Gero Müller committed
49
    @cherrypy.tools.ajax(on=False)
50
    @cherrypy.tools.method(accept="GET")
51
52
53
54
55
    def getfile(self, path, download=None, deleteoncomplete=None, **kwargs):
        truthy = ("true", "1", "yes")
        deleteoncomplete = deleteoncomplete and deleteoncomplete.lower() in truthy
        download = download and download.lower() in truthy

56
        fs = self.get('fs')
57
58
59
        stats = fs.stat(path)
        if not stat.S_ISREG(stats.st_mode):
            raise cherrypy.HTTPError(404, "Not a File!")
60
61
62

        # Set the Last-Modified response header, so that
        # modified-since validation code can work.
63
64
65
66
67
68
69
        headers = cherrypy.response.headers
        headers[
            'Cache-Control'] = 'max-age=1, private, must-revalidate, no-cache'
        headers['Pragma'] = 'no-cache'
        headers['Last-Modified'] = httputil.HTTPDate(stats.st_mtime)
        headers['Expires'] = httputil.HTTPDate(stats.st_mtime + 1)
        headers['Content-Length'] = stats.st_size
70
71
        cptools.validate_since()

72
        if download:
73
            disposition = 'attachment; filename=%s' % path.split('/')[-1]
74
75
76
        else:
            disposition = 'inline; filename=%s' % path.split('/')[-1]
        headers['Content-Disposition'] = disposition
77
78
79
80

        mimetype = fs.get_mime_type(path)
        if mimetype is not None:
            headers['Content-Type'] = mimetype
81
82
        else:
            headers['Content-Type'] = "application/octet-stream"
83
84

        self.release()
Gero Müller's avatar
Gero Müller committed
85

86
87
88
89
90
91
        callback = None
        if deleteoncomplete:
            def callback():
                fs.remove(path)

        return FSController._stream_remote_file(fs, path, callback)
92
93

    getfile._cp_config = {'response.stream': True}
94

95
96
    @cherrypy.expose
    @cherrypy.tools.ajax(on=False)
97
    @cherrypy.tools.method(accept="GET")
98
    def thumbnail(self, path, width=100, height=100, **kwargs):
Gero Müller's avatar
Gero Müller committed
99
        self.release_session()
100
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
101
        self.release_database()
102
103
104
105
106
        contents = fs.thumbnail(path, int(width), int(height))
        cherrypy.response.headers['Content-Type'] = "image/jpeg"
        contents = StringIO.StringIO(contents)
        return file_generator(contents)

Marcel's avatar
Marcel committed
107

108
class FSAjaxController(AbstractController):
109
110

    @cherrypy.expose
111
    @cherrypy.tools.method(accept="GET")
Gero Müller's avatar
Gero Müller committed
112
    def exists(self, path, filetype=None):
Gero Müller's avatar
Gero Müller committed
113
        self.release_session()
Gero Müller's avatar
Gero Müller committed
114
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
115
        self.release_database()
Gero Müller's avatar
Gero Müller committed
116
117
118
119

        # type = type if type else type
        target_type = fs.exists(path, type=filetype)
        if target_type:
120
            return target_type
Gero Müller's avatar
Gero Müller committed
121
        else:
122
            return "Failed"
Marcel's avatar
Marcel committed
123

124
    @cherrypy.expose
125
    @cherrypy.tools.method(accept="GET")
126
    def filecount(self, path, watch_id=None):
127
128
129
130
        self.release_session()
        fs = self.get('fs')
        self.release_database()

131
132
133
134
        count = fs.get_file_count(path,
                                  window_id=self.get('window_id'),
                                  view_id=self.get('view_id'),
                                  watch_id=watch_id)
135
        if count == -1:
136
            raise AjaxException("%s does not exist" % path)
137
138
139
        # instead of raising an exception we return -2 as count to account for
        # the fact that the user does not have the permission to read the path
        # in the GUI elif count == -2:
140
        #   raise AjaxException("You do not have rights to read %s." % path)
141
        elif not isinstance(count, int):
142
            raise AjaxException(count)
143
144
145
        else:
            return {"count": count}

146
    @cherrypy.expose
147
    @cherrypy.tools.ajax(encoded=True)
148
    @cherrypy.tools.method(accept="GET")
149
    def filelist(self, path, filefilter=None, reverse=False, watch_id=None, hide_hidden=True):
Gero Müller's avatar
Gero Müller committed
150
        self.release_session()
151
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
152
        self.release_database()
153

154
        reverse = self.convert(reverse, bool)
155
        hide_hidden = self.convert(hide_hidden, bool)
156

157
158
159
        if not fs.exists(path):
            raise AjaxException("there is no such file: %s" % path)

160
        # get the files with the filter
161
        return fs.get_file_list(path, filter=filefilter,
162
                                reverse=reverse, encode_json=True,
163
                                hide_hidden=hide_hidden,
164
165
166
                                window_id=self.get('window_id'),
                                view_id=self.get('view_id'),
                                watch_id=watch_id)
167

168
    @cherrypy.expose
169
    @cherrypy.tools.method(accept="POST")
Gero Müller's avatar
Gero Müller committed
170
    def createfolder(self, path, name):
Gero Müller's avatar
Gero Müller committed
171
        self.release_session()
172
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
173
174
        self.release_database()

175
176
177
        err = fs.create_folder(path, name)
        if err:
            raise AjaxException(err)
178
179

    @cherrypy.expose
180
    @cherrypy.tools.method(accept="POST")
Gero Müller's avatar
Gero Müller committed
181
    def createfile(self, path, name):
Gero Müller's avatar
Gero Müller committed
182
        self.release_session()
183
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
184
185
        self.release_database()

186
187
188
        err = fs.create_file(path, name)
        if err:
            raise AjaxException(err)
189
190

    @cherrypy.expose
191
    @cherrypy.tools.method(accept="POST")
192
    def rename(self, path, name, new_name):
Gero Müller's avatar
Gero Müller committed
193
        self.release_session()
194
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
195
        self.release_database()
196
197
198
199
        try:
            fs.rename(path, name, new_name)
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])
200
201

    @cherrypy.expose
202
    @cherrypy.tools.method(accept="POST")
Gero Müller's avatar
Gero Müller committed
203
    def remove(self, path):
Gero Müller's avatar
Gero Müller committed
204
        self.release_session()
205
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
206
        self.release_database()
207
        path = json.loads(path)
208
209
        # 'path' can be a unicode/string or list of unicodes/strings
        # so convert it with the convert function
ThorbenQuast's avatar
ThorbenQuast committed
210
        try:
Martin Urban's avatar
Martin Urban committed
211
212
            async_remove = rpyc.async(fs.remove)
            async_remove(path)
ThorbenQuast's avatar
ThorbenQuast committed
213
214
215
            return
        except:
            return
216

217
    @cherrypy.expose
218
    @cherrypy.tools.method(accept="POST")
219
220
221
222
223
224
225
226
    def move(self, source, destination):
        self.release_session()
        fs = self.get('fs')
        self.release_database()
        source = json.loads(source)
        destination = json.loads(destination)
        # 'source' and 'destination' can be a unicode/string or list of unicodes/strings
        # so convert it with the convert function
227
228
229
230
        try:
            fs.move(source, destination)
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])
231

232
    @cherrypy.expose
233
    @cherrypy.tools.method(accept="POST")
234
235
236
    def compress(self, paths, path, name="", isTmp=False):
        isTmp = isTmp and isTmp.lower() in ("true", "1", "yes")
        if not isTmp and not name:
237
            raise AjaxException("Either name or isTmp must be given.")
Gero Müller's avatar
Gero Müller committed
238
        self.release_session()
239
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
240
        self.release_database()
241
242
243
        # 'paths' can be a unicode/string or list of unicodes/strings
        # so convert it with the convert function
        paths = json.loads(paths)
244
        try:
245
            return fs.compress(paths, path, name, isTmp)
246
247
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])
248

249
    @cherrypy.expose
250
    @cherrypy.tools.method(accept="POST")
251
252
253
254
    def decompress(self, file):
        self.release_session()
        fs = self.get('fs')
        self.release_database()
255
256
257
258
259
        try:
            fs.decompress(file)
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])

260

261
    @cherrypy.expose
262
    @cherrypy.tools.method(accept="POST")
Gero Müller's avatar
Gero Müller committed
263
    def paste(self, path, paths, cut):
Gero Müller's avatar
Gero Müller committed
264
        self.release_session()
Gero Müller's avatar
Gero Müller committed
265
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
266
        self.release_database()
Gero Müller's avatar
Gero Müller committed
267

268
269
270
271
        paths = json.loads(paths)
        # path = fs.expand(path.encode("utf-8"))
        # 'paths' can be a unicode/string or list of unicodes/strings
        # so convert it with the convert function
272
273
274
275
        try:
            fs.paste(path, paths, self.convert(cut, bool))
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])
276
277

    @cherrypy.expose
278
    @cherrypy.tools.method(accept="POST")
Gero Müller's avatar
Gero Müller committed
279
    def upload(self, *args, **kwargs):
Gero Müller's avatar
Gero Müller committed
280
        self.release_session()
Gero Müller's avatar
Gero Müller committed
281
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
282
        self.release_database()
283
284
285
286
287
288
289
290
291

        # the html5 uploader provides following kwargs:
        # index, type, name, size, files[]
        # Since "files[]" ends with "[]"-brackets
        # we have to use kwargs instead of args
        # extract the path

        # prepare the parts
        parts = kwargs['files[]']
292
293
294
        # force parts to be a list
        if not isinstance(parts, list):
            parts = [parts]
295

296
297
298
299
300
301
302
303
304
305
306
        # get the byterange if chunked
        chunked = False
        if 'Content-Range' in cherrypy.request.headers:
            chunked = True
            tmp = cherrypy.request.headers['Content-Range']
            tmp = tmp.split()[1].split('/')
            maxbytes = int(tmp[1])
            tmp = tmp[0].split('-')
            firstbyte = int(tmp[0])
            lastbyte = int(tmp[1])

Gero Müller's avatar
Gero Müller committed
307
        path = kwargs['path']
308

309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
        try:
            for part in parts:
                filename = fs.handle_file_name_collision(part.filename, path)
                if chunked:
                    filename = '~' + filename
                if chunked and (firstbyte != 0):
                    # check correct size of previous chunks
                    for file in fs.get_file_list(path, encode_json=False)['filelist']:
                        if file['name'] == filename and file['size'] != firstbyte:
                            raise AjaxException("Chunked upload failed")
                    append = True
                else:
                    append   = False

                while True:
                    data = part.file.read(1024 ** 2)
                    if len(data) <= 0:
                        break
327

328
329
330
331
332
                    success, msg = fs.save_file_content(filename, data,
                                                        path=path, force=True,
                                                        append=append)
                    if not success:
                        raise AjaxException(msg)
333

334
335
                    if not append:
                        append = True
336

337
338
339
340
                if chunked and (lastbyte + 1 == maxbytes):
                    fs.rename(path, filename, filename.lstrip('~'))
        except Exception as e:
            raise AjaxException(str(e).split("\n")[0])
341

342
    @cherrypy.expose
343
    @cherrypy.tools.method(accept="GET")
Gero Müller's avatar
Gero Müller committed
344
    def isbrowserfile(self, path):
Gero Müller's avatar
Gero Müller committed
345
        self.release_session()
Gero Müller's avatar
Gero Müller committed
346
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
347
        self.release_database()
Marcel's avatar
Marcel committed
348
        try:
349
            # return fs.is_browser_file(str(path))
Gero Müller's avatar
Gero Müller committed
350
            if fs.is_browser_file(path):
Martin Urban's avatar
Martin Urban committed
351
                return
Marcel's avatar
Marcel committed
352
            else:
353
                return "File can not be opened in browser."
354
        except Exception as e:
355
            raise AjaxException(str(e).split("\n")[0])
Marcel's avatar
Marcel committed
356
357

    @cherrypy.expose
358
    @cherrypy.tools.method(accept="GET")
Gero Müller's avatar
Gero Müller committed
359
    def getsuggestions(self, path, length=10, append_hidden=True):
Gero Müller's avatar
Gero Müller committed
360
        self.release_session()
Gero Müller's avatar
Gero Müller committed
361
        fs = self.get('fs')
Gero Müller's avatar
Gero Müller committed
362
        self.release_database()
363
        try:
Marcel's avatar
Marcel committed
364
            length = length or 1
365
            suggestions = fs.get_suggestions(path, length=int(
Gero Müller's avatar
Gero Müller committed
366
                length), append_hidden=self.convert(append_hidden, bool),
367
                encode_json=True)
368
            return self.success(suggestions=suggestions, encode_json=True)
369
        except Exception as e:
370
            return self.fail(msg=str(e), encode_json=True)
371

372
373
    @cherrypy.expose
    @cherrypy.tools.ajax(encoded=True)
374
    @cherrypy.tools.method(accept="POST")
375
    def savefile(self, path, content, watch_id=None, utf8=False):
376
377
378
379
380
381
382
383
384
385
        self.release_session()
        fs = self.get('fs')
        self.release_database()
        return fs.save_file(path, content, utf8=utf8,
                            window_id=self.get('window_id'),
                            view_id=self.get('view_id'),
                            watch_id=watch_id)

    @cherrypy.expose
    @cherrypy.tools.ajax(encoded=True)
386
    @cherrypy.tools.method(accept="GET")
387
    def getfile(self, path, watch_id=None, utf8=False):
388
389
390
391
        self.release_session()
        fs = self.get('fs')
        self.release_database()
        return fs.get_file(path, utf8=utf8,
392
393
                           window_id=self.get('window_id'),
                           view_id=self.get('view_id'),
394
395
                           watch_id=watch_id,
                           max_size=vispa.config("filesystem", "max_get_size", 15))
396

397
    @cherrypy.expose
398
    @cherrypy.tools.method(accept="POST")
399
400
401
402
403
    def watch(self, path, watch_id):
        self.release_session()
        fs = self.get('fs')
        self.release_database()

404
405
406
407
        err = fs.watch(path,
                       window_id=self.get('window_id'),
                       view_id=self.get('view_id'),
                       watch_id=watch_id)
408
        if err:
409
            raise AjaxException(err)
410
411
412
        return {"success": not err}

    @cherrypy.expose
413
    @cherrypy.tools.method(accept="POST")
414
415
416
417
    def unwatch(self, watch_id=None):
        self.release_session()
        fs = self.get('fs')
        self.release_database()
418

419
        err = fs.unwatch(window_id=self.get('window_id'),
420
421
                         view_id=self.get('view_id'),
                         watch_id=watch_id)
422
        if err:
423
            raise AjaxException(err)
424
        return {"success": not err}
425
426
427

    @cherrypy.expose
    @cherrypy.tools.ajax(encoded=True)
428
    @cherrypy.tools.method(accept="GET")
429
    def getworkspaceini(self, request, fail_on_missing=False):
430
431
432
        self.release_session()
        fs = self.get('fs')
        self.release_database()
433
434
        fail_on_missing = self.convert(fail_on_missing, bool)
        return fs.get_workspaceini(request, fail_on_missing=fail_on_missing)
asseldonk's avatar
asseldonk committed
435

436
    @cherrypy.expose
437
    @cherrypy.tools.method(accept="POST")
438
    def setworkspaceini(self, request):
439
440
441
        self.release_session()
        fs = self.get('fs')
        self.release_database()
442

443
444
        err = fs.set_workspaceini(request)
        if err:
445
            raise AjaxException(err)
asseldonk's avatar
asseldonk committed
446
447
448
        return {"success": not err}

    @cherrypy.expose
449
    @cherrypy.tools.method(accept="GET")
asseldonk's avatar
asseldonk committed
450
451
452
453
454
455
456
457
    def expand(self, path):
        self.release_session()
        fs = self.get('fs')
        self.release_database()
        path = json.loads(path)
        # 'path' can be a unicode/string or list of unicodes/strings
        # so convert it with the convert function
        return fs.expand(path)
asseldonk's avatar
asseldonk committed
458
459

    @cherrypy.expose
460
    @cherrypy.tools.method(accept="GET")
461
    def checkpermissions(self, path):
asseldonk's avatar
asseldonk committed
462
463
464
465
466
467
        self.release_session()
        fs = self.get('fs')
        self.release_database()
        path = json.loads(path)
        path = fs.expand(path)
        return {"permission": fs.checkPermissions(path)}
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483

    @cherrypy.expose
    @cherrypy.tools.method(accept="GET")
    def getfacl(self, path):
        """
        Get the facl of a certain path.

        :param path: path of interest
        :returns: list of tuples with (type, user, mode[, default mode])
        """
        self.release_session()
        fs = self.get("fs")
        self.release_database()
        path = fs.expand(path)
        return list(fs.getfacl(path))

484
485
486
487
488
489
490
491
    @cherrypy.expose
    @cherrypy.tools.method(accept="GET")
    def getuserandgroupids(self):
        self.release_session()
        fs = self.get("fs")
        self.release_database()
        return fs.get_user_and_group_ids()

492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
    @cherrypy.expose
    @cherrypy.tools.method(accept="POST")
    def setfacl(self, path, type, name, mode, remove=False, recursive=False, default=False):
        """
        Set the facl entry for a certain user or group. On remote side, the commandline tool
        'setfacl' is used, so this method delivers an interface to that.

        :param path: path of interest
        :param type: type, either 'user', 'group', 'mask' or 'other'
        :param name: name of the user or group of interest
        :param mode: mode to be set
        :param remove: remove all extended facl entries (option '-x')
        :param recursive: apply changes to all files and directories recursivley (option '-R')
        :param default: edit the default values of an facl entry (option '-d')
        :raises: AjaxException if type is not 'user', 'group', 'mask' or 'other'
        """
        if type not in ("user", "group", "mask", "other"):
            raise AjaxException("unknown type \"%s\"" % type)
        self.release_session()
        fs = self.get("fs")
        self.release_database()
        path = fs.expand(path)
        if not isinstance(remove, bool): remove = json.loads(remove)
        if not isinstance(recursive, bool): recursive = json.loads(recursive)
        if not isinstance(default, bool): default = json.loads(default)
        fs.setfacl(path, type, name, mode, remove, recursive, default)