filesystem.py 28.2 KB
Newer Older
Marcel's avatar
Marcel committed
1
# -*- coding: utf-8 -*-
2

Marcel's avatar
Marcel committed
3
# imports
4
5
from StringIO import StringIO
from distutils.spawn import find_executable
6
7
from mimetypes import guess_type
from zipfile import ZipFile
8
9
from threading import Lock, Thread
from time import time
10
import ConfigParser
11
import json
12
13
14
15
import logging
import os
import re
import shutil
Gero Müller's avatar
Gero Müller committed
16
import stat
17
import subprocess
18
19
import fsmonitor
import vispa
20
21
22

try:
    import Image
Gero Müller's avatar
Gero Müller committed
23
    import ImageFilter
24
25
26
27
28
29
30
31
32
33
    HAVE_PIL = True
except:
    HAVE_PIL = False

if not HAVE_PIL:
    convert_executable = find_executable('convert')
    if convert_executable and os.path.isfile(convert_executable):
        HAVE_CONVERT = True
    else:
        HAVE_CONVERT = False
34

35
36
logger = logging.getLogger(__name__)

37

38
39
class FileSystem(object):

40
41
    FILE_EXTENSIONS = ["png", "jpg", "jpeg", "bmp", "ps", "eps", "pdf",
                       "txt", "xml", "py", "c", "cpp", "root", "pxlio"]
Marcel's avatar
Marcel committed
42
43
44
    BROWSER_EXTENSIONS = ["png", "jpg", "jpeg", "bmp"]
    ADDITIONAL_MIMES = {
        "pxlio": "text/plain",
45
        "root": "text/plain"
Marcel's avatar
Marcel committed
46
    }
47
48
    PRIVATE_WORKSPACE_CONF = "~/.vispa/workspace.ini"
    GLOBAL_WORKSPACE_CONF = "/etc/vispa/workspace.ini"
49

50
    def __init__(self, userid, workspaceid):
51
52
        # allowed extensions
        self.allowed_extensions = FileSystem.FILE_EXTENSIONS
53
        self.watchservice = WatchService()
54
55
        self._userid = userid
        self._workspaceid = workspaceid
56
57
58
    
    def __del__(self):
        self.close()
59

60
    def setup(self, basedir=None):
Marcel's avatar
Marcel committed
61
62
        if basedir is None:
            basedir = self.expand('~/')
murban's avatar
murban committed
63
        if not os.path.isdir(basedir):
Marcel's avatar
Marcel committed
64
            raise Exception("Basedir '%s' does not exist!" % basedir)
murban's avatar
murban committed
65
66
        # the basedir
        self.basedir = os.path.join(basedir, ".vispa")
67
68
        if os.path.isdir(self.basedir):
            return "Basedir already exists"
69
        else:
Gero Müller's avatar
Gero Müller committed
70
            os.makedirs(self.basedir, 0o700)
71
            return "Basedir now exists"
72

73
74
75
76
77
    def close(self):
        if self._monitor_thread:
            self._monitor_thread.remove_all_watches()
            self._monitor_thread.running = False
    
murban's avatar
murban committed
78
    def get_mime_type(self, filepath):
Marcel's avatar
Marcel committed
79
        filepath = self.expand(filepath)
80
81
82
83
        mime, encoding = guess_type(filepath)
        if mime is not None:
            return mime
        ext = filepath.split(".")[-1]
Gero Müller's avatar
Gero Müller committed
84
85
        if ext is not None and ext != "" and ext.lower(
        ) in FileSystem.ADDITIONAL_MIMES.keys():
86
87
88
            return FileSystem.ADDITIONAL_MIMES[ext]
        return None

murban's avatar
murban committed
89
    def check_file_extension(self, path, extensions=[]):
Marcel's avatar
Marcel committed
90
        path = self.expand(path)
