def __init__(self, uuid: str, archive_agent): """Create the version agent from a version uid. :type archive_agent: ArchiveAgent""" self.uuid, self.archive_agent = uuid, archive_agent self.version_config_path = os.path.join(archive_agent.archive_dir, 'meta', 'version_config.json') self.hasher = HashAgent(archive_agent.algorithm) self.load_config()
class VersionAgent: """Actual version agent.""" def __init__(self, uuid: str, archive_agent): """Create the version agent from a version uid. :type archive_agent: ArchiveAgent""" self.uuid, self.archive_agent = uuid, archive_agent self.version_config_path = os.path.join(archive_agent.archive_dir, 'meta', 'version_config.json') self.hasher = HashAgent(archive_agent.algorithm) self.load_config() def load_config(self): """Load configuration for this version.""" with get_config(self.version_config_path) as version_config: for version in version_config['VersionRecords']: if version['UUID'] == self.uuid: self.version_config = dict(version) ABUNDANT_LOGGER.debug('Version record found: %s' % self.uuid) return ABUNDANT_LOGGER.error('Cannot find config for version %s' % self.uuid) raise FileNotFoundError('Cannot find config for version %s' % self.uuid) @property def is_base_version(self) -> bool: """Tell if this version is a base version.""" return self.version_config['IsBaseVersion'] @is_base_version.setter def is_base_version(self, is_base_version: bool): """Set if this version is a base version.""" with get_config(config_path=self.version_config_path, save_change=True) as version_config: for version in version_config['VersionRecords']: if version['UUID'] == self.uuid: version['IsBaseVersion'] = is_base_version break if is_base_version: ABUNDANT_LOGGER.info('Version %s is now base version' % self.uuid) else: ABUNDANT_LOGGER.info('Version %s is now non-base version' % self.uuid) @property def time_of_creation(self) -> float: """Get the time of creation of this version.""" return self.version_config['TimeOfCreation'] @property def version_dir(self) -> str: """Get the directory of this version.""" return os.path.join(self.archive_agent.archive_dir, 'archive', self.uuid) @property def _base_version(self) -> 'VersionAgent': """Get the base version of the archive this version is in.""" return self.archive_agent.base_version @property def exact_files(self): """Generator for files in the directory of this version.""" for root_dir, dirs, files in os.walk(self.version_dir): for file in files: absolute_path = os.path.join(root_dir, file) relative_path = self._get_relative_path_of_file(absolute_path) yield relative_path, absolute_path @property def files(self): """Generator for all files in this version.""" base_version = self.archive_agent.base_version # find the currently effective version for all files existing since base version for root_dir, dirs, files in os.walk(base_version.version_dir): for file in files: effective_absolute_path = os.path.join(root_dir, file) relative_path = base_version._get_relative_path_of_file(effective_absolute_path) # find last appearance of this file last_appearance_version = base_version._get_last_appearance_of_file(relative_path) yield relative_path, last_appearance_version._get_full_path_of_file(relative_path) # find all files that was added after the base version version_in_work = self while version_in_work and version_in_work != base_version: for root_dir, dirs, files in os.walk(version_in_work.version_dir): for file in files: absolute_path = os.path.join(root_dir, file) relative_path = version_in_work._get_relative_path_of_file(absolute_path) # if file exists in base version than ignore it if base_version.has_file(relative_path): continue # otherwise find the last appearance of that file # and yield it if that version is current version last_appearance_version = version_in_work._get_last_appearance_of_file(relative_path) if last_appearance_version == version_in_work: yield relative_path, version_in_work._get_full_path_of_file(relative_path) version_in_work = version_in_work.previous_version def __str__(self): return 'Version %s' % self.uuid def __repr__(self): return self.__str__() def __eq__(self, other: 'VersionAgent'): return self.uuid == other.uuid and self.archive_agent.uuid == other.archive_agent.uuid def __gt__(self, other: 'VersionAgent'): return self.time_of_creation > other.time_of_creation def __lt__(self, other: 'VersionAgent'): return self.time_of_creation < other.time_of_creation def __ge__(self, other: 'VersionAgent'): return self.time_of_creation >= other.time_of_creation def __le__(self, other: 'VersionAgent'): return self.time_of_creation <= other.time_of_creation def has_file(self, relative_path: str) -> bool: """Tell if this version contains a file.""" return os.path.exists(self._get_full_path_of_file(relative_path)) def _get_full_path_of_file(self, relative_path: str) -> str: """Get the full path of a file.""" return os.path.join(self.version_dir, relative_path) def _get_relative_path_of_file(self, absolute_path: str) -> str: """Get the relative path of a file.""" return absolute_path.replace(self.version_dir, '', 1).lstrip('/').lstrip('\\') def _get_previous_version_of_file(self, relative_path: str, from_version=None, until_version=None): """Get the last version of a file.""" version_candidate = self.previous_version while version_candidate: if from_version and version_candidate > from_version: continue full_path_in_that_version = version_candidate._get_full_path_of_file(relative_path) if os.path.exists(full_path_in_that_version): return version_candidate version_candidate = version_candidate.previous_version if until_version and version_candidate < until_version: break return None def _get_first_appearance_of_file(self, relative_path: str): """Get the first appearance of a file. File must exist in current version.""" assert self.has_file(relative_path) version = version_candidate = self while version: if version.has_file(relative_path): version_candidate = version version = version.previous_version return version_candidate def _get_last_appearance_of_file(self, relative_path: str): """Get the last appearance of a file. File must exist in current version.""" assert self.has_file(relative_path) version = version_candidate = self while version: if version.has_file(relative_path): version_candidate = version version = version.next_version return version_candidate def _get_next_version_of_file(self, relative_path: str, from_version=None, until_version=None): """Get next version of a file.""" version_candidate = self.next_version while version_candidate: if from_version and version_candidate < from_version: continue full_path_in_that_version = version_candidate._get_full_path_of_file(relative_path) if os.path.exists(full_path_in_that_version): return version_candidate if until_version and version_candidate > until_version: break version_candidate = version_candidate.next_version return None @property def previous_version(self) -> 'VersionAgent': """Get the previous version.""" if len(self.archive_agent.versions) == 1 or self.archive_agent.base_version == self: return None for i in range(0, len(self.archive_agent.versions)): if self.archive_agent.versions[i] == self: return self.archive_agent.versions[i - 1] @property def next_version(self) -> 'VersionAgent': """Get the next version.""" if len(self.archive_agent.versions) == 1: return None for i in range(0, len(self.archive_agent.versions) - 1): if self.archive_agent.versions[i] == self: return self.archive_agent.versions[i + 1] return None def migrate_to_next_version(self): """Migrate this version to its next version.""" next_version = self.next_version ABUNDANT_LOGGER.debug('Migrating version %s to %s...' % (self.uuid, next_version.uuid)) # copy all files from current version to another version # unless they already exists number_of_file_copied = 0 for relative_path, absolute_path in self.exact_files: if not next_version.has_file(relative_path): absolute_path_in_another_version = next_version._get_full_path_of_file(relative_path) shutil.move(absolute_path, absolute_path_in_another_version) number_of_file_copied += 1 ABUNDANT_LOGGER.debug('Copied %s' % absolute_path_in_another_version) ABUNDANT_LOGGER.info('Copied %s file(s)' % number_of_file_copied) # set base version if self.is_base_version: next_version.is_base_version = True # remove this version self.remove(base_version_pardon=True) # refresh status next_version.load_config() ABUNDANT_LOGGER.info('Migrated %s to %s' % (self.uuid, next_version.uuid)) def copy_files(self): """Copy files from source directory to version directory.""" ABUNDANT_LOGGER.debug('Copying files...') # copy new or modified files source_dir = self.archive_agent.source_dir number_of_file_copied = 0 for root_dir, dirs, files in os.walk(source_dir): for dir in dirs: absolute_dir = os.path.join(root_dir, dir) relative_dir = get_relative_path(absolute_dir, source_dir) os.makedirs(self._get_full_path_of_file(relative_dir), exist_ok=True) for file in files: source_absolute_path = os.path.join(root_dir, file) relative_path = get_relative_path(source_absolute_path, source_dir) # find the previous version of this file previous_version = self._get_previous_version_of_file(relative_path) # under following circumstances file will be treated # as already existing in previous versions and will # not be copied # if this is not a base version # and if there is a previous version for this file # and if that previous version is identical to current one if not self.is_base_version \ and previous_version is not None \ and self.hasher.hash(previous_version._get_full_path_of_file(relative_path)) \ == self.hasher.hash(source_absolute_path): ABUNDANT_LOGGER.debug('Skipping %s' % source_absolute_path) continue # otherwise just copy the file shutil.copy(source_absolute_path, self._get_full_path_of_file(relative_path)) number_of_file_copied += 1 ABUNDANT_LOGGER.debug('Copied file %s' % relative_path) ABUNDANT_LOGGER.info('Copied %s file(s)' % number_of_file_copied) def remove(self, base_version_pardon=False): """Remove this version.""" if not base_version_pardon and self.is_base_version: raise PermissionError('Base version cannot be removed') # delete version record with get_config(self.version_config_path, save_change=True) as version_config: current_version_record = [version for version in version_config['VersionRecords'] if version['UUID'] == self.uuid][0] version_config['VersionRecords'].remove(current_version_record) # delete directory shutil.rmtree(self.version_dir) # update version records self.archive_agent.load_versions() ABUNDANT_LOGGER.info('Removed version %s' % self.uuid) def export(self, destination_dir: str, exact=False): """Export files in this version to destination directory.""" ABUNDANT_LOGGER.debug('Exporting version %s to %s' % (self.uuid, destination_dir)) if not os.path.exists(destination_dir): ABUNDANT_LOGGER.error('Cannot find destination directory: %s' % destination_dir) raise FileNotFoundError('Cannot find destination directory: %s' % destination_dir) file_source = self.files if not exact else self.exact_files for relative_path, absolute_path in file_source: destination_path = os.path.join(destination_dir, relative_path) os.makedirs(os.path.dirname(destination_path), exist_ok=True) shutil.copy(absolute_path, destination_path) ABUNDANT_LOGGER.debug('Copied %s' % destination_path) ABUNDANT_LOGGER.info('Exported version %s to %s' % (self.uuid, destination_dir))