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 @@
+
+
+ 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...