mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-08-21 13:53:15 -07:00
add jaraco to test sym links on Windows. Fixes #894
This commit is contained in:
parent
fd5ee775e0
commit
52cc753881
50 changed files with 3862 additions and 7 deletions
255
libs/jaraco/windows/filesystem/change.py
Normal file
255
libs/jaraco/windows/filesystem/change.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
|
||||
"""
|
||||
FileChange
|
||||
Classes and routines for monitoring the file system for changes.
|
||||
|
||||
Copyright © 2004, 2011, 2013 Jason R. Coombs
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import re
|
||||
from threading import Thread
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from more_itertools.recipes import consume
|
||||
import jaraco.text
|
||||
|
||||
import jaraco.windows.api.filesystem as fs
|
||||
from jaraco.windows.api import event
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class NotifierException(Exception):
|
||||
pass
|
||||
|
||||
class FileFilter(object):
|
||||
def set_root(self, root):
|
||||
self.root = root
|
||||
|
||||
def _get_file_path(self, filename):
|
||||
try:
|
||||
filename = os.path.join(self.root, filename)
|
||||
except AttributeError: pass
|
||||
return filename
|
||||
|
||||
class ModifiedTimeFilter(FileFilter):
|
||||
"""
|
||||
Returns true for each call where the modified time of the file is after
|
||||
the cutoff time.
|
||||
"""
|
||||
def __init__(self, cutoff):
|
||||
self.cutoff = cutoff
|
||||
|
||||
def __call__(self, file):
|
||||
filepath = self._get_file_path(file)
|
||||
last_mod = datetime.datetime.utcfromtimestamp(
|
||||
os.stat(filepath).st_mtime)
|
||||
log.debug('{filepath} last modified at {last_mod}.'.format(**vars()))
|
||||
return last_mod > self.cutoff
|
||||
|
||||
class PatternFilter(FileFilter):
|
||||
"""
|
||||
Filter that returns True for files that match pattern (a regular
|
||||
expression).
|
||||
"""
|
||||
def __init__(self, pattern):
|
||||
self.pattern = (
|
||||
re.compile(pattern) if isinstance(pattern, basestring)
|
||||
else pattern
|
||||
)
|
||||
|
||||
def __call__(self, file):
|
||||
return bool(self.pattern.match(file, re.I))
|
||||
|
||||
class GlobFilter(PatternFilter):
|
||||
"""
|
||||
Filter that returns True for files that match the pattern (a glob
|
||||
expression.
|
||||
"""
|
||||
def __init__(self, expression):
|
||||
super(GlobFilter, self).__init__(
|
||||
self.convert_file_pattern(expression))
|
||||
|
||||
@staticmethod
|
||||
def convert_file_pattern(p):
|
||||
r"""
|
||||
converts a filename specification (such as c:\*.*) to an equivelent
|
||||
regular expression
|
||||
>>> GlobFilter.convert_file_pattern('/*')
|
||||
'/.*'
|
||||
"""
|
||||
subs = (('\\', '\\\\'), ('.', '\\.'), ('*', '.*'), ('?', '.'))
|
||||
return jaraco.text.multi_substitution(*subs)(p)
|
||||
|
||||
|
||||
class AggregateFilter(FileFilter):
|
||||
"""
|
||||
This file filter will aggregate the filters passed to it, and when called,
|
||||
will return the results of each filter ANDed together.
|
||||
"""
|
||||
def __init__(self, *filters):
|
||||
self.filters = filters
|
||||
|
||||
def set_root(self, root):
|
||||
consume(f.set_root(root) for f in self.filters)
|
||||
|
||||
def __call__(self, file):
|
||||
return all(fil(file) for fil in self.filters)
|
||||
|
||||
class OncePerModFilter(FileFilter):
|
||||
def __init__(self):
|
||||
self.history = list()
|
||||
|
||||
def __call__(self, file):
|
||||
file = os.path.join(self.root, file)
|
||||
key = file, os.stat(file).st_mtime
|
||||
result = key not in self.history
|
||||
self.history.append(key)
|
||||
if len(self.history) > 100:
|
||||
del self.history[-50:]
|
||||
return result
|
||||
|
||||
def files_with_path(files, path):
|
||||
return (os.path.join(path, file) for file in files)
|
||||
|
||||
def get_file_paths(walk_result):
|
||||
root, dirs, files = walk_result
|
||||
return files_with_path(files, root)
|
||||
|
||||
class Notifier(object):
|
||||
def __init__(self, root = '.', filters = []):
|
||||
# assign the root, verify it exists
|
||||
self.root = root
|
||||
if not os.path.isdir(self.root):
|
||||
raise NotifierException(
|
||||
'Root directory "%s" does not exist' % self.root)
|
||||
self.filters = filters
|
||||
|
||||
self.watch_subtree = False
|
||||
self.quit_event = event.CreateEvent(None, 0, 0, None)
|
||||
self.opm_filter = OncePerModFilter()
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
fs.FindCloseChangeNotification(self.hChange)
|
||||
except: pass
|
||||
|
||||
def _get_change_handle(self):
|
||||
# set up to monitor the directory tree specified
|
||||
self.hChange = fs.FindFirstChangeNotification(
|
||||
self.root,
|
||||
self.watch_subtree,
|
||||
fs.FILE_NOTIFY_CHANGE_LAST_WRITE,
|
||||
)
|
||||
|
||||
# make sure it worked; if not, bail
|
||||
INVALID_HANDLE_VALUE = fs.INVALID_HANDLE_VALUE
|
||||
if self.hChange == INVALID_HANDLE_VALUE:
|
||||
raise NotifierException('Could not set up directory change '
|
||||
'notification')
|
||||
|
||||
@staticmethod
|
||||
def _filtered_walk(path, file_filter):
|
||||
"""
|
||||
static method that calls os.walk, but filters out
|
||||
anything that doesn't match the filter
|
||||
"""
|
||||
for root, dirs, files in os.walk(path):
|
||||
log.debug('looking in %s', root)
|
||||
log.debug('files is %s', files)
|
||||
file_filter.set_root(root)
|
||||
files = filter(file_filter, files)
|
||||
log.debug('filtered files is %s', files)
|
||||
yield (root, dirs, files)
|
||||
|
||||
def quit(self):
|
||||
event.SetEvent(self.quit_event)
|
||||
|
||||
class BlockingNotifier(Notifier):
|
||||
|
||||
@staticmethod
|
||||
def wait_results(*args):
|
||||
""" calls WaitForMultipleObjects repeatedly with args """
|
||||
return itertools.starmap(
|
||||
event.WaitForMultipleObjects,
|
||||
itertools.repeat(args))
|
||||
|
||||
def get_changed_files(self):
|
||||
self._get_change_handle()
|
||||
check_time = datetime.datetime.utcnow()
|
||||
# block (sleep) until something changes in the
|
||||
# target directory or a quit is requested.
|
||||
# timeout so we can catch keyboard interrupts or other exceptions
|
||||
events = (self.hChange, self.quit_event)
|
||||
for result in self.wait_results(events, False, 1000):
|
||||
if result == event.WAIT_TIMEOUT:
|
||||
continue
|
||||
index = result - event.WAIT_OBJECT_0
|
||||
if events[index] is self.quit_event:
|
||||
# quit was received; stop yielding results
|
||||
return
|
||||
|
||||
# something has changed.
|
||||
log.debug('Change notification received')
|
||||
fs.FindNextChangeNotification(self.hChange)
|
||||
next_check_time = datetime.datetime.utcnow()
|
||||
log.debug('Looking for all files changed after %s', check_time)
|
||||
for file in self.find_files_after(check_time):
|
||||
yield file
|
||||
check_time = next_check_time
|
||||
|
||||
def find_files_after(self, cutoff):
|
||||
mtf = ModifiedTimeFilter(cutoff)
|
||||
af = AggregateFilter(mtf, self.opm_filter, *self.filters)
|
||||
results = Notifier._filtered_walk(self.root, af)
|
||||
results = itertools.imap(get_file_paths, results)
|
||||
if self.watch_subtree:
|
||||
result = itertools.chain(*results)
|
||||
else:
|
||||
result = next(results)
|
||||
return result
|
||||
|
||||
class ThreadedNotifier(BlockingNotifier, Thread):
|
||||
r"""
|
||||
ThreadedNotifier provides a simple interface that calls the handler
|
||||
for each file rooted in root that passes the filters. It runs as its own
|
||||
thread, so must be started as such.
|
||||
|
||||
>>> notifier = ThreadedNotifier('c:\\', handler = StreamHandler()) # doctest: +SKIP
|
||||
>>> notifier.start() # doctest: +SKIP
|
||||
C:\Autoexec.bat changed.
|
||||
"""
|
||||
def __init__(self, root = '.', filters = [], handler = lambda file: None):
|
||||
# init notifier stuff
|
||||
BlockingNotifier.__init__(self, root, filters)
|
||||
# init thread stuff
|
||||
Thread.__init__(self)
|
||||
# set it as a daemon thread so that it doesn't block waiting to close.
|
||||
# I tried setting __del__(self) to .quit(), but unfortunately, there
|
||||
# are references to this object in the win32api stuff, so __del__
|
||||
# never gets called.
|
||||
self.setDaemon(True)
|
||||
|
||||
self.handle = handler
|
||||
|
||||
def run(self):
|
||||
for file in self.get_changed_files():
|
||||
self.handle(file)
|
||||
|
||||
class StreamHandler(object):
|
||||
"""
|
||||
StreamHandler: a sample handler object for use with the threaded
|
||||
notifier that will announce by writing to the supplied stream
|
||||
(stdout by default) the name of the file.
|
||||
"""
|
||||
def __init__(self, output = sys.stdout):
|
||||
self.output = output
|
||||
|
||||
def __call__(self, filename):
|
||||
self.output.write('%s changed.\n' % filename)
|
Loading…
Add table
Add a link
Reference in a new issue