1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2025-03-10 05:34:36 +00:00

[#194 state:fixed] Added the "Replace with symlink" deletion option.

This commit is contained in:
Virgil Dupras 2012-08-01 12:36:23 -04:00
parent 5247ac8abd
commit 5a5a74d0e1
9 changed files with 71 additions and 53 deletions

View File

@ -15,12 +15,14 @@ http://www.hardcoded.net/licenses/bsd_license
PyDeletionOptions *model; PyDeletionOptions *model;
NSTextField *messageTextField; NSTextField *messageTextField;
NSButton *hardlinkButton; NSButton *linkButton;
NSMatrix *linkTypeRadio;
NSButton *directButton; NSButton *directButton;
} }
@property (readwrite, retain) NSTextField *messageTextField; @property (readwrite, retain) NSTextField *messageTextField;
@property (readwrite, retain) NSButton *hardlinkButton; @property (readwrite, retain) NSButton *linkButton;
@property (readwrite, retain) NSMatrix *linkTypeRadio;
@property (readwrite, retain) NSButton *directButton; @property (readwrite, retain) NSButton *directButton;
- (id)initWithPyRef:(PyObject *)aPyRef; - (id)initWithPyRef:(PyObject *)aPyRef;

View File

@ -13,7 +13,8 @@ http://www.hardcoded.net/licenses/bsd_license
@implementation DeletionOptions @implementation DeletionOptions
@synthesize messageTextField; @synthesize messageTextField;
@synthesize hardlinkButton; @synthesize linkButton;
@synthesize linkTypeRadio;
@synthesize directButton; @synthesize directButton;
- (id)initWithPyRef:(PyObject *)aPyRef - (id)initWithPyRef:(PyObject *)aPyRef
@ -33,7 +34,8 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)updateOptions - (void)updateOptions
{ {
[model setHardlink:[hardlinkButton state] == NSOnState]; [model setLinkDeleted:[linkButton state] == NSOnState];
[model setUseHardlinks:[linkTypeRadio selectedColumn] == 1];
[model setDirect:[directButton state] == NSOnState]; [model setDirect:[directButton state] == NSOnState];
} }
@ -55,8 +57,9 @@ http://www.hardcoded.net/licenses/bsd_license
- (BOOL)show - (BOOL)show
{ {
[hardlinkButton setState:NSOffState]; [linkButton setState:NSOffState];
[directButton setState:NSOffState]; [directButton setState:NSOffState];
[linkTypeRadio selectCellAtRow:0 column:0];
NSInteger r = [NSApp runModalForWindow:[self window]]; NSInteger r = [NSApp runModalForWindow:[self window]];
[[self window] close]; [[self window] close];
return r == NSOKButton; return r == NSOKButton;

View File

@ -1,47 +1,49 @@
ownerclass = 'DeletionOptions' ownerclass = 'DeletionOptions'
ownerimport = 'DeletionOptions.h' ownerimport = 'DeletionOptions.h'
result = Window(450, 215, "Deletion Options") result = Window(450, 240, "Deletion Options")
messageLabel = Label(result, "") messageLabel = Label(result, "")
hardlinkCheckbox = Checkbox(result, "Hardlink deleted files") linkCheckbox = Checkbox(result, "Link deleted files")
hardlinkLabel = Label(result, "After having deleted a duplicate, place a hardlink targeting the " linkLabel = Label(result, "After having deleted a duplicate, place a link targeting the "
"reference file to replace the deleted file.") "reference file to replace the deleted file.")
linkTypeChoice = RadioButtons(result, ["Symlink", "Hardlink"], columns=2)
directCheckbox = Checkbox(result, "Directly delete files") directCheckbox = Checkbox(result, "Directly delete files")
directLabel = Label(result, "Instead of sending files to trash, delete them directly. This option " directLabel = Label(result, "Instead of sending files to trash, delete them directly. This option "
"is usually used as a workaround when the normal deletion method doesn't work.") "is usually used as a workaround when the normal deletion method doesn't work.")
proceedButton = Button(result, "Proceed") proceedButton = Button(result, "Proceed")
cancelButton = Button(result, "Cancel") cancelButton = Button(result, "Cancel")
owner.hardlinkButton = hardlinkCheckbox owner.linkButton = linkCheckbox
owner.linkTypeRadio = linkTypeChoice
owner.directButton = directCheckbox owner.directButton = directCheckbox
owner.messageTextField = messageLabel owner.messageTextField = messageLabel
result.canMinimize = False result.canMinimize = False
result.canResize = False result.canResize = False
hardlinkLabel.controlSize = const.NSSmallControlSize linkLabel.controlSize = ControlSize.Small
directLabel.controlSize = const.NSSmallControlSize directLabel.controlSize = ControlSize.Small
linkTypeChoice.controlSize = ControlSize.Small
proceedButton.keyEquivalent = '\\r' proceedButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e' cancelButton.keyEquivalent = '\\e'
hardlinkCheckbox.action = directCheckbox.action = Action(owner, 'updateOptions') linkCheckbox.action = directCheckbox.action = linkTypeChoice.action = Action(owner, 'updateOptions')
proceedButton.action = Action(owner, 'proceed') proceedButton.action = Action(owner, 'proceed')
cancelButton.action = Action(owner, 'cancel') cancelButton.action = Action(owner, 'cancel')
hardlinkLabel.height *= 2 # 2 lines linkLabel.height *= 2 # 2 lines
directLabel.height *= 3 # 3 lines directLabel.height *= 3 # 3 lines
proceedButton.width = 92 proceedButton.width = 92
cancelButton.width = 92 cancelButton.width = 92
messageLabel.packToCorner(Pack.UpperLeft) mainLayout = VLayout([messageLabel, linkCheckbox, linkLabel, linkTypeChoice, directCheckbox,
hardlinkCheckbox.packRelativeTo(messageLabel, Pack.Below) directLabel])
hardlinkLabel.packRelativeTo(hardlinkCheckbox, Pack.Below) mainLayout.packToCorner(Pack.UpperLeft)
directCheckbox.packRelativeTo(hardlinkLabel, Pack.Below) mainLayout.fill(Pack.Right)
directLabel.packRelativeTo(directCheckbox, Pack.Below) buttonLayout = HLayout([cancelButton, proceedButton])
for view in (messageLabel, hardlinkCheckbox, hardlinkLabel, directCheckbox, directLabel): buttonLayout.packToCorner(Pack.LowerRight)
view.fill(Pack.Right)
proceedButton.packToCorner(Pack.LowerRight)
cancelButton.packRelativeTo(proceedButton, Pack.Left)
# indent the labels under checkboxes a little bit to the right # indent the labels under checkboxes a little bit to the right
for label in (hardlinkLabel, directLabel): for indentedView in (linkLabel, directLabel, linkTypeChoice):
label.x += 20 indentedView.x += 20
label.width -= 20 indentedView.width -= 20
# We actually don't want the link choice radio buttons to take all the width, it looks weird.
linkTypeChoice.width = 170

View File

@ -151,12 +151,12 @@ class DupeGuruME(DupeGuruBase):
self.directories = Directories(fileclasses=self.directories.fileclasses) self.directories = Directories(fileclasses=self.directories.fileclasses)
self.dead_tracks = [] self.dead_tracks = []
def _do_delete(self, j, replace_with_hardlinks, direct_deletion): def _do_delete(self, j, *args):
# XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so # XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so
# we'll be able to replace "op" below with DupeGuruBase._do_delete.op. # we'll be able to replace "op" below with DupeGuruBase._do_delete.op.
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion) return self._do_delete_dupe(dupe, *args)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash")) j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
@ -169,10 +169,10 @@ class DupeGuruME(DupeGuruBase):
pass pass
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, ITunesSong): if isinstance(dupe, ITunesSong):
dupe.remove_from_library() dupe.remove_from_library()
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion) DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path): def _create_file(self, path):
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]): if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]):

