diff options
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/ChangeLog | 27 | ||||
-rw-r--r-- | rapid/backupfile.py | 187 | ||||
-rw-r--r-- | rapid/config.py | 2 | ||||
-rw-r--r-- | rapid/copyfiles.py | 38 | ||||
-rw-r--r-- | rapid/downloadtracker.py | 61 | ||||
-rw-r--r-- | rapid/glade3/prefs.ui | 42 | ||||
-rw-r--r-- | rapid/preferencesdialog.py | 10 | ||||
-rw-r--r-- | rapid/prefsrapid.py | 3 | ||||
-rwxr-xr-x | rapid/rapid.py | 494 | ||||
-rw-r--r-- | rapid/rpdfile.py | 4 | ||||
-rw-r--r-- | rapid/rpdmultiprocessing.py | 1 | ||||
-rw-r--r-- | rapid/thumbnail.py | 8 | ||||
-rw-r--r-- | rapid/utilities.py | 1 |
13 files changed, 718 insertions, 160 deletions
diff --git a/rapid/ChangeLog b/rapid/ChangeLog index c9437d5..944119d 100644 --- a/rapid/ChangeLog +++ b/rapid/ChangeLog @@ -1,3 +1,30 @@ +Version 0.4.0 RC 1 +------------------ + +2011-04-21 + +Features added since beta 1: + +Backups have been implemented. If you are backing up to more than one device, +Rapid Photo Downloader will backup to each device simultaneously instead of one +after the other. + +When clicking the Download button before thumbnails are finished generating, +the download proceeds immediately and the thumbnails remaining to be generated +will rendered during the download itself. + +Added preferences option to disable thumbnail generation. When auto start is +enabled, this can speed-up transfers when downloading from high-speed devices. + +Access to the preferences window is now disable while a download is occurring, +as changing preferences when files are being download can cause prolems. + +Bug fix: don't crash when downloading some files after having previously +downloaded some others in the same session. + +Updated Brazilian, Dutch, German and Russian translations. + + Version 0.4.0 beta 1 -------------------- diff --git a/rapid/backupfile.py b/rapid/backupfile.py new file mode 100644 index 0000000..6b6d11d --- /dev/null +++ b/rapid/backupfile.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com> + +### This program 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 2 of the License, or +### (at your option) any later version. + +### This program 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 this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import multiprocessing +import tempfile +import os + +import gio + +import logging +logger = multiprocessing.get_logger() + +import rpdmultiprocessing as rpdmp +import rpdfile +import problemnotification as pn +import config + + +from gettext import gettext as _ + + +class BackupFiles(multiprocessing.Process): + def __init__(self, path, name, + batch_size_MB, results_pipe, terminate_queue, + run_event): + multiprocessing.Process.__init__(self) + self.results_pipe = results_pipe + self.terminate_queue = terminate_queue + self.batch_size_bytes = batch_size_MB * 1048576 # * 1024 * 1024 + self.path = path + self.mount_name = name + self.run_event = run_event + + def check_termination_request(self): + """ + Check to see this process has not been requested to immediately terminate + """ + if not self.terminate_queue.empty(): + x = self.terminate_queue.get() + # terminate immediately + logger.info("Terminating file backup") + return True + return False + + + def update_progress(self, amount_downloaded, total): + # first check if process is being terminated + self.amount_downloaded = amount_downloaded + if not self.terminate_queue.empty(): + # it is - cancel the current copy + self.cancel_copy.cancel() + else: + if not self.total_reached: + chunk_downloaded = amount_downloaded - self.bytes_downloaded + if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): + self.bytes_downloaded = amount_downloaded + + if amount_downloaded == total: + # this function is called a couple of times when total is reached + self.total_reached = True + + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) + if amount_downloaded == total: + self.bytes_downloaded = 0 + + def progress_callback(self, amount_downloaded, total): + self.update_progress(amount_downloaded, total) + + + def run(self): + + self.cancel_copy = gio.Cancellable() + self.bytes_downloaded = 0 + self.total_downloaded = 0 + + while True: + + self.amount_downloaded = 0 + move_succeeded, rpd_file, path_suffix, backup_duplicate_overwrite = self.results_pipe.recv() + if rpd_file is None: + # this is a termination signal + return None + # pause if instructed by the caller + self.run_event.wait() + + if self.check_termination_request(): + return None + + backup_succeeded = False + self.scan_pid = rpd_file.scan_pid + + if move_succeeded: + self.total_reached = False + + source = gio.File(path=rpd_file.download_full_file_name) + + if path_suffix is None: + dest_base_dir = self.path + else: + dest_base_dir = os.path.join(self.path, 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) + + subfolder = gio.File(path=dest_dir) + if not subfolder.query_exists(cancellable=None): + # create the subfolders on the backup path + try: + subfolder.make_directory_with_parents(cancellable=gio.Cancellable()) + except gio.Error, inst: + # There is a tiny 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 such errors. + if inst.code <> gio.ERROR_EXISTS: + logger.error("Failed to create backup subfolder: %s", dest_dir) + logger.error(inst) + rpd_file.add_problem(None, pn.BACKUP_DIRECTORY_CREATION, self.mount_name) + rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_DIRECTORY_CREATION, self.mount_name), inst) + rpd_file.error_title = _('Backing up error') + rpd_file.error_msg = \ + _("Destination directory could not be created: %(directory)s\n") % \ + {'directory': subfolder, } + \ + _("Source: %(source)s\nDestination: %(destination)s") % \ + {'source': rpd_file.download_full_file_name, + 'destination': backup_full_file_name} + "\n" + \ + _("Error: %(inst)s") % {'inst': inst} + + dest = gio.File(path=backup_full_file_name) + if backup_duplicate_overwrite: + flags = gio.FILE_COPY_OVERWRITE + else: + flags = gio.FILE_COPY_NONE + + try: + source.copy(dest, self.progress_callback, flags, + cancellable=self.cancel_copy) + backup_succeeded = True + except gio.Error, inst: + fileNotBackedUpMessageDisplayed = True + rpd_file.add_problem(None, pn.BACKUP_ERROR, self.mount_name) + rpd_file.add_extra_detail('%s%s' % (pn.BACKUP_ERROR, self.mount_name), inst) + rpd_file.error_title = _('Backing up error') + rpd_file.error_msg = \ + _("Source: %(source)s\nDestination: %(destination)s") % \ + {'source': rpd_file.download_full_file_name, 'destination': backup_full_file_name} + "\n" + \ + _("Error: %(inst)s") % {'inst': inst} + logger.error("%s:\n%s", rpd_file.error_title, rpd_file.error_msg) + + if not backup_succeeded: + if rpd_file.status == config.STATUS_DOWNLOAD_FAILED: + rpd_file.status = config.STATUS_DOWNLOAD_AND_BACKUP_FAILED + else: + rpd_file.status = config.STATUS_BACKUP_PROBLEM + + self.total_downloaded += rpd_file.size + bytes_not_downloaded = rpd_file.size - self.amount_downloaded + if bytes_not_downloaded: + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.pid, self.total_downloaded, bytes_not_downloaded)))) + + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, + (backup_succeeded, rpd_file)))) + + + + + + diff --git a/rapid/config.py b/rapid/config.py index da2f5c0..6019d6a 100644 --- a/rapid/config.py +++ b/rapid/config.py @@ -15,7 +15,7 @@ ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -version = '0.4.0~b1' +version = '0.4.0~rc1' GCONF_KEY="/apps/rapid-photo-downloader" diff --git a/rapid/copyfiles.py b/rapid/copyfiles.py index 08daafe..65a6c6d 100644 --- a/rapid/copyfiles.py +++ b/rapid/copyfiles.py @@ -38,7 +38,7 @@ from gettext import gettext as _ class CopyFiles(multiprocessing.Process): def __init__(self, photo_download_folder, video_download_folder, files, - generate_thumbnails, scan_pid, + scan_pid, batch_size_MB, results_pipe, terminate_queue, run_event): multiprocessing.Process.__init__(self) @@ -48,7 +48,6 @@ class CopyFiles(multiprocessing.Process): self.photo_download_folder = photo_download_folder self.video_download_folder = video_download_folder self.files = files - self.generate_thumbnails = generate_thumbnails self.scan_pid = scan_pid self.no_files= len(self.files) self.run_event = run_event @@ -71,15 +70,17 @@ class CopyFiles(multiprocessing.Process): # it is - cancel the current copy self.cancel_copy.cancel() else: - chunk_downloaded = amount_downloaded - self.bytes_downloaded - if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): - self.bytes_downloaded = amount_downloaded - if amount_downloaded == total: - # this function is called a couple of times when total is reached - chunk_downloaded = 0 - self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) - if amount_downloaded == total: - self.bytes_downloaded = 0 + if not self.total_reached: + chunk_downloaded = amount_downloaded - self.bytes_downloaded + if (chunk_downloaded > self.batch_size_bytes) or (amount_downloaded == total): + self.bytes_downloaded = amount_downloaded + if amount_downloaded == total: + # this function is called a couple of times when total is reached + self.total_reached = True + + self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_BYTES, (self.scan_pid, self.total_downloaded + amount_downloaded, chunk_downloaded)))) + if amount_downloaded == total: + self.bytes_downloaded = 0 def progress_callback(self, amount_downloaded, total): self.update_progress(amount_downloaded, total) @@ -105,11 +106,11 @@ class CopyFiles(multiprocessing.Process): if self.photo_temp_dir or self.video_temp_dir: - if self.generate_thumbnails: - self.thumbnail_maker = tn.Thumbnail() + self.thumbnail_maker = tn.Thumbnail() for i in range(len(self.files)): rpd_file = self.files[i] + self.total_reached = False # pause if instructed by the caller self.run_event.wait() @@ -151,20 +152,21 @@ class CopyFiles(multiprocessing.Process): # succeeded or not. It's neccessary to keep the user informed. self.total_downloaded += rpd_file.size - if copy_succeeded and self.generate_thumbnails: + if copy_succeeded and rpd_file.generate_thumbnail: thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail( temp_full_file_name, rpd_file.file_type, (160, 120), (100,100)) - self.results_pipe.send((rpdmp.CONN_PARTIAL, - (rpdmp.MSG_THUMB, (rpd_file.unique_id, - thumbnail_icon, thumbnail)))) + else: + thumbnail = None + thumbnail_icon = None if rpd_file.metadata is not None: rpd_file.metadata = None self.results_pipe.send((rpdmp.CONN_PARTIAL, (rpdmp.MSG_FILE, - (copy_succeeded, rpd_file, i + 1, temp_full_file_name)))) + (copy_succeeded, rpd_file, i + 1, temp_full_file_name, + thumbnail_icon, thumbnail)))) self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) diff --git a/rapid/downloadtracker.py b/rapid/downloadtracker.py index 309da71..75c9c9d 100644 --- a/rapid/downloadtracker.py +++ b/rapid/downloadtracker.py @@ -19,7 +19,8 @@ import time from rpdfile import FILE_TYPE_PHOTO, FILE_TYPE_VIDEO -from config import STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOADED_WITH_WARNING +from config import STATUS_DOWNLOAD_FAILED, STATUS_DOWNLOADED_WITH_WARNING, \ + STATUS_DOWNLOAD_AND_BACKUP_FAILED, STATUS_BACKUP_PROBLEM from gettext import gettext as _ @@ -28,10 +29,17 @@ class DownloadTracker: Track file downloads - their size, number, and any problems """ def __init__(self): + self.file_types_present_by_scan_pid = dict() + self._refresh_values() + + def _refresh_values(self): + """ these values are reset when a download is completed""" self.size_of_download_in_bytes_by_scan_pid = dict() - self.total_bytes_copied_in_bytes_by_scan_pid = dict() + self.raw_size_of_download_in_bytes_by_scan_pid = dict() + self.total_bytes_copied_by_scan_pid = dict() + self.total_bytes_backed_up_by_scan_pid = dict() self.no_files_in_download_by_scan_pid = dict() - self.file_types_present_by_scan_pid = dict() + # 'Download count' tracks the index of the file being downloaded # into the list of files that need to be downloaded -- much like # a counter in a for loop, e.g. 'for i in list', where i is the counter @@ -50,11 +58,16 @@ class DownloadTracker: self.total_video_failures = 0 self.total_warnings = 0 self.total_bytes_to_download = 0 + self.backups_performed_by_unique_id = dict() + + def set_no_backup_devices(self, no_backup_devices): + self.no_backup_devices = no_backup_devices def init_stats(self, scan_pid, bytes, no_files): self.no_files_in_download_by_scan_pid[scan_pid] = no_files self.rename_chunk[scan_pid] = bytes / 10 / no_files self.size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes + self.rename_chunk[scan_pid] * no_files + self.raw_size_of_download_in_bytes_by_scan_pid[scan_pid] = bytes self.total_bytes_to_download += self.size_of_download_in_bytes_by_scan_pid[scan_pid] self.files_downloaded[scan_pid] = 0 self.photos_downloaded[scan_pid] = 0 @@ -62,6 +75,7 @@ class DownloadTracker: self.photo_failures[scan_pid] = 0 self.video_failures[scan_pid] = 0 self.warnings[scan_pid] = 0 + self.total_bytes_backed_up_by_scan_pid[scan_pid] = 0 def get_no_files_in_download(self, scan_pid): return self.no_files_in_download_by_scan_pid[scan_pid] @@ -81,11 +95,19 @@ class DownloadTracker: def get_no_warnings(self, scan_pid): return self.warnings.get(scan_pid, 0) + + def file_backed_up(self, unique_id): + self.backups_performed_by_unique_id[unique_id] = \ + self.backups_performed_by_unique_id.get(unique_id, 0) + 1 + + def all_files_backed_up(self, unique_id): + v = self.backups_performed_by_unique_id[unique_id] == self.no_backup_devices + return v def file_downloaded_increment(self, scan_pid, file_type, status): self.files_downloaded[scan_pid] += 1 - if status <> STATUS_DOWNLOAD_FAILED: + if status <> STATUS_DOWNLOAD_FAILED and status <> STATUS_DOWNLOAD_AND_BACKUP_FAILED: if file_type == FILE_TYPE_PHOTO: self.photos_downloaded[scan_pid] += 1 self.total_photos_downloaded += 1 @@ -93,7 +115,7 @@ class DownloadTracker: self.videos_downloaded[scan_pid] += 1 self.total_videos_downloaded += 1 - if status == STATUS_DOWNLOADED_WITH_WARNING: + if status == STATUS_DOWNLOADED_WITH_WARNING or status == STATUS_BACKUP_PROBLEM: self.warnings[scan_pid] += 1 self.total_warnings += 1 else: @@ -112,16 +134,19 @@ class DownloadTracker: """ # three components: copy (download), rename, and backup - percent_complete = ((float( - self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid]) + percent_complete = (((float( + self.total_bytes_copied_by_scan_pid[scan_pid]) + self.rename_chunk[scan_pid] * self.files_downloaded[scan_pid]) - / self.size_of_download_in_bytes_by_scan_pid[scan_pid]) * 100 + + self.total_bytes_backed_up_by_scan_pid[scan_pid]) + / (self.size_of_download_in_bytes_by_scan_pid[scan_pid] + + self.raw_size_of_download_in_bytes_by_scan_pid[scan_pid] * + self.no_backup_devices)) * 100 return percent_complete def get_overall_percent_complete(self): total = 0 - for scan_pid in self.total_bytes_copied_in_bytes_by_scan_pid: - total += (self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid] + + for scan_pid in self.total_bytes_copied_by_scan_pid: + total += (self.total_bytes_copied_by_scan_pid[scan_pid] + (self.rename_chunk[scan_pid] * self.files_downloaded[scan_pid])) @@ -129,7 +154,10 @@ class DownloadTracker: return percent_complete def set_total_bytes_copied(self, scan_pid, total_bytes): - self.total_bytes_copied_in_bytes_by_scan_pid[scan_pid] = total_bytes + self.total_bytes_copied_by_scan_pid[scan_pid] = total_bytes + + def increment_bytes_backed_up(self, scan_pid, chunk_downloaded): + self.total_bytes_backed_up_by_scan_pid[scan_pid] += chunk_downloaded def set_download_count_for_file(self, unique_id, download_count): self.download_count_for_file_by_unique_id[unique_id] = download_count @@ -152,12 +180,13 @@ class DownloadTracker: else return False """ return (self.total_warnings == 0 and - self.photo_failures == 0 and - self.video_failures == 0) + self.total_photo_failures == 0 and + self.total_video_failures == 0) def purge(self, scan_pid): del self.no_files_in_download_by_scan_pid[scan_pid] del self.size_of_download_in_bytes_by_scan_pid[scan_pid] + del self.raw_size_of_download_in_bytes_by_scan_pid[scan_pid] del self.photos_downloaded[scan_pid] del self.videos_downloaded[scan_pid] del self.files_downloaded[scan_pid] @@ -166,7 +195,7 @@ class DownloadTracker: del self.warnings[scan_pid] def purge_all(self): - self.__init__() + self._refresh_values() @@ -241,9 +270,9 @@ class TimeRemaining: t.time_mark = time.time() self.times[scan_pid] = t - def update(self, scan_pid, total_size): + def update(self, scan_pid, bytes_downloaded): if scan_pid in self.times: - self.times[scan_pid].downloaded = total_size + self.times[scan_pid].downloaded += bytes_downloaded now = time.time() tm = self.times[scan_pid].time_mark amt_time = now - tm diff --git a/rapid/glade3/prefs.ui b/rapid/glade3/prefs.ui index ddbacdf..9d1eb31 100644 --- a/rapid/glade3/prefs.ui +++ b/rapid/glade3/prefs.ui @@ -1434,6 +1434,10 @@ <property name="width_chars">2</property> <property name="xalign">1</property> <property name="truncate_multiline">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> <property name="adjustment">hour_adjustment</property> <property name="numeric">True</property> <signal name="value-changed" handler="on_hour_spinbutton_value_changed" swapped="no"/> @@ -1465,6 +1469,10 @@ <property name="width_chars">2</property> <property name="xalign">1</property> <property name="truncate_multiline">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> <property name="adjustment">minute_adjustment</property> <property name="numeric">True</property> <signal name="value-changed" handler="on_minute_spinbutton_value_changed" swapped="no"/> @@ -2476,6 +2484,10 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="can_focus">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="invisible_char">•</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> <signal name="changed" handler="on_backup_identifier_entry_changed" swapped="no"/> </object> <packing> @@ -2510,6 +2522,10 @@ You can download photos from multiple devices simultaneously, or you can specify <property name="can_focus">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="invisible_char">•</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> <signal name="changed" handler="on_video_backup_identifier_entry_changed" swapped="no"/> </object> <packing> @@ -2850,7 +2866,21 @@ You can download photos from multiple devices simultaneously, or you can specify </packing> </child> <child> - <placeholder/> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">Performance</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> </child> <child> <object class="GtkHBox" id="hbox25"> @@ -2879,7 +2909,15 @@ You can download photos from multiple devices simultaneously, or you can specify <placeholder/> </child> <child> - <placeholder/> + <object class="GtkCheckButton" id="generate_thumbnails_checkbutton"> + <property name="label" translatable="yes">Generate thumbnails (slower)</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_action_appearance">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_generate_thumbnails_checkbutton_toggled" swapped="no"/> + </object> </child> </object> <packing> diff --git a/rapid/preferencesdialog.py b/rapid/preferencesdialog.py index 0f46406..368b064 100644 --- a/rapid/preferencesdialog.py +++ b/rapid/preferencesdialog.py @@ -1177,6 +1177,8 @@ class PreferencesDialog(): self.prefs.auto_exit_force) self.auto_delete_checkbutton.set_active( self.prefs.auto_delete) + self.generate_thumbnails_checkbutton.set_active( + self.prefs.generate_thumbnails) self.update_misc_controls() @@ -1484,16 +1486,16 @@ class PreferencesDialog(): def on_auto_exit_force_checkbutton_toggled(self, checkbutton): self.prefs.auto_exit_force = checkbutton.get_active() - def on_scan_metadata_checkbutton_toggled(self, checkbutton): - self.prefs.enable_previews = checkbutton.get_active() - def on_autodetect_device_checkbutton_toggled(self, checkbutton): self.prefs.device_autodetection = checkbutton.get_active() self.update_device_controls() - def on_autodetect_psd_checkbutton_toggled(self, checkbutton): + def on_autodetect_psd_checkbutton_toggled(self, checkbutton): self.prefs.device_autodetection_psd = checkbutton.get_active() + def on_generate_thumbnails_checkbutton_toggled(self, checkbutton): + self.prefs.generate_thumbnails = checkbutton.get_active() + def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): self.prefs.backup_duplicate_overwrite = widget.get_active() diff --git a/rapid/prefsrapid.py b/rapid/prefsrapid.py index 81a425b..f13530f 100644 --- a/rapid/prefsrapid.py +++ b/rapid/prefsrapid.py @@ -125,14 +125,13 @@ class RapidPreferences(prefs.Preferences): _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'), _('Jakarta'), _('Madrid'), _('Stockholm')]), "synchronize_raw_jpg": prefs.Value(prefs.BOOL, False), - #~ "hpaned_pos": prefs.Value(prefs.INT, 0), "vpaned_pos": prefs.Value(prefs.INT, 0), "main_window_size_x": prefs.Value(prefs.INT, 0), "main_window_size_y": prefs.Value(prefs.INT, 0), "main_window_maximized": prefs.Value(prefs.INT, 0), "show_warning_downloading_from_camera": prefs.Value(prefs.BOOL, True), #~ "preview_zoom": prefs.Value(prefs.INT, zoom), - "enable_previews": prefs.Value(prefs.BOOL, True), + "generate_thumbnails": prefs.Value(prefs.BOOL, True), } def __init__(self): diff --git a/rapid/rapid.py b/rapid/rapid.py index df60790..335db34 100755 --- a/rapid/rapid.py +++ b/rapid/rapid.py @@ -67,6 +67,7 @@ import metadatavideo import scan as scan_process import copyfiles import subfolderfile +import backupfile import errorlog @@ -212,6 +213,7 @@ class DeviceCollection(gtk.TreeView): # please note, at program startup, self.row_height() will be less than it will be when already running # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program # (with one card at startup), it could be 21 + # must account for header row at the top row_height = self.get_background_area(0, self.get_column(0))[3] + 1 height = (len(self.map_process_to_row) + 1) * row_height self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height) @@ -283,15 +285,16 @@ class DeviceCollection(gtk.TreeView): Look for left single click on eject button """ if event.button == 1: - x = int(event.x) - y = int(event.y) - path, column, cell_x, cell_y = self.get_path_at_pos(x, y) - if path is not None: - if column == self.get_column(0): - if cell_x >= column.get_width() - self.eject_pixbuf.get_width(): - iter = self.liststore.get_iter(path) - if self.liststore.get_value(iter, 5) is not None: - self.unmount(process_id = self.liststore.get_value(iter, 6)) + if len (self.liststore): + x = int(event.x) + y = int(event.y) + path, column, cell_x, cell_y = self.get_path_at_pos(x, y) + if path is not None: + if column == self.get_column(0): + if cell_x >= column.get_width() - self.eject_pixbuf.get_width(): + iter = self.liststore.get_iter(path) + if self.liststore.get_value(iter, 5) is not None: + self.unmount(process_id = self.liststore.get_value(iter, 6)) def unmount(self, process_id): device = self.devices_by_scan_pid[process_id] @@ -467,6 +470,12 @@ class ThumbnailDisplay(gtk.IconView): self.total_thumbs_to_generate = 0 self.thumbnails_generated = 0 + # dict of scan_pids that are having thumbnails generated + # value is the thumbnail process id + # this is needed when terminating thumbnailing early such as when + # user clicks download before the thumbnailing is finished + self.generating_thumbnails = {} + self.thumbnails = {} self.previews = {} self.previews_being_fetched = set() @@ -518,16 +527,11 @@ class ThumbnailDisplay(gtk.IconView): self.add_attribute(image, "filename", 3) self.add_attribute(image, "status", 8) - - #set the background color to a darkish grey self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444')) self.show_all() - self._setup_icons() - - self.connect('item-activated', self.on_item_activated) @@ -870,7 +874,14 @@ class ThumbnailDisplay(gtk.IconView): """Initiate thumbnail generation for files scanned in one process """ rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]] - self.thumbnail_manager.add_task(rpd_files) + thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files)) + self.generating_thumbnails[scan_pid] = thumbnail_pid + + def _set_thumbnail(self, unique_id, icon): + treerowref = self.treerow_index[unique_id] + path = treerowref.get_path() + iter = self.liststore.get_iter(path) + self.liststore.set(iter, 0, icon) def update_thumbnail(self, thumbnail_data): """ @@ -886,24 +897,51 @@ class ThumbnailDisplay(gtk.IconView): # get the thumbnail icon in PIL format thumbnail_icon = thumbnail_icon.get_image() - treerowref = self.treerow_index[unique_id] - path = treerowref.get_path() - iter = self.liststore.get_iter(path) - if thumbnail_icon: - self.liststore.set(iter, 0, thumbnail_icon) + self._set_thumbnail(unique_id, thumbnail_icon) if len(thumbnail_data) > 2: # get the 2nd image in PIL format self.thumbnails[unique_id] = thumbnail_data[2].get_image() + def terminate_thumbnail_generation(self, scan_pid): + """ + Terminates thumbnail generation if thumbnails are currently + being generated for this scan_pid + """ + + if scan_pid in self.generating_thumbnails: + terminated = True + self.thumbnail_manager.terminate_process( + self.generating_thumbnails[scan_pid]) + del self.generating_thumbnails[scan_pid] + + if len(self.generating_thumbnails) == 0: + self._reset_thumbnail_tracking_and_display() + else: + terminated = False + return terminated + + def mark_thumbnails_needed(self, rpd_files): + for rpd_file in rpd_files: + if rpd_file.unique_id not in self.thumbnails: + rpd_file.generate_thumbnail = True + + def _reset_thumbnail_tracking_and_display(self): + self.rapid_app.download_progressbar.set_fraction(0.0) + self.rapid_app.download_progressbar.set_text('') + self.thumbnails_generated = 0 + self.total_thumbs_to_generate = 0 + def thumbnail_results(self, source, condition): connection = self.thumbnail_manager.get_pipe(source) conn_type, data = connection.recv() if conn_type == rpdmp.CONN_COMPLETE: + scan_pid = data + del self.generating_thumbnails[scan_pid] connection.close() return False else: @@ -916,14 +954,11 @@ class ThumbnailDisplay(gtk.IconView): # clear progress bar information if all thumbnails have been # extracted if self.thumbnails_generated == self.total_thumbs_to_generate: - self.rapid_app.download_progressbar.set_fraction(0.0) - self.rapid_app.download_progressbar.set_text('') - self.thumbnails_generated = 0 - self.total_thumbs_to_generate = 0 - + self._reset_thumbnail_tracking_and_display() else: - self.rapid_app.download_progressbar.set_fraction( - float(self.thumbnails_generated) / self.total_thumbs_to_generate) + if self.total_thumbs_to_generate: + self.rapid_app.download_progressbar.set_fraction( + float(self.thumbnails_generated) / self.total_thumbs_to_generate) return True @@ -937,7 +972,11 @@ class ThumbnailDisplay(gtk.IconView): preview_image = preview_full_size.get_image() self.previews[unique_id] = preview_image self.rapid_app.update_preview_image(unique_id, preview_image) - + + # user can turn off option for thumbnail generation after a scan + if unique_id not in self.thumbnails: + self._set_thumbnail(unique_id, preview_small.get_image()) + def clear_all(self, scan_pid=None, keep_downloaded_files=False): """ @@ -991,7 +1030,7 @@ class TaskManager: def _setup_task(self, task): - task_results_conn, task_process_conn = Pipe(duplex=False) + task_results_conn, task_process_conn = self._setup_pipe() source = task_results_conn.fileno() self._pipes[source] = task_results_conn @@ -1001,7 +1040,11 @@ class TaskManager: run_event = Event() run_event.set() - return self._initiate_task(task, task_process_conn, terminate_queue, run_event) + return self._initiate_task(task, task_results_conn, task_process_conn, + terminate_queue, run_event) + + def _setup_pipe(self): + return Pipe(duplex=False) def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): logger.error("Implement child class method!") @@ -1009,7 +1052,7 @@ class TaskManager: def processes(self): for i in range(len(self._processes)): - yield self._processes[i] + yield self._processes[i] def start(self): self.paused = False @@ -1024,6 +1067,26 @@ class TaskManager: run_event = scan[2] if run_event.is_set(): run_event.clear() + + def _terminate_process(self, p): + self._send_termination_msg(p) + # The process might be paused: let it run + run_event = p[2] + if not run_event.is_set(): + run_event.set() + + def _send_termination_msg(self, p): + p[1].put(None) + + def terminate_process(self, process_id): + """ + Send a signal to process with matching process_id that it should + immediately terminate + """ + for p in self.processes(): + if p[0].pid == process_id: + if p[0].is_alive(): + self._terminate_process(p) def request_termination(self): """ @@ -1033,11 +1096,7 @@ class TaskManager: for p in self.processes(): if p[0].is_alive(): requested = True - p[1].put(None) - # The process might be paused: let it run - run_event = p[2] - if not run_event.is_set(): - run_event.set() + self._terminate_process(p) return requested @@ -1053,6 +1112,8 @@ class TaskManager: for p in self.processes(): if p[0].is_alive(): + logger.info("Forcefully terminating %s in %s" , p[0].name, + self.__class__.__name__) p[0].terminate() @@ -1078,7 +1139,9 @@ class ScanManager(TaskManager): self.add_device_function = add_device_function self.generate_folder = generate_folder - def _initiate_task(self, device, task_process_conn, terminate_queue, run_event): + def _initiate_task(self, device, task_results_conn, task_process_conn, + terminate_queue, run_event): + scan = scan_process.Scan(device.get_path(), self.batch_size, self.generate_folder, task_process_conn, terminate_queue, run_event) scan.start() @@ -1094,16 +1157,16 @@ class ScanManager(TaskManager): class CopyFilesManager(TaskManager): - def _initiate_task(self, task, task_process_conn, terminate_queue, run_event): + def _initiate_task(self, task, task_results_conn, + task_process_conn, terminate_queue, run_event): photo_download_folder = task[0] video_download_folder = task[1] scan_pid = task[2] files = task[3] - generate_thumbnails = task[4] copy_files = copyfiles.CopyFiles(photo_download_folder, video_download_folder, - files, generate_thumbnails, + files, scan_pid, self.batch_size, task_process_conn, terminate_queue, run_event) copy_files.start() @@ -1111,14 +1174,68 @@ class CopyFilesManager(TaskManager): return copy_files.pid class ThumbnailManager(TaskManager): - - def _initiate_task(self, files, task_process_conn, terminate_queue, run_event): - generator = tn.GenerateThumbnails(files, self.batch_size, task_process_conn, terminate_queue, run_event) + def _initiate_task(self, task, task_results_conn, + task_process_conn, terminate_queue, run_event): + scan_pid = task[0] + files = task[1] + generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size, + task_process_conn, terminate_queue, + run_event) generator.start() self._processes.append((generator, terminate_queue, run_event)) return generator.pid + +class BackupFilesManager(TaskManager): + """ + Handles backup processes. This is a little different from other Task + Manager classes in that its pipe is Duplex, and the work done by it + is not pre-assigned when the process is started. + """ + def __init__(self, results_callback, batch_size): + TaskManager.__init__(self, results_callback, batch_size) + self.backup_devices_by_path = {} - + def _setup_pipe(self): + return Pipe(duplex=True) + + def _send_termination_msg(self, p): + p[1].put(None) + p[3].send((None, None, None, None)) + + def _initiate_task(self, task, task_results_conn, task_process_conn, + terminate_queue, run_event): + path = task[0] + name = task[1] + backup_files = backupfile.BackupFiles(path, name, self.batch_size, + task_process_conn, terminate_queue, + run_event) + backup_files.start() + self._processes.append((backup_files, terminate_queue, run_event, + task_results_conn)) + + self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid) + + return backup_files.pid + + def backup_file(self, move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite): + for path in self.backup_devices_by_path: + task_results_conn = self.backup_devices_by_path[path][0] + task_results_conn.send((move_succeeded, rpd_file, path_suffix, + backup_duplicate_overwrite)) + + def add_device(self, path, name): + """ + Convenience function to setup adding a backup device + """ + return self.add_task((path, name)) + + def remove_device(self, path): + pid = self.backup_devices_by_path[path][1] + self.terminate_process(pid) + del self.backup_devices_by_path[path] + + class SingleInstanceTaskManager: """ Base class to manage single instance processes. Examples are daemon @@ -1174,8 +1291,6 @@ class SubfolderFileManager(SingleInstanceTaskManager): self.results_callback(move_succeeded, rpd_file) return True - - class ResizblePilImage(gtk.DrawingArea): def __init__(self, bg_color=None): gtk.DrawingArea.__init__(self) @@ -1361,27 +1476,30 @@ class RapidApp(dbus.service.Object): def _terminate_processes(self, terminate_file_copies=False): - # FIXME: need more fine grained tuning here - must cancel large file - # copies midstream if terminate_file_copies: logger.info("Terminating all processes...") scan_termination_requested = self.scan_manager.request_termination() thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination() + backup_termination_requested = self.backup_manager.request_termination() + if terminate_file_copies: copy_files_termination_requested = self.copy_files_manager.request_termination() else: copy_files_termination_requested = False - if scan_termination_requested or thumbnails_termination_requested: + if (scan_termination_requested or thumbnails_termination_requested or + backup_termination_requested): time.sleep(1) if (self.scan_manager.get_no_active_processes() > 0 or - self.thumbnails.thumbnail_manager.get_no_active_processes() > 0): + self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or + self.backup_manager.get_no_active_processes() > 0): time.sleep(1) # must try again, just in case a new scan has meanwhile started! self.scan_manager.request_termination() self.thumbnails.thumbnail_manager.terminate_forcefully() self.scan_manager.terminate_forcefully() + self.backup_manager.terminate_forcefully() if terminate_file_copies and copy_files_termination_requested: time.sleep(1) @@ -1497,7 +1615,14 @@ class RapidApp(dbus.service.Object): self.vmonitor.connect("mount-added", self.on_mount_added) self.vmonitor.connect("mount-removed", self.on_mount_removed) - + + def _backup_device_name(self, path): + if self.backup_devices[path] is None: + name = path + else: + name = self.backup_devices[path].get_name() + return name + def setup_devices(self, on_startup, on_preference_change, block_auto_start): """ @@ -1527,9 +1652,6 @@ class RapidApp(dbus.service.Object): mounts = [] self.backup_devices = {} - # Clear download statistics and tracking - # FIXME - if self.using_volume_monitor(): # either using automatically detected backup devices # or download devices @@ -1567,6 +1689,12 @@ class RapidApp(dbus.service.Object): # will backup to this path, but don't need any volume info # associated with it self.backup_devices[self.prefs.backup_location] = None + + for path in self.backup_devices: + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) + + self.update_no_backup_devices() # Display amount of free space in a status bar message self.display_free_space() @@ -1580,12 +1708,6 @@ class RapidApp(dbus.service.Object): (self.prefs.auto_download_upon_device_insertion and not on_startup))) - - self.testing_auto_exit = False - self.testing_auto_exit_trip = len(mounts) - self.testing_auto_exit_trip_counter = 0 - - for m in mounts: path, mount = m device = dv.Device(path=path, mount=mount) @@ -1654,6 +1776,43 @@ class RapidApp(dbus.service.Object): # user manually specified the path return True return False + + def update_no_backup_devices(self): + self.download_tracker.set_no_backup_devices(len(self.backup_devices)) + + def refresh_backup_media(self): + """ + Setup the backup media + + Assumptions: this is being called after the user has changed their + preferences AND download media has already been setup + """ + + # terminate any running backup processes + self.backup_manager.request_termination() + + self.backup_devices = {} + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + # will backup to this path, but don't need any volume info associated with it + self.backup_devices[self.prefs.backup_location] = None + else: + for mount in self.vmonitor.get_mounts(): + if not mount.is_shadowed(): + path = mount.get_root().get_path() + if path: + if self.check_if_backup_mount(path): + # is a backup volume + if path not in self.backup_devices: + self.backup_devices[path] = mount + + for path in self.backup_devices: + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) + + self.update_no_backup_devices() + self.display_free_space() def using_volume_monitor(self): """ @@ -1688,6 +1847,9 @@ class RapidApp(dbus.service.Object): if is_backup_mount: if path not in self.backup_devices: self.backup_devices[path] = mount + name = self._backup_device_name(path) + self.backup_manager.add_device(path, name) + self.update_no_backup_devices() self.display_free_space() elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or @@ -1720,8 +1882,6 @@ class RapidApp(dbus.service.Object): del self.mounts_by_path[path] # temp directory should be cleaned by finishing of process - #~ if scan_pid in self.download_active_by_scan_pid: - #~ self._clean_temp_dirs_for_scan_pid(scan_pid) self.thumbnails.clear_all(scan_pid = scan_pid, keep_downloaded_files = True) self.device_collection.remove_device(scan_pid) @@ -1732,6 +1892,8 @@ class RapidApp(dbus.service.Object): elif path in self.backup_devices: del self.backup_devices[path] self.display_free_space() + self.backup_manager.remove_device(path) + self.update_no_backup_devices() # may need to disable download button and menu self.set_download_action_sensitivity() @@ -1892,8 +2054,9 @@ class RapidApp(dbus.service.Object): # Track which temporary directories are created when downloading files self.temp_dirs_by_scan_pid = dict() - # Track which downloads are running + # Track which downloads and backups are running self.download_active_by_scan_pid = [] + self.backups_active_by_scan_pid = [] @@ -1920,10 +2083,19 @@ class RapidApp(dbus.service.Object): # it is unset when all downloads are completed if self.download_start_time is None: self.download_start_time = datetime.datetime.now() - + + # Set status to download pending self.thumbnails.mark_download_pending(files_by_scan_pid) + + # disable refresh and preferences change while download is occurring + self.enable_prefs_and_refresh(enabled=False) + for scan_pid in files_by_scan_pid: files = files_by_scan_pid[scan_pid] + # if generating thumbnails for this scan_pid, stop it + if self.thumbnails.terminate_thumbnail_generation(scan_pid): + self.thumbnails.mark_thumbnails_needed(files) + self.download_files(files, scan_pid) self.set_download_action_label(is_download = False) @@ -1967,6 +2139,9 @@ class RapidApp(dbus.service.Object): bytes=download_size, no_files=len(files)) + if self.prefs.backup_images: + download_size = download_size * (len(self.backup_devices) + 1) + self.time_remaining.set(scan_pid, download_size) self.time_check.set_download_mark() @@ -1976,10 +2151,14 @@ class RapidApp(dbus.service.Object): if len(self.download_active_by_scan_pid) > 1: self.display_summary_notification = True + if self.auto_start_is_on and self.prefs.generate_thumbnails: + for rpd_file in files: + rpd_file.generate_thumbnail = True + # Initiate copy files process self.copy_files_manager.add_task((photo_download_folder, video_download_folder, scan_pid, - files, self.auto_start_is_on)) + files)) def copy_files_results(self, source, condition): """ @@ -2002,9 +2181,14 @@ class RapidApp(dbus.service.Object): percent_complete = self.download_tracker.get_percent_complete(scan_pid) self.device_collection.update_progress(scan_pid, percent_complete, None, None) - self.time_remaining.update(scan_pid, total_downloaded) + self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) elif msg_type == rpdmp.MSG_FILE: - download_succeeded, rpd_file, download_count, temp_full_file_name = data + download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data + + if thumbnail is not None or thumbnail_icon is not None: + self.thumbnails.update_thumbnail((rpd_file.unique_id, + thumbnail_icon, + thumbnail)) self.download_tracker.set_download_count_for_file( rpd_file.unique_id, download_count) @@ -2025,11 +2209,7 @@ class RapidApp(dbus.service.Object): download_succeeded, download_count, rpd_file - ) - elif msg_type == rpdmp.MSG_THUMB: - #~ unique_id, thumbnail, thumbnail_icon = data - #~ thumbnail_data = (unique_id - self.thumbnails.update_thumbnail(data) + ) return True else: @@ -2040,11 +2220,10 @@ class RapidApp(dbus.service.Object): def download_is_occurring(self): - """Returns True if a file is currently being downloaded or renamed + """Returns True if a file is currently being downloaded, renamed or + backed up """ - v = not len(self.download_active_by_scan_pid) == 0 - #~ logger.info("Download is occurring: %s", v) - return v + return not len(self.download_active_by_scan_pid) == 0 # # # # Create folder and file names for downloaded files @@ -2058,16 +2237,65 @@ class RapidApp(dbus.service.Object): scan_pid = rpd_file.scan_pid unique_id = rpd_file.unique_id - self.thumbnails.update_status_post_download(rpd_file) - + if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: + self.log_error(config.WARNING, rpd_file.error_title, + rpd_file.error_msg, rpd_file.error_extra_detail) + self.error_title = '' + self.error_msg = '' + self.error_extra_detail = '' + + + if self.prefs.backup_images and len(self.backup_devices): + if self.prefs.backup_device_autodetection: + if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO: + path_suffix = self.prefs.backup_identifier + else: + path_suffix = self.prefs.video_backup_identifier + else: + path_suffix = None + + self.backup_manager.backup_file(move_succeeded, rpd_file, + path_suffix, + self.prefs.backup_duplicate_overwrite) + else: + self.file_download_finished(move_succeeded, rpd_file) + + + def backup_results(self, source, condition): + connection = self.backup_manager.get_pipe(source) + conn_type, msg_data = connection.recv() + if conn_type == rpdmp.CONN_PARTIAL: + msg_type, data = msg_data + + if msg_type == rpdmp.MSG_BYTES: + scan_pid, backup_pid, total_downloaded, chunk_downloaded = data + self.download_tracker.increment_bytes_backed_up(scan_pid, + chunk_downloaded) + self.time_check.increment(bytes_downloaded=chunk_downloaded) + percent_complete = self.download_tracker.get_percent_complete(scan_pid) + self.device_collection.update_progress(scan_pid, percent_complete, + None, None) + self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded) + + elif msg_type == rpdmp.MSG_FILE: + backup_succeeded, rpd_file = data + self.download_tracker.file_backed_up(rpd_file.unique_id) + if self.download_tracker.all_files_backed_up(rpd_file.unique_id): + self.file_download_finished(backup_succeeded, rpd_file) + return True + else: + return False + + + def file_download_finished(self, succeeded, rpd_file): + scan_pid = rpd_file.scan_pid + unique_id = rpd_file.unique_id # Update error log window if neccessary - if not move_succeeded: + if not succeeded: self.log_error(config.SERIOUS_ERROR, rpd_file.error_title, rpd_file.error_msg, rpd_file.error_extra_detail) - elif rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING: - self.log_error(config.WARNING, rpd_file.error_title, - rpd_file.error_msg, rpd_file.error_extra_detail) - + + self.thumbnails.update_status_post_download(rpd_file) self.download_tracker.file_downloaded_increment(scan_pid, rpd_file.file_type, rpd_file.status) @@ -2090,6 +2318,7 @@ class RapidApp(dbus.service.Object): if not self.download_is_occurring(): logger.debug("Download completed") + self.enable_prefs_and_refresh(enabled=True) self.notify_download_complete() self.download_progressbar.set_fraction(0.0) @@ -2101,6 +2330,7 @@ class RapidApp(dbus.service.Object): if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings()) or self.prefs.auto_exit_force): if not self.thumbnails.files_remain_to_download(): + self._terminate_processes() gtk.main_quit() self.download_tracker.purge_all() @@ -2113,8 +2343,8 @@ class RapidApp(dbus.service.Object): self.job_code = '' self.download_start_time = None - - + + def update_time_remaining(self): update, download_speed = self.time_check.check_for_update() if update: @@ -2167,9 +2397,11 @@ class RapidApp(dbus.service.Object): device = self.device_collection.get_device(scan_pid) if device.mount is None: - notificationName = PROGRAM_NAME + notification_name = PROGRAM_NAME + icon = self.application_icon else: - notificationName = device.get_name() + notification_name = device.get_name() + icon = device.get_icon(self.notification_icon_size) no_photos_downloaded = self.download_tracker.get_no_files_downloaded( scan_pid, rpdfile.FILE_TYPE_PHOTO) @@ -2194,8 +2426,8 @@ class RapidApp(dbus.service.Object): if no_warnings: message = "%s\n%s " % (message, no_warnings) + _("warnings") - n = pynotify.Notification(notificationName, message) - n.set_icon_from_pixbuf(device.get_icon(self.notification_icon_size)) + n = pynotify.Notification(notification_name, message) + n.set_icon_from_pixbuf(icon) n.show() @@ -2265,6 +2497,8 @@ class RapidApp(dbus.service.Object): files_to_download = self.download_tracker.get_no_files_in_download(scan_pid) file_types = self.download_tracker.get_file_types_present(scan_pid) completed = files_downloaded == files_to_download + if completed and self.prefs.backup_images: + completed = self.download_tracker.all_files_backed_up(unique_id) if completed: files_remaining = self.thumbnails.get_no_files_remaining(scan_pid) @@ -2393,7 +2627,6 @@ class RapidApp(dbus.service.Object): Called when user changes the program's preferences """ logger.debug("Preference change detected: %s", key) - if key == 'show_log_dialog': self.menu_log_window.set_active(value) @@ -2423,10 +2656,6 @@ class RapidApp(dbus.service.Object): # Check if stored sequence no is being used self._check_for_sequence_value_use() - #~ elif key == 'job_codes': - #~ # update job code list in left pane - #~ self.selection_vbox.update_job_code_combo() - elif key in ['download_folder', 'video_download_folder']: self.display_free_space() @@ -2449,7 +2678,7 @@ class RapidApp(dbus.service.Object): self.start_volume_monitor() logger.info("Backup preferences were changed.") - logger.info("self.refreshBackupMedia()") + self.refresh_backup_media() self.rerun_setup_available_backup_media = False @@ -2516,6 +2745,8 @@ class RapidApp(dbus.service.Object): self.prev_image_action = builder.get_object("prev_image_action") self.menu_log_window = builder.get_object("menu_log_window") self.speed_label = builder.get_object("speed_label") + self.refresh_action = builder.get_object("refresh_action") + self.preferences_action = builder.get_object("preferences_action") # Only enable this action when actually displaying a preview self.next_image_action.set_sensitive(False) @@ -2583,11 +2814,10 @@ class RapidApp(dbus.service.Object): """ Set the size of the device collection scrolled window widget """ - - if self.device_collection.map_process_to_row: height = self.device_collection_viewport.size_request()[1] self.device_collection_scrolledwindow.set_size_request(-1, height) + self.main_vpaned.set_position(height) else: # don't allow the media collection to be absolutely empty self.device_collection_scrolledwindow.set_size_request(-1, 47) @@ -2633,18 +2863,56 @@ class RapidApp(dbus.service.Object): self.warning_image.hide() self.warning_vseparator.hide() + def enable_prefs_and_refresh(self, enabled): + """ + If enable is true, then the user is able to activate the preferences + or refresh command. + The intention is to be able to disable this during a download + """ + self.refresh_action.set_sensitive(enabled) + self.preferences_action.set_sensitive(enabled) + def statusbar_message(self, msg): self.rapid_statusbar.push(self.statusbar_context_id, msg) def statusbar_message_remove(self): self.rapid_statusbar.pop(self.statusbar_context_id) + + def display_backup_mounts(self): + """ + Create a message to be displayed to the user showing which backup + mounts will be used + """ + message = '' + + paths = self.backup_devices.keys() + i = 0 + v = len(paths) + prefix = '' + for b in paths: + if v > 1: + if i < (v -1) and i > 0: + prefix = ', ' + elif i == (v - 1) : + prefix = " " + _("and") + " " + i += 1 + message = "%s%s'%s'" % (message, prefix, self.backup_devices[b].get_name()) + + if v > 1: + message = _("Using backup devices") + " %s" % message + elif v == 1: + message = _("Using backup device") + " %s" % message + else: + message = _("No backup devices detected") + + return message def display_free_space(self): """ Displays the amount of space free on the filesystem the files will be downloaded to. - Also displays backup volumes / path being used. (NOT IMPLEMENTED YET) + Also displays backup volumes / path being used. """ photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True) video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True) @@ -2697,12 +2965,12 @@ class RapidApp(dbus.service.Object): msg = " " + _("%(free)s free") % {'free': free} - if self.prefs.backup_images and False: #FIXME: skip this for now! + if self.prefs.backup_images: if not self.prefs.backup_device_autodetection: # user manually specified backup location msg2 = _('Backing up to %(path)s') % {'path':self.prefs.backup_location} else: - msg2 = self.displayBackupVolumes() #FIXME + msg2 = self.display_backup_mounts() if msg: msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2} @@ -2927,7 +3195,10 @@ class RapidApp(dbus.service.Object): self.scan_manager = ScanManager(self.scan_results, self.batch_size, self.generate_folder, self.device_collection.add_device) self.copy_files_manager = CopyFilesManager(self.copy_files_results, - self.batch_size_MB) + self.batch_size_MB) + self.backup_manager = BackupFilesManager(self.backup_results, + self.batch_size_MB) + def scan_results(self, source, condition): """ @@ -2948,20 +3219,17 @@ class RapidApp(dbus.service.Object): logger.info('Files total %s' % size) self.device_collection.update_device(scan_pid, size) self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0) - self.testing_auto_exit_trip_counter += 1 self.set_download_action_sensitivity() - if self.testing_auto_exit_trip_counter == self.testing_auto_exit_trip and self.testing_auto_exit: - self.on_rapidapp_destroy(self.rapidapp) - else: - if not self.testing_auto_exit and not self.auto_start_is_on: - self.download_progressbar.set_text(_("Thumbnails")) - self.thumbnails.generate_thumbnails(scan_pid) - elif self.auto_start_is_on: - if self.need_job_code_for_naming and not self.job_code: - self.get_job_code() - else: - self.start_download(scan_pid=scan_pid) + if (not self.auto_start_is_on and + self.prefs.generate_thumbnails): + self.download_progressbar.set_text(_("Thumbnails")) + self.thumbnails.generate_thumbnails(scan_pid) + elif self.auto_start_is_on: + if self.need_job_code_for_naming and not self.job_code: + self.get_job_code() + else: + self.start_download(scan_pid=scan_pid) self.set_thumbnail_sort() diff --git a/rapid/rpdfile.py b/rapid/rpdfile.py index a0969f0..d9454d7 100644 --- a/rapid/rpdfile.py +++ b/rapid/rpdfile.py @@ -170,6 +170,10 @@ class RPDFile: self.problem = None self.job_code = None + # indicates whether to generate a thumbnail during the copy + # files process + self.generate_thumbnail = False + # generated values self.temp_full_file_name = '' diff --git a/rapid/rpdmultiprocessing.py b/rapid/rpdmultiprocessing.py index 4a06fc9..7fdd252 100644 --- a/rapid/rpdmultiprocessing.py +++ b/rapid/rpdmultiprocessing.py @@ -21,6 +21,5 @@ CONN_COMPLETE = 1 MSG_BYTES = 0 MSG_FILE = 1 MSG_TEMP_DIRS = 2 -MSG_THUMB = 3 MSG_SEQUENCE_VALUE = 0 diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py index e9c7e66..7a41e54 100644 --- a/rapid/thumbnail.py +++ b/rapid/thumbnail.py @@ -333,7 +333,7 @@ class GetPreviewImage(multiprocessing.Process): def run(self): while True: unique_id, full_file_name, file_type, size_max = self.results_pipe.recv() - full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=None) + full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=(100,100)) if full_size_preview is None: full_size_preview = self.get_stock_image(file_type) self.results_pipe.send((unique_id, full_size_preview, reduced_size_preview)) @@ -341,7 +341,7 @@ class GetPreviewImage(multiprocessing.Process): class GenerateThumbnails(multiprocessing.Process): - def __init__(self, files, batch_size, results_pipe, terminate_queue, + def __init__(self, scan_pid, files, batch_size, results_pipe, terminate_queue, run_event): multiprocessing.Process.__init__(self) self.results_pipe = results_pipe @@ -353,6 +353,8 @@ class GenerateThumbnails(multiprocessing.Process): self.thumbnail_maker = Thumbnail() + self.scan_pid = scan_pid + def run(self): counter = 0 @@ -385,6 +387,6 @@ class GenerateThumbnails(multiprocessing.Process): if counter > 0: # send any remaining results self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results)) - self.results_pipe.send((rpdmp.CONN_COMPLETE, None)) + self.results_pipe.send((rpdmp.CONN_COMPLETE, self.scan_pid)) self.results_pipe.close() diff --git a/rapid/utilities.py b/rapid/utilities.py index 07be833..ad77a29 100644 --- a/rapid/utilities.py +++ b/rapid/utilities.py @@ -146,5 +146,6 @@ def human_readable_version(v): """ returns a version in human readable form""" v = v.replace('~a', ' alpha ') v = v.replace('~b', ' beta ') + v = v.replace('~rc', ' RC ') return v |