91
92
93
94
95
96
97
98
        if (len(extensions) == 0):
            return True
        for elem in extensions:
            elem = elem if elem.startswith(".") else "." + elem
            if path.lower().endswith(elem.lower()):
                return True
        return False

Marcel's avatar
Marcel committed
99
100
    def exists(self, path, type=None):
        # type may be 'f' or 'd'
Marcel's avatar
Marcel committed
101
        path = self.expand(path)
102
103
        # path exists physically?
        if not os.path.exists(path):
Marcel's avatar
Marcel committed
104
105
            return None
        # type correct?
106
107
108
109
110
111
112
        target_type = 'd' if os.path.isdir(path) else 'f'
        if not type:
            return target_type
        type = type.lower()
        if type not in ['f', 'd']:
            return None
        return target_type if target_type == type else None
113

114
115
116
117
118
119
120
    def get_file_count(self, path, window_id=None, view_id=None, watch_id=None):
        # inline watch
        if window_id and view_id and watch_id:
            if self.watch(path, window_id, view_id, watch_id):
                pass
                # return -2 # don't fail atm since it would emit the wrong error message
        # actual function
121
122
123
124
125
126
        path = self.expand(path)
        if os.path.exists(path):
            return len(os.listdir(path))
        else:
            return -1

Gero Müller's avatar
Gero Müller committed
127
128
    def get_file_list(self, path, deep=False,
                      filter=None, reverse=False,
129
130
131
132
                      hide_hidden=True, encode_json=True,
                      window_id=None, view_id=None, watch_id=None):
        # inline watch
        if window_id and view_id and watch_id:
133
            if self.watch(path, window_id, view_id, watch_id, filter, reverse):
134
135
136
                pass
                # return "" # don't fail atm since it would not be caught on the client side
        # actual function
137
        filter = re.compile(filter) if filter else None
138
        filelist = []
Marcel's avatar
Marcel committed
139
        path_expand = self.expand(path)
140
141
142
143
144
145
146
        try:
            for elem in os.listdir(path_expand):
                # hide hidden files?
                if elem.startswith('.') and hide_hidden:
                    continue

                # excluded by filters?
147
                if filter and bool(filter.search(elem)) != reverse:
148
                    continue
149
                
150
                root, ext = os.path.splitext(elem)
Gero Müller's avatar
Gero Müller committed
151
                info = {
152
153
154
                    'name': elem,
                    'root': root,
                    'ext': ext
Gero Müller's avatar
Gero Müller committed
155
                }
156

Gero Müller's avatar
Gero Müller committed
157
                fullpath = os.path.join(path_expand, elem)
Gero Müller's avatar
Gero Müller committed
158
                stats = os.lstat(fullpath)
Gero Müller's avatar
Gero Müller committed
159
                is_symlink = stat.S_ISLNK(stats.st_mode)
Gero Müller's avatar
Gero Müller committed
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
                if is_symlink:
                    realfullpath = os.path.realpath(fullpath)
                    info.update({'symlink': True, 'realpath': realfullpath})
                    if os.path.exists(realfullpath):
                        stats = os.stat(realfullpath)

                info.update({
                    'size': stats.st_size,
                    'mtime': stats.st_mtime,
                    'type': 'd' if stat.S_ISDIR(stats.st_mode) else 'f'
                })

                filelist.append(info)
                if deep:
                    filelist.extend(self.get_file_list(fullpath, deep,
                                                       filter, reverse))
        except:
            pass
178
179

        # Determine the parent
Gero Müller's avatar
Gero Müller committed
180
        # parentpath = path_expand[:-1] if path_expand.endswith(os.sep) and
Gero Müller's avatar
Gero Müller committed
181
        #    path_expand != os.sep else path_expand
182
        parentpath = os.path.dirname(path_expand)
183
184
185
        data = {'filelist': filelist, 'parentpath': parentpath}
        if encode_json:
            return json.dumps(data)
186
187
        else:
            return data
188

Gero Müller's avatar
Gero Müller committed
189
190
    def get_suggestions(
            self, path, length=1, append_hidden=True, encode_json=True):
