diff --git a/source/net/sourceforge/filebot/cli/ArgumentBean.java b/source/net/sourceforge/filebot/cli/ArgumentBean.java index 489bed74..75082123 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentBean.java +++ b/source/net/sourceforge/filebot/cli/ArgumentBean.java @@ -31,6 +31,9 @@ public class ArgumentBean { @Option(name = "--order", usage = "Episode order", metaVar = "[Airdate, Absolute, DVD]") public String order = "Airdate"; + @Option(name = "--action", usage = "Rename action", metaVar = "[move, copy, symlink, test]") + public String action = "move"; + @Option(name = "--format", usage = "Episode naming scheme", metaVar = "expression") public String format; diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 768a50ff..4ea86672 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -73,7 +73,7 @@ public class ArgumentProcessor { } if (args.rename) { - cli.rename(files, args.query, args.output, args.format, args.db, args.order, args.lang, !args.nonStrict); + cli.rename(files, args.action, args.output, args.format, args.db, args.query, args.order, args.lang, !args.nonStrict); } if (args.check) { diff --git a/source/net/sourceforge/filebot/cli/CmdlineInterface.java b/source/net/sourceforge/filebot/cli/CmdlineInterface.java index c31395fc..24c01d10 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineInterface.java +++ b/source/net/sourceforge/filebot/cli/CmdlineInterface.java @@ -9,7 +9,7 @@ import java.util.List; public interface CmdlineInterface { - List rename(Collection files, String query, String output, String format, String db, String sortOrder, String lang, boolean strict) throws Exception; + List rename(Collection files, String action, String output, String format, String db, String query, String sortOrder, String lang, boolean strict) throws Exception; List getSubtitles(Collection files, String query, String lang, String output, String encoding, boolean strict) throws Exception; diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index 5f30ec4f..af675a26 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -22,10 +22,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -80,10 +80,11 @@ import net.sourceforge.tuned.FileUtilities.FolderFilter; public class CmdlineOperations implements CmdlineInterface { @Override - public List rename(Collection files, String query, String output, String expression, String db, String sortOrder, String languageName, boolean strict) throws Exception { + public List rename(Collection files, String action, String output, String expression, String db, String query, String sortOrder, String lang, boolean strict) throws Exception { ExpressionFormat format = (expression != null) ? new ExpressionFormat(expression) : null; - Locale locale = getLanguage(languageName).toLocale(); File outputDir = (output != null && output.length() > 0) ? new File(output) : null; + Locale locale = getLanguage(lang).toLocale(); + RenameAction renameAction = StandardRenameAction.forName(action); List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); if (mediaFiles.isEmpty()) { @@ -92,12 +93,12 @@ public class CmdlineOperations implements CmdlineInterface { if (getEpisodeListProvider(db) != null) { // tv series mode - return renameSeries(files, query, outputDir, format, getEpisodeListProvider(db), SortOrder.forName(sortOrder), locale, strict); + return renameSeries(files, renameAction, outputDir, format, getEpisodeListProvider(db), query, SortOrder.forName(sortOrder), locale, strict); } if (getMovieIdentificationService(db) != null) { // movie mode - return renameMovie(files, query, outputDir, format, getMovieIdentificationService(db), locale, strict); + return renameMovie(files, renameAction, outputDir, format, getMovieIdentificationService(db), query, locale, strict); } // auto-determine mode @@ -128,14 +129,14 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.finest(format("Filename pattern: [%.02f] SxE, [%.02f] CWS", sxe / max, cws / max)); if (sxe >= (max * 0.65) || cws >= (max * 0.65)) { - return renameSeries(files, query, outputDir, format, getEpisodeListProviders()[0], SortOrder.forName(sortOrder), locale, strict); // use default episode db + return renameSeries(files, renameAction, outputDir, format, getEpisodeListProviders()[0], query, SortOrder.forName(sortOrder), locale, strict); // use default episode db } else { - return renameMovie(files, query, outputDir, format, getMovieIdentificationServices()[0], locale, strict); // use default movie db + return renameMovie(files, renameAction, outputDir, format, getMovieIdentificationServices()[0], query, locale, strict); // use default movie db } } - public List renameSeries(Collection files, String query, File outputDir, ExpressionFormat format, EpisodeListProvider db, SortOrder sortOrder, Locale locale, boolean strict) throws Exception { + public List renameSeries(Collection files, RenameAction renameAction, File outputDir, ExpressionFormat format, EpisodeListProvider db, String query, SortOrder sortOrder, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename episodes using [%s]", db.getName())); List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); @@ -198,7 +199,7 @@ public class CmdlineOperations implements CmdlineInterface { // rename episodes Analytics.trackEvent("CLI", "Rename", "Episode", renameMap.size()); - return renameAll(renameMap); + return renameAll(renameMap, renameAction); } @@ -271,7 +272,7 @@ public class CmdlineOperations implements CmdlineInterface { } - public List renameMovie(Collection files, String query, File outputDir, ExpressionFormat format, MovieIdentificationService service, Locale locale, boolean strict) throws Exception { + public List renameMovie(Collection files, RenameAction renameAction, File outputDir, ExpressionFormat format, MovieIdentificationService service, String query, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename movies using [%s]", service.getName())); // handle movie files @@ -421,11 +422,33 @@ public class CmdlineOperations implements CmdlineInterface { // rename movies Analytics.trackEvent("CLI", "Rename", "Movie", renameMap.size()); - return renameAll(renameMap); + return renameAll(renameMap, renameAction); } - public List renameAll(Map renameMap) throws Exception { + public List renameAll(Map renameMap, RenameAction renameAction) throws Exception { + // perform some sanity checks + Set destinationSet = new HashSet(); + + for (Entry mapping : renameMap.entrySet()) { + File source = mapping.getKey(); + File destination = mapping.getValue(); + + // resolve destination + if (!destination.isAbsolute()) { + // same folder, different name + destination = new File(source.getParentFile(), destination.getPath()); + } + + if (destinationSet.contains(destination)) + throw new IllegalArgumentException("Conflict detected: " + mapping.getValue()); + + if (destination.exists() && !source.equals(destination)) + throw new IllegalArgumentException("File already exists: " + mapping.getValue()); + + destinationSet.add(destination); + } + // rename files final List> renameLog = new ArrayList>(); @@ -433,35 +456,16 @@ public class CmdlineOperations implements CmdlineInterface { for (Entry it : renameMap.entrySet()) { try { // rename file, throw exception on failure - File destination = moveRename(it.getKey(), it.getValue()); - CLILogger.info(format("Renamed [%s] to [%s]", it.getKey(), it.getValue())); + File destination = renameAction.rename(it.getKey(), it.getValue()); + CLILogger.info(format("[%s] Renamed [%s] to [%s]", renameAction, it.getKey(), it.getValue())); // remember successfully renamed matches for history entry and possible revert renameLog.add(new SimpleImmutableEntry(it.getKey(), destination)); } catch (IOException e) { - CLILogger.warning(format("Failed to rename [%s]", it.getKey())); + CLILogger.warning(format("[%s] Failed to rename [%s]", renameAction, it.getKey())); throw e; } } - } catch (Exception e) { - // could not rename one of the files, revert all changes - CLILogger.severe(e.getMessage()); - - // revert rename operations in reverse order - for (ListIterator> iterator = renameLog.listIterator(renameLog.size()); iterator.hasPrevious();) { - Entry mapping = iterator.previous(); - - // revert rename - if (mapping.getValue().renameTo(mapping.getKey())) { - // remove reverted rename operation from log - CLILogger.info("Reverted filename: " + mapping.getKey()); - } else { - // failed to revert rename operation - CLILogger.severe("Failed to revert filename: " + mapping.getValue()); - } - } - - throw new Exception("Renaming failed", e); } finally { if (renameLog.size() > 0) { // update rename history diff --git a/source/net/sourceforge/filebot/cli/RenameAction.java b/source/net/sourceforge/filebot/cli/RenameAction.java new file mode 100644 index 00000000..8157f17b --- /dev/null +++ b/source/net/sourceforge/filebot/cli/RenameAction.java @@ -0,0 +1,12 @@ + +package net.sourceforge.filebot.cli; + + +import java.io.File; + + +public interface RenameAction { + + File rename(File from, File to) throws Exception; + +} diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 39e3eb8f..99c00863 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -187,7 +187,7 @@ List.metaClass.sortBySimilarity = { prime, Closure toStringFunction = { obj -> o // CLI bindings def rename(args) { args = _defaults(args) synchronized (_cli) { - _guarded { _cli.rename(_files(args), args.query, args.output, args.format, args.db, args.order, args.lang, args.strict) } + _guarded { _cli.rename(_files(args), args.action, args.output, args.format, args.db, args.query, args.order, args.lang, args.strict) } } } @@ -251,6 +251,7 @@ def _files(args) { * Fill in default values from cmdline arguments */ def _defaults(args) { + args.action = args.action ?: _args.action args.query = args.query ?: _args.query args.format = args.format ?: _args.format args.db = args.db ?: _args.db diff --git a/source/net/sourceforge/filebot/cli/StandardRenameAction.java b/source/net/sourceforge/filebot/cli/StandardRenameAction.java new file mode 100644 index 00000000..d53a5912 --- /dev/null +++ b/source/net/sourceforge/filebot/cli/StandardRenameAction.java @@ -0,0 +1,79 @@ + +package net.sourceforge.filebot.cli; + + +import java.io.File; +import java.io.IOException; + +import net.sourceforge.tuned.FileUtilities; + + +public enum StandardRenameAction implements RenameAction { + + MOVE { + + @Override + public File rename(File from, File to) throws Exception { + return FileUtilities.moveRename(from, to); + } + }, + + COPY { + + @Override + public File rename(File from, File to) throws Exception { + return FileUtilities.copyAs(from, to); + } + }, + + SYMLINK { + + @Override + public File rename(File from, File to) throws Exception { + File destionation = FileUtilities.resolveDestination(from, to); + + // create symlink via NIO.2 + try { + java.nio.file.Files.createSymbolicLink(destionation.toPath(), from.toPath()); + } catch (LinkageError e) { + throw new Exception("Unsupported Operation: createSymbolicLink"); + } + + return destionation; + } + }, + + HARDLINK { + + @Override + public File rename(File from, File to) throws Exception { + File destionation = FileUtilities.resolveDestination(from, to); + + // create hardlink via NIO.2 + try { + java.nio.file.Files.createLink(destionation.toPath(), from.toPath()); + } catch (LinkageError e) { + throw new Exception("Unsupported Operation: createLink"); + } + + return destionation; + } + }, + + TEST { + + @Override + public File rename(File from, File to) throws IOException { + return FileUtilities.resolveDestination(from, to); + } + }; + + public static StandardRenameAction forName(String action) { + for (StandardRenameAction it : values()) { + if (it.name().equalsIgnoreCase(action)) + return it; + } + + throw new IllegalArgumentException("Illegal rename action: " + action); + } +} diff --git a/source/net/sourceforge/filebot/ui/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/rename/RenameAction.java index e792131f..aff74625 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/rename/RenameAction.java @@ -22,7 +22,6 @@ 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.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; @@ -90,7 +89,7 @@ class RenameAction extends AbstractAction { private Map checkRenamePlan(List> renamePlan) { // build rename map and perform some sanity checks Map renameMap = new HashMap(); - Set destinationSet = new TreeSet(); + Set destinationSet = new HashSet(); for (Entry mapping : renamePlan) { File source = mapping.getKey(); @@ -113,6 +112,7 @@ class RenameAction extends AbstractAction { // use original mapping values renameMap.put(mapping.getKey(), mapping.getValue()); + destinationSet.add(destination); } return renameMap; diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index 6b9ef546..ebd48927 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -44,18 +44,7 @@ public final class FileUtilities { public static File moveRename(File source, File destination) throws IOException { // resolve destination - if (!destination.isAbsolute()) { - // same folder, different name - destination = new File(source.getParentFile(), destination.getPath()); - } - - // make sure we that we can create the destination folder structure - File destinationFolder = destination.getParentFile(); - - // create parent folder if necessary - if (!destinationFolder.isDirectory() && !destinationFolder.mkdirs()) { - throw new IOException("Failed to create folder: " + destinationFolder); - } + destination = resolveDestination(source, destination); if (source.isDirectory()) { // move folder moveFolderIO(source, destination); @@ -89,6 +78,20 @@ public final class FileUtilities { public static File copyAs(File source, File destination) throws IOException { + // resolve destination + destination = resolveDestination(source, destination); + + if (source.isDirectory()) { // copy folder + org.apache.commons.io.FileUtils.copyDirectory(source, destination); + } else { // copy file + org.apache.commons.io.FileUtils.copyFile(source, destination); + } + + return destination; + } + + + public static File resolveDestination(File source, File destination) throws IOException { // resolve destination if (!destination.isAbsolute()) { // same folder, different name @@ -103,12 +106,6 @@ public final class FileUtilities { throw new IOException("Failed to create folder: " + destinationFolder); } - if (source.isDirectory()) { // copy folder - org.apache.commons.io.FileUtils.copyDirectory(source, destination); - } else { // copy file - org.apache.commons.io.FileUtils.copyFile(source, destination); - } - return destination; }