View File

@ -176,10 +176,10 @@ class DupeGuruPE(DupeGuruBase):
DupeGuruBase.__init__(self, view, appdata) DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories() self.directories = Directories()
def _do_delete(self, j, replace_with_hardlinks, direct_deletion): def _do_delete(self, j, *args):
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion) return self._do_delete_dupe(dupe, *args)
self.deleted_aperture_photos = False self.deleted_aperture_photos = False
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
@ -202,7 +202,7 @@ class DupeGuruPE(DupeGuruBase):
pass pass
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, IPhoto): if isinstance(dupe, IPhoto):
try: try:
a = app('iPhoto') a = app('iPhoto')
@ -244,7 +244,7 @@ class DupeGuruPE(DupeGuruBase):
except (CommandError, RuntimeError) as e: except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e)) raise EnvironmentError(str(e))
else: else:
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion) DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path): def _create_file(self, path):
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]): if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):

View File

@ -13,8 +13,11 @@ class DeletionOptionsView(GUIObjectView):
def show(self) -> bool: pass def show(self) -> bool: pass
class PyDeletionOptions(PyGUIObject): class PyDeletionOptions(PyGUIObject):
def setHardlink_(self, hardlink: bool): def setLinkDeleted_(self, link_deleted: bool):
self.model.hardlink = hardlink self.model.link_deleted = link_deleted
def setUseHardlinks_(self, use_hardlinks: bool):
self.model.use_hardlinks = use_hardlinks
def setDirect_(self, direct: bool): def setDirect_(self, direct: bool):
self.model.direct = direct self.model.direct = direct

View File

