# coding=utf-8 from __future__ import absolute_import __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" import logging import os import octoprint.plugin from octoprint.events import eventManager, Events from .destinations import FileDestinations from .analysis import QueueEntry, AnalysisQueue from .storage import LocalFileStorage extensions = dict( # extensions for 3d model files model=dict( stl=["stl"] ), # extensions for printable machine code machinecode=dict( gcode=["gcode", "gco", "g"] ) ) def get_extensions(type, subtree=None): if not subtree: subtree = extensions for key, value in subtree.items(): if key == type: return get_all_extensions(subtree=value) elif isinstance(value, dict): sub_extensions = get_extensions(type, subtree=value) if sub_extensions: return sub_extensions return None def get_all_extensions(subtree=None): if not subtree: subtree = extensions result = [] if isinstance(subtree, dict): for key, value in subtree.items(): if isinstance(value, dict): result += get_all_extensions(value) elif isinstance(value, (list, tuple)): result += value elif isinstance(subtree, (list, tuple)): result = subtree return result def get_path_for_extension(extension, subtree=None): if not subtree: subtree = extensions for key, value in subtree.items(): if isinstance(value, (list, tuple)) and extension in value: return [key] elif isinstance(value, dict): path = get_path_for_extension(extension, subtree=value) if path: return [key] + path return None all_extensions = get_all_extensions() def valid_extension(extension, type=None): if not type: return extension in all_extensions else: extensions = get_extensions(type) if extensions: return extension in extensions def valid_file_type(filename, type=None): _, extension = os.path.splitext(filename) extension = extension[1:].lower() return valid_extension(extension, type=type) def get_file_type(filename): _, extension = os.path.splitext(filename) extension = extension[1:].lower() return get_path_for_extension(extension) class NoSuchStorage(Exception): pass class FileManager(object): def __init__(self, analysis_queue, slicing_manager, printer_profile_manager, initial_storage_managers=None): self._logger = logging.getLogger(__name__) self._analysis_queue = analysis_queue self._analysis_queue.register_finish_callback(self._on_analysis_finished) self._storage_managers = dict() if initial_storage_managers: self._storage_managers.update(initial_storage_managers) self._slicing_manager = slicing_manager self._printer_profile_manager = printer_profile_manager import threading self._slicing_jobs = dict() self._slicing_jobs_mutex = threading.Lock() self._slicing_progress_callbacks = [] self._last_slicing_progress = None self._progress_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.ProgressPlugin) for storage_type, storage_manager in self._storage_managers.items(): self._determine_analysis_backlog(storage_type, storage_manager) def register_slicingprogress_callback(self, callback): self._slicing_progress_callbacks.append(callback) def unregister_slicingprogress_callback(self, callback): self._slicing_progress_callbacks.remove(callback) def _determine_analysis_backlog(self, storage_type, storage_manager): self._logger.info("Adding backlog items from {storage_type} to analysis queue".format(**locals())) for entry, path, printer_profile in storage_manager.analysis_backlog: file_type = get_file_type(path)[-1] # we'll use the default printer profile for the backlog since we don't know better queue_entry = QueueEntry(entry, file_type, storage_type, path, self._printer_profile_manager.get_default()) self._analysis_queue.enqueue(queue_entry, high_priority=False) def add_storage(self, storage_type, storage_manager): self._storage_managers[storage_type] = storage_manager self._determine_analysis_backlog(storage_type, storage_manager) def remove_storage(self, type): if not type in self._storage_managers: return del self._storage_managers[type] @property def slicing_enabled(self): return self._slicing_manager.slicing_enabled @property def registered_slicers(self): return self._slicing_manager.registered_slicers @property def default_slicer(self): return self._slicing_manager.default_slicer def slice(self, slicer_name, source_location, source_path, dest_location, dest_path, position=None, profile=None, printer_profile_id=None, overrides=None, callback=None, callback_args=None): absolute_source_path = self.path_on_disk(source_location, source_path) def stlProcessed(source_location, source_path, tmp_path, dest_location, dest_path, start_time, printer_profile_id, callback, callback_args, _error=None, _cancelled=False, _analysis=None): try: if _error: eventManager().fire(Events.SLICING_FAILED, {"stl": source_path, "gcode": dest_path, "reason": _error}) elif _cancelled: eventManager().fire(Events.SLICING_CANCELLED, {"stl": source_path, "gcode": dest_path}) else: source_meta = self.get_metadata(source_location, source_path) hash = source_meta["hash"] class Wrapper(object): def __init__(self, stl_name, temp_path, hash): self.stl_name = stl_name self.temp_path = temp_path self.hash = hash def save(self, absolute_dest_path): with open(absolute_dest_path, "w") as d: d.write(";Generated from {stl_name} {hash}\r".format(**vars(self))) with open(tmp_path, "r") as s: import shutil shutil.copyfileobj(s, d) links = [("model", dict(name=source_path))] _, stl_name = self.split_path(source_location, source_path) file_obj = Wrapper(stl_name, temp_path, hash) printer_profile = self._printer_profile_manager.get(printer_profile_id) self.add_file(dest_location, dest_path, file_obj, links=links, allow_overwrite=True, printer_profile=printer_profile, analysis=_analysis) end_time = time.time() eventManager().fire(Events.SLICING_DONE, {"stl": source_path, "gcode": dest_path, "time": end_time - start_time}) if callback is not None: if callback_args is None: callback_args = () callback(*callback_args) finally: os.remove(tmp_path) source_job_key = (source_location, source_path) dest_job_key = (dest_location, dest_path) with self._slicing_jobs_mutex: if source_job_key in self._slicing_jobs: del self._slicing_jobs[source_job_key] if dest_job_key in self._slicing_jobs: del self._slicing_jobs[dest_job_key] slicer = self._slicing_manager.get_slicer(slicer_name) import time start_time = time.time() eventManager().fire(Events.SLICING_STARTED, {"stl": source_path, "gcode": dest_path, "progressAvailable": slicer.get_slicer_properties()["progress_report"] if slicer else False}) import tempfile f = tempfile.NamedTemporaryFile(suffix=".gco", delete=False) temp_path = f.name f.close() with self._slicing_jobs_mutex: source_job_key = (source_location, source_path) dest_job_key = (dest_location, dest_path) if dest_job_key in self._slicing_jobs: job_slicer_name, job_absolute_source_path, job_temp_path = self._slicing_jobs[dest_job_key] self._slicing_manager.cancel_slicing(job_slicer_name, job_absolute_source_path, job_temp_path) del self._slicing_jobs[dest_job_key] self._slicing_jobs[dest_job_key] = self._slicing_jobs[source_job_key] = (slicer_name, absolute_source_path, temp_path) args = (source_location, source_path, temp_path, dest_location, dest_path, start_time, printer_profile_id, callback, callback_args) self._slicing_manager.slice(slicer_name, absolute_source_path, temp_path, profile, stlProcessed, position=position, callback_args=args, overrides=overrides, printer_profile_id=printer_profile_id, on_progress=self.on_slicing_progress, on_progress_args=(slicer_name, source_location, source_path, dest_location, dest_path)) def on_slicing_progress(self, slicer, source_location, source_path, dest_location, dest_path, _progress=None): if not _progress: return progress_int = int(_progress * 100) if self._last_slicing_progress != progress_int: self._last_slicing_progress = progress_int for callback in self._slicing_progress_callbacks: try: callback.sendSlicingProgress(slicer, source_location, source_path, dest_location, dest_path, progress_int) except: self._logger.exception("Exception while pushing slicing progress") if progress_int: def call_plugins(slicer, source_location, source_path, dest_location, dest_path, progress): for plugin in self._progress_plugins: try: plugin.on_slicing_progress(slicer, source_location, source_path, dest_location, dest_path, progress) except: self._logger.exception("Exception while sending slicing progress to plugin %s" % plugin._identifier) import threading thread = threading.Thread(target=call_plugins, args=(slicer, source_location, source_path, dest_location, dest_path, progress_int)) thread.daemon = False thread.start() def get_busy_files(self): return self._slicing_jobs.keys() def file_exists(self, destination, path): return self._storage(destination).file_exists(path) def list_files(self, destinations=None, path=None, filter=None, recursive=None): if not destinations: destinations = self._storage_managers.keys() if isinstance(destinations, (str, unicode, basestring)): destinations = [destinations] result = dict() for dst in destinations: result[dst] = self._storage_managers[dst].list_files(path=path, filter=filter, recursive=recursive) return result def add_file(self, destination, path, file_object, links=None, allow_overwrite=False, printer_profile=None, analysis=None): if printer_profile is None: printer_profile = self._printer_profile_manager.get_current_or_default() file_path = self._storage(destination).add_file(path, file_object, links=links, printer_profile=printer_profile, allow_overwrite=allow_overwrite) absolute_path = self._storage(destination).path_on_disk(file_path) if analysis is None: file_type = get_file_type(absolute_path) if file_type: queue_entry = QueueEntry(file_path, file_type[-1], destination, absolute_path, printer_profile) self._analysis_queue.enqueue(queue_entry, high_priority=True) else: self._add_analysis_result(destination, path, analysis) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) return file_path def remove_file(self, destination, path): self._storage(destination).remove_file(path) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def add_folder(self, destination, path, ignore_existing=True): folder_path = self._storage(destination).add_folder(path, ignore_existing=ignore_existing) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) return folder_path def remove_folder(self, destination, path, recursive=True): self._storage(destination).remove_folder(path, recursive=recursive) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) def get_metadata(self, destination, path): return self._storage(destination).get_metadata(path) def add_link(self, destination, path, rel, data): self._storage(destination).add_link(path, rel, data) def remove_link(self, destination, path, rel, data): self._storage(destination).remove_link(path, rel, data) def log_print(self, destination, path, timestamp, print_time, success, printer_profile): try: if success: self._storage(destination).add_history(path, dict(timestamp=timestamp, printTime=print_time, success=success, printerProfile=printer_profile)) else: self._storage(destination).add_history(path, dict(timestamp=timestamp, success=success, printerProfile=printer_profile)) eventManager().fire(Events.METADATA_STATISTICS_UPDATED, dict(storage=destination, path=path)) except NoSuchStorage: # if there's no storage configured where to log the print, we'll just not log it pass def set_additional_metadata(self, destination, path, key, data, overwrite=False, merge=False): self._storage(destination).set_additional_metadata(path, key, data, overwrite=overwrite, merge=merge) def remove_additional_metadata(self, destination, path, key): self._storage(destination).remove_additional_metadata(path, key) def path_on_disk(self, destination, path): return self._storage(destination).path_on_disk(path) def sanitize(self, destination, path): return self._storage(destination).sanitize(path) def sanitize_name(self, destination, name): return self._storage(destination).sanitize_name(name) def sanitize_path(self, destination, path): return self._storage(destination).sanitize_path(path) def split_path(self, destination, path): return self._storage(destination).split_path(path) def join_path(self, destination, *path): return self._storage(destination).join_path(*path) def path_in_storage(self, destination, path): return self._storage(destination).path_in_storage(path) def _storage(self, destination): if not destination in self._storage_managers: raise NoSuchStorage("No storage configured for destination {destination}".format(**locals())) return self._storage_managers[destination] def _add_analysis_result(self, destination, path, result): if not destination in self._storage_managers: return storage_manager = self._storage_managers[destination] storage_manager.set_additional_metadata(path, "analysis", result) def _on_analysis_finished(self, entry, result): self._add_analysis_result(entry.location, entry.path, result)