From 359f9c06809ab90d339eb88644a7f4091d915d90 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Sat, 25 Sep 2010 15:37:18 +0200 Subject: [PATCH] [#92 state:fixed] Added an action to delete duplicates and then create hardlinks to group ref. --- cocoa/base/PyDupeGuru.h | 1 + cocoa/base/ResultWindow.h | 2 + cocoa/base/ResultWindow.m | 37 +++++++++++++---- cocoa/base/xib/MainMenu.xib | 79 ++++++++++++++++++++++++++++++++----- core/app.py | 20 +++++----- core/app_cocoa.py | 5 ++- core/app_cocoa_inter.py | 3 ++ core/tests/app_test.py | 4 +- core_pe/app_cocoa.py | 8 ++-- qt/base/app.py | 5 ++- qt/base/main_window.py | 15 ++++++- qt/base/main_window.ui | 9 +++++ 12 files changed, 150 insertions(+), 38 deletions(-) diff --git a/cocoa/base/PyDupeGuru.h b/cocoa/base/PyDupeGuru.h index ddfb2ed8..b46ef1be 100644 --- a/cocoa/base/PyDupeGuru.h +++ b/cocoa/base/PyDupeGuru.h @@ -39,6 +39,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)copyOrMove:(NSNumber *)aCopy markedTo:(NSString *)destination recreatePath:(NSNumber *)aRecreateType; - (void)deleteMarked; +- (void)hardlinkMarked; - (void)removeMarked; //Data diff --git a/cocoa/base/ResultWindow.h b/cocoa/base/ResultWindow.h index a3e89955..5b1337f9 100644 --- a/cocoa/base/ResultWindow.h +++ b/cocoa/base/ResultWindow.h @@ -38,6 +38,7 @@ http://www.hardcoded.net/licenses/hs_license - (NSDictionary *)getColumnsWidth; - (void)initResultColumns; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; +- (void)sendMarkedToTrash:(BOOL)hardlinkDeleted; /* Actions */ - (IBAction)clearIgnoreList:(id)sender; @@ -45,6 +46,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)changePowerMarker:(id)sender; - (IBAction)copyMarked:(id)sender; - (IBAction)deleteMarked:(id)sender; +- (IBAction)hardlinkMarked:(id)sender; - (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; - (IBAction)ignoreSelected:(id)sender; diff --git a/cocoa/base/ResultWindow.m b/cocoa/base/ResultWindow.m index 7e131743..8b481f5c 100644 --- a/cocoa/base/ResultWindow.m +++ b/cocoa/base/ResultWindow.m @@ -126,6 +126,29 @@ http://www.hardcoded.net/licenses/hs_license } } +- (void)sendMarkedToTrash:(BOOL)hardlinkDeleted +{ + NSInteger mark_count = [[py getMarkCount] intValue]; + if (!mark_count) { + return; + } + NSString *msg = @"You are about to send %d files to Trash. Continue?"; + if (hardlinkDeleted) { + msg = @"You are about to send %d files to Trash (and hardlink them afterwards). Continue?"; + } + if ([Dialogs askYesNo:[NSString stringWithFormat:msg,mark_count]] == NSAlertSecondButtonReturn) { // NO + return; + } + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])]; + if (hardlinkDeleted) { + [py hardlinkMarked]; + } + else { + [py deleteMarked]; + } +} + /* Actions */ - (IBAction)clearIgnoreList:(id)sender { @@ -168,14 +191,12 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)deleteMarked:(id)sender { - NSInteger mark_count = [[py getMarkCount] intValue]; - if (!mark_count) - return; - if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to send %d files to Trash. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO - return; - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - [py setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])]; - [py deleteMarked]; + [self sendMarkedToTrash:NO]; +} + +- (IBAction)hardlinkMarked:(id)sender +{ + [self sendMarkedToTrash:YES]; } - (IBAction)exportToXHTML:(id)sender diff --git a/cocoa/base/xib/MainMenu.xib b/cocoa/base/xib/MainMenu.xib index e0df4bfb..c9913a67 100644 --- a/cocoa/base/xib/MainMenu.xib +++ b/cocoa/base/xib/MainMenu.xib @@ -12,8 +12,8 @@ YES - - + + YES @@ -87,7 +87,6 @@ 256 {{7, 14}, {67, 25}} - YES 67239424 @@ -183,7 +182,6 @@ 258 {{0, 14}, {81, 22}} - YES 343014976 @@ -329,7 +327,6 @@ 256 {{1, 14}, {40, 25}} - YES -2076049856 @@ -385,6 +382,16 @@ _popUpItemAction: + + + Delete Marked and Replace with Hardlinks + + 2147483647 + + + _popUpItemAction: + + Move Marked to... @@ -505,6 +512,7 @@ + 2 YES 3 YES @@ -535,7 +543,6 @@ 256 {{4, 14}, {67, 25}} - YES 67239424 @@ -1226,6 +1233,15 @@ + + + Delete Marked and Replace with Hardlinks + T + 1048576 + 2147483647 + + + Move Marked to... @@ -2274,6 +2290,22 @@ 1226 + + + hardlinkMarked: + + + + 1229 + + + + hardlinkMarked: + + + + 1231 + @@ -2556,6 +2588,7 @@ + @@ -2925,6 +2958,7 @@ + @@ -3172,6 +3206,16 @@ + + 1227 + + + + + 1230 + + + @@ -3232,6 +3276,10 @@ 1222.IBPluginDependency 1223.IBPluginDependency 1224.IBPluginDependency + 1227.IBPluginDependency + 1227.ImportedFromIB2 + 1230.IBPluginDependency + 1230.ImportedFromIB2 134.IBPluginDependency 134.ImportedFromIB2 136.IBPluginDependency @@ -3444,7 +3492,7 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - {{294, 689}, {617, 0}} + {{294, 462}, {617, 227}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3488,6 +3536,10 @@ com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + {{324, 289}, {557, 400}} @@ -3529,7 +3581,7 @@ com.apple.InterfaceBuilder.CocoaPlugin - {{328, 475}, {361, 293}} + {{328, 455}, {383, 313}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3569,7 +3621,7 @@ com.apple.InterfaceBuilder.CocoaPlugin - {{94, 408}, {331, 243}} + {{310, 310}, {353, 263}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3669,7 +3721,7 @@ - 1226 + 1231 @@ -4065,6 +4117,7 @@ deleteMarked: exportToXHTML: filter: + hardlinkMarked: ignoreSelected: invokeCustomCommand: loadResults: @@ -4121,6 +4174,7 @@ id id id + id @@ -4134,6 +4188,7 @@ deleteMarked: exportToXHTML: filter: + hardlinkMarked: ignoreSelected: invokeCustomCommand: loadResults: @@ -4188,6 +4243,10 @@ filter: id + + hardlinkMarked: + id + ignoreSelected: id diff --git a/core/app.py b/core/app.py index eba5977c..66adb315 100644 --- a/core/app.py +++ b/core/app.py @@ -6,8 +6,6 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license - - import os import os.path as op import logging @@ -63,18 +61,22 @@ class DupeGuru(RegistrableApplication, Broadcaster): else: self.action_count += count - def _do_delete(self, j): + def _do_delete(self, j, replace_with_hardlinks): def op(dupe): j.add_progress() - return self._do_delete_dupe(dupe) + return self._do_delete_dupe(dupe, replace_with_hardlinks) j.start_job(self.results.mark_count) self.results.perform_on_marked(op, True) - def _do_delete_dupe(self, dupe): + def _do_delete_dupe(self, dupe, replace_with_hardlinks): if not io.exists(dupe.path): return send2trash(str(dupe.path)) # Raises OSError when there's a problem + if replace_with_hardlinks: + group = self.results.get_group_of_duplicate(dupe) + ref = group.ref + os.link(str(ref.path), str(dupe.path)) self.clean_empty_dirs(dupe.path[:-1]) def _do_load(self, j): @@ -135,8 +137,8 @@ class DupeGuru(RegistrableApplication, Broadcaster): self.selected_dupes = dupes self.notify('dupes_selected') - def _start_job(self, jobid, func): - # func(j) + def _start_job(self, jobid, func, *args): + # func(j, *args) raise NotImplementedError() def add_directory(self, d): @@ -208,9 +210,9 @@ class DupeGuru(RegistrableApplication, Broadcaster): jobid = JOB_COPY if copy else JOB_MOVE self._start_job(jobid, do) - def delete_marked(self): + def delete_marked(self, replace_with_hardlinks=False): self._demo_check() - self._start_job(JOB_DELETE, self._do_delete) + self._start_job(JOB_DELETE, self._do_delete, replace_with_hardlinks) def export_to_xhtml(self, column_ids): column_ids = [colid for colid in column_ids if colid.isdigit()] diff --git a/core/app_cocoa.py b/core/app_cocoa.py index 4807ffb6..215f8ab3 100644 --- a/core/app_cocoa.py +++ b/core/app_cocoa.py @@ -55,10 +55,11 @@ class DupeGuru(app.DupeGuru): def _reveal_path(path): NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(str(path), '') - def _start_job(self, jobid, func): + def _start_job(self, jobid, func, *args): try: j = self.progress.create_job() - self.progress.run_threaded(func, args=(j, )) + args = tuple([j] + list(args)) + self.progress.run_threaded(func, args=args) except job.JobInProgressError: NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self) else: diff --git a/core/app_cocoa_inter.py b/core/app_cocoa_inter.py index 87e77e6e..2931f5a9 100644 --- a/core/app_cocoa_inter.py +++ b/core/app_cocoa_inter.py @@ -80,6 +80,9 @@ class PyDupeGuruBase(PyRegistrable): def deleteMarked(self): self.py.delete_marked() + def hardlinkMarked(self): + self.py.delete_marked(replace_with_hardlinks=True) + def applyFilter_(self, filter): self.py.apply_filter(filter) diff --git a/core/tests/app_test.py b/core/tests/app_test.py index abfc463a..a9181e47 100644 --- a/core/tests/app_test.py +++ b/core/tests/app_test.py @@ -30,8 +30,8 @@ class DupeGuru(DupeGuruBase): def __init__(self): DupeGuruBase.__init__(self, data, '/tmp', appid=4) - def _start_job(self, jobid, func): - func(nulljob) + def _start_job(self, jobid, func, *args): + func(nulljob, *args) class CallLogger(object): diff --git a/core_pe/app_cocoa.py b/core_pe/app_cocoa.py index 39c78046..d1b89904 100644 --- a/core_pe/app_cocoa.py +++ b/core_pe/app_cocoa.py @@ -139,10 +139,10 @@ class DupeGuruPE(app_cocoa.DupeGuru): self.directories = Directories() self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db') - def _do_delete(self, j): + def _do_delete(self, j, replace_with_hardlinks): def op(dupe): j.add_progress() - return self._do_delete_dupe(dupe) + return self._do_delete_dupe(dupe, replace_with_hardlinks) marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] self.path2iphoto = {} @@ -164,7 +164,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): self.results.perform_on_marked(op, True) del self.path2iphoto - def _do_delete_dupe(self, dupe): + def _do_delete_dupe(self, dupe, replace_with_hardlinks): if isinstance(dupe, IPhoto): if str(dupe.path) in self.path2iphoto: photo = self.path2iphoto[str(dupe.path)] @@ -177,7 +177,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): msg = "Could not find photo %s in iPhoto Library" % str(dupe.path) raise EnvironmentError(msg) else: - app_cocoa.DupeGuru._do_delete_dupe(self, dupe) + app_cocoa.DupeGuru._do_delete_dupe(self, dupe, replace_with_hardlinks) def _get_file(self, str_path): p = Path(str_path) diff --git a/qt/base/app.py b/qt/base/app.py index 7b4639a2..bb19d21d 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -131,11 +131,12 @@ class DupeGuru(DupeGuruBase, QObject): def _reveal_path(path): DupeGuru._open_path(path[:-1]) - def _start_job(self, jobid, func): + def _start_job(self, jobid, func, *args): title = JOBID2TITLE[jobid] try: j = self._progress.create_job() - self._progress.run(jobid, title, func, args=(j, )) + args = tuple([j] + list(args)) + self._progress.run(jobid, title, func, args=args) except job.JobInProgressError: msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." QMessageBox.information(self.main_window, 'Action in progress', msg) diff --git a/qt/base/main_window.py b/qt/base/main_window.py index f680f5c2..fc0af9a9 100644 --- a/qt/base/main_window.py +++ b/qt/base/main_window.py @@ -44,6 +44,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actionInvokeCustomCommand.triggered.connect(self.app.invokeCustomCommand) self.actionLoadResults.triggered.connect(self.loadResultsTriggered) self.actionSaveResults.triggered.connect(self.saveResultsTriggered) + self.actionHardlinkMarked.triggered.connect(self.hardlinkTriggered) def _setupUi(self): self.setupUi(self) @@ -73,6 +74,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): actionMenu = QMenu('Actions', self.toolBar) actionMenu.setIcon(QIcon(QPixmap(":/actions"))) actionMenu.addAction(self.actionDeleteMarked) + actionMenu.addAction(self.actionHardlinkMarked) actionMenu.addAction(self.actionMoveMarked) actionMenu.addAction(self.actionCopyMarked) actionMenu.addAction(self.actionRemoveMarked) @@ -99,9 +101,11 @@ class MainWindow(QMainWindow, Ui_MainWindow): if self.app.prefs.mainWindowRect is not None and not self.app.prefs.mainWindowIsMaximized: self.setGeometry(self.app.prefs.mainWindowRect) - # Linux setup + # Platform-specific setup if sys.platform == 'linux2': self.actionCheckForUpdate.setVisible(False) # This only works on Windows + if sys.platform not in {'darwin', 'linux2'}: + self.actionHardlinkMarked.setVisible(False) #--- Private def _confirm(self, title, msg, default_button=QMessageBox.Yes): @@ -194,6 +198,15 @@ class MainWindow(QMainWindow, Ui_MainWindow): url = QUrl.fromLocalFile(exported_path) QDesktopServices.openUrl(url) + def hardlinkTriggered(self): + count = self.app.results.mark_count + if not count: + return + title = "Delete and hardlink duplicates" + msg = "You are about to send {0} files to the trash and hardlink them afterwards. Continue?".format(count) + if self._confirm(title, msg): + self.app.delete_marked(replace_with_hardlinks=True) + def loadResultsTriggered(self): title = "Select a results file to load" files = "dupeGuru Results (*.dupeguru)" diff --git a/qt/base/main_window.ui b/qt/base/main_window.ui index eb8f48e8..299c7193 100644 --- a/qt/base/main_window.ui +++ b/qt/base/main_window.ui @@ -64,6 +64,7 @@ Actions + @@ -265,6 +266,14 @@ Ctrl+D + + + Delete Marked and Replace with Hardlinks + + + Ctrl+Shift+D + + Move Marked to...