mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-03-09 21:24:36 +00:00
[#194 state:fixed] Added the "Replace with symlink" deletion option.
This commit is contained in:
parent
5247ac8abd
commit
5a5a74d0e1
@ -15,12 +15,14 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
PyDeletionOptions *model;
|
||||
|
||||
NSTextField *messageTextField;
|
||||
NSButton *hardlinkButton;
|
||||
NSButton *linkButton;
|
||||
NSMatrix *linkTypeRadio;
|
||||
NSButton *directButton;
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
- (id)initWithPyRef:(PyObject *)aPyRef;
|
||||
|
@ -13,7 +13,8 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
@implementation DeletionOptions
|
||||
|
||||
@synthesize messageTextField;
|
||||
@synthesize hardlinkButton;
|
||||
@synthesize linkButton;
|
||||
@synthesize linkTypeRadio;
|
||||
@synthesize directButton;
|
||||
|
||||
- (id)initWithPyRef:(PyObject *)aPyRef
|
||||
@ -33,7 +34,8 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
- (void)updateOptions
|
||||
{
|
||||
[model setHardlink:[hardlinkButton state] == NSOnState];
|
||||
[model setLinkDeleted:[linkButton state] == NSOnState];
|
||||
[model setUseHardlinks:[linkTypeRadio selectedColumn] == 1];
|
||||
[model setDirect:[directButton state] == NSOnState];
|
||||
}
|
||||
|
||||
@ -55,8 +57,9 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
- (BOOL)show
|
||||
{
|
||||
[hardlinkButton setState:NSOffState];
|
||||
[linkButton setState:NSOffState];
|
||||
[directButton setState:NSOffState];
|
||||
[linkTypeRadio selectCellAtRow:0 column:0];
|
||||
NSInteger r = [NSApp runModalForWindow:[self window]];
|
||||
[[self window] close];
|
||||
return r == NSOKButton;
|
||||
|
@ -1,47 +1,49 @@
|
||||
ownerclass = 'DeletionOptions'
|
||||
ownerimport = 'DeletionOptions.h'
|
||||
|
||||
result = Window(450, 215, "Deletion Options")
|
||||
result = Window(450, 240, "Deletion Options")
|
||||
messageLabel = Label(result, "")
|
||||
hardlinkCheckbox = Checkbox(result, "Hardlink deleted files")
|
||||
hardlinkLabel = Label(result, "After having deleted a duplicate, place a hardlink targeting the "
|
||||
linkCheckbox = Checkbox(result, "Link deleted files")
|
||||
linkLabel = Label(result, "After having deleted a duplicate, place a link targeting the "
|
||||
"reference file to replace the deleted file.")
|
||||
linkTypeChoice = RadioButtons(result, ["Symlink", "Hardlink"], columns=2)
|
||||
directCheckbox = Checkbox(result, "Directly delete files")
|
||||
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.")
|
||||
proceedButton = Button(result, "Proceed")
|
||||
cancelButton = Button(result, "Cancel")
|
||||
|
||||
owner.hardlinkButton = hardlinkCheckbox
|
||||
owner.linkButton = linkCheckbox
|
||||
owner.linkTypeRadio = linkTypeChoice
|
||||
owner.directButton = directCheckbox
|
||||
owner.messageTextField = messageLabel
|
||||
|
||||
result.canMinimize = False
|
||||
result.canResize = False
|
||||
hardlinkLabel.controlSize = const.NSSmallControlSize
|
||||
directLabel.controlSize = const.NSSmallControlSize
|
||||
linkLabel.controlSize = ControlSize.Small
|
||||
directLabel.controlSize = ControlSize.Small
|
||||
linkTypeChoice.controlSize = ControlSize.Small
|
||||
proceedButton.keyEquivalent = '\\r'
|
||||
cancelButton.keyEquivalent = '\\e'
|
||||
hardlinkCheckbox.action = directCheckbox.action = Action(owner, 'updateOptions')
|
||||
linkCheckbox.action = directCheckbox.action = linkTypeChoice.action = Action(owner, 'updateOptions')
|
||||
proceedButton.action = Action(owner, 'proceed')
|
||||
cancelButton.action = Action(owner, 'cancel')
|
||||
|
||||
hardlinkLabel.height *= 2 # 2 lines
|
||||
linkLabel.height *= 2 # 2 lines
|
||||
directLabel.height *= 3 # 3 lines
|
||||
proceedButton.width = 92
|
||||
cancelButton.width = 92
|
||||
|
||||
messageLabel.packToCorner(Pack.UpperLeft)
|
||||
hardlinkCheckbox.packRelativeTo(messageLabel, Pack.Below)
|
||||
hardlinkLabel.packRelativeTo(hardlinkCheckbox, Pack.Below)
|
||||
directCheckbox.packRelativeTo(hardlinkLabel, Pack.Below)
|
||||
directLabel.packRelativeTo(directCheckbox, Pack.Below)
|
||||
for view in (messageLabel, hardlinkCheckbox, hardlinkLabel, directCheckbox, directLabel):
|
||||
view.fill(Pack.Right)
|
||||
proceedButton.packToCorner(Pack.LowerRight)
|
||||
cancelButton.packRelativeTo(proceedButton, Pack.Left)
|
||||
mainLayout = VLayout([messageLabel, linkCheckbox, linkLabel, linkTypeChoice, directCheckbox,
|
||||
directLabel])
|
||||
mainLayout.packToCorner(Pack.UpperLeft)
|
||||
mainLayout.fill(Pack.Right)
|
||||
buttonLayout = HLayout([cancelButton, proceedButton])
|
||||
buttonLayout.packToCorner(Pack.LowerRight)
|
||||
|
||||
# indent the labels under checkboxes a little bit to the right
|
||||
for label in (hardlinkLabel, directLabel):
|
||||
label.x += 20
|
||||
label.width -= 20
|
||||
for indentedView in (linkLabel, directLabel, linkTypeChoice):
|
||||
indentedView.x += 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
|
||||
|
@ -151,12 +151,12 @@ class DupeGuruME(DupeGuruBase):
|
||||
self.directories = Directories(fileclasses=self.directories.fileclasses)
|
||||
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
|
||||
# we'll be able to replace "op" below with DupeGuruBase._do_delete.op.
|
||||
def op(dupe):
|
||||
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)]
|
||||
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
|
||||
@ -169,10 +169,10 @@ class DupeGuruME(DupeGuruBase):
|
||||
pass
|
||||
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):
|
||||
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):
|
||||
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]):
|
||||
|
@ -176,10 +176,10 @@ class DupeGuruPE(DupeGuruBase):
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
self.directories = Directories()
|
||||
|
||||
def _do_delete(self, j, replace_with_hardlinks, direct_deletion):
|
||||
def _do_delete(self, j, *args):
|
||||
def op(dupe):
|
||||
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
|
||||
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
|
||||
@ -202,7 +202,7 @@ class DupeGuruPE(DupeGuruBase):
|
||||
pass
|
||||
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):
|
||||
try:
|
||||
a = app('iPhoto')
|
||||
@ -244,7 +244,7 @@ class DupeGuruPE(DupeGuruBase):
|
||||
except (CommandError, RuntimeError) as e:
|
||||
raise EnvironmentError(str(e))
|
||||
else:
|
||||
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion)
|
||||
DupeGuruBase._do_delete_dupe(self, dupe, *args)
|
||||
|
||||
def _create_file(self, path):
|
||||
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):
|
||||
|
@ -13,8 +13,11 @@ class DeletionOptionsView(GUIObjectView):
|
||||
def show(self) -> bool: pass
|
||||
|
||||
class PyDeletionOptions(PyGUIObject):
|
||||
def setHardlink_(self, hardlink: bool):
|
||||
self.model.hardlink = hardlink
|
||||
def setLinkDeleted_(self, link_deleted: bool):
|
||||
self.model.link_deleted = link_deleted
|
||||
|
||||
def setUseHardlinks_(self, use_hardlinks: bool):
|
||||
self.model.use_hardlinks = use_hardlinks
|
||||
|
||||
def setDirect_(self, direct: bool):
|
||||
self.model.direct = direct
|
||||
|
14
core/app.py
14
core/app.py
@ -159,15 +159,15 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
|
||||
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):
|
||||
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)
|
||||
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):
|
||||
return
|
||||
logging.debug("Sending '%s' to trash", dupe.path)
|
||||
@ -179,10 +179,11 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
os.remove(str_path)
|
||||
else:
|
||||
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)
|
||||
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])
|
||||
|
||||
def _create_file(self, path):
|
||||
@ -365,7 +366,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
return
|
||||
if not self.deletion_options.show(self.results.mark_count):
|
||||
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)
|
||||
self.view.start_job(JobType.Delete, self._do_delete, args=args)
|
||||
|
||||
|
@ -15,7 +15,8 @@ class DeletionOptions(GUIObject):
|
||||
#
|
||||
|
||||
def show(self, mark_count):
|
||||
self.hardlink = False
|
||||
self.link_deleted = False
|
||||
self.use_hardlinks = False
|
||||
self.direct = False
|
||||
msg = tr("You are sending {} file(s) to the Trash.").format(mark_count)
|
||||
self.view.update_msg(msg)
|
||||
|
@ -11,6 +11,7 @@ from PyQt4.QtGui import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBo
|
||||
|
||||
from hscommon.plat import ISOSX, ISLINUX
|
||||
from hscommon.trans import trget
|
||||
from qtlib.radio_box import RadioBox
|
||||
|
||||
tr = trget('ui')
|
||||
|
||||
@ -27,20 +28,22 @@ class DeletionOptions(QDialog):
|
||||
|
||||
def _setupUi(self):
|
||||
self.setWindowTitle(tr("Deletion Options"))
|
||||
self.resize(400, 250)
|
||||
self.resize(400, 270)
|
||||
self.verticalLayout = QVBoxLayout(self)
|
||||
self.msgLabel = QLabel()
|
||||
self.verticalLayout.addWidget(self.msgLabel)
|
||||
self.hardlinkCheckbox = QCheckBox(tr("Hardlink deleted files"))
|
||||
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
||||
if not (ISOSX or ISLINUX):
|
||||
self.hardlinkCheckbox.setEnabled(False)
|
||||
self.hardlinkCheckbox.setText(self.hardlinkCheckbox.text() + tr(" (Mac OS X or Linux only)"))
|
||||
self.verticalLayout.addWidget(self.hardlinkCheckbox)
|
||||
text = tr("After having deleted a duplicate, place a hardlink targeting the reference file "
|
||||
self.linkCheckbox.setEnabled(False)
|
||||
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (Mac OS X or Linux only)"))
|
||||
self.verticalLayout.addWidget(self.linkCheckbox)
|
||||
text = tr("After having deleted a duplicate, place a link targeting the reference file "
|
||||
"to replace the deleted file.")
|
||||
self.hardlinkMessageLabel = QLabel(text)
|
||||
self.hardlinkMessageLabel.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.hardlinkMessageLabel)
|
||||
self.linkMessageLabel = QLabel(text)
|
||||
self.linkMessageLabel.setWordWrap(True)
|
||||
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.verticalLayout.addWidget(self.directCheckbox)
|
||||
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)
|
||||
|
||||
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)
|
||||
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()
|
||||
return result == QDialog.Accepted
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user