@ -159,15 +159,15 @@ class DupeGuru(RegistrableApplication, Broadcaster):
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)]) return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
return cmp_value(group.ref, key) return cmp_value(group.ref, key)
def _do_delete(self, j, replace_with_hardlinks, direct_deletion): def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion) return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
j.start_job(self.results.mark_count) j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
if not io.exists(dupe.path): if not io.exists(dupe.path):
return return
logging.debug("Sending '%s' to trash", dupe.path) logging.debug("Sending '%s' to trash", dupe.path)
@ -179,10 +179,11 @@ class DupeGuru(RegistrableApplication, Broadcaster):
os.remove(str_path) os.remove(str_path)
else: else:
send2trash(str_path) # Raises OSError when there's a problem send2trash(str_path) # Raises OSError when there's a problem
if replace_with_hardlinks: if link_deleted:
group = self.results.get_group_of_duplicate(dupe) group = self.results.get_group_of_duplicate(dupe)
ref = group.ref ref = group.ref
os.link(str(ref.path), str_path) linkfunc = os.link if use_hardlinks else os.symlink
linkfunc(str(ref.path), str_path)
self.clean_empty_dirs(dupe.path[:-1]) self.clean_empty_dirs(dupe.path[:-1])
def _create_file(self, path): def _create_file(self, path):
@ -365,7 +366,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
return return
if not self.deletion_options.show(self.results.mark_count): if not self.deletion_options.show(self.results.mark_count):
return return
args = [self.deletion_options.hardlink, self.deletion_options.direct] args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
self.deletion_options.direct]
logging.debug("Starting deletion job with args %r", args) logging.debug("Starting deletion job with args %r", args)
self.view.start_job(JobType.Delete, self._do_delete, args=args) self.view.start_job(JobType.Delete, self._do_delete, args=args)

View File

@ -15,7 +15,8 @@ class DeletionOptions(GUIObject):
# #
def show(self, mark_count): def show(self, mark_count):
self.hardlink = False self.link_deleted = False
self.use_hardlinks = False
self.direct = False self.direct = False
msg = tr("You are sending {} file(s) to the Trash.").format(mark_count) msg = tr("You are sending {} file(s) to the Trash.").format(mark_count)
self.view.update_msg(msg) self.view.update_msg(msg)

View File

@ -11,6 +11,7 @@ from PyQt4.QtGui import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBo
from hscommon.plat import ISOSX, ISLINUX from hscommon.plat import ISOSX, ISLINUX
from hscommon.trans import trget from hscommon.trans import trget
from qtlib.radio_box import RadioBox
tr = trget('ui') tr = trget('ui')
@ -27,20 +28,22 @@ class DeletionOptions(QDialog):
def _setupUi(self): def _setupUi(self):
self.setWindowTitle(tr("Deletion Options")) self.setWindowTitle(tr("Deletion Options"))
self.resize(400, 250) self.resize(400, 270)
self.verticalLayout = QVBoxLayout(self) self.verticalLayout = QVBoxLayout(self)
self.msgLabel = QLabel() self.msgLabel = QLabel()
self.verticalLayout.addWidget(self.msgLabel) self.verticalLayout.addWidget(self.msgLabel)
self.hardlinkCheckbox = QCheckBox(tr("Hardlink deleted files")) self.linkCheckbox = QCheckBox(tr("Link deleted files"))
if not (ISOSX or ISLINUX): if not (ISOSX or ISLINUX):
self.hardlinkCheckbox.setEnabled(False) self.linkCheckbox.setEnabled(False)
self.hardlinkCheckbox.setText(self.hardlinkCheckbox.text() + tr(" (Mac OS X or Linux only)")) self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (Mac OS X or Linux only)"))
self.verticalLayout.addWidget(self.hardlinkCheckbox) self.verticalLayout.addWidget(self.linkCheckbox)
text = tr("After having deleted a duplicate, place a hardlink targeting the reference file " text = tr("After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file.") "to replace the deleted file.")
self.hardlinkMessageLabel = QLabel(text) self.linkMessageLabel = QLabel(text)
self.hardlinkMessageLabel.setWordWrap(True) self.linkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.hardlinkMessageLabel) self.verticalLayout.addWidget(self.linkMessageLabel)
self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False)
self.verticalLayout.addWidget(self.linkTypeRadio)
self.directCheckbox = QCheckBox(tr("Directly delete files")) self.directCheckbox = QCheckBox(tr("Directly delete files"))
self.verticalLayout.addWidget(self.directCheckbox) self.verticalLayout.addWidget(self.directCheckbox)
text = tr("Instead of sending files to trash, delete them directly. This option is usually " text = tr("Instead of sending files to trash, delete them directly. This option is usually "
@ -58,10 +61,12 @@ class DeletionOptions(QDialog):
self.msgLabel.setText(msg) self.msgLabel.setText(msg)
def show(self): def show(self):
self.hardlinkCheckbox.setChecked(self.model.hardlink) self.linkCheckbox.setChecked(self.model.link_deleted)
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
self.directCheckbox.setChecked(self.model.direct) self.directCheckbox.setChecked(self.model.direct)
result = self.exec() result = self.exec()
self.model.hardlink = self.hardlinkCheckbox.isChecked() self.model.link_deleted = self.linkCheckbox.isChecked()
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
self.model.direct = self.directCheckbox.isChecked() self.model.direct = self.directCheckbox.isChecked()
return result == QDialog.Accepted return result == QDialog.Accepted