summaryrefslogtreecommitdiff
path: root/rapid
diff options
context:
space:
mode:
Diffstat (limited to 'rapid')
-rw-r--r--rapid/ChangeLog27
-rw-r--r--rapid/backupfile.py187
-rw-r--r--rapid/config.py2
-rw-r--r--rapid/copyfiles.py38
-rw-r--r--rapid/downloadtracker.py61
-rw-r--r--rapid/glade3/prefs.ui42
-rw-r--r--rapid/preferencesdialog.py10
-rw-r--r--rapid/prefsrapid.py3
-rwxr-xr-xrapid/rapid.py494
-rw-r--r--rapid/rpdfile.py4
-rw-r--r--rapid/rpdmultiprocessing.py1
-rw-r--r--rapid/thumbnail.py8
-rw-r--r--rapid/utilities.py1
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