More documentation and unit tests
This commit is contained in:
parent
258b824ff8
commit
28738a5179
11 changed files with 510 additions and 238 deletions
10
docs/development/index.rst
Normal file
10
docs/development/index.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.. _sec-development:
|
||||||
|
|
||||||
|
###########
|
||||||
|
Development
|
||||||
|
###########
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
interface/index.rst
|
||||||
35
docs/development/interface/filemanager.rst
Normal file
35
docs/development/interface/filemanager.rst
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
.. _sec-development-interface-filemanager:
|
||||||
|
|
||||||
|
``octoprint.filemanager``
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.filemanager
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. _sec-development-interface-filemanager-analysis:
|
||||||
|
|
||||||
|
``octoprint.filemanager.analysis``
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.filemanager.analysis
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. _sec-development-interface-filemanager-destinations:
|
||||||
|
|
||||||
|
``octoprint.filemanager.destinations``
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.filemanager.destinations
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. _sec-development-interface-filemanager-storage:
|
||||||
|
|
||||||
|
``octoprint.filemanager.storage``
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.filemanager.storage
|
||||||
|
:members: StorageInterface, LocalFileStorage
|
||||||
|
|
||||||
11
docs/development/interface/index.rst
Normal file
11
docs/development/interface/index.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.. _sec-development-interface:
|
||||||
|
|
||||||
|
Interfaces
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
filemanager.rst
|
||||||
|
plugin.rst
|
||||||
|
printer.rst
|
||||||
8
docs/development/interface/plugin.rst
Normal file
8
docs/development/interface/plugin.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.. _sec-development-interface-plugin:
|
||||||
|
|
||||||
|
``octoprint.plugin``
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.plugin
|
||||||
|
:members: plugin_manager, plugin_settings, call_plugin, PluginSettings
|
||||||
|
:undoc-members:
|
||||||
8
docs/development/interface/printer.rst
Normal file
8
docs/development/interface/printer.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.. _sec-development-interface-plugin:
|
||||||
|
|
||||||
|
``octoprint.printer``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: octoprint.printer
|
||||||
|
:members: Printer
|
||||||
|
:undoc-members:
|
||||||
|
|
@ -19,4 +19,5 @@ Contents
|
||||||
api/index.rst
|
api/index.rst
|
||||||
events/index.rst
|
events/index.rst
|
||||||
plugins/index.rst
|
plugins/index.rst
|
||||||
|
development/index.rst
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ class FileManager(object):
|
||||||
|
|
||||||
def slice(self, slicer_name, source_location, source_path, dest_location, dest_path,
|
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):
|
position=None, profile=None, printer_profile_id=None, overrides=None, callback=None, callback_args=None):
|
||||||
absolute_source_path = self.get_absolute_path(source_location, source_path)
|
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):
|
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:
|
try:
|
||||||
|
|
@ -291,7 +291,7 @@ class FileManager(object):
|
||||||
printer_profile = self._printer_profile_manager.get_current_or_default()
|
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)
|
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).get_absolute_path(file_path)
|
absolute_path = self._storage(destination).path_on_disk(file_path)
|
||||||
|
|
||||||
if analysis is None:
|
if analysis is None:
|
||||||
file_type = get_file_type(absolute_path)
|
file_type = get_file_type(absolute_path)
|
||||||
|
|
@ -343,8 +343,8 @@ class FileManager(object):
|
||||||
def remove_additional_metadata(self, destination, path, key):
|
def remove_additional_metadata(self, destination, path, key):
|
||||||
self._storage(destination).remove_additional_metadata(path, key)
|
self._storage(destination).remove_additional_metadata(path, key)
|
||||||
|
|
||||||
def get_absolute_path(self, destination, path):
|
def path_on_disk(self, destination, path):
|
||||||
return self._storage(destination).get_absolute_path(path)
|
return self._storage(destination).path_on_disk(path)
|
||||||
|
|
||||||
def sanitize(self, destination, path):
|
def sanitize(self, destination, path):
|
||||||
return self._storage(destination).sanitize(path)
|
return self._storage(destination).sanitize(path)
|
||||||
|
|
@ -361,8 +361,8 @@ class FileManager(object):
|
||||||
def join_path(self, destination, *path):
|
def join_path(self, destination, *path):
|
||||||
return self._storage(destination).join_path(*path)
|
return self._storage(destination).join_path(*path)
|
||||||
|
|
||||||
def rel_path(self, destination, path):
|
def path_in_storage(self, destination, path):
|
||||||
return self._storage(destination).rel_path(path)
|
return self._storage(destination).path_in_storage(path)
|
||||||
|
|
||||||
def _storage(self, destination):
|
def _storage(self, destination):
|
||||||
if not destination in self._storage_managers:
|
if not destination in self._storage_managers:
|
||||||
|
|
|
||||||
|
|
@ -15,73 +15,289 @@ import octoprint.filemanager
|
||||||
|
|
||||||
from octoprint.util import safeRename
|
from octoprint.util import safeRename
|
||||||
|
|
||||||
|
|
||||||
class StorageInterface(object):
|
class StorageInterface(object):
|
||||||
|
"""
|
||||||
|
Interface of storage adapters for OctoPrint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def analysis_backlog(self):
|
def analysis_backlog(self):
|
||||||
|
"""
|
||||||
|
Get an iterator over all items stored in the storage that need to be analysed by the :class:`~octoprint.filemanager.AnalysisQueue`.
|
||||||
|
|
||||||
|
The yielded elements are expected as storage specific absolute paths to the respective files. Don't forget
|
||||||
|
to recurse into folders if your storage adapter supports those.
|
||||||
|
|
||||||
|
:return: an iterator yielding all un-analysed files in the storage
|
||||||
|
"""
|
||||||
# empty generator pattern, yield is intentionally unreachable
|
# empty generator pattern, yield is intentionally unreachable
|
||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def file_exists(self, path):
|
def file_exists(self, path):
|
||||||
|
"""
|
||||||
|
Returns whether the file indicated by ``path`` exists or not.
|
||||||
|
:param string path: the path to check for existence
|
||||||
|
:return: ``True`` if the file exists, ``False`` otherwise
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def list_files(self, path=None, filter=None, recursive=True):
|
def list_files(self, path=None, filter=None, recursive=True):
|
||||||
|
"""
|
||||||
|
List all files in storage starting at ``path``. If ``recursive`` is set to True (the default), also dives into
|
||||||
|
subfolders.
|
||||||
|
|
||||||
|
An optional filter function can be supplied which will be called with a file name and file data and which has
|
||||||
|
to return True if the file is to be included in the result or False if not.
|
||||||
|
|
||||||
|
The data structure of the returned result will be a dictionary mapping from file names to entry data. File nodes
|
||||||
|
will contain their metadata here, folder nodes will contain their contained files and folders. Example::
|
||||||
|
|
||||||
|
{
|
||||||
|
"some_folder": {
|
||||||
|
"type": "folder",
|
||||||
|
"children": {
|
||||||
|
"some_sub_folder": {
|
||||||
|
"type": "folder",
|
||||||
|
"children": { ... }
|
||||||
|
},
|
||||||
|
"some_file.gcode": {
|
||||||
|
"type": "machinecode",
|
||||||
|
"hash": "<sha1 hash>",
|
||||||
|
"links": [ ... ],
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"test.gcode": {
|
||||||
|
"type": "machinecode",
|
||||||
|
"hash": "<sha1 hash>",
|
||||||
|
"links": [...],
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"test.stl": {
|
||||||
|
"type": "model",
|
||||||
|
"hash": "<sha1 hash>",
|
||||||
|
"links": [...],
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
:param string path: base path from which to recursively list all files, optional, if not supplied listing will start
|
||||||
|
from root of base folder
|
||||||
|
:param function filter: a filter that matches the files that are to be returned, may be left out in which case no
|
||||||
|
filtering will take place
|
||||||
|
:param bool recursive: will also step into sub folders for building the complete list if set to True
|
||||||
|
:return: a dictionary mapping entry names to entry data that represents the whole file list
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def add_folder(self, path, ignore_existing=True):
|
def add_folder(self, path, ignore_existing=True):
|
||||||
|
"""
|
||||||
|
Adds a folder as ``path``. The ``path`` will be sanitized.
|
||||||
|
|
||||||
|
:param string path: the path of the new folder
|
||||||
|
:param bool ignore_existing: if set to True, no error will be raised if the folder to be added already exists
|
||||||
|
:return: the sanitized name of the new folder to be used for future references to the folder
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def remove_folder(self, path, recursive=True):
|
def remove_folder(self, path, recursive=True):
|
||||||
|
"""
|
||||||
|
Removes the folder at ``path``.
|
||||||
|
|
||||||
|
:param string path: the path of the folder to remove
|
||||||
|
:param bool recursive: if set to True, contained folders and files will also be removed, otherwise and error will
|
||||||
|
be raised if the folder is not empty (apart from ``.metadata.yaml``) when it's to be removed
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def add_file(self, path, file_object, printer_profile=None, links=None, allow_overwrite=False):
|
def add_file(self, path, file_object, printer_profile=None, links=None, allow_overwrite=False):
|
||||||
|
"""
|
||||||
|
Adds the file ``file_object`` as ``path``
|
||||||
|
|
||||||
|
:param string path: the file's new path, will be sanitized
|
||||||
|
:param object file_object: a file object that provides a ``save`` method which will be called with the destination path
|
||||||
|
where the object should then store its contents
|
||||||
|
:param object printer_profile: the printer profile associated with this file (if any)
|
||||||
|
:param list links: any links to add with the file
|
||||||
|
:param bool allow_overwrite: if set to True no error will be raised if the file already exists and the existing file
|
||||||
|
and its metadata will just be silently overwritten
|
||||||
|
:return: the sanitized name of the file to be used for future references to it
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def remove_file(self, path):
|
def remove_file(self, path):
|
||||||
|
"""
|
||||||
|
Removes the file at ``path``. Will also take care of deleting the corresponding entries
|
||||||
|
in the metadata and deleting all links pointing to the file.
|
||||||
|
|
||||||
|
:param string path: path of the file to remove
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_metadata(self, path):
|
def get_metadata(self, path):
|
||||||
|
"""
|
||||||
|
Retrieves the metadata for the file ``path``.
|
||||||
|
|
||||||
|
:param path: virtual path to the file for which to retrieve the metadata
|
||||||
|
:return: the metadata associated with the file
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def add_link(self, path, rel, data):
|
def add_link(self, path, rel, data):
|
||||||
|
"""
|
||||||
|
Adds a link of relation ``rel`` to file ``path`` with the given ``data``.
|
||||||
|
|
||||||
|
The following relation types are currently supported:
|
||||||
|
|
||||||
|
* ``model``: adds a link to a model from which the file was created/sliced, expected additional data is the ``name``
|
||||||
|
and optionally the ``hash`` of the file to link to. If the link can be resolved against another file on the
|
||||||
|
current ``path``, not only will it be added to the links of ``name`` but a reverse link of type ``machinecode``
|
||||||
|
refering to ``name`` and its hash will also be added to the linked ``model`` file
|
||||||
|
* ``machinecode``: adds a link to a file containing machine code created from the current file (model), expected
|
||||||
|
additional data is the ``name`` and optionally the ``hash`` of the file to link to. If the link can be resolved
|
||||||
|
against another file on the current ``path``, not only will it be added to the links of ``name`` but a reverse
|
||||||
|
link of type ``model`` refering to ``name`` and its hash will also be added to the linked ``model`` file.
|
||||||
|
* ``web``: adds a location on the web associated with this file (e.g. a website where to download a model),
|
||||||
|
expected additional data is a ``href`` attribute holding the website's URL and optionally a ``retrieved``
|
||||||
|
attribute describing when the content was retrieved
|
||||||
|
|
||||||
|
Note that adding ``model`` links to files identifying as models or ``machinecode`` links to files identifying
|
||||||
|
as machine code will be refused.
|
||||||
|
|
||||||
|
:param path: path of the file for which to add a link
|
||||||
|
:param rel: type of relation of the link to add (currently ``model``, ``machinecode`` and ``web`` are supported)
|
||||||
|
:param data: additional data of the link to add
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def remove_link(self, path, rel, data):
|
def remove_link(self, path, rel, data):
|
||||||
|
"""
|
||||||
|
Removes the link consisting of ``rel`` and ``data`` from file ``name`` on ``path``.
|
||||||
|
|
||||||
|
:param path: path of the file from which to remove the link
|
||||||
|
:param rel: type of relation of the link to remove (currently ``model``, ``machinecode`` and ``web`` are supported)
|
||||||
|
:param data: additional data of the link to remove, must match existing link
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def set_additional_metadata(self, path, key, data, overwrite=False, merge=False):
|
def set_additional_metadata(self, path, key, data, overwrite=False, merge=False):
|
||||||
|
"""
|
||||||
|
Adds additional metadata to the metadata of ``path``. Metadata in ``data`` will be saved under ``key``.
|
||||||
|
|
||||||
|
If ``overwrite`` is set and ``key`` already exists in ``name``'s metadata, the current value will be overwritten.
|
||||||
|
|
||||||
|
If ``merge`` is set and ``key`` already exists and both ``data`` and the existing data under ``key`` are dictionaries,
|
||||||
|
the two dictionaries will be merged recursively.
|
||||||
|
|
||||||
|
:param path: the virtual path to the file for which to add additional metadata
|
||||||
|
:param key: key of metadata to add
|
||||||
|
:param data: metadata to add
|
||||||
|
:param overwrite: if True and ``key`` already exists, it will be overwritten
|
||||||
|
:param merge: if True and ``key`` already exists and both ``data`` and the existing data are dictionaries, they
|
||||||
|
will be merged
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def remove_additional_metadata(self, path, key):
|
def remove_additional_metadata(self, path, key):
|
||||||
|
"""
|
||||||
|
Removes additional metadata under ``key`` for ``name`` on ``path``
|
||||||
|
|
||||||
|
:param path: the virtual path to the file for which to remove the metadata under ``key``
|
||||||
|
:param key: the key to remove
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def sanitize(self, path):
|
def sanitize(self, path):
|
||||||
|
"""
|
||||||
|
Sanitizes the given ``path``, stripping it of all invalid characters. The ``path`` may consist of both
|
||||||
|
folder and file name, the underlying implementation must separate those if necessary and sanitize individually.
|
||||||
|
|
||||||
|
:param string path: the path to sanitize
|
||||||
|
:return: a 2-tuple containing the sanitized path and file name
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def sanitize_path(self, path):
|
def sanitize_path(self, path):
|
||||||
|
"""
|
||||||
|
Sanitizes the given folder-only ``path``, stripping it of all invalid characters.
|
||||||
|
:param string path: the path to sanitize
|
||||||
|
:return: the sanitized path
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def sanitize_name(self, name):
|
def sanitize_name(self, name):
|
||||||
|
"""
|
||||||
|
Sanitizes the given file ``name``, stripping it of all invalid characters.
|
||||||
|
:param string name: the file name to sanitize
|
||||||
|
:return: the sanitized name
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def split_path(self, path):
|
def split_path(self, path):
|
||||||
|
"""
|
||||||
|
Split ``path`` into base directory and file name.
|
||||||
|
:param path: the path to split
|
||||||
|
:return: a tuple (base directory, file name)
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def join_path(self, *path):
|
def join_path(self, *path):
|
||||||
|
"""
|
||||||
|
Join path elements together
|
||||||
|
:param path: path elements to join
|
||||||
|
:return: joined representation of the path to be usable as fully qualified path for further operations
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def rel_path(self, path):
|
def path_on_disk(self, path):
|
||||||
|
"""
|
||||||
|
Retrieves the path on disk for ``path``.
|
||||||
|
|
||||||
|
Note: if the storage is not on disk and there exists no path on disk to refer to it, this method should
|
||||||
|
raise an :class:`io.UnsupportedOperation`
|
||||||
|
|
||||||
|
Opposite of :func:`path_in_storage`.
|
||||||
|
|
||||||
|
:param string path: the virtual path for which to retrieve the path on disk
|
||||||
|
:return: the path on disk to ``path``
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_absolute_path(self, path):
|
def path_in_storage(self, path):
|
||||||
|
"""
|
||||||
|
Retrieves the equivalent in the storage adapter for ``path``.
|
||||||
|
|
||||||
|
Opposite of :func:`path_on_disk`.
|
||||||
|
|
||||||
|
:param string path: the path for which to retrieve the storage path
|
||||||
|
:return: the path in storage to ``path``
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LocalFileStorage(StorageInterface):
|
class LocalFileStorage(StorageInterface):
|
||||||
|
"""
|
||||||
|
The ``LocalFileStorage`` is a storage implementation which holds all files, folders and metadata on disk.
|
||||||
|
|
||||||
|
Metadata is managed inside ``.metadata.yaml`` files in the respective folders, indexed by the sanitized filenames
|
||||||
|
stored within the folder. Metadata access is managed through an LRU cache to minimize access overhead.
|
||||||
|
|
||||||
|
This storage type implements :func:`path_on_disk`.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, basefolder, create=False):
|
def __init__(self, basefolder, create=False):
|
||||||
|
"""
|
||||||
|
Initializes a ``LocalFileStorage`` instance under the given ``basefolder``, creating the necessary folder
|
||||||
|
if necessary and ``create`` is set to ``True``.
|
||||||
|
|
||||||
|
:param string basefolder: the path to the folder under which to create the storage
|
||||||
|
:param bool create: ``True`` if the folder should be created if it doesn't exist yet, ``False`` otherwise
|
||||||
|
"""
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
self.basefolder = os.path.realpath(os.path.abspath(basefolder))
|
self.basefolder = os.path.realpath(os.path.abspath(basefolder))
|
||||||
|
|
@ -131,55 +347,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
return os.path.exists(file_path) and os.path.isfile(file_path)
|
return os.path.exists(file_path) and os.path.isfile(file_path)
|
||||||
|
|
||||||
def list_files(self, path=None, filter=None, recursive=True):
|
def list_files(self, path=None, filter=None, recursive=True):
|
||||||
"""
|
|
||||||
List all files in storage starting at ``path``. If ``recursive`` is set to True (the default), also dives into
|
|
||||||
subfolders.
|
|
||||||
|
|
||||||
An optional filter function can be supplied which will be called with a file name and file data and which has
|
|
||||||
to return True if the file is to be included in the result or False if not.
|
|
||||||
|
|
||||||
The data structure of the returned result will be a dictionary mapping from file names to entry data. File nodes
|
|
||||||
will contain their metadata here, folder nodes will contain their contained files and folders. Example::
|
|
||||||
|
|
||||||
{
|
|
||||||
"some_folder": {
|
|
||||||
"type": "folder",
|
|
||||||
"children": {
|
|
||||||
"some_sub_folder": {
|
|
||||||
"type": "folder",
|
|
||||||
"children": { ... }
|
|
||||||
},
|
|
||||||
"some_file.gcode": {
|
|
||||||
"type": "machinecode",
|
|
||||||
"hash": "<sha1 hash>",
|
|
||||||
"links": [ ... ],
|
|
||||||
...
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
"test.gcode": {
|
|
||||||
"type": "machinecode",
|
|
||||||
"hash": "<sha1 hash>",
|
|
||||||
"links": [...],
|
|
||||||
...
|
|
||||||
},
|
|
||||||
"test.stl": {
|
|
||||||
"type": "model",
|
|
||||||
"hash": "<sha1 hash>",
|
|
||||||
"links": [...],
|
|
||||||
...
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
:param path: base path from which to recursively list all files, optional, if not supplied listing will start
|
|
||||||
from root of base folder
|
|
||||||
:param filter: a filter that matches the files that are to be returned, may be left out in which case no
|
|
||||||
filtering will take place
|
|
||||||
:param recursive: will also step into sub folders for building the complete list if set to True
|
|
||||||
:return: a dictionary mapping entry names to entry data that represents the whole file list
|
|
||||||
"""
|
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
path = self.sanitize_path(path)
|
path = self.sanitize_path(path)
|
||||||
else:
|
else:
|
||||||
|
|
@ -187,14 +354,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
return self._list_folder(path, filter=filter, recursive=recursive)
|
return self._list_folder(path, filter=filter, recursive=recursive)
|
||||||
|
|
||||||
def add_folder(self, path, ignore_existing=True):
|
def add_folder(self, path, ignore_existing=True):
|
||||||
"""
|
|
||||||
Adds a folder as ``path``. The ``path`` will be sanitized.
|
|
||||||
|
|
||||||
:param path: the path of the new folder
|
|
||||||
:param ignore_existing: if set to True, no error will be raised if the folder to be added already exists
|
|
||||||
:return: the sanitized name of the new folder to be used for future references to the folder
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
|
|
||||||
folder_path = os.path.join(path, name)
|
folder_path = os.path.join(path, name)
|
||||||
|
|
@ -204,17 +363,9 @@ class LocalFileStorage(StorageInterface):
|
||||||
else:
|
else:
|
||||||
os.mkdir(folder_path)
|
os.mkdir(folder_path)
|
||||||
|
|
||||||
return self.rel_path((path, name))
|
return self.path_in_storage((path, name))
|
||||||
|
|
||||||
def remove_folder(self, path, recursive=True):
|
def remove_folder(self, path, recursive=True):
|
||||||
"""
|
|
||||||
Removes the folder at ``path``.
|
|
||||||
|
|
||||||
:param path: the path of the folder to remove
|
|
||||||
:param recursive: if set to True, contained folders and files will also be removed, otherwise and error will
|
|
||||||
be raised if the folder is not empty (apart from ``.metadata.yaml``) when it's to be removed
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
|
|
||||||
folder_path = os.path.join(path, name)
|
folder_path = os.path.join(path, name)
|
||||||
|
|
@ -231,19 +382,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
shutil.rmtree(folder_path)
|
shutil.rmtree(folder_path)
|
||||||
|
|
||||||
def add_file(self, path, file_object, printer_profile=None, links=None, allow_overwrite=False):
|
def add_file(self, path, file_object, printer_profile=None, links=None, allow_overwrite=False):
|
||||||
"""
|
|
||||||
Adds the file ``file_object`` as ``path``
|
|
||||||
|
|
||||||
:param path: the file's new path, will be sanitized
|
|
||||||
:param file_object: a file object that provides a ``save`` method which will be called with the destination path
|
|
||||||
where the object should then store its contents
|
|
||||||
:param printer_profile: the printer profile associated with this file (if any)
|
|
||||||
:param links: any links to add with the file
|
|
||||||
:param allow_overwrite: if set to True no error will be raised if the file already exists and the existing file
|
|
||||||
and its metadata will just be silently overwritten
|
|
||||||
:return: the sanitized name of the file to be used for future references to it
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
if not octoprint.filemanager.valid_file_type(name):
|
if not octoprint.filemanager.valid_file_type(name):
|
||||||
raise RuntimeError("{name} is an unrecognized file type".format(**locals()))
|
raise RuntimeError("{name} is an unrecognized file type".format(**locals()))
|
||||||
|
|
@ -284,16 +422,9 @@ class LocalFileStorage(StorageInterface):
|
||||||
|
|
||||||
self._add_links(name, path, links)
|
self._add_links(name, path, links)
|
||||||
|
|
||||||
return self.rel_path((path, name))
|
return self.path_in_storage((path, name))
|
||||||
|
|
||||||
def remove_file(self, path):
|
def remove_file(self, path):
|
||||||
"""
|
|
||||||
Removes the file at ``path``. Will also take care of deleting the corresponding entries
|
|
||||||
in the metadata and deleting all links pointing to the file.
|
|
||||||
|
|
||||||
:param path: path of the file to remove
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
|
|
||||||
metadata = self._get_metadata(path)
|
metadata = self._get_metadata(path)
|
||||||
|
|
@ -322,13 +453,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
self._save_metadata(path, metadata)
|
self._save_metadata(path, metadata)
|
||||||
|
|
||||||
def get_metadata(self, path):
|
def get_metadata(self, path):
|
||||||
"""
|
|
||||||
Retrieves the metadata for the file ``path``.
|
|
||||||
|
|
||||||
:param path: virtual path to the file for which to retrieve the metadata
|
|
||||||
:return: the metadata associated with the file
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
|
|
||||||
metadata = self._get_metadata(path)
|
metadata = self._get_metadata(path)
|
||||||
|
|
@ -343,43 +467,10 @@ class LocalFileStorage(StorageInterface):
|
||||||
|
|
||||||
|
|
||||||
def add_link(self, path, rel, data):
|
def add_link(self, path, rel, data):
|
||||||
"""
|
|
||||||
Adds a link of relation ``rel`` to file ``path`` with the given ``data``.
|
|
||||||
|
|
||||||
The following relation types are currently supported:
|
|
||||||
|
|
||||||
* ``model``: adds a link to a model from which the file was created/sliced, expected additional data is the ``name``
|
|
||||||
and optionally the ``hash`` of the file to link to. If the link can be resolved against another file on the
|
|
||||||
current ``path``, not only will it be added to the links of ``name`` but a reverse link of type ``machinecode``
|
|
||||||
refering to ``name`` and its hash will also be added to the linked ``model`` file
|
|
||||||
* ``machinecode``: adds a link to a file containing machine code created from the current file (model), expected
|
|
||||||
additional data is the ``name`` and optionally the ``hash`` of the file to link to. If the link can be resolved
|
|
||||||
against another file on the current ``path``, not only will it be added to the links of ``name`` but a reverse
|
|
||||||
link of type ``model`` refering to ``name`` and its hash will also be added to the linked ``model`` file.
|
|
||||||
* ``web``: adds a location on the web associated with this file (e.g. a website where to download a model),
|
|
||||||
expected additional data is a ``href`` attribute holding the website's URL and optionally a ``retrieved``
|
|
||||||
attribute describing when the content was retrieved
|
|
||||||
|
|
||||||
Note that adding ``model`` links to files identifying as models or ``machinecode`` links to files identifying
|
|
||||||
as machine code will be refused.
|
|
||||||
|
|
||||||
:param path: path of the file for which to add a link
|
|
||||||
:param rel: type of relation of the link to add (currently ``model``, ``machinecode`` and ``web`` are supported)
|
|
||||||
:param data: additional data of the link to add
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
self._add_links(name, path, [(rel, data)])
|
self._add_links(name, path, [(rel, data)])
|
||||||
|
|
||||||
def remove_link(self, path, rel, data):
|
def remove_link(self, path, rel, data):
|
||||||
"""
|
|
||||||
Removes the link consisting of ``rel`` and ``data`` from file ``name`` on ``path``.
|
|
||||||
|
|
||||||
:param path: path of the file from which to remove the link
|
|
||||||
:param rel: type of relation of the link to remove (currently ``model``, ``machinecode`` and ``web`` are supported)
|
|
||||||
:param data: additional data of the link to remove, must match existing link
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
self._remove_links(name, path, [(rel, data)])
|
self._remove_links(name, path, [(rel, data)])
|
||||||
|
|
||||||
|
|
@ -396,22 +487,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
self._update_history(name, path, index)
|
self._update_history(name, path, index)
|
||||||
|
|
||||||
def set_additional_metadata(self, path, key, data, overwrite=False, merge=False):
|
def set_additional_metadata(self, path, key, data, overwrite=False, merge=False):
|
||||||
"""
|
|
||||||
Adds additional metadata to the metadata of ``path``. Metadata in ``data`` will be saved under ``key``.
|
|
||||||
|
|
||||||
If ``overwrite`` is set and ``key`` already exists in ``name``'s metadata, the current value will be overwritten.
|
|
||||||
|
|
||||||
If ``merge`` is set and ``key`` already exists and both ``data`` and the existing data under ``key`` are dictionaries,
|
|
||||||
the two dictionaries will be merged recursively.
|
|
||||||
|
|
||||||
:param path: the virtual path to the file for which to add additional metadata
|
|
||||||
:param key: key of metadata to add
|
|
||||||
:param data: metadata to add
|
|
||||||
:param overwrite: if True and ``key`` already exists, it will be overwritten
|
|
||||||
:param merge: if True and ``key`` already exists and both ``data`` and the existing data are dictionaries, they
|
|
||||||
will be merged
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
metadata = self._get_metadata(path)
|
metadata = self._get_metadata(path)
|
||||||
metadata_dirty = False
|
metadata_dirty = False
|
||||||
|
|
@ -437,13 +512,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
self._save_metadata(path, metadata)
|
self._save_metadata(path, metadata)
|
||||||
|
|
||||||
def remove_additional_metadata(self, path, key):
|
def remove_additional_metadata(self, path, key):
|
||||||
"""
|
|
||||||
Removes additional metadata under ``key`` for ``name`` on ``path``
|
|
||||||
|
|
||||||
:param path: the virtual path to the file for which to remove the metadata under ``key``
|
|
||||||
:param key: the key to remove
|
|
||||||
"""
|
|
||||||
|
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
metadata = self._get_metadata(path)
|
metadata = self._get_metadata(path)
|
||||||
|
|
||||||
|
|
@ -457,13 +525,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
self._save_metadata(path, metadata)
|
self._save_metadata(path, metadata)
|
||||||
|
|
||||||
def split_path(self, path):
|
def split_path(self, path):
|
||||||
"""
|
|
||||||
Split ``path`` into base directory and file name.
|
|
||||||
|
|
||||||
:param path: the path to split
|
|
||||||
:return: a tuple (base directory, file name)
|
|
||||||
"""
|
|
||||||
|
|
||||||
split = path.split("/")
|
split = path.split("/")
|
||||||
if len(split) == 1:
|
if len(split) == 1:
|
||||||
return "", split[0]
|
return "", split[0]
|
||||||
|
|
@ -471,21 +532,143 @@ class LocalFileStorage(StorageInterface):
|
||||||
return self.join_path(*split[:-1]), split[-1]
|
return self.join_path(*split[:-1]), split[-1]
|
||||||
|
|
||||||
def join_path(self, *path):
|
def join_path(self, *path):
|
||||||
"""
|
|
||||||
Join path elements together
|
|
||||||
:param path: path elements to join
|
|
||||||
:return: joined representation of the path to be usable as fully qualified path for further operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
return "/".join(path)
|
return "/".join(path)
|
||||||
|
|
||||||
def get_absolute_path(self, path):
|
def sanitize(self, path):
|
||||||
"""
|
"""
|
||||||
Retrieves the absolute path on disk for ``path``
|
Returns a ``(path, name)`` tuple derived from the provided ``path``.
|
||||||
|
|
||||||
:param path: the virtual path for which to retrieve the absolute path on disk
|
``path`` may be:
|
||||||
:return: the absolute path on disk to ``path``
|
* a storage path
|
||||||
|
* an absolute file system path
|
||||||
|
* a tuple or list containing all individual path elements
|
||||||
|
* a string representation of the path
|
||||||
|
* with or without a file name
|
||||||
|
|
||||||
|
Note that for a ``path`` without a trailing slash the last part will be considered a file name and
|
||||||
|
hence be returned at second position. If you only need to convert a folder path, be sure to
|
||||||
|
include a trailing slash for a string ``path`` or an empty last element for a list ``path``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
>>> storage = LocalFileStorage("/some/base/folder")
|
||||||
|
>>> storage.sanitize("some/folder/and/some file.gco")
|
||||||
|
("/some/base/folder/some/folder/and", "some_file.gco")
|
||||||
|
>>> storage.sanitize(("some", "folder", "and", "some file.gco"))
|
||||||
|
("/some/base/folder/some/folder/and", "some_file.gco")
|
||||||
|
>>> storage.sanitize("some file.gco")
|
||||||
|
("/some/base/folder", "some_file.gco")
|
||||||
|
>>> storage.sanitize(("some file.gco",))
|
||||||
|
("/some/base/folder", "some_file.gco")
|
||||||
|
>>> storage.sanitize("")
|
||||||
|
("/some/base/folder", "")
|
||||||
|
>>> storage.sanitize("some/folder/with/trailing/slash/")
|
||||||
|
("/some/base/folder/some/folder/with/trailing/slash", "")
|
||||||
|
>>> storage.sanitize("some", "folder", "")
|
||||||
|
("/some/base/folder/some/folder", "")
|
||||||
"""
|
"""
|
||||||
|
name = None
|
||||||
|
if isinstance(path, (str, unicode, basestring)):
|
||||||
|
if path.startswith(self.basefolder):
|
||||||
|
path = path[len(self.basefolder):]
|
||||||
|
path = path.replace(os.path.sep, "/")
|
||||||
|
path = path.split("/")
|
||||||
|
if isinstance(path, (list, tuple)):
|
||||||
|
if len(path) == 1:
|
||||||
|
name = path[0]
|
||||||
|
path = "/"
|
||||||
|
else:
|
||||||
|
name = path[-1]
|
||||||
|
path = "/" + self.join_path(*path[:-1])
|
||||||
|
if not path:
|
||||||
|
path = "/"
|
||||||
|
|
||||||
|
name = self.sanitize_name(name)
|
||||||
|
path = self.sanitize_path(path)
|
||||||
|
return path, name
|
||||||
|
|
||||||
|
def sanitize_name(self, name):
|
||||||
|
"""
|
||||||
|
Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise strips any characters from the
|
||||||
|
given ``name`` that are not any of the ASCII characters, digits, ``-``, ``_``, ``.``, ``(``, ``)`` or space and
|
||||||
|
replaces and spaces with ``_``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
>>> storage = LocalFileStorage("/some/base/folder")
|
||||||
|
>>> storage.sanitize_name("some_file.gco")
|
||||||
|
"some_file.gco"
|
||||||
|
>>> storage.sanitize_name("some_file with (parentheses) and ümläuts and digits 123.gco")
|
||||||
|
"some_file_with_(parentheses)_and_mluts_and_digits_123.gco"
|
||||||
|
>>> storage.sanitize_name("pengüino pequeño.stl")
|
||||||
|
"pengino_pequeo.stl"
|
||||||
|
>>> storage.sanitize_name("some/folder/still/left.gco")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
ValueError: name must not contain / or \
|
||||||
|
>>> storage.sanitize_name("also\\no\\backslashes.gco")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
ValueError: name must not contain / or \
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "/" in name or "\\" in name:
|
||||||
|
raise ValueError("name must not contain / or \\")
|
||||||
|
|
||||||
|
import string
|
||||||
|
valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits)
|
||||||
|
sanitized_name = ''.join(c for c in name if c in valid_chars)
|
||||||
|
sanitized_name = sanitized_name.replace(" ", "_")
|
||||||
|
return sanitized_name
|
||||||
|
|
||||||
|
def sanitize_path(self, path):
|
||||||
|
"""
|
||||||
|
Ensures that the on disk representation of ``path`` is located under the configured basefolder. Resolves all
|
||||||
|
relative path elements (e.g. ``..``) and sanitizes folder names using :func:`sanitize_name`. Final path is the
|
||||||
|
absolute path including leading ``basefolder`` path.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
>>> storage = LocalFileStorage("/some/base/folder")
|
||||||
|
>>> storage.sanitize_path("folder/with/subfolder")
|
||||||
|
"/some/base/folder/folder/with/subfolder"
|
||||||
|
>>> storage.sanitize_path("folder/with/subfolder/../other/folder")
|
||||||
|
"/some/base/folder/folder/with/other/folder"
|
||||||
|
>>> storage.sanitize_path("/folder/with/leading/slash")
|
||||||
|
"/some/base/folder/folder/with/leading/slash"
|
||||||
|
>>> storage.sanitize_path(".folder/with/leading/dot")
|
||||||
|
"/some/base/folder/folder/with/leading/dot
|
||||||
|
>>> storage.sanitize_path("../../folder/out/of/the/basefolder")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
ValueError: path not contained in base folder: /some/folder/out/of/the/basefolder
|
||||||
|
"""
|
||||||
|
if path[0] == "/" or path[0] == ".":
|
||||||
|
path = path[1:]
|
||||||
|
path_elements = path.split("/")
|
||||||
|
joined_path = self.basefolder
|
||||||
|
for path_element in path_elements:
|
||||||
|
joined_path = os.path.join(joined_path, self.sanitize_name(path_element))
|
||||||
|
path = os.path.realpath(joined_path)
|
||||||
|
if not path.startswith(self.basefolder):
|
||||||
|
raise ValueError("path not contained in base folder: {path}".format(**locals()))
|
||||||
|
return path
|
||||||
|
|
||||||
|
def path_in_storage(self, path):
|
||||||
|
if isinstance(path, (tuple, list)):
|
||||||
|
path = self.join_path(*path)
|
||||||
|
if isinstance(path, (str, unicode, basestring)):
|
||||||
|
if path.startswith(self.basefolder):
|
||||||
|
path = path[len(self.basefolder):]
|
||||||
|
path = path.replace(os.path.sep, "/")
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def path_on_disk(self, path):
|
||||||
path, name = self.sanitize(path)
|
path, name = self.sanitize(path)
|
||||||
return os.path.join(path, name)
|
return os.path.join(path, name)
|
||||||
|
|
||||||
|
|
@ -791,64 +974,6 @@ class LocalFileStorage(StorageInterface):
|
||||||
|
|
||||||
return hash.hexdigest()
|
return hash.hexdigest()
|
||||||
|
|
||||||
def sanitize(self, path):
|
|
||||||
name = None
|
|
||||||
if isinstance(path, (str, unicode, basestring)):
|
|
||||||
if path.startswith(self.basefolder):
|
|
||||||
path = path[len(self.basefolder):]
|
|
||||||
path = path.replace(os.path.sep, "/")
|
|
||||||
path = path.split("/")
|
|
||||||
if isinstance(path, (list, tuple)):
|
|
||||||
if len(path) == 1:
|
|
||||||
name = path[0]
|
|
||||||
path = "/"
|
|
||||||
else:
|
|
||||||
name = path[-1]
|
|
||||||
path = "/" + self.join_path(*path[:-1])
|
|
||||||
if not path:
|
|
||||||
path = "/"
|
|
||||||
|
|
||||||
name = self.sanitize_name(name)
|
|
||||||
path = self.sanitize_path(path)
|
|
||||||
return path, name
|
|
||||||
|
|
||||||
def sanitize_name(self, name):
|
|
||||||
if name is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "/" in name or "\\" in name:
|
|
||||||
raise ValueError("name must not contain / or \\")
|
|
||||||
|
|
||||||
import string
|
|
||||||
valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits)
|
|
||||||
sanitized_name = ''.join(c for c in name if c in valid_chars)
|
|
||||||
sanitized_name = sanitized_name.replace(" ", "_")
|
|
||||||
return sanitized_name
|
|
||||||
|
|
||||||
def sanitize_path(self, path):
|
|
||||||
if path[0] == "/" or path[0] == ".":
|
|
||||||
path = path[1:]
|
|
||||||
path_elements = path.split("/")
|
|
||||||
joined_path = self.basefolder
|
|
||||||
for path_element in path_elements:
|
|
||||||
joined_path = os.path.join(joined_path, self.sanitize_name(path_element))
|
|
||||||
path = os.path.realpath(joined_path)
|
|
||||||
if not path.startswith(self.basefolder):
|
|
||||||
raise ValueError("path not contained in base folder: {path}".format(**locals()))
|
|
||||||
return path
|
|
||||||
|
|
||||||
def rel_path(self, path):
|
|
||||||
if isinstance(path, (tuple, list)):
|
|
||||||
path = self.join_path(*path)
|
|
||||||
if isinstance(path, (str, unicode, basestring)):
|
|
||||||
if path.startswith(self.basefolder):
|
|
||||||
path = path[len(self.basefolder):]
|
|
||||||
path = path.replace(os.path.sep, "/")
|
|
||||||
if path.startswith("/"):
|
|
||||||
path = path[1:]
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _get_metadata(self, path):
|
def _get_metadata(self, path):
|
||||||
if path in self._metadata_cache:
|
if path in self._metadata_cache:
|
||||||
return self._metadata_cache[path]
|
return self._metadata_cache[path]
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ def uploadGcodeFile(target):
|
||||||
filename = added_file
|
filename = added_file
|
||||||
done = True
|
done = True
|
||||||
else:
|
else:
|
||||||
filename = fileProcessingFinished(added_file, fileManager.get_absolute_path(FileDestinations.LOCAL, added_file), target)
|
filename = fileProcessingFinished(added_file, fileManager.path_on_disk(FileDestinations.LOCAL, added_file), target)
|
||||||
done = True
|
done = True
|
||||||
|
|
||||||
sdFilename = None
|
sdFilename = None
|
||||||
|
|
@ -293,7 +293,7 @@ def gcodeFileCommand(filename, target):
|
||||||
filenameToSelect = filename
|
filenameToSelect = filename
|
||||||
sd = True
|
sd = True
|
||||||
else:
|
else:
|
||||||
filenameToSelect = fileManager.get_absolute_path(target, filename)
|
filenameToSelect = fileManager.path_on_disk(target, filename)
|
||||||
printer.selectFile(filenameToSelect, sd, printAfterLoading)
|
printer.selectFile(filenameToSelect, sd, printAfterLoading)
|
||||||
|
|
||||||
elif command == "slice":
|
elif command == "slice":
|
||||||
|
|
@ -371,7 +371,7 @@ def gcodeFileCommand(filename, target):
|
||||||
filenameToSelect = gcode_name
|
filenameToSelect = gcode_name
|
||||||
sd = True
|
sd = True
|
||||||
else:
|
else:
|
||||||
filenameToSelect = fileManager.get_absolute_path(target, gcode_name)
|
filenameToSelect = fileManager.path_on_disk(target, gcode_name)
|
||||||
printer.selectFile(filenameToSelect, sd, print_after_slicing)
|
printer.selectFile(filenameToSelect, sd, print_after_slicing)
|
||||||
|
|
||||||
ok, result = fileManager.slice(slicer, target, filename, target, gcode_name,
|
ok, result = fileManager.slice(slicer, target, filename, target, gcode_name,
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel, slicing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name",
|
"name",
|
||||||
["machinecode"],
|
[],
|
||||||
[["sd", "local"], ["machinecode", "model"]],
|
[["sd", "local"], ["machinecode", "model"]],
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
||||||
import unittest
|
import unittest
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from ddt import ddt, unpack, data
|
||||||
|
|
||||||
import octoprint.filemanager.storage
|
import octoprint.filemanager.storage
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,6 +35,7 @@ FILE_BP_CASE_STL = FileWrapper("bp_case.stl")
|
||||||
FILE_BP_CASE_GCODE = FileWrapper("bp_case.gcode")
|
FILE_BP_CASE_GCODE = FileWrapper("bp_case.gcode")
|
||||||
FILE_CRAZYRADIO_STL = FileWrapper("crazyradio.stl")
|
FILE_CRAZYRADIO_STL = FileWrapper("crazyradio.stl")
|
||||||
|
|
||||||
|
@ddt
|
||||||
class LocalStorageTest(unittest.TestCase):
|
class LocalStorageTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -286,6 +289,77 @@ class LocalStorageTest(unittest.TestCase):
|
||||||
self.assertEquals(0, len(gcode_metadata["links"]))
|
self.assertEquals(0, len(gcode_metadata["links"]))
|
||||||
self.assertEquals(1, len(stl_metadata["links"]))
|
self.assertEquals(1, len(stl_metadata["links"]))
|
||||||
|
|
||||||
|
@data(
|
||||||
|
("some_file.gco", "some_file.gco"),
|
||||||
|
("some_file with (parentheses) and ümläuts and digits 123.gco", "some_file_with_(parentheses)_and_mluts_and_digits_123.gco"),
|
||||||
|
("pengüino pequeño.stl", "pengino_pequeo.stl")
|
||||||
|
)
|
||||||
|
@unpack
|
||||||
|
def test_sanitize_name(self, input, expected):
|
||||||
|
actual = self.storage.sanitize_name(input)
|
||||||
|
self.assertEquals(expected, actual)
|
||||||
|
|
||||||
|
@data(
|
||||||
|
"some/folder/still/left.gco",
|
||||||
|
"also\\no\\backslashes.gco"
|
||||||
|
)
|
||||||
|
def test_sanitize_name_invalid(self, input):
|
||||||
|
try:
|
||||||
|
self.storage.sanitize_name(input)
|
||||||
|
self.fail("expected a ValueError")
|
||||||
|
except ValueError as e:
|
||||||
|
self.assertEquals("name must not contain / or \\", e.message)
|
||||||
|
|
||||||
|
@data(
|
||||||
|
("folder/with/subfolder", "/folder/with/subfolder"),
|
||||||
|
("folder/with/subfolder/../other/folder", "/folder/with/other/folder"),
|
||||||
|
("/folder/with/leading/slash", "/folder/with/leading/slash"),
|
||||||
|
("folder/with/leading/dot", "/folder/with/leading/dot")
|
||||||
|
)
|
||||||
|
@unpack
|
||||||
|
def test_sanitize_path(self, input, expected):
|
||||||
|
actual = self.storage.sanitize_path(input)
|
||||||
|
self.assertTrue(actual.startswith(self.basefolder))
|
||||||
|
self.assertEquals(expected, actual[len(self.basefolder):].replace(os.path.sep, "/"))
|
||||||
|
|
||||||
|
@data(
|
||||||
|
"../../folder/out/of/the/basefolder",
|
||||||
|
"some/folder/../../../and/then/back"
|
||||||
|
)
|
||||||
|
def test_sanitize_path_invalid(self, input):
|
||||||
|
try:
|
||||||
|
self.storage.sanitize_path(input)
|
||||||
|
self.fail("expected a ValueError")
|
||||||
|
except ValueError as e:
|
||||||
|
self.assertTrue(e.message.startswith("path not contained in base folder: "))
|
||||||
|
|
||||||
|
@data(
|
||||||
|
("some/folder/and/some file.gco", "/some/folder/and", "some_file.gco"),
|
||||||
|
(("some", "folder", "and", "some file.gco"), "/some/folder/and", "some_file.gco"),
|
||||||
|
("some file.gco", "/", "some_file.gco"),
|
||||||
|
(("some file.gco",), "/", "some_file.gco"),
|
||||||
|
("", "/", ""),
|
||||||
|
("some/folder/with/trailing/slash/", "/some/folder/with/trailing/slash", ""),
|
||||||
|
(("some", "folder", ""), "/some/folder", "")
|
||||||
|
)
|
||||||
|
@unpack
|
||||||
|
def test_sanitize(self, input, expected_path, expected_name):
|
||||||
|
actual = self.storage.sanitize(input)
|
||||||
|
self.assertTrue(isinstance(actual, tuple))
|
||||||
|
self.assertEquals(2, len(actual))
|
||||||
|
|
||||||
|
actual_path, actual_name = actual
|
||||||
|
self.assertTrue(actual_path.startswith(self.basefolder))
|
||||||
|
actual_path = actual_path[len(self.basefolder):].replace(os.path.sep, "/")
|
||||||
|
if not actual_path.startswith("/"):
|
||||||
|
# if the actual path originally was just the base folder, we just stripped
|
||||||
|
# away everything, so let's add a / again so the behaviour matches the
|
||||||
|
# other preprocessing of our test data here
|
||||||
|
actual_path = "/" + actual_path
|
||||||
|
|
||||||
|
self.assertEquals(expected_path, actual_path)
|
||||||
|
self.assertEquals(expected_name, actual_name)
|
||||||
|
|
||||||
def _add_file(self, path, expected_path, file_object, links=None, overwrite=False):
|
def _add_file(self, path, expected_path, file_object, links=None, overwrite=False):
|
||||||
sanitized_path = self.storage.add_file(path, file_object, links=links, allow_overwrite=overwrite)
|
sanitized_path = self.storage.add_file(path, file_object, links=links, allow_overwrite=overwrite)
|
||||||
split_path = sanitized_path.split("/")
|
split_path = sanitized_path.split("/")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue