file_manager: implement inotify using a bidirectional tree
Rather than tracking watch paths directly, wrap each watch in a node. This makes makes it a bit easier to handle move and delete changes, as we don't have to manage several different dicts to store the information we need. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
e4d61de406
commit
9da74f7bc6
|
@ -553,6 +553,220 @@ class FileManager:
|
|||
INOTIFY_BUNDLE_TIME = .25
|
||||
INOTIFY_MOVE_TIME = 1.
|
||||
|
||||
class InotifyNode:
|
||||
def __init__(self, ihdlr, parent, name):
|
||||
self.ihdlr = ihdlr
|
||||
self.name = name
|
||||
self.parent_node = parent
|
||||
self.child_nodes = {}
|
||||
self.watch_desc = self.ihdlr.add_watch(self)
|
||||
self.pending_node_events = {}
|
||||
self.pending_deleted_children = set()
|
||||
self.pending_file_events = {}
|
||||
|
||||
def _finish_create_node(self):
|
||||
# Finish a node's creation. All children that were created
|
||||
# with this node (ie: a directory is copied) are bundled into
|
||||
# this notification. We also scan the node to extract metadata
|
||||
# here, as we know all files have been copied.
|
||||
if "create_node" not in self.pending_node_events:
|
||||
return
|
||||
del self.pending_node_events['create_node']
|
||||
node_path = self.get_path()
|
||||
root = self.get_root()
|
||||
# Scan child nodes for unwatched directories and metadata
|
||||
self.scan_node(notify_created=False)
|
||||
self.ihdlr.log_nodes()
|
||||
self.ihdlr.notify_filelist_changed(
|
||||
"create_dir", root, node_path)
|
||||
|
||||
def _finish_delete_child(self):
|
||||
# Items deleted in a child (node or file) are batched.
|
||||
# Individual files get notifications if their parent
|
||||
# node stil exists. Otherwise notififications are
|
||||
# bundled into the topmost deleted parent.
|
||||
if "delete_child" not in self.pending_node_events:
|
||||
self.pending_deleted_children.clear()
|
||||
return
|
||||
del self.pending_node_events['delete_child']
|
||||
node_path = self.get_path()
|
||||
root = self.get_root()
|
||||
for (name, is_node) in self.pending_deleted_children:
|
||||
item_path = os.path.join(node_path, name)
|
||||
item_type = "dir" if is_node else "file"
|
||||
self.ihdlr.clear_metadata(root, item_path, is_node)
|
||||
self.ihdlr.notify_filelist_changed(
|
||||
f"delete_{item_type}", root, item_path)
|
||||
self.pending_deleted_children.clear()
|
||||
|
||||
def scan_node(self, notify_created=True, visited_dirs=set()):
|
||||
dir_path = self.get_path()
|
||||
st = os.stat(dir_path)
|
||||
if st in visited_dirs:
|
||||
return
|
||||
visited_dirs.add((st.st_dev, st.st_ino))
|
||||
for fname in os.listdir(dir_path):
|
||||
if fname[0] == ".":
|
||||
continue
|
||||
item_path = os.path.join(dir_path, fname)
|
||||
ext = os.path.splitext(fname)[-1].lower()
|
||||
if os.path.isdir(item_path):
|
||||
new_child = self.create_child_node(fname, notify_created)
|
||||
new_child.scan_node(notify_created, visited_dirs)
|
||||
elif os.path.isfile(item_path) and \
|
||||
self.get_root() == "gcodes" and \
|
||||
ext in VALID_GCODE_EXTS:
|
||||
self.ihdlr.parse_gcode_metadata(item_path)
|
||||
|
||||
def move_child_node(self, child_name, new_name, new_parent):
|
||||
child_node = self.pop_child_node(child_name)
|
||||
if child_node is None:
|
||||
logging.info(f"No child for node at path: {self.get_path()}")
|
||||
return
|
||||
prev_path = child_node.get_path()
|
||||
prev_root = child_node.get_root()
|
||||
child_node.name = new_name
|
||||
new_parent.add_child_node(child_node)
|
||||
new_path = child_node.get_path()
|
||||
new_root = child_node.get_root()
|
||||
logging.debug(f"Moving node from '{prev_path}' to '{new_path}'")
|
||||
# TODO: It is possible to "move" metadata rather
|
||||
# than rescan.
|
||||
child_node.scan_node(notify_created=False)
|
||||
self.ihdlr.notify_filelist_changed(
|
||||
"move_dir", new_root, new_path,
|
||||
prev_root, prev_path)
|
||||
|
||||
def schedule_file_event(self, file_name, evt_name):
|
||||
if file_name in self.pending_file_events:
|
||||
return
|
||||
pending_node = self.search_pending_event("create_node")
|
||||
if pending_node is not None:
|
||||
pending_node.stop_event("create_node")
|
||||
self.pending_file_events[file_name] = evt_name
|
||||
|
||||
def complete_file_write(self, file_name):
|
||||
evt_name = self.pending_file_events.pop(file_name, None)
|
||||
if evt_name is None:
|
||||
logging.info(f"Invalid file write event: {file_name}")
|
||||
return
|
||||
pending_node = self.search_pending_event("create_node")
|
||||
if pending_node is not None:
|
||||
# if this event was generated as a result of a created parent
|
||||
# node it should be ignored in favor of the parent event.
|
||||
pending_node.reset_event("create_node", INOTIFY_BUNDLE_TIME)
|
||||
return
|
||||
file_path = os.path.join(self.get_path(), file_name)
|
||||
root = self.get_root()
|
||||
if root == "gcodes":
|
||||
self.ihdlr.parse_gcode_metadata(file_path)
|
||||
if os.path.splitext(file_path)[1].lower() == ".ufp":
|
||||
# don't notify .ufp files
|
||||
return
|
||||
self.ihdlr.notify_filelist_changed(evt_name, root, file_path)
|
||||
|
||||
def add_child_node(self, node):
|
||||
self.child_nodes[node.name] = node
|
||||
node.parent_node = self
|
||||
|
||||
def get_child_node(self, name):
|
||||
return self.child_nodes.get(name, None)
|
||||
|
||||
def pop_child_node(self, name):
|
||||
return self.child_nodes.pop(name, None)
|
||||
|
||||
def create_child_node(self, name, notify=True):
|
||||
if name in self.child_nodes:
|
||||
return self.child_nodes[name]
|
||||
new_child = InotifyNode(self.ihdlr, self, name)
|
||||
self.child_nodes[name] = new_child
|
||||
if notify:
|
||||
pending_node = self.search_pending_event("create_node")
|
||||
if pending_node is None:
|
||||
# schedule a pending create event for the child
|
||||
new_child.add_event("create_node", INOTIFY_BUNDLE_TIME)
|
||||
else:
|
||||
pending_node.reset_event("create_node", INOTIFY_BUNDLE_TIME)
|
||||
return new_child
|
||||
|
||||
def schedule_child_delete(self, child_name, is_node):
|
||||
if is_node:
|
||||
child_node = self.child_nodes.pop(child_name, None)
|
||||
if child_node is None:
|
||||
return
|
||||
self.ihdlr.remove_watch(
|
||||
child_node.watch_desc, need_low_level_rm=False)
|
||||
child_node.remove_event("delete_child")
|
||||
self.pending_deleted_children.add((child_name, is_node))
|
||||
self.add_event("delete_child", INOTIFY_BUNDLE_TIME)
|
||||
|
||||
def clear_watches(self):
|
||||
for cnode in self.child_nodes.values():
|
||||
# Delete all of the children's children
|
||||
cnode.clear_watches()
|
||||
self.ihdlr.remove_watch(self.watch_desc)
|
||||
|
||||
def get_path(self):
|
||||
return os.path.join(self.parent_node.get_path(), self.name)
|
||||
|
||||
def get_root(self):
|
||||
return self.parent_node.get_root()
|
||||
|
||||
def add_event(self, evt_name, timeout):
|
||||
if evt_name in self.pending_node_events:
|
||||
self.reset_event(evt_name, timeout)
|
||||
return
|
||||
callback = getattr(self, f"_finish_{evt_name}")
|
||||
hdl = IOLoop.current().call_later(timeout, callback)
|
||||
self.pending_node_events[evt_name] = hdl
|
||||
|
||||
def reset_event(self, evt_name, timeout):
|
||||
if evt_name in self.pending_node_events:
|
||||
ioloop = IOLoop.current()
|
||||
hdl = self.pending_node_events[evt_name]
|
||||
ioloop.remove_timeout(hdl)
|
||||
callback = getattr(self, f"_finish_{evt_name}")
|
||||
hdl = ioloop.call_later(timeout, callback)
|
||||
self.pending_node_events[evt_name] = hdl
|
||||
|
||||
def stop_event(self, evt_name):
|
||||
if evt_name in self.pending_node_events:
|
||||
hdl = self.pending_node_events[evt_name]
|
||||
IOLoop.current().remove_timeout(hdl)
|
||||
|
||||
def remove_event(self, evt_name):
|
||||
hdl = self.pending_node_events.pop(evt_name, None)
|
||||
if hdl is not None:
|
||||
IOLoop.current().remove_timeout(hdl)
|
||||
|
||||
def clear_events(self, include_children=True):
|
||||
if include_children:
|
||||
for child in self.child_nodes.values():
|
||||
child.clear_events(include_children)
|
||||
for hdl in self.pending_node_events.values():
|
||||
IOLoop.current().remove_timeout(hdl)
|
||||
self.pending_node_events.clear()
|
||||
self.pending_deleted_children.clear()
|
||||
self.pending_file_events.clear()
|
||||
|
||||
def search_pending_event(self, name):
|
||||
if name in self.pending_node_events:
|
||||
return self
|
||||
if self.parent_node is None:
|
||||
return None
|
||||
return self.parent_node.search_pending_event(name)
|
||||
|
||||
class InotifyRootNode(InotifyNode):
|
||||
def __init__(self, ihdlr, root_name, root_path):
|
||||
self.root_name = root_name
|
||||
super().__init__(ihdlr, None, root_path)
|
||||
|
||||
def get_path(self):
|
||||
return self.name
|
||||
|
||||
def get_root(self):
|
||||
return self.root_name
|
||||
|
||||
class INotifyHandler:
|
||||
def __init__(self, config, file_manager, gcode_metadata):
|
||||
self.server = config.get_server()
|
||||
|
@ -560,113 +774,47 @@ class INotifyHandler:
|
|||
'enable_debug_logging', False)
|
||||
self.file_manager = file_manager
|
||||
self.gcode_metadata = gcode_metadata
|
||||
self.ioloop = IOLoop.current()
|
||||
self.inotify = INotify(nonblocking=True)
|
||||
self.ioloop.add_handler(
|
||||
IOLoop.current().add_handler(
|
||||
self.inotify.fileno(), self._handle_inotify_read,
|
||||
IOLoop.READ | IOLoop.ERROR)
|
||||
|
||||
self.watches = {}
|
||||
self.watched_dirs = {}
|
||||
self.pending_move_events = {}
|
||||
self.pending_create_file_events = {}
|
||||
self.pending_create_dir_events = {}
|
||||
self.pending_modify_file_events = {}
|
||||
self.pending_delete_events = {}
|
||||
self.watched_roots = {}
|
||||
self.watched_nodes = {}
|
||||
self.pending_moves = {}
|
||||
|
||||
def add_root_watch(self, root, root_path):
|
||||
# remove all exisiting watches on root
|
||||
for (wroot, wdir) in list(self.watched_dirs.values()):
|
||||
if root == wroot:
|
||||
self.remove_watch(wdir)
|
||||
# remove pending move notifications on root
|
||||
for cookie, pending in list(self.pending_move_events.items()):
|
||||
if root == pending[0]:
|
||||
self.ioloop.remove_timeout(pending[2])
|
||||
del self.pending_move_events[cookie]
|
||||
# remove pending create notifications on root
|
||||
for fpath, pending in list(self.pending_create_file_events.items()):
|
||||
if root == pending[0]:
|
||||
del self.pending_create_file_events[fpath]
|
||||
# remove pending modify notifications on root
|
||||
for fpath, mroot in list(self.pending_modify_file_events.items()):
|
||||
if root == mroot:
|
||||
del self.pending_modify_file_events[fpath]
|
||||
# remove pending create notifications on root
|
||||
for dpath, pending in list(self.pending_create_dir_events.items()):
|
||||
if root == pending[0]:
|
||||
del self.pending_create_dir_events[dpath]
|
||||
# remove pending delete notifications on root
|
||||
for dir_path, pending in list(self.pending_delete_events.items()):
|
||||
if root == pending[0]:
|
||||
self.ioloop.remove_timeout(pending[2])
|
||||
del self.pending_delete_events[dir_path]
|
||||
self._scan_directory(root, root_path)
|
||||
|
||||
def add_watch(self, root, dir_path):
|
||||
if dir_path in self.watches or \
|
||||
root not in FULL_ACCESS_ROOTS:
|
||||
if root not in FULL_ACCESS_ROOTS:
|
||||
return
|
||||
watch = self.inotify.add_watch(dir_path, WATCH_FLAGS)
|
||||
self.watches[dir_path] = watch
|
||||
self.watched_dirs[watch] = (root, dir_path)
|
||||
# remove all exisiting watches on root
|
||||
if root in self.watched_roots:
|
||||
old_root = self.watched_roots.pop(root)
|
||||
old_root.clear_watches()
|
||||
old_root.clear_events()
|
||||
root_node = InotifyRootNode(self, root, root_path)
|
||||
self.watched_roots[root] = root_node
|
||||
root_node.scan_node(notify_created=False)
|
||||
self.log_nodes()
|
||||
|
||||
def remove_watch(self, dir_path, need_low_level_rm=True):
|
||||
wd = self.watches.pop(dir_path)
|
||||
self.watched_dirs.pop(wd)
|
||||
def add_watch(self, node):
|
||||
dir_path = node.get_path()
|
||||
try:
|
||||
watch = self.inotify.add_watch(dir_path, WATCH_FLAGS)
|
||||
except OSError:
|
||||
logging.exception(
|
||||
f"Error adding watch, already exists: {dir_path}")
|
||||
self.watched_nodes[watch] = node
|
||||
return watch
|
||||
|
||||
def remove_watch(self, wdesc, need_low_level_rm=True):
|
||||
node = self.watched_nodes.pop(wdesc, None)
|
||||
if need_low_level_rm:
|
||||
try:
|
||||
self.inotify.rm_watch(wd)
|
||||
except OSError:
|
||||
logging.exception(f"Error removing watch: '{dir_path}'")
|
||||
self.inotify.rm_watch(wdesc)
|
||||
except Exception:
|
||||
logging.exception(f"Error removing watch: '{node.get_path()}'")
|
||||
|
||||
def _reset_watch(self, prev_path, new_root, new_path):
|
||||
wd = self.watches.pop(prev_path, None)
|
||||
if wd is not None:
|
||||
self.watches[new_path] = wd
|
||||
self.watched_dirs[wd] = (new_root, new_path)
|
||||
|
||||
def _process_deleted_items(self, dir_path):
|
||||
if dir_path not in self.pending_delete_events:
|
||||
return
|
||||
root, items, hdl = self.pending_delete_events.pop(dir_path)
|
||||
for (item_name, isdir) in items:
|
||||
item_path = os.path.join(dir_path, item_name)
|
||||
item_type = "dir" if isdir else "file"
|
||||
self._clear_metadata(root, item_path, isdir)
|
||||
self._notify_filelist_changed(
|
||||
f"delete_{item_type}", root, item_path)
|
||||
|
||||
def _process_created_directory(self, dir_path):
|
||||
if dir_path not in self.pending_create_dir_events:
|
||||
return
|
||||
root, hdl = self.pending_create_dir_events.pop(dir_path)
|
||||
self._scan_directory(root, dir_path)
|
||||
self._notify_filelist_changed(
|
||||
"create_dir", root, dir_path)
|
||||
|
||||
def _remove_stale_cookie(self, cookie):
|
||||
# This is a file or directory moved out of a watched parent.
|
||||
# We treat this as a deleted file/directory.
|
||||
pending_evt = self.pending_move_events.pop(cookie, None)
|
||||
if pending_evt is None:
|
||||
# Event already processed
|
||||
return
|
||||
prev_root, prev_path, hdl, is_dir = pending_evt
|
||||
logging.debug("Inotify stale cookie removed: "
|
||||
f"{prev_root}, {prev_path}")
|
||||
item_type = "file"
|
||||
if is_dir:
|
||||
item_type = "dir"
|
||||
for wpath in list(self.watches.keys()):
|
||||
if wpath.startswith(prev_path):
|
||||
self.remove_watch(wpath)
|
||||
# Metadata should have been cleared in the MOVE_TO event,
|
||||
# so no need to clear it here
|
||||
self._notify_filelist_changed(
|
||||
f"delete_{item_type}", prev_root, prev_path)
|
||||
|
||||
def _clear_metadata(self, root, path, is_dir=False):
|
||||
def clear_metadata(self, root, path, is_dir=False):
|
||||
if root == "gcodes":
|
||||
rel_path = self.file_manager.get_relative_path(root, path)
|
||||
if is_dir:
|
||||
|
@ -674,52 +822,17 @@ class INotifyHandler:
|
|||
else:
|
||||
self.gcode_metadata.remove_file_metadata(rel_path)
|
||||
|
||||
def _scan_directory(self, root, dir_path, moved_path=None):
|
||||
# Walk through a directory. Create or reset watches as necessary
|
||||
if moved_path is None:
|
||||
self.add_watch(root, dir_path)
|
||||
else:
|
||||
self._reset_watch(moved_path, root, dir_path)
|
||||
st = os.stat(dir_path)
|
||||
visited_dirs = {(st.st_dev, st.st_ino)}
|
||||
for dpath, dnames, files in os.walk(dir_path, followlinks=True):
|
||||
scan_dirs = []
|
||||
for dname in dnames:
|
||||
full_path = os.path.join(dpath, dname)
|
||||
st = os.stat(full_path)
|
||||
key = (st.st_dev, st.st_ino)
|
||||
if key not in visited_dirs:
|
||||
# Don't watch hidden directories
|
||||
if dname[0] != ".":
|
||||
if moved_path is not None:
|
||||
rel_path = os.path.relpath(
|
||||
full_path, start=dir_path)
|
||||
prev_path = os.path.join(moved_path, rel_path)
|
||||
self._reset_watch(prev_path, root, full_path)
|
||||
else:
|
||||
self.add_watch(root, full_path)
|
||||
visited_dirs.add(key)
|
||||
scan_dirs.append(dname)
|
||||
dnames[:] = scan_dirs
|
||||
if root != "gcodes":
|
||||
# No need check for metadata in non-gcode roots.
|
||||
continue
|
||||
for name in files:
|
||||
fpath = os.path.join(dpath, name)
|
||||
ext = os.path.splitext(name)[-1].lower()
|
||||
if name[0] == "." or ext not in VALID_GCODE_EXTS:
|
||||
continue
|
||||
self._parse_gcode_metadata(fpath)
|
||||
def log_nodes(self):
|
||||
if self.debug_enabled:
|
||||
debug_msg = f"Inotify Watches After Scan: {dir_path}"
|
||||
for wdir, watch in self.watches.items():
|
||||
wroot, wpath = self.watched_dirs[watch]
|
||||
match = wdir == wpath
|
||||
debug_msg = f"Inotify Watches After Scan:"
|
||||
for wdesc, node in self.watched_nodes.items():
|
||||
wdir = node.get_path()
|
||||
wroot = node.get_root()
|
||||
debug_msg += f"\nRoot: {wroot}, Directory: {wdir}, " \
|
||||
f"Watch: {watch}, Dir Match: {match}"
|
||||
f"Watch: {wdesc}"
|
||||
logging.debug(debug_msg)
|
||||
|
||||
def _parse_gcode_metadata(self, file_path):
|
||||
def parse_gcode_metadata(self, file_path):
|
||||
rel_path = self.file_manager.get_relative_path("gcodes", file_path)
|
||||
if not rel_path:
|
||||
logging.info(
|
||||
|
@ -733,6 +846,33 @@ class INotifyHandler:
|
|||
path_info['ufp_path'] = file_path
|
||||
self.gcode_metadata.parse_metadata(rel_path, path_info, notify=True)
|
||||
|
||||
def _handle_move_timeout(self, cookie, is_dir):
|
||||
if cookie not in self.pending_moves:
|
||||
return
|
||||
parent_node, name, hdl = self.pending_moves.pop(cookie)
|
||||
item_path = os.path.join(parent_node.get_path(), name)
|
||||
root = parent_node.get_root()
|
||||
action = "delete_file"
|
||||
if is_dir:
|
||||
# The supplied node is a child node
|
||||
child_node = parent_node.pop_child_node(name)
|
||||
if child_node is None:
|
||||
return
|
||||
child_node.clear_watches()
|
||||
child_node.clear_events(include_children=True)
|
||||
self.log_nodes()
|
||||
action = "delete_dir"
|
||||
self.notify_filelist_changed(action, root, item_path)
|
||||
|
||||
def _schedule_pending_move(self, evt, parent_node, is_dir):
|
||||
item_path = os.path.join(parent_node.get_path(), evt.name)
|
||||
root = parent_node.get_root()
|
||||
self.clear_metadata(root, item_path, is_dir)
|
||||
hdl = IOLoop.current().call_later(
|
||||
INOTIFY_MOVE_TIME, self._handle_move_timeout,
|
||||
evt.cookie, is_dir)
|
||||
self.pending_moves[evt.cookie] = (parent_node, evt.name, hdl)
|
||||
|
||||
def _handle_inotify_read(self, fd, events):
|
||||
if events & IOLoop.ERROR:
|
||||
logging.info("INotify Read Error")
|
||||
|
@ -740,174 +880,103 @@ class INotifyHandler:
|
|||
for evt in self.inotify.read(timeout=0):
|
||||
if evt.mask & iFlags.IGNORED:
|
||||
continue
|
||||
if evt.wd not in self.watched_dirs:
|
||||
if evt.wd not in self.watched_nodes:
|
||||
flags = " ".join([str(f) for f in iFlags.from_mask(evt.mask)])
|
||||
logging.info(
|
||||
f"Error, inotify watch descriptor {evt.wd} "
|
||||
f"not currently tracked: name: {evt.name}, "
|
||||
f"flags: {flags}")
|
||||
continue
|
||||
root, watch_path = self.watched_dirs[evt.wd]
|
||||
child_path = watch_path
|
||||
if evt.name:
|
||||
child_path = os.path.join(watch_path, evt.name)
|
||||
node = self.watched_nodes[evt.wd]
|
||||
if evt.mask & iFlags.ISDIR:
|
||||
self._process_dir_event(evt, root, child_path)
|
||||
self._process_dir_event(evt, node)
|
||||
else:
|
||||
self._process_file_event(evt, root, child_path)
|
||||
self._process_file_event(evt, node)
|
||||
|
||||
def _schedule_delete_event(self, root, item_path, is_dir):
|
||||
if is_dir:
|
||||
self.remove_watch(item_path, need_low_level_rm=False)
|
||||
# Remove pending delete events for children if they exist
|
||||
pending_evt = self.pending_delete_events.pop(item_path, None)
|
||||
if pending_evt is not None:
|
||||
delete_hdl = pending_evt[2]
|
||||
self.ioloop.remove_timeout(delete_hdl)
|
||||
parent_path, item_name = os.path.split(item_path)
|
||||
items = set()
|
||||
if parent_path in self.pending_delete_events:
|
||||
root, items, delete_hdl = self.pending_delete_events[parent_path]
|
||||
self.ioloop.remove_timeout(delete_hdl)
|
||||
items.add((item_name, is_dir))
|
||||
delete_hdl = self.ioloop.call_later(
|
||||
INOTIFY_BUNDLE_TIME, self._process_deleted_items, parent_path)
|
||||
self.pending_delete_events[parent_path] = (root, items, delete_hdl)
|
||||
|
||||
def _process_dir_event(self, evt, root, child_path):
|
||||
def _process_dir_event(self, evt, node):
|
||||
if evt.name and evt.name[0] == ".":
|
||||
# ignore changes to the hidden directories
|
||||
return
|
||||
root = node.get_root()
|
||||
node_path = node.get_path()
|
||||
if evt.mask & iFlags.CREATE:
|
||||
logging.debug(f"Inotify directory create: {root}, {evt.name}")
|
||||
# Add a watch for this directory immediately so we can catch
|
||||
# events for its children
|
||||
self.add_watch(root, child_path)
|
||||
cb_path = child_path
|
||||
for parent_path, pending in self.pending_create_dir_events.items():
|
||||
if child_path.startswith(parent_path):
|
||||
# This directory has a parent with a pending notification.
|
||||
# Reset the parent's timeout and suppress the notification
|
||||
# for this child
|
||||
self.ioloop.remove_timeout(pending[1])
|
||||
cb_path = parent_path
|
||||
break
|
||||
hdl = self.ioloop.call_later(
|
||||
INOTIFY_BUNDLE_TIME, self._process_created_directory,
|
||||
cb_path)
|
||||
self.pending_create_dir_events[cb_path] = (root, hdl)
|
||||
logging.debug(f"Inotify directory create: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
node.create_child_node(evt.name)
|
||||
elif evt.mask & iFlags.DELETE:
|
||||
logging.debug(f"Inotify directory delete: {root}, {evt.name}")
|
||||
self._schedule_delete_event(root, child_path, True)
|
||||
logging.debug(f"Inotify directory delete: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
node.schedule_child_delete(evt.name, True)
|
||||
elif evt.mask & iFlags.MOVED_FROM:
|
||||
logging.debug(f"Inotify directory move from: {root}, {evt.name}")
|
||||
hdl = self.ioloop.call_later(
|
||||
INOTIFY_MOVE_TIME, self._remove_stale_cookie, evt.cookie)
|
||||
self.pending_move_events[evt.cookie] = (
|
||||
root, child_path, hdl, True)
|
||||
self._clear_metadata(root, child_path, True)
|
||||
logging.debug(f"Inotify directory move from: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
self._schedule_pending_move(evt, node, True)
|
||||
elif evt.mask & iFlags.MOVED_TO:
|
||||
logging.debug(f"Inotify directory move to: {root}, {evt.name}")
|
||||
pending_evt = self.pending_move_events.pop(evt.cookie, None)
|
||||
if pending_evt is not None:
|
||||
logging.debug(f"Inotify directory move to: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
moved_evt = self.pending_moves.pop(evt.cookie, None)
|
||||
if moved_evt is not None:
|
||||
# Moved from a currently watched directory
|
||||
prev_root, prev_path, hdl, is_dir = pending_evt
|
||||
if not is_dir:
|
||||
logging.debug(
|
||||
f"Cookie matched to a file: {pending_evt}")
|
||||
return
|
||||
self.ioloop.remove_timeout(hdl)
|
||||
self._scan_directory(root, child_path, prev_path)
|
||||
self._notify_filelist_changed(
|
||||
"move_dir", root, child_path,
|
||||
prev_root, prev_path)
|
||||
prev_parent, child_name, hdl = moved_evt
|
||||
IOLoop.current().remove_timeout(hdl)
|
||||
prev_parent.move_child_node(child_name, evt.name, node)
|
||||
else:
|
||||
# Moved from an unwatched directory, for our
|
||||
# purposes this is the same as creating a
|
||||
# directory
|
||||
self._scan_directory(root, child_path)
|
||||
self._notify_filelist_changed(
|
||||
"create_dir", root, child_path)
|
||||
node.create_child_node(evt.name)
|
||||
|
||||
def _process_file_event(self, evt, root, child_path):
|
||||
def _process_file_event(self, evt, node):
|
||||
ext = os.path.splitext(evt.name)[-1].lower()
|
||||
root = node.get_root()
|
||||
node_path = node.get_path()
|
||||
if root == "gcodes" and ext not in VALID_GCODE_EXTS:
|
||||
# Don't notify files with invalid gcode extensions
|
||||
return
|
||||
if evt.mask & iFlags.CREATE:
|
||||
logging.debug(f"Inotify file create: {root}, {evt.name}")
|
||||
parent = None
|
||||
for dpath, pending in self.pending_create_dir_events.items():
|
||||
if child_path.startswith(dpath):
|
||||
parent = dpath
|
||||
self.ioloop.remove_timeout(pending[1])
|
||||
break
|
||||
self.pending_create_file_events[child_path] = (root, parent)
|
||||
logging.debug(f"Inotify file create: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
node.schedule_file_event(evt.name, "create_file")
|
||||
elif evt.mask & iFlags.DELETE:
|
||||
logging.debug(f"Inotify file delete: {root}, {evt.name}")
|
||||
logging.debug(f"Inotify file delete: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
if root == "gcodes" and ext == ".ufp":
|
||||
# Don't notify deleted ufp files
|
||||
return
|
||||
self._schedule_delete_event(root, child_path, False)
|
||||
node.schedule_child_delete(evt.name, False)
|
||||
elif evt.mask & iFlags.MOVED_FROM:
|
||||
logging.debug(f"Inotify file move from: {root}, {evt.name}")
|
||||
hdl = self.ioloop.call_later(
|
||||
INOTIFY_MOVE_TIME, self._remove_stale_cookie, evt.cookie)
|
||||
self.pending_move_events[evt.cookie] = (
|
||||
root, child_path, hdl, False)
|
||||
self._clear_metadata(root, child_path)
|
||||
logging.debug(f"Inotify file move from: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
self._schedule_pending_move(evt, node, False)
|
||||
elif evt.mask & iFlags.MOVED_TO:
|
||||
logging.debug(f"Inotify file move to: {root}, {evt.name}")
|
||||
logging.debug(f"Inotify file move to: {root}, "
|
||||
f"{node_path}, {evt.name}")
|
||||
file_path = os.path.join(node_path, evt.name)
|
||||
if root == "gcodes":
|
||||
self._parse_gcode_metadata(child_path)
|
||||
pending_evt = self.pending_move_events.pop(evt.cookie, None)
|
||||
if pending_evt is not None:
|
||||
self.parse_gcode_metadata(file_path)
|
||||
moved_evt = self.pending_moves.pop(evt.cookie, None)
|
||||
if moved_evt is not None:
|
||||
# Moved from a currently watched directory
|
||||
prev_root, prev_path, hdl, is_dir = pending_evt
|
||||
if is_dir:
|
||||
logging.debug(
|
||||
f"Cookie matched to directory: {pending_evt}")
|
||||
return
|
||||
self._notify_filelist_changed(
|
||||
"move_file", root, child_path,
|
||||
prev_parent, prev_name, hdl = moved_evt
|
||||
IOLoop.current().remove_timeout(hdl)
|
||||
prev_root = prev_parent.get_root()
|
||||
prev_path = os.path.join(prev_parent.get_path(), prev_name)
|
||||
self.notify_filelist_changed(
|
||||
"move_file", root, file_path,
|
||||
prev_root, prev_path)
|
||||
else:
|
||||
self._notify_filelist_changed(
|
||||
"create_file", root, child_path)
|
||||
self.notify_filelist_changed(
|
||||
"create_file", root, file_path)
|
||||
elif evt.mask & iFlags.MODIFY:
|
||||
if child_path not in self.pending_create_file_events:
|
||||
self.pending_modify_file_events[child_path] = root
|
||||
node.schedule_file_event(evt.name, "modify_file")
|
||||
elif evt.mask & iFlags.CLOSE_WRITE:
|
||||
logging.debug(f"Inotify writable file closed: {child_path}")
|
||||
file_path = os.path.join(node_path, evt.name)
|
||||
logging.debug(f"Inotify writable file closed: {file_path}")
|
||||
# Only process files that have been created or modified
|
||||
if child_path in self.pending_create_file_events:
|
||||
parent = self.pending_create_file_events.pop(child_path)[1]
|
||||
if parent is not None:
|
||||
# This is part of a created parent. Reschedule the
|
||||
# directory notification callback. The parent will
|
||||
# handle metadata/gcode processing, so we can skip it here
|
||||
hdl = self.ioloop.call_later(
|
||||
INOTIFY_BUNDLE_TIME, self._process_created_directory,
|
||||
parent)
|
||||
self.pending_create_dir_events[parent] = (root, hdl)
|
||||
return
|
||||
action = "create_file"
|
||||
elif child_path in self.pending_modify_file_events:
|
||||
del self.pending_modify_file_events[child_path]
|
||||
action = "modify_file"
|
||||
else:
|
||||
# Some other event, ignore it
|
||||
return
|
||||
if root == "gcodes":
|
||||
self._parse_gcode_metadata(child_path)
|
||||
if ext == ".ufp":
|
||||
# Don't notify ufp creation in the gcodes directory,
|
||||
# it will be removed after it has been unzipped
|
||||
return
|
||||
self._notify_filelist_changed(action, root, child_path)
|
||||
node.complete_file_write(evt.name)
|
||||
|
||||
def _notify_filelist_changed(self, action, root, full_path,
|
||||
source_root=None, source_path=None):
|
||||
def notify_filelist_changed(self, action, root, full_path,
|
||||
source_root=None, source_path=None):
|
||||
rel_path = self.file_manager.get_relative_path(root, full_path)
|
||||
file_info = {'size': 0, 'modified': 0}
|
||||
if os.path.exists(full_path):
|
||||
|
@ -922,11 +991,11 @@ class INotifyHandler:
|
|||
self.server.send_event("file_manager:filelist_changed", result)
|
||||
|
||||
def close(self):
|
||||
self.ioloop.remove_handler(self.inotify.fileno())
|
||||
for watch in self.watches.values():
|
||||
IOLoop.current().remove_handler(self.inotify.fileno())
|
||||
for watch in self.watched_nodes.keys():
|
||||
try:
|
||||
self.inotify.rm_watch(watch)
|
||||
except OSError:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -984,6 +1053,8 @@ class MetadataStorage:
|
|||
return True
|
||||
|
||||
def remove_directory_metadata(self, dir_name):
|
||||
if dir_name[-1] != "/":
|
||||
dir_name += "/"
|
||||
for fname in list(self.mddb.keys()):
|
||||
if fname.startswith(dir_name):
|
||||
self.remove_file_metadata(fname)
|
||||
|
|
Loading…
Reference in New Issue