Marcel's avatar
Marcel committed
191
192
193
        suggestions = []
        source, filter = None, None
        # does the path exist?
Marcel's avatar
Marcel committed
194
195
        path_expanded = self.expand(path)
        if os.path.exists(path_expanded):
Marcel's avatar
Marcel committed
196
            # dir case
Marcel's avatar
Marcel committed
197
            if os.path.isdir(path_expanded):
Marcel's avatar
Marcel committed
198
199
200
201
202
203
204
205
206
207
208
                if path.endswith('/'):
                    source = path
                else:
                    suggestions.append(path + os.sep)
                    return suggestions
            # file case
            else:
                return suggestions
        else:
            # try to estimate source and filter
            head, tail = os.path.split(path)
Marcel's avatar
Marcel committed
209
            if os.path.isdir(os.path.expanduser(os.path.expandvars(head))):
Marcel's avatar
Marcel committed
210
211
212
213
214
215
216
                source = head
                filter = tail

        # return empty suggestions when source is not set
        if not source:
            return suggestions

Marcel's avatar
Marcel committed
217
        files = os.listdir(os.path.expanduser(os.path.expandvars(source)))
Marcel's avatar
Marcel committed
218
219
        # resort?
        if append_hidden:
Gero Müller's avatar
Gero Müller committed
220
221
            files = sorted(
                map(lambda f: str(f), files), cmp=file_compare, key=str.lower)
222
        while (len(suggestions) < length or length == 0) and len(files):
Marcel's avatar
Marcel committed
223
224
225
226
            file = files.pop(0)
            if filter and not file.startswith(filter):
                continue
            suggestion = os.path.join(source, file)
Gero Müller's avatar
Gero Müller committed
227
228
            if not suggestion.endswith(
                    '/') and os.path.isdir(os.path.expanduser(os.path.expandvars(suggestion))):
Marcel's avatar
Marcel committed
229
230
231
                suggestion += '/'
            suggestions.append(suggestion)

232
        return suggestions if not encode_json else json.dumps(suggestions)
233

murban's avatar
murban committed
234
    def cut_slashs(self, path):
Marcel's avatar
Marcel committed
235
        path = self.expand(path)
murban's avatar
murban committed
236
        path = path[1:] if path.startswith(os.sep) else path
237
238
        if path == "":
            return path
murban's avatar
murban committed
239
        path = path[:-1] if path.endswith(os.sep) else path
240
241
        return path

murban's avatar
murban committed
242
    def create_folder(self, path, name):
Marcel's avatar
Marcel committed
243
        path = self.expand(path)
Gero Müller's avatar
Gero Müller committed
244
245
        name = self.expand(name)

246
        # folder with the same name existent?
247
        name = self.handle_file_name_collision(name, path)
Marcel's avatar
Marcel committed
248
        fullpath = os.path.join(path, name)
249
250
251
        try:
            os.mkdir(fullpath)
        except Exception as e:
252
            # raise Exception("You don't have the permission to create this folder!")
253
254
            raise Exception(str(e))

murban's avatar
murban committed
255
    def create_file(self, path, name):
Marcel's avatar
Marcel committed
256
        path = self.expand(path)
Gero Müller's avatar
Gero Müller committed
257
258
        name = self.expand(name)

259
        # file with the same name existent?
260
        name = self.handle_file_name_collision(name, path)
Marcel's avatar
Marcel committed
261
        fullpath = os.path.join(path, name)
262
263
264
265
266
267
        try:
            f = file(fullpath, "w")
            f.close()
        except Exception as e:
            raise Exception(str(e))

268
    def rename(self, path, name, new_name, force=False):
Gero Müller's avatar
Gero Müller committed
269
270
271
272
        path = self.expand(path)
        name = self.expand(name)
        new_name = self.expand(new_name)

