filesystem.py 27.9 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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
            watch_error = ""
        
        return json.dumps({
            "mtime": os.path.getmtime(path),
            "success": mtime > 0,
            "watch_error": watch_error
        })
    
    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')
            mtime = os.path.getmtime(path)
        except Exception as e:
            mtime  = 0
            content = ""
        
        return json.dumps({
            "content": content,
            "mtime": mtime,
            "success": mtime > 0,
            "watch_error": watch_error
        })
418

Gero Müller's avatar
Gero Müller committed
419
    def save_file_content(self, filename, content,
Gero Müller's avatar
Gero Müller committed
420
                          path=None, force=True, append=False):
421
422
        # check write permissions
        # if not self.checkPermission(username, vPath, 'w'):
Gero Müller's avatar
Gero Müller committed
423
424
425
426
427
428
        #    return False, "Permission denied!"

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

429
        # check if file already exists
Gero Müller's avatar
Gero Müller committed
430
431
432
433
434
435
436
437
438
        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
439
    def get_file_content(self, path):
Marcel's avatar
Marcel committed
440
        path = self.expand(path)
Gero Müller's avatar
Gero Müller committed
441
        f = open(path, "rb")
442
443
        content = f.read()
        f.close()
Marcel's avatar
Marcel committed
444
        return content
445

Marcel's avatar
Marcel committed
446
    def get_mtime(self, path):
Marcel's avatar
Marcel committed
447
        path = self.expand(path)
Marcel's avatar
Marcel committed
448
449
        return os.path.getmtime(path)

murban's avatar
murban committed
450
    def is_browser_file(self, path):
Marcel's avatar
Marcel committed
451
        path = self.expand(path)
452
        extension = path.split(".")[-1]
453
        return extension.lower() in FileSystem.BROWSER_EXTENSIONS
454

murban's avatar
murban committed
455
    def handle_file_name_collision(self, name, path):
456
        # collision?
murban's avatar
murban committed
457
        files = os.listdir(path)
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
        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
488
489
490
            newname = "%s_%d%s" % (preprename,
                                   counter,
                                   "" if extension == "" else "." + extension)
491
        else:
Gero Müller's avatar
Gero Müller committed
492
493
            newname = "%s_1%s" % (
                prename, "" if extension == "" else "." + extension)
494
495

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

Marcel's avatar
Marcel committed
498
499
    def expand(self, path):
        return os.path.expanduser(os.path.expandvars(path))
Marcel's avatar
Marcel committed
500

Gero Müller's avatar
Gero Müller committed
501
    def thumbnail(self, path, width=100, height=100, sharpen=True):
502
503
504
        path = self.expand(path)
        if HAVE_PIL:
            output = StringIO()
Gero Müller's avatar
Gero Müller committed
505
            img = Image.open(path)
Gero Müller's avatar
Gero Müller committed
506
            img.thumbnail((width, height), Image.ANTIALIAS)
Gero Müller's avatar
Gero Müller committed
507
            if sharpen:
508
                img.filter(ImageFilter.SHARPEN)
Gero Müller's avatar
Gero Müller committed
509
            img.save(output, "JPEG")
510
511
512
513
514
515
516
517
518
519
520
521
            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)

522
    def watch(self, path, window_id, view_id, watch_id, pattern=None, reverse=False):
523
524
525
526
527
        # fail if there is no such fie
        path = self.expand(path)
        if not os.path.exists(path):
            return "The file does not exist"
        
528
        self.watchservice.subscribe((window_id, view_id, watch_id), path, pattern, reverse)
529
530
531
        return ""
        
    def unwatch(self, window_id, view_id, watch_id=None):
532
        self.watchservice.unsubscribe((window_id, view_id, watch_id))
533
534
        return ""

535
    def get_workspaceini(self, request, fail_on_missing=False):
536
        try:
537
            request_dict = json.loads(request)
538
            config = ConfigParser.ConfigParser()
539
540
            config.read([FileSystem.GLOBAL_WORKSPACE_CONF,
                    self.expand(FileSystem.PRIVATE_WORKSPACE_CONF)])
541
542
543
544
545
            if self.exists(FileSystem.PRIVATE_WORKSPACE_CONF):
                mtime = self.get_mtime(FileSystem.PRIVATE_WORKSPACE_CONF)
                self._watch_workspaceini() 
            else:
                mtime = -1
546
            if not isinstance(request_dict, dict):
547
                request_dict = dict.fromkeys(config.sections(), True)
548
549
550
551
552
553
            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):
554
                        data[section] = dict(config.items(section))
555
556
557
558
559
560
561
562
563
                    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)
564
            return json.dumps({
565
                "content": data,
566
567
                "success": True,
                "mtime": mtime
568
569
570
                })
        except Exception as e:
            return json.dumps({
571
572
573
                "content": "",
                "success": False,
                "error": str(e)
574
575
                })

576
    def set_workspaceini(self, request):
577
        try:
578
579
580
            request_dict = json.loads(request)
            if not isinstance(request_dict, dict):
                raise Exception('Given values to be set in workspace.ini in wrong format')
581
            config = ConfigParser.ConfigParser()
582
            config.read(self.expand(FileSystem.PRIVATE_WORKSPACE_CONF))
583
584
585
586
587
588
589
            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)
590
591
            with open(self.expand(FileSystem.PRIVATE_WORKSPACE_CONF), 'w') as f:
                self._watch_workspaceini()
592
593
                config.write(f)
            return ""
594
        except Exception as e:
595
            return str(e)
asseldonk's avatar
asseldonk committed
596

597
    def _watch_workspaceini(self):
598
599
600
601
602
603
604
605
606
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
        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:
700
            return
701
702
703
704
705
706
707
708
709
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
        
        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:
780
            return
781
782
783
784
785
786
787
        
        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
788

Marcel's avatar
Marcel committed
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
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