file_manager: refactor the MetadataStorage class

Storage is now updated by individual calls to "parse_metadata()" instead of passing a full list.  This will allow the manager to

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2020-11-06 08:57:42 -05:00
parent e5f3aeca78
commit e51dbb45c1

View File

@ -10,7 +10,7 @@ import io
import zipfile import zipfile
import logging import logging
import json import json
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.locks import Lock from tornado.locks import Lock
VALID_GCODE_EXTS = ['.gcode', '.g', '.gco'] VALID_GCODE_EXTS = ['.gcode', '.g', '.gco']
@ -87,6 +87,8 @@ class FileManager:
if path != self.file_paths.get(base, ""): if path != self.file_paths.get(base, ""):
self.file_paths[base] = path self.file_paths[base] = path
self.server.register_static_file_handler(base, path) self.server.register_static_file_handler(base, path)
if base == "gcodes":
self.gcode_metadata.update_gcode_path(path)
try: try:
self._update_file_list(base=base) self._update_file_list(base=base)
except Exception: except Exception:
@ -106,8 +108,8 @@ class FileManager:
async def _handle_metadata_request(self, path, method, args): async def _handle_metadata_request(self, path, method, args):
requested_file = args.get('filename') requested_file = args.get('filename')
metadata = dict(self.gcode_metadata.get(requested_file, {})) metadata = self.gcode_metadata.get(requested_file, None)
if not metadata: if metadata is None:
raise self.server.error( raise self.server.error(
f"Metadata not available for <{requested_file}>", 404) f"Metadata not available for <{requested_file}>", 404)
metadata['filename'] = requested_file metadata['filename'] = requested_file
@ -295,17 +297,19 @@ class FileManager:
raise self.server.error(msg) raise self.server.error(msg)
logging.info(f"Updating File List <{base}>...") logging.info(f"Updating File List <{base}>...")
new_list = {} new_list = {}
for root, dirs, files in os.walk(path, followlinks=True): for root_path, dirs, files in os.walk(path, followlinks=True):
for name in files: for name in files:
ext = os.path.splitext(name)[-1].lower() ext = os.path.splitext(name)[-1].lower()
if base == 'gcodes' and ext not in VALID_GCODE_EXTS: if base == 'gcodes' and ext not in VALID_GCODE_EXTS:
continue continue
full_path = os.path.join(root, name) full_path = os.path.join(root_path, name)
r_path = full_path[len(path) + 1:] fname = full_path[len(path) + 1:]
new_list[r_path] = self._get_path_info(full_path) finfo = self._get_path_info(full_path)
self.file_lists[base] = new_list new_list[fname] = finfo
if base == 'gcodes': if base == 'gcodes':
self.gcode_metadata.refresh_metadata(new_list, path, do_notify) self.gcode_metadata.parse_metadata(
fname, finfo['size'], finfo['modified'], do_notify)
self.file_lists[base] = new_list
return dict(new_list) return dict(new_list)
async def process_file_upload(self, request): async def process_file_upload(self, request):
@ -480,7 +484,7 @@ class FileManager:
filename = filename[7:] filename = filename[7:]
flist = self.get_file_list() flist = self.get_file_list()
return dict(self.gcode_metadata.get(filename, flist.get(filename, {}))) return self.gcode_metadata.get(filename, flist.get(filename, {}))
def list_dir(self, directory, simple_format=False): def list_dir(self, directory, simple_format=False):
# List a directory relative to its root. Currently the only # List a directory relative to its root. Currently the only
@ -551,18 +555,41 @@ class FileManager:
result.update({'source_item': source_item}) result.update({'source_item': source_item})
self.server.send_event("file_manager:filelist_changed", result) self.server.send_event("file_manager:filelist_changed", result)
def close(self):
self.gcode_metadata.close()
METADATA_PRUNE_TIME = 600000
class MetadataStorage: class MetadataStorage:
def __init__(self, server): def __init__(self, server):
self.server = server self.server = server
self.lock = Lock()
self.metadata = {} self.metadata = {}
self.pending_requests = {}
self.script_response = None self.script_response = None
self.busy = False
self.gc_path = os.path.expanduser("~")
self.prune_cb = PeriodicCallback(
self._prune_metadata, METADATA_PRUNE_TIME)
def update_gcode_path(self, path):
if path == self.gc_path:
return
self.metadata = {}
self.gc_path = path
if not self.prune_cb.is_running():
self.prune_cb.start()
def close(self):
self.prune_cb.stop()
def get(self, key, default=None): def get(self, key, default=None):
return self.metadata.get(key, default) if key not in self.metadata:
return default
return dict(self.metadata[key])
def __getitem__(self, key): def __getitem__(self, key):
return self.metadata[key] return dict(self.metadata[key])
def _handle_script_response(self, result): def _handle_script_response(self, result):
try: try:
@ -577,40 +604,52 @@ class MetadataStorage:
if 'file' in proc_resp: if 'file' in proc_resp:
self.script_response = proc_resp self.script_response = proc_resp
def refresh_metadata(self, filelist, gc_path, do_notify=False): def _prune_metadata(self):
IOLoop.current().spawn_callback( for fname in list(self.metadata.keys()):
self._do_metadata_update, filelist, gc_path, do_notify) fpath = os.path.join(self.gc_path, fname)
if not os.path.exists(fpath):
del self.metadata[fname]
logging.info(f"Pruned file: {fname}")
continue
async def _do_metadata_update(self, filelist, gc_path, do_notify=False): def _has_valid_data(self, fname, fsize, modified):
async with self.lock: mdata = self.metadata.get(fname, {'size': "", 'modified': 0})
exisiting_data = {} return mdata['size'] == fsize and mdata['modified'] == modified
update_list = []
for fname, fdata in filelist.items(): def parse_metadata(self, fname, fsize, modified, notify=False):
mdata = self.metadata.get(fname, {}) if fname in self.pending_requests or \
if mdata.get('size', "") == fdata.get('size') \ self._has_valid_data(fname, fsize, modified):
and mdata.get('modified', 0) == fdata.get('modified'): # request already pending or not necessary
# file metadata has already been extracted return
exisiting_data[fname] = mdata self.pending_requests[fname] = (fsize, modified, notify)
else: if self.busy:
update_list.append(fname) return
self.metadata = exisiting_data self.busy = True
for fname in update_list: IOLoop.current().spawn_callback(self._process_metadata_update)
async def _process_metadata_update(self):
while self.pending_requests:
fname, (fsize, modified, notify) = self.pending_requests.popitem()
if self._has_valid_data(fname, fsize, modified):
continue
retries = 3 retries = 3
while retries: while retries:
try: try:
await self._extract_metadata(fname, gc_path, do_notify) await self._run_extract_metadata(fname, notify)
except Exception: except Exception:
logging.exception("Error running extract_metadata.py") logging.exception("Error running extract_metadata.py")
retries -= 1 retries -= 1
else: else:
break break
else: else:
self.metadata[fname] = {'size': fsize, 'modified': modified}
logging.info( logging.info(
f"Unable to extract medatadata from file: {fname}") f"Unable to extract medatadata from file: {fname}")
self.busy = False
async def _extract_metadata(self, filename, path, do_notify=False): async def _run_extract_metadata(self, filename, notify):
cmd = " ".join([sys.executable, METADATA_SCRIPT, "-p", cmd = " ".join([sys.executable, METADATA_SCRIPT, "-p",
path, "-f", "'" + filename + "'"]) self.gc_path, "-f", "'" + filename + "'"])
shell_command = self.server.lookup_plugin('shell_command') shell_command = self.server.lookup_plugin('shell_command')
scmd = shell_command.build_shell_command( scmd = shell_command.build_shell_command(
cmd, self._handle_script_response) cmd, self._handle_script_response)
@ -625,7 +664,7 @@ class MetadataStorage:
raise self.server.error("Unable to extract metadata") raise self.server.error("Unable to extract metadata")
self.metadata[path] = dict(metadata) self.metadata[path] = dict(metadata)
metadata['filename'] = path metadata['filename'] = path
if do_notify: if notify:
self.server.send_event( self.server.send_event(
"file_manager:metadata_update", metadata) "file_manager:metadata_update", metadata)