file_manager: save metadata to the database

This allows gcode file metadata to persist through restarts, reducing the time spent parsing.  The gcode file path also persists, so it is available even when Klippy is not.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-02-23 20:26:56 -05:00
parent 7ff54d8f2f
commit 87e136003f
1 changed files with 29 additions and 16 deletions

View File

@ -22,7 +22,9 @@ class FileManager:
def __init__(self, config): def __init__(self, config):
self.server = config.get_server() self.server = config.get_server()
self.file_paths = {} self.file_paths = {}
self.gcode_metadata = MetadataStorage(self.server) database = self.server.load_plugin(config, "database")
gc_path = database.get_item("moonraker", "file_manager.gcode_path", "")
self.gcode_metadata = MetadataStorage(self.server, gc_path, database)
self.fixed_path_args = {} self.fixed_path_args = {}
# Register file management endpoints # Register file management endpoints
@ -58,6 +60,10 @@ class FileManager:
raise config.error( raise config.error(
"Option 'config_path' is not a valid directory") "Option 'config_path' is not a valid directory")
# If gcode path is in the database, register it
if gc_path:
self.register_directory('gcodes', gc_path)
def _update_fixed_paths(self): def _update_fixed_paths(self):
kinfo = self.server.get_klippy_info() kinfo = self.server.get_klippy_info()
paths = {k: kinfo.get(k) for k in paths = {k: kinfo.get(k) for k in
@ -108,7 +114,10 @@ class FileManager:
self.file_paths[root] = path self.file_paths[root] = path
self.server.register_static_file_handler(root, path) self.server.register_static_file_handler(root, path)
if root == "gcodes": if root == "gcodes":
# scan metadata database = self.server.lookup_plugin(
"database").wrap_namespace("moonraker")
database["file_manager.gcode_path"] = path
# scan for metadata changes
self.gcode_metadata.update_gcode_path(path) self.gcode_metadata.update_gcode_path(path)
try: try:
self.get_file_list("gcodes") self.get_file_list("gcodes")
@ -612,22 +621,25 @@ class FileManager:
METADATA_PRUNE_TIME = 600000 METADATA_PRUNE_TIME = 600000
METADATA_NAMESPACE = "gcode_metadata"
class MetadataStorage: class MetadataStorage:
def __init__(self, server): def __init__(self, server, gc_path, database):
self.server = server self.server = server
self.metadata = {} database.register_local_namespace(METADATA_NAMESPACE)
self.mddb = database.wrap_namespace(
METADATA_NAMESPACE, parse_keys=False)
self.pending_requests = {} self.pending_requests = {}
self.events = {} self.events = {}
self.busy = False self.busy = False
self.gc_path = os.path.expanduser("~") self.gc_path = gc_path
self.prune_cb = PeriodicCallback( self.prune_cb = PeriodicCallback(
self.prune_metadata, METADATA_PRUNE_TIME) self.prune_metadata, METADATA_PRUNE_TIME)
def update_gcode_path(self, path): def update_gcode_path(self, path):
if path == self.gc_path: if path == self.gc_path:
return return
self.metadata = {} self.mddb.clear()
self.gc_path = path self.gc_path = path
if not self.prune_cb.is_running(): if not self.prune_cb.is_running():
self.prune_cb.start() self.prune_cb.start()
@ -636,27 +648,28 @@ class MetadataStorage:
self.prune_cb.stop() self.prune_cb.stop()
def get(self, key, default=None): def get(self, key, default=None):
if key not in self.metadata: return self.mddb.get(key, default)
return default
return dict(self.metadata[key])
def __getitem__(self, key): def __getitem__(self, key):
return dict(self.metadata[key]) return self.mddb[key]
def prune_metadata(self): def prune_metadata(self):
for fname in list(self.metadata.keys()): for fname in list(self.mddb.keys()):
fpath = os.path.join(self.gc_path, fname) fpath = os.path.join(self.gc_path, fname)
if not os.path.exists(fpath): if not os.path.exists(fpath):
del self.metadata[fname] del self.mddb[fname]
logging.info(f"Pruned file: {fname}") logging.info(f"Pruned file: {fname}")
continue continue
def _has_valid_data(self, fname, fsize, modified): def _has_valid_data(self, fname, fsize, modified):
mdata = self.metadata.get(fname, {'size': "", 'modified': 0}) mdata = self.mddb.get(fname, {'size': "", 'modified': 0})
return mdata['size'] == fsize and mdata['modified'] == modified return mdata['size'] == fsize and mdata['modified'] == modified
def remove_file(self, fname): def remove_file(self, fname):
self.metadata.pop(fname) try:
del self.mddb[fname]
except Exception:
pass
def parse_metadata(self, fname, fsize, modified, notify=False): def parse_metadata(self, fname, fsize, modified, notify=False):
evt = Event() evt = Event()
@ -689,7 +702,7 @@ class MetadataStorage:
else: else:
break break
else: else:
self.metadata[fname] = {'size': fsize, 'modified': modified} self.mddb[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}")
evt.set() evt.set()
@ -716,7 +729,7 @@ class MetadataStorage:
if not metadata: if not metadata:
# This indicates an error, do not add metadata for this # This indicates an error, do not add metadata for this
raise self.server.error("Unable to extract metadata") raise self.server.error("Unable to extract metadata")
self.metadata[path] = dict(metadata) self.mddb[path] = dict(metadata)
metadata['filename'] = path metadata['filename'] = path
if notify: if notify:
self.server.send_event( self.server.send_event(