273
274
275
276
277
278
279
        try:
            if force == False:
                new_name = self.handle_file_name_collision(new_name, path)
            name = os.path.join(path, name)
            new_name = os.path.join(path, new_name)
            os.renames(name, new_name)
            return
Gero Müller's avatar
Gero Müller committed
280
        except Exception as e:
281
            return str(e)
282

murban's avatar
murban committed
283
284
    def remove(self, path):
        if isinstance(path, list):
285
286
287
            for p in path:
                self.remove(p)
        else:
Marcel's avatar
Marcel committed
288
289
290
291
292
            path = self.expand(path)
            if os.path.isdir(path):
                shutil.rmtree(path)
            else:
                os.remove(path)
293

294
295
296
297
298
299
    def move(self, source, destination):
        source = self.expand(source)
        destination = self.expand(destination)
        if os.path.isdir(destination):
            shutil.move(source, destination)

300
    def compress(self, path, paths, name):
Gero Müller's avatar
Gero Müller committed
301
302
        # paths has to be a list of strings
        paths = paths if isinstance(paths, (list, tuple)) else [paths]
Gero Müller's avatar
Gero Müller committed
303
        paths = [self.expand(p) for p in paths]
Gero Müller's avatar
Gero Müller committed
304
305
306

        path = path if not path.endswith(os.sep) else path[:-1]
        path = self.expand(path)
Martin Urban's avatar
Martin Urban committed
307

Gero Müller's avatar
Gero Müller committed
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
        name = name if name.endswith(".zip") else name + ".zip"
        name = self.handle_file_name_collision(name, path)

        fullpath = os.path.join(path, name)

        with ZipFile(fullpath, "w") as archive:
            i = 0
            while i < len(paths):
                if not paths[i]:
                    i += 1
                    continue
                p = self.expand(paths[i])
                i += 1

                if os.path.isdir(p):
                    for elem in os.listdir(p):
                        fullp = os.path.join(p, elem)
                        if os.path.isdir(fullp):
                            paths.append(fullp)
                        else:
Gero Müller's avatar
Gero Müller committed
328
329
                            ap = fullp[
                                len(path):] if fullp.startswith(path) else fullp
Gero Müller's avatar
Gero Müller committed
330
331
332
                            logger.debug(fullp)
                            archive.write(fullp, ap)
                else:
Martin Urban's avatar
Martin Urban committed
333
                    ap = p[len(path):] if p.startswith(path) else p
Gero Müller's avatar
Gero Müller committed
334
                    logger.debug(p)
Martin Urban's avatar
Martin Urban committed
335
                    archive.write(p, ap)
336

337
    def paste(self, path, fullsrc, cut):
Marcel's avatar
Marcel committed
338
        # TODO
339
340
341
342
        path = self.expand(path)
        if isinstance(fullsrc, (list, tuple)):
            for p in fullsrc:
                p = self.expand(p)
murban's avatar
murban committed
343
                self.paste(path, p, cut)
344
345
            return True

346
347
        # fulltarget = os.path.join(path, fullsrc.split(os.sep)[-1])
        logger.error("")
Gero Müller's avatar
Gero Müller committed
348
349
        target = self.handle_file_name_collision(
            fullsrc.split(os.sep)[-1], path)
350
        fulltarget = os.path.join(path, target)
351

352
353
        if os.path.isdir(fullsrc):
            shutil.copytree(fullsrc, fulltarget)
354
            if cut:
355
                shutil.rmtree(fullsrc)
356
        else:
357
            shutil.copy2(fullsrc, fulltarget)
358
            if cut:
359
                os.remove(fullsrc)
360

361
362
    def save_file(self, path, content, force=True, binary=False,
                   utf8=False, window_id=None, view_id=None, watch_id=None):
Marcel's avatar
Marcel committed
363
        path = self.expand(path)
Marcel's avatar
Marcel committed
364
        # check if file already exists
murban's avatar
murban committed
365
        if os.path.exists(path) and not force:
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
            return json.dumps({
                "mtime": 0,
                "success": False,
                "watch_error": ""
            })
        if utf8:
            content = content.encode('utf8')
        try:
            with open(path, "wb" if binary else "w") as f:
                f.write(content)
            mtime = os.path.getmtime(path)
        except Exception as e:
            mtime  = 0
        
        # inline watch
        if window_id and view_id and watch_id:
            watch_error = self.watch(path, window_id, view_id, watch_id)
383
        else:
384
385
386
387
            watch_error = ""
        
        return json.dumps({
            "mtime": os.path.getmtime(path),
388
389
390
            "success": mtime > 0 and self.checkPermissions(path),   #save is not successful, if file not writable    
            "watch_error": watch_error, 
            "path": path
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
        })
    
    def get_file(self, path, binary=False,
                  utf8=False, window_id=None, view_id=None, watch_id=None):
        # inline watch
        if window_id and view_id and watch_id:
            watch_error = self.watch(path, window_id, view_id, watch_id)
        else:
            watch_error = ""
        
        # actual function
        path = self.expand(path)
        try:
            with open(path, "rb" if binary else "r") as f:
                content = f.read()
            if utf8:
                content = content.decode('utf8')
408
409
410
            #new: check for writing rights
            writable = self.checkPermissions(path)

411
412
413
414
            mtime = os.path.getmtime(path)
        except Exception as e:
            mtime  = 0
            content = ""
415
            writable = None
416
417
418
419
420
        
        return json.dumps({
            "content": content,
            "mtime": mtime,
            "success": mtime > 0,
421
422
            "watch_error": watch_error,
            "writable": writable
423
        })
424

425
426
427
    def checkPermissions(self, path):
            return os.access(path, os.W_OK)

Gero Müller's avatar
Gero Müller committed
428
    def save_file_content(self, filename, content,
Gero Müller's avatar
Gero Müller committed
429
                          path=None, force=True, append=False):
430
431
        # check write permissions
        # if not self.checkPermission(username, vPath, 'w'):
Gero Müller's avatar
Gero Müller committed
432
433
434
435
436
437
        #    return False, "Permission denied!"

        if path:
            filename = os.path.join(path, filename)
        filename = self.expand(filename)

438
        # check if file already exists
Gero Müller's avatar
Gero Müller committed
439
440
441
442
443
444
445
446
447
        if os.path.exists(filename) and not force:
            return False, "The file '%s' already exists!" % filename

        out = open(filename, "ab+" if append else "wb")
        out.write(content)
        out.close()

        return True, "File saved!"

murban's avatar
murban committed
448
    def get_file_content(self, path):
Marcel's avatar
Marcel committed
449
        path = self.expand(path)
Gero Müller's avatar
Gero Müller committed
450
        f = open(path, "rb")
451
452
        content = f.read()
        f.close()
Marcel's avatar
Marcel committed
453
        return content
454

Marcel's avatar
Marcel committed
455
    def get_mtime(self, path):
Marcel's avatar
Marcel committed
456
        path = self.expand(path)
Marcel's avatar
Marcel committed
457
458
        return os.path.getmtime(path)

murban's avatar
murban committed
459
    def is_browser_file(self, path):
Marcel's avatar
Marcel committed
460
        path = self.expand(path)
461
        extension = path.split(".")[-1]
462
        return extension.lower() in FileSystem.BROWSER_EXTENSIONS
463

murban's avatar
murban committed
464
    def handle_file_name_collision(self, name, path):
465
        # collision?
murban's avatar
murban committed
466
        files = os.listdir(path)
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
        if name not in files:
            return name

        # when this line is reached, there is a collision!

        # cut the file extension
        extension = name.split(".")[-1]
        prename = None
        if name == extension:
            extension = ""
            prename = name
        else:
            prename = name.split("." + extension)[0]

        # has the name already a counter at its end?
        hasCounter = False
        preprename = None
        counter = prename.split("_")[-1]

        if counter != prename:
            try:
                counter = int(counter)
                hasCounter = True
                preprename = "_".join(prename.split("_")[:-1])
            except:
                pass

        if hasCounter:
            # increment and try again
            counter += 1
