# -*- coding: utf-8 -*-
# Licensed under the EUPL v1.2
# © 2020 bicobus <bicobus@keemail.me>
import logging
from abc import ABC, abstractmethod
import enum
from typing import Dict, Generator, Iterable, List, Tuple, Union
from qmm import bucket
from qmm.fileutils import FILE_IGNORED, FILE_MATCHED, FILE_MISMATCHED, FILE_MISSING, file_status
logger = logging.getLogger(__name__)
@enum.unique
class ArchiveType(enum.IntEnum):
FILE = enum.auto()
VIRTUAL = enum.auto()
class ABCArchiveInstance(ABC):
_conflicts: Dict[str, List[Union[str, bucket.FileMetadata]]]
_meta: List[Tuple[bucket.FileMetadata, int]]
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 = {}
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)))
@abstractmethod
def reset_conflicts(self):
pass
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
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
@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] == FILE_MATCHED, self._meta):
yield item[0]
@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] == FILE_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
@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] == FILE_MISSING, self._meta):
yield item[0]
@abstractmethod
def ignored(self) -> Iterable[bucket.FileMetadata]:
"""Yield file metadata of ignored entries of the archive."""
for item in filter(lambda x: x[1] == FILE_IGNORED, self._meta):
yield item[0]
@abstractmethod
def conflicts(self):
"""Yield :py:attr:`FileMetadata` of conflicting entries of the archive."""
for path, archives in self._conflicts.items():
yield path, archives
@abstractmethod
def uninstall_info(self):
"""Informations necessary to the uninstall function."""
@abstractmethod
def install_info(self):
pass
def status(self) -> Generator[Tuple[bucket.FileMetadata, int], None, None]:
for name, status in self._meta:
yield name, status
def find(self, fmd):
"""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
"""
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
def find_metadata_by_path(self, path):
for item in filter(lambda x: x[0].path == path, self._meta):
return item
return None
def get_status(self, file):
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(FILE_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 (FILE_MATCHED, FILE_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(FILE_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(FILE_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(FILE_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] == FILE_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)