Source code for audformat.core.attachment

import os
import typing

import audeer

from audformat.core.common import HeaderBase
from audformat.core.common import is_relative_path


[docs]class Attachment(HeaderBase): r"""Database attachment. Adds a file or folder as attachment to a database. If a folder is provided, all of its sub-folders and files are included. Args: path: relative path to file or folder description: attachment description meta: additional meta fields Raises: ValueError: if ``path`` is absolute or contains ``\``, ``..`` or ``.`` RuntimeError: when assigning an attachment to a database, but the database contains another attachment with an path that is identical or nested compared to the current attachment path Examples: >>> Attachment("file.txt", description="Attached file") {description: Attached file, path: file.txt} """ def __init__( self, path: str, *, description: str = None, meta: dict = None, ): super().__init__(description=description, meta=meta) if not is_relative_path(path): raise ValueError( f"The provided path '{path}' needs to be relative " "and not contain '\\', '.', or '..'." ) self._db = None self._id = None self.path = path r"""Attachment path""" @property def files( self, ) -> typing.List[str]: r"""List all files that are part of the attachment. List recursively the relative path of all files that exist under :attr:`audformat.Attachment.path` on hard disk. Raises: FileNotFoundError: if a path associated with the attachment cannot be found RuntimeError: if a path associated with the attachment is a symlink RuntimeError: if attachment is not part of a database RuntimeError: if database is not saved to disk """ if not self._db: raise RuntimeError( "The attachment needs to be assigned to a database " "before attached files can be listed." ) if not self._db.root: raise RuntimeError( "The database needs to be saved to disk " "before attachment files can be listed." ) self._check_path(self._db.root) files = [] path = audeer.path(self._db.root, self.path) if os.path.isdir(path): files = audeer.list_file_names( path, recursive=True, hidden=True, ) dirs = audeer.list_dir_names( path, recursive=True, hidden=True, ) for path in files + dirs: if os.path.islink(path): raise RuntimeError( f"The path '{path}' " f"included in attachment '{self._id}' " "must not be a symlink." ) else: files = [path] # Remove absolute path files = [f.replace(f"{self._db.root}{os.path.sep}", "") for f in files] # Make sure we use `/` as sep files = [f.replace(os.path.sep, "/") for f in files] return files def _check_overlap( self, other: str, ): r"""Check if two attachment paths are nested.""" if ( self.path == other.path or self.path.startswith(other.path) or other.path.startswith(self.path) ): raise RuntimeError( f"Attachments '{self.path}' and '{other.path}' " "are nested." ) def _check_path( self, root: str, ): r"""Check if path exists and is not a symlink.""" if not os.path.exists(audeer.path(root, self.path, follow_symlink=True)): raise FileNotFoundError( f"The provided path '{self.path}' " f"of attachment '{self._id}' " "does not exist." ) if os.path.islink(os.path.join(root, self.path)): raise RuntimeError( f"The provided path '{self.path}' " f"of attachment '{self._id}' " "must not be a symlink." )