#!/usr/bin/env python3 # Copyright (C) 2011-2017 Damon Lynch # This file is part of Rapid Photo Downloader. # # Rapid Photo Downloader is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Rapid Photo Downloader is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Rapid Photo Downloader. If not, # see . __author__ = 'Damon Lynch' __copyright__ = "Copyright 2011-2017, Damon Lynch" import pickle import os import errno import hashlib from datetime import datetime import shutil import logging import locale # Use the default locale as defined by the LANG variable locale.setlocale(locale.LC_ALL, '') from typing import Optional, Tuple from gettext import gettext as _ from raphodo.interprocess import (BackupFileData, BackupResults, BackupArguments, WorkerInPublishPullPipeline) from raphodo.copyfiles import FileCopy from raphodo.constants import (FileType, DownloadStatus, BackupStatus) from raphodo.rpdfile import RPDFile from raphodo.cache import FdoCacheNormal, FdoCacheLarge from raphodo.copyfiles import copy_file_metadata from raphodo.problemnotification import ( BackingUpProblems, BackupSubfolderCreationProblem, make_href, BackupOverwrittenProblem, BackupAlreadyExistsProblem, FileWriteProblem ) from raphodo.storage import get_uri class BackupFilesWorker(WorkerInPublishPullPipeline, FileCopy): def __init__(self): self.problems = BackingUpProblems() super().__init__('BackupFiles') def update_progress(self, amount_downloaded, total): self.amount_downloaded = amount_downloaded chunk_downloaded = amount_downloaded - self.bytes_downloaded if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): self.bytes_downloaded = amount_downloaded self.content= pickle.dumps(BackupResults( scan_id=self.scan_id, device_id=self.device_id, total_downloaded=self.total_downloaded + amount_downloaded, chunk_downloaded=chunk_downloaded), pickle.HIGHEST_PROTOCOL) self.send_message_to_sink() # if amount_downloaded == total: # self.bytes_downloaded = 0 def backup_associate_file(self, dest_dir: str, full_file_name: str) -> None: """ Backs up small files like XMP or THM files """ base_name = os.path.basename(full_file_name) full_dest_name = os.path.join(dest_dir, base_name) try: logging.debug("Backing up additional file %s...", full_dest_name) shutil.copyfile(full_file_name, full_dest_name) logging.debug("...backing up additional file %s succeeded", full_dest_name) except Exception as e: logging.error("Backup of %s failed", full_file_name) logging.error(str(e)) uri = get_uri(full_file_name=full_dest_name) self.problems.append(FileWriteProblem(name=base_name, uri=uri, exception=e)) else: # ignore any metadata copying errors copy_file_metadata(full_file_name, full_dest_name) def do_backup(self, data: BackupFileData) -> None: rpd_file = data.rpd_file backup_succeeded = False self.scan_id = rpd_file.scan_id self.verify_file = data.verify_file mdata_exceptions = None if not (data.move_succeeded and data.do_backup): backup_full_file_name = '' else: self.total_reached = False if data.path_suffix is None: dest_base_dir = self.path else: dest_base_dir = os.path.join(self.path, data.path_suffix) dest_dir = os.path.join(dest_base_dir, rpd_file.download_subfolder) backup_full_file_name = os.path.join(dest_dir, rpd_file.download_name) if not os.path.isdir(dest_dir): # create the subfolders on the backup path try: logging.debug("Creating subfolder %s on backup device %s...", dest_dir, self.device_name) os.makedirs(dest_dir) logging.debug("...backup subfolder created") except (OSError, PermissionError, FileNotFoundError) as inst: # There is a minuscule chance directory may have been # created by another process between the time it # takes to query and the time it takes to create a # new directory. Ignore that error. if inst.errno != errno.EEXIST: logging.error("Failed to create backup subfolder: %s", rpd_file.download_path) logging.error(inst) self.problems.append( BackupSubfolderCreationProblem( folder=make_href( name=rpd_file.download_subfolder, uri=get_uri(path=dest_dir) ), exception=inst ) ) backup_already_exists = os.path.exists(backup_full_file_name) if backup_already_exists: try: modification_time = os.path.getmtime(backup_full_file_name) dt = datetime.fromtimestamp(modification_time) date = dt.strftime("%x") time = dt.strftime("%X") except Exception: logging.error("Could not determine the file modification time of %s", backup_full_file_name) date = time = '' source = rpd_file.get_souce_href() device = make_href(name=rpd_file.device_display_name, uri=rpd_file.device_uri) if data.backup_duplicate_overwrite: self.problems.append(BackupOverwrittenProblem( file_type_capitalized=rpd_file.title_capitalized, file_type=rpd_file.title, name=rpd_file.download_name, uri=get_uri(full_file_name=backup_full_file_name), source=source, device=device, date=date, time=time )) msg = "Overwriting backup file %s" % backup_full_file_name else: self.problems.append(BackupAlreadyExistsProblem( file_type_capitalized=rpd_file.title_capitalized, file_type=rpd_file.title, name=rpd_file.download_name, uri=get_uri(full_file_name=backup_full_file_name), source=source, device=device, date=date, time=time )) msg = "Skipping backup of file %s because it already exists" % \ backup_full_file_name logging.warning(msg) if not backup_already_exists or data.backup_duplicate_overwrite: logging.debug("Backing up file %s on device %s...", data.download_count, self.device_name) source = rpd_file.download_full_file_name destination = backup_full_file_name backup_succeeded = self.copy_from_filesystem(source, destination, rpd_file) if backup_succeeded and self.verify_file: md5 = hashlib.md5(open(backup_full_file_name).read()).hexdigest() if md5 != rpd_file.md5: pass if backup_succeeded: logging.debug("...backing up file %s on device %s succeeded", data.download_count, self.device_name) if backup_succeeded: mdata_exceptions = copy_file_metadata( rpd_file.download_full_file_name, backup_full_file_name ) if not backup_succeeded: if rpd_file.status == DownloadStatus.download_failed: rpd_file.status = DownloadStatus.download_and_backup_failed else: rpd_file.status = DownloadStatus.backup_problem else: # backup any THM, audio or XMP files if rpd_file.download_thm_full_name: self.backup_associate_file(dest_dir, rpd_file.download_thm_full_name) if rpd_file.download_audio_full_name: self.backup_associate_file(dest_dir, rpd_file.download_audio_full_name) if rpd_file.download_xmp_full_name: self.backup_associate_file(dest_dir, rpd_file.download_xmp_full_name) if rpd_file.download_log_full_name: self.backup_associate_file(dest_dir, rpd_file.download_log_full_name) self.total_downloaded += rpd_file.size bytes_not_downloaded = rpd_file.size - self.amount_downloaded if bytes_not_downloaded and data.do_backup: self.content = pickle.dumps( BackupResults( scan_id=self.scan_id, device_id=self.device_id, total_downloaded=self.total_downloaded, chunk_downloaded=bytes_not_downloaded ), pickle.HIGHEST_PROTOCOL ) self.send_message_to_sink() self.content = pickle.dumps( BackupResults( scan_id=self.scan_id, device_id=self.device_id, backup_succeeded=backup_succeeded, do_backup=data.do_backup, rpd_file=rpd_file, backup_full_file_name=backup_full_file_name, mdata_exceptions=mdata_exceptions ), pickle.HIGHEST_PROTOCOL ) self.send_message_to_sink() def reset_problems(self) -> None: self.problems = BackingUpProblems( name=self.device_name, uri=self.uri ) def send_problems(self) -> None: if self.problems: self.content = pickle.dumps( BackupResults( scan_id=self.scan_id, device_id=self.device_id, problems=self.problems ), pickle.HIGHEST_PROTOCOL ) self.send_message_to_sink() self.reset_problems() def cleanup_pre_stop(self): self.send_problems() def do_work(self): backup_arguments = pickle.loads(self.content) self.path = backup_arguments.path self.device_name = backup_arguments.device_name self.uri = get_uri(path=self.path) self.fdo_cache_normal = FdoCacheNormal() self.fdo_cache_large = FdoCacheLarge() while True: worker_id, directive, content = self.receiver.recv_multipart() self.device_id = int(worker_id) self.check_for_command(directive, content) data = pickle.loads(content) # type: BackupFileData if data.message == BackupStatus.backup_started: self.reset_problems() elif data.message == BackupStatus.backup_completed: self.send_problems() else: self.amount_downloaded = 0 self.init_copy_progress() self.do_backup(data=data) if __name__ == "__main__": backup = BackupFilesWorker()