Gero Müller's avatar
Gero Müller committed
497
498
499
            newname = "%s_%d%s" % (preprename,
                                   counter,
                                   "" if extension == "" else "." + extension)
500
        else:
Gero Müller's avatar
Gero Müller committed
501
502
            newname = "%s_1%s" % (
                prename, "" if extension == "" else "." + extension)
503
504

        # return
murban's avatar
murban committed
505
        return self.handle_file_name_collision(newname, path)
Marcel's avatar
Marcel committed
506

Marcel's avatar
Marcel committed
507
508
    def expand(self, path):
        return os.path.expanduser(os.path.expandvars(path))
Marcel's avatar
Marcel committed
509

Gero Müller's avatar
Gero Müller committed
510
    def thumbnail(self, path, width=100, height=100, sharpen=True):
511
512
513
        path = self.expand(path)
        if HAVE_PIL:
            output = StringIO()
Gero Müller's avatar
Gero Müller committed
514
            img = Image.open(path)
Gero Müller's avatar
Gero Müller committed
515
            img.thumbnail((width, height), Image.ANTIALIAS)
Gero Müller's avatar
Gero Müller committed
516
            if sharpen:
517
                img.filter(ImageFilter.SHARPEN)
Gero Müller's avatar
Gero Müller committed
518
            img.save(output, "JPEG")
519
520
521
522
523
524
525
526
527
528
529
530
            contents = output.getvalue()
            output.close()
            return contents
        elif HAVE_CONVERT:
            cmd = ['convert', path,
                   '-thumbnail', '%dx%d' % (width, height),
                   'jpeg:-']
            p = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE)
            return p.communicate()[0]
        else:
            return self.get_file_content(path)

531
    def watch(self, path, window_id, view_id, watch_id, pattern=None, reverse=False):
532
533
534
535
536
        # fail if there is no such fie
        path = self.expand(path)
        if not os.path.exists(path):
            return "The file does not exist"
        
537
        self.watchservice.subscribe((window_id, view_id, watch_id), path, pattern, reverse)
538
539
540
        return ""
        
    def unwatch(self, window_id, view_id, watch_id=None):
541
        self.watchservice.unsubscribe((window_id, view_id, watch_id))
542
543
        return ""

544
    def get_workspaceini(self, request, fail_on_missing=False):
545
        try:
546
            request_dict = json.loads(request)
547
            config = ConfigParser.ConfigParser()
548
549
            config.read([FileSystem.GLOBAL_WORKSPACE_CONF,
                    self.expand(FileSystem.PRIVATE_WORKSPACE_CONF)])
550
551
552
553
554
            if self.exists(FileSystem.PRIVATE_WORKSPACE_CONF):
                mtime = self.get_mtime(FileSystem.PRIVATE_WORKSPACE_CONF)
                self._watch_workspaceini() 
            else:
                mtime = -1
555
            if not isinstance(request_dict, dict):
556
                request_dict = dict.fromkeys(config.sections(), True)
557
558
559
560
561
562
            data = {}
            for section, name_list in request_dict.iteritems():
                if config.has_section(section):
                    if isinstance(name_list, basestring):
                        name_list = [name_list]
                    if not isinstance(name_list, list):
563
                        data[section] = dict(config.items(section))
564
565
566
567
568
569
570
571
572
                    else:
                        data[section] = {}
                        for name in name_list:
                            if config.has_option(section, name):
                                data[section][name] = config.get(section, name)
                            elif fail_on_missing:
                                raise Exception('workspace.ini is missing the option "%s" in section [%s] ' % (name, section))
                elif fail_on_missing:
                    raise Exception('workspace.ini is missing the section [%s]' % section)
