package net.filebot.ui.rename; import static java.util.Arrays.*; import static java.util.Collections.*; import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; import static net.filebot.Settings.*; import static net.filebot.media.MediaDetection.*; import static net.filebot.media.XattrMetaInfo.*; import static net.filebot.util.ExceptionUtilities.*; import static net.filebot.util.FileUtilities.*; import static net.filebot.util.ui.SwingUI.*; import java.awt.Window; import java.awt.event.ActionEvent; import java.io.File; import java.util.AbstractList; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Stream; import javax.swing.AbstractAction; import net.filebot.HistorySpooler; import net.filebot.NativeRenameAction; import net.filebot.ResourceManager; import net.filebot.StandardRenameAction; import net.filebot.UserFiles; import net.filebot.platform.mac.MacAppUtilities; import net.filebot.similarity.Match; import net.filebot.util.ui.ProgressMonitor; import net.filebot.util.ui.ProgressMonitor.ProgressWorker; class RenameAction extends AbstractAction { public static final String RENAME_ACTION = "RENAME_ACTION"; private final RenameModel model; public RenameAction(RenameModel model) { this.model = model; resetValues(); } public void resetValues() { putValue(RENAME_ACTION, StandardRenameAction.MOVE); putValue(NAME, "Rename"); putValue(SMALL_ICON, ResourceManager.getIcon("action.rename")); } @Override public void actionPerformed(ActionEvent evt) { if (model.names().isEmpty() || model.files().isEmpty()) { log.info("Nothing to rename. New Names is empty. Please <Fetch Data> first."); return; } Window window = getWindow(evt.getSource()); withWaitCursor(window, () -> { Map<File, File> renameMap = validate(model.getRenameMap(), window); if (renameMap.isEmpty()) { return; } List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>(model.matches()); StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION); // start processing Map<File, File> renameLog = new LinkedHashMap<File, File>(); try { if (useNativeShell() && NativeRenameAction.isSupported(action)) { // call on EDT NativeRenameWorker worker = new NativeRenameWorker(renameMap, renameLog, NativeRenameAction.valueOf(action.name())); worker.call(null, null, null); } else { // call and wait StandardRenameWorker worker = new StandardRenameWorker(renameMap, renameLog, action); String message = String.format("%s %d %s. This may take a while.", action.getDisplayVerb(), renameMap.size(), renameMap.size() == 1 ? "file" : "files"); ProgressMonitor.runTask(action.getDisplayName(), message, worker).get(); } } catch (CancellationException e) { debug.finest(e::toString); } catch (Throwable e) { log.log(Level.SEVERE, e, cause(getRootCause(e))); } // abort if nothing happened if (renameLog.isEmpty()) { return; } log.info(String.format("%d files renamed.", renameLog.size())); // remove renamed matches renameLog.forEach((from, to) -> { model.matches().remove(model.files().indexOf(from)); }); HistorySpooler.getInstance().append(renameLog.entrySet()); // store xattr storeMetaInfo(renameMap, matches); // delete empty folders if (action == StandardRenameAction.MOVE) { deleteEmptyFolders(renameLog); } }); } private void storeMetaInfo(Map<File, File> renameMap, List<Match<Object, File>> matches) { // write metadata into xattr if xattr is enabled for (Match<Object, File> match : matches) { File file = match.getCandidate(); Object info = match.getValue(); File destination = renameMap.get(file); if (info != null && destination != null) { destination = resolve(file, destination); if (destination.isFile()) { String original = file.getName(); debug.finest(format("Store xattr: [%s, %s] => %s", info, original, destination)); xattr.setMetaInfo(destination, info, original); } } } } private void deleteEmptyFolders(Map<File, File> renameMap) { // collect empty folders and files in reverse order Set<File> deleteFiles = new TreeSet<File>(); renameMap.forEach((s, d) -> { File sourceFolder = s.getParentFile(); File destinationFolder = resolve(s, d).getParentFile(); // destination folder is the source, or is inside the source folder if (d.getParentFile() == null || destinationFolder.getPath().startsWith(sourceFolder.getPath())) { return; } try { // guess affected folder depth int tailSize = listStructurePathTail(d.getParentFile()).size(); for (int i = 0; i < tailSize && !isStructureRoot(sourceFolder); sourceFolder = sourceFolder.getParentFile(), i++) { File[] children = sourceFolder.listFiles(); if (children == null || !stream(children).allMatch(f -> deleteFiles.contains(f) || isThumbnailStore(f))) { return; } stream(children).forEach(deleteFiles::add); deleteFiles.add(sourceFolder); } } catch (Exception e) { debug.warning(e::toString); } }); // use system trash to delete left-behind empty folders / hidden files try { for (File file : deleteFiles) { if (file.exists()) { UserFiles.trash(file); } } } catch (Throwable e) { debug.log(Level.WARNING, e, e::getMessage); } } private Map<File, File> validate(Map<File, File> renameMap, Window parent) { // rename map values as modifiable list List<File> destinationPathView = new AbstractList<File>() { private File[] keyIndex = renameMap.keySet().toArray(new File[0]); @Override public File get(int i) { return renameMap.get(keyIndex[i]); } @Override public File set(int i, File value) { return renameMap.put(keyIndex[i], value); } @Override public int size() { return keyIndex.length; } }; if (ValidateDialog.validate(parent, destinationPathView)) { // ask for user permissions for output folders so we can check them if (isMacSandbox()) { if (!MacAppUtilities.askUnlockFolders(parent, renameMap.entrySet().stream().flatMap(e -> Stream.of(e.getKey(), resolve(e.getKey(), e.getValue()))).collect(toList()))) { return emptyMap(); } } if (ConflictDialog.check(parent, renameMap)) { return renameMap; } } // return empty list if validation was cancelled return emptyMap(); } protected static class StandardRenameWorker implements ProgressWorker<Map<File, File>> { private Map<File, File> renameMap; private Map<File, File> renameLog; private StandardRenameAction action; public StandardRenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, StandardRenameAction action) { this.renameMap = renameMap; this.renameLog = renameLog; this.action = action; } @Override public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception { for (Entry<File, File> mapping : renameMap.entrySet()) { if (cancelled.get()) { return renameLog; } message.accept(mapping.getKey().getName()); // rename file, throw exception on failure File source = mapping.getKey(); File destination = resolve(mapping.getKey(), mapping.getValue()); if (!equalsCaseSensitive(source, destination)) { action.rename(source, destination); } // remember successfully renamed matches for history entry and possible revert renameLog.put(mapping.getKey(), mapping.getValue()); } return renameLog; } } protected static class NativeRenameWorker implements ProgressWorker<Map<File, File>> { private Map<File, File> renameMap; private Map<File, File> renameLog; private NativeRenameAction action; public NativeRenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, NativeRenameAction action) { this.renameMap = renameMap; this.renameLog = renameLog; this.action = action; } @Override public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception { // prepare delta, ignore files already named as desired Map<File, File> renamePlan = new LinkedHashMap<File, File>(); renameMap.forEach((from, to) -> { // resolve relative paths to = resolve(from, to); if (!equalsCaseSensitive(from, to)) { renamePlan.put(from, to); } }); // call native shell move/copy try { action.rename(renamePlan); } catch (CancellationException e) { debug.finest(e::getMessage); } // confirm results renameMap.forEach((from, to) -> { // resolve relative paths if (resolve(from, to).exists()) { renameLog.put(from, to); } }); return renameLog; } } }