Source code for qmm.ab.archives

# -*- coding: utf-8 -*-
#  Licensed under the EUPL v1.2
#  © 2020 bicobus <bicobus@keemail.me>

import logging
from abc import ABC, abstractmethod
from enum import IntEnum, auto, unique
from typing import Dict, Generator, Iterable, List, Tuple, Union

from qmm import bucket
from qmm.fileutils import FileState, file_status

logger = logging.getLogger(__name__)


[docs]@unique class ArchiveType(IntEnum): FILE = auto() VIRTUAL = auto()
[docs]class ABCArchiveInstance(ABC): _conflicts: Dict[str, List[Union[str, bucket.FileMetadata]]] _meta: List[Tuple[bucket.FileMetadata, FileState]] ar_type = None def __init__(self, archive_name, file_list): """Inintialize needed information pertaining to an archive file. Args: archive_name (str or bytes): Name of the file. Bytes is for special cases. file_list (List[bucket.FileMetadata]): List of :py:attr:`FileMetadata` that an archive contains. """ if not self.ar_type: raise ValueError("Object type not defined.") self._archive_name = archive_name self._file_list = file_list # NOTE: folders are not filtered out of meta. self._meta = [] self.reset_status() # Contains a list of archives or, if the conflict is with a game file, # a FileMetadata instance. self._conflicts = {}
[docs] def reset_status(self): """ Called whenever the state of an archive becomes dirty, which is also the default state. Populate 'self._meta' with tuples containing the 'FileMetadata' object of each individual file alongside the current status of that file. The status can be either :py:attr:`FILE_MATCHED`, :py:attr:`FILE_MISMATCHED`, :py:attr:`FILE_IGNORED` or :py:attr:`FILE_MISSING`. """ self._meta = [] for item in self._file_list: self._meta.append((item, file_status(item)))
[docs] @abstractmethod def reset_conflicts(self): pass
[docs] def files(self, exclude_directories=False) -> Generator[bucket.FileMetadata, None, None]: if exclude_directories: for filename in filter(lambda x: not x.is_dir(), self._file_list): yield filename else: for filename in self._file_list: yield filename
[docs] def folders(self) -> Generator[bucket.FileMetadata, None, None]: """Yield folders present in the archive.""" for folder in filter(lambda x: x.is_dir(), sorted(self._file_list, reverse=True)): yield folder
[docs] @abstractmethod def matched(self) -> Generator[bucket.FileMetadata, None, None]: """Yield file metadata of matched entries of the archive.""" for item in filter(lambda x: x[1] == FileState.MATCHED, self._meta): yield item[0]
[docs] @abstractmethod def mismatched(self) -> Generator[bucket.FileMetadata, None, None]: """Yield file metadata of mismatched entries of the archive.""" if not self.has_mismatched: return for item in filter(lambda x: x[1] == FileState.MISMATCHED, self._meta): # File is mismatched against something else, find it and store it for mfile in bucket.loosefiles.values(): for f in filter(lambda x, i=item: x.path == i[0].path, mfile): logger.debug("Found mismatched as '%s'", f) yield f
[docs] @abstractmethod def missing(self) -> Generator[bucket.FileMetadata, None, None]: """Yield file metadata of missing entries of the archive.""" for item in filter(lambda x: x[1] == FileState.MISSING, self._meta): yield item[0]
[docs] @abstractmethod def ignored(self) -> Iterable[bucket.FileMetadata]: """Yield file metadata of ignored entries of the archive.""" for item in filter(lambda x: x[1] == FileState.IGNORED, self._meta): yield item[0]
[docs] @abstractmethod def conflicts(self): """Yield :py:attr:`FileMetadata` of conflicting entries of the archive.""" for path, archives in self._conflicts.items(): yield path, archives
[docs] @abstractmethod def uninstall_info(self): """Informations necessary to the uninstall function."""
[docs] @abstractmethod def install_info(self): pass
[docs] def status(self) -> Generator[Tuple[bucket.FileMetadata, int], None, None]: for name, status in self._meta: yield name, status
[docs] def find(self, fmd: bucket.FileMetadata): """Return a FileMetadata object if managed by the archive. The comparison is done on path and crc, not origin. Args: fmd (FileMetadata): a FileMetadata object Returns: tuple: (FileMetadata, int) """ if not isinstance(fmd, bucket.FileMetadata): raise TypeError(f"path must be FileMetadata, not {type(fmd)}") for item in filter(lambda x: x[0] == fmd, self._meta): return item return None
[docs] def find_metadata_by_path(self, path): for item in filter(lambda x: x[0].path == path, self._meta): return item return None
[docs] def get_status(self, file: bucket.FileMetadata) -> FileState: return self.find(file)[1]
def _has_status(self, status): return any(x[1] == status for x in self._meta) @property def has_matched(self): """Return True if a file of the archive is of status :py:attr:`FILE_MATCHED`.""" return self._has_status(FileState.MATCHED) @property def all_matching(self): """Return `True` if all files in the archive matches on the drive.""" no_directory = filter(lambda x: x[0].attributes != "D", self._meta) return all(x[1] in (FileState.MATCHED, FileState.IGNORED) for x in no_directory) @property def has_mismatched(self): """ Value is `True` if a file of the archive is of status :py:attr:`FILE_MISMATCHED`. """ return self._has_status(FileState.MISMATCHED) @property def has_missing(self): """ Value is `True` if a file of the archive is of status :py:attr:`FILE_MISSING`. """ return self._has_status(FileState.MISSING) @property def has_ignored(self): """ Value is `True` if a file of the archive is of status :py:attr:`FILE_IGNORED`. """ return self._has_status(FileState.IGNORED) @property def all_ignored(self): """ Value is `True` if all files of the archive are of status :py:attr:`FILE_IGNORED`. """ return all(x[1] == FileState.IGNORED or x[0].attributes == "D" for x in self._meta) @property def has_conflicts(self): """Value is `True` if conflicts exists for this archive.""" return bool(self._conflicts) @property def empty(self): """A boolean if wether the archive contains anything useful. list7z will return an empty list if none of the files present in the archive are valid. That could happen for a variety of reasons, like an archive containing only folders or empty files. In those cases, the FileState for the entries won't be set and the software won't know what to ignore. """ return False if self._file_list else True