573
            return json.dumps({
574
                "content": data,
575
576
                "success": True,
                "mtime": mtime
577
578
579
                })
        except Exception as e:
            return json.dumps({
580
581
582
                "content": "",
                "success": False,
                "error": str(e)
583
584
                })

585
    def set_workspaceini(self, request):
586
        try:
587
588
589
            request_dict = json.loads(request)
            if not isinstance(request_dict, dict):
                raise Exception('Given values to be set in workspace.ini in wrong format')
590
            config = ConfigParser.ConfigParser()
591
            config.read(self.expand(FileSystem.PRIVATE_WORKSPACE_CONF))
592
593
594
595
596
597
598
            for section, options in request_dict.iteritems():
                if not isinstance(options, dict):
                    raise Exception('Given values to be set in workspace.ini in wrong format')
                if not config.has_section(section):
                    config.add_section(section)
                for name, value in options.iteritems():
                    config.set(section, name, value)
599
600
            with open(self.expand(FileSystem.PRIVATE_WORKSPACE_CONF), 'w') as f:
                self._watch_workspaceini()
601
602
                config.write(f)
            return ""
603
        except Exception as e:
604
            return str(e)
asseldonk's avatar
asseldonk committed
605

606
    def _watch_workspaceini(self):
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
        if self.exists(FileSystem.PRIVATE_WORKSPACE_CONF, 'f'):
            self.watchservice.subscribe((self._userid, self._workspaceid), FileSystem.PRIVATE_WORKSPACE_CONF)

class WatchService(object):
    def __init__(self):
        self.subscriber_buffer = []
        self.subscribers = {}
        self.watches = {}
        self.lock = Lock()
        self.monitor = fsmonitor.FSMonitor()
        self.run = True
        self.thread = Thread(target=self._worker)
        self.thread.start()
    
    def subscribe(self, id, path, pattern=None, reverse=False):
        if not path:
            return self.unsubscribe(id)
        
        path = os.path.expanduser(os.path.expandvars(path))
        
        with self.lock:
            if id not in self.subscribers:
                WatchSubscriber(self, id)
            
            self.subscribers[id].update(path, pattern, reverse)
    
    def unsubscribe(self, id):
        with self.lock:
            if hasattr(id, '__contains__') and None in id:
                for subscriber in self.subscribers.values():
                    if False not in map(lambda e,c: c is None or e == c, subscriber.id, id):
                        subscriber.destroy()
            elif id in self.subscribers:
                self.subscribers[id].destroy()
    
    def stop(self):
        self.run = False
    
    def _worker(self):
        while self.run:
            events = self.monitor.read_events(0.05)
            if events:
                with self.lock:
                    for event in events:
                        if event.action_name in ['delete self','move self']:
                            kind = 'vanish'
                        elif event.action_name == 'modify':
                            kind = 'modify'
                        elif event.watch.isdir and event.action_name in ['create','delete','move from','move to']:
                            kind = 'change'
                        else:
                            kind = None
                        if kind:
                            if not event.watch.isdir:
                                if os.path.exists(event.watch.path):
                                    event.watch.mtime = os.path.getmtime(event.watch.path)
                                else:
                                    event.watch.mtime = -1
                            for subscriber in event.watch.subscribers[:]:
                                subscriber.process(kind, event.name)
            
            if self.subscriber_buffer:
                with self.lock:
                    for subscriber in self.subscriber_buffer[:]:
                        subscriber.flush(False)
        
        for subscriber in self.subscribers:
            subscriber.destroy()
        
        self.monitor.remove_all_watches()
        self.monitor.close()
    
class WatchSubscriber(object): # this should never be instanced manually
    EVENT_DELAYS = {
        'change': [1.0,0.1],
        'modify': [1.0,0.2]
    }
    
    def __init__(self, service, id):
        if not isinstance(service, WatchService):
            raise TypeError("No valid WatchService instance was provided")
        if id in service.subscribers:
            raise RuntimeError("There is already a subscriber with this id: "+str(id))
        self.id = id
        self.service = service
        self.service.subscribers[self.id] = self
        self.watch = None
        self.pattern = None
        self.reverse = None
        self.event_buffer = {}
    
    def destroy(self):
        self.unbind()
        if self in self.service.subscriber_buffer:
            self.service.subscriber_buffer.remove(self)
        del self.service.subscribers[self.id]
        del self.service
        del self.watch
        del self.event_buffer
    
    def process(self, event, subject=""):
        if self.watch.isdir and subject and self.pattern and self.pattern.search(subject) != self.reverse:
709
            return
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
        
        if event in WatchSubscriber.EVENT_DELAYS:
            now = time()
            if event in self.event_buffer:
                self.event_buffer[event][1] = now + WatchSubscriber.EVENT_DELAYS[event][1]
            else:
                self.event_buffer[event] = [now + delay for delay in WatchSubscriber.EVENT_DELAYS[event]] #first & last event
                if self not in self.service.subscriber_buffer:
                    self.service.subscriber_buffer.append(self)
        else:
            self.emit(event)
    
    def flush(self, force=False):
        now = time()
        for event, delays in self.event_buffer.items():
            if force or min(delays) < now:
                self.emit(event)
                del self.event_buffer[event]
        if not self.event_buffer and self in self.service.subscriber_buffer:
            self.service.subscriber_buffer.remove(self)

    def emit(self, event):
        if len(self.id) == 3: # window_id, view_id, watch_id
            data = {
                'event': event,
                'path': self.watch.path,
                'watch_id': self.id[2]
            }
            if not self.watch.isdir:
                data['mtime'] = self.watch.mtime
            vispa.remote.send_topic("extension.%s.socket.watch" % self.id[1], window_id=self.id[0], data=data)
        elif len(self.id) == 2: # userid, workspaceid
            vispa.remote.send_topic('workspace.ini_modified', user_id=self.id[0], data={
                "workspaceId": self.id[1],
                "mtime": self.watch.mtime
            })
        elif hasattr(self.id, '__call__'):
            self.id(event, self)
    
    def update(self, path, pattern="", reverse=False):
        self.bind(path)
        if self.watch.isdir and pattern:
            if not self.pattern or self.pattern.pattern != pattern:
                self.pattern = re.compile(pattern)
            self.reverse = reverse
        else:
            self.pattern = None
            self.reverse = None
    
    def bind(self, path):
        if self.watch:
            if self.watch.path == path:
                return
            else:
                self.unbind()
        
        if path not in self.service.watches:
            if not os.path.exists(path):
                raise IOError("File to be watched does not exist: %s" % path)
            if os.path.isfile(path):
                isdir = False
                watch = self.service.monitor.add_file_watch(path)
            elif os.path.isdir(path):
                isdir = True
                watch = self.service.monitor.add_dir_watch(path)
            else:
                raise IOError("This kind of file can't be watched!")
            watch.isdir = isdir
            watch.subscribers = []
            self.service.watches[path] = watch
        else:
            watch = self.service.watches[path]
        
        self.watch = watch
        if self not in watch.subscribers:
            watch.subscribers.append(self)
    
    def unbind(self):
        if not self.watch:
789
            return
790
791
792
793
794
795
796
        
        self.watch.subscribers.remove(self)
        if len(self.watch.subscribers) == 0:
            del self.service.watches[self.watch.path]
            self.service.monitor.remove_watch(self.watch)
        
        self.watch = None
797

Marcel's avatar
Marcel committed
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
def string_compare(a, b):
    if a == b:
        return 0
    elif a > b:
        return 1
    else:
        return -1

def file_compare(a, b):
    if not a.startswith('.') and not b.startswith('.'):
        return string_compare(a, b)
    elif a.startswith('.') and b.startswith('.'):
        return string_compare(a, b)
    elif a.startswith('.') and not b.startswith('.'):
        return 1
    elif not a.startswith('.') and b.startswith('.'):
        return -1