package org.gudy.azureus2.core3.download.impl; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.gudy.azureus2.core3.download.DownloadManager; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.util.FileUtil; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.download.savelocation.DefaultSaveLocationManager; import org.gudy.azureus2.plugins.download.savelocation.SaveLocationChange; import org.gudy.azureus2.pluginsimpl.local.download.DownloadImpl; import com.aelitis.azureus.core.tag.Tag; import com.aelitis.azureus.core.tag.TagFeature; import com.aelitis.azureus.core.tag.TagFeatureFileLocation; import com.aelitis.azureus.core.tag.TagManagerFactory; public class DownloadManagerDefaultPaths extends DownloadManagerMoveHandlerUtils { public final static DefaultSaveLocationManager DEFAULT_HANDLER = new DefaultSaveLocationManager() { public SaveLocationChange onInitialization(Download d, boolean for_move, boolean on_event) { /** * This manager object isn't the sort of object which decides on * an alternate initialisation place for a download - if a user * has chosen a path for it, we don't interfere with it under any * circumstances (though if plugins want to, then that's up to them). */ if (on_event) {return null;} DownloadManager dm = ((DownloadImpl)d).getDownload(); return determinePaths(dm, UPDATE_FOR_MOVE_DETAILS[1], for_move, false); // 1 - incomplete downloads } public SaveLocationChange onCompletion(Download d, boolean for_move, boolean on_event) { DownloadManager dm = ((DownloadImpl)d).getDownload(); MovementInformation mi = getTagMovementInformation( dm, COMPLETION_DETAILS ); return determinePaths(dm, mi, for_move, false); } public SaveLocationChange testOnCompletion(Download d, boolean for_move, boolean on_event) { DownloadManager dm = ((DownloadImpl)d).getDownload(); MovementInformation mi = getTagMovementInformation( dm, COMPLETION_DETAILS ); return determinePaths(dm, mi, for_move, true ); } public SaveLocationChange onRemoval(Download d, boolean for_move, boolean on_event) { DownloadManager dm = ((DownloadImpl)d).getDownload(); return determinePaths(dm, REMOVAL_DETAILS, for_move, false ); } public boolean isInDefaultSaveDir(Download d) { DownloadManager dm = ((DownloadImpl)d).getDownload(); return DownloadManagerDefaultPaths.isInDefaultDownloadDir(dm); } }; private final static MovementInformation COMPLETION_DETAILS; private final static MovementInformation REMOVAL_DETAILS; private final static MovementInformation[] UPDATE_FOR_MOVE_DETAILS; private final static TargetSpecification[] DEFAULT_DIRS; private final static String SUBDIR_PARAM = "File.move.subdir_is_default"; static { SourceSpecification source; TargetSpecification dest; TransferSpecification trans; MovementInformation mi_1, mi_2; /** * There are three sets of directories that we consider a "default" * directory (perhaps it should just be two): * * - default save dir * - completed save dir * - removed save dir */ DEFAULT_DIRS = new TargetSpecification[3]; dest = new TargetSpecification(); dest.setBoolean("enabled", true); dest.setString("target", "Default save path"); dest.setContext("default save dir"); DEFAULT_DIRS[0] = dest; // First - download completion details. source = new SourceSpecification(); source.setBoolean("default dir", "Move Only When In Default Save Dir"); source.setBoolean("default subdir", SUBDIR_PARAM); source.setBoolean("incomplete dl", false); dest = new TargetSpecification(); dest.setBoolean("enabled", "Move Completed When Done"); dest.setString("target", "Completed Files Directory"); dest.setContext("completed files dir"); trans = new TransferSpecification(); trans.setBoolean("torrent", "Move Torrent When Done"); mi_1 = new MovementInformation(source, dest, trans, "Move on completion"); COMPLETION_DETAILS = mi_1; DEFAULT_DIRS[1] = dest; // Next - download removal details. source = new SourceSpecification(); source.setBoolean("default dir", "File.move.download.removed.only_in_default"); source.setBoolean("default subdir", SUBDIR_PARAM); source.setBoolean("incomplete dl", false); dest = new TargetSpecification(); dest.setBoolean("enabled", "File.move.download.removed.enabled"); dest.setString("target", "File.move.download.removed.path"); dest.setContext("removed files dir"); trans = new TransferSpecification(); trans.setBoolean("torrent", "File.move.download.removed.move_torrent"); mi_1 = new MovementInformation(source, dest, trans, "Move on removal"); REMOVAL_DETAILS = mi_1; DEFAULT_DIRS[2] = dest; /** * Next - updating the current path (complete dl's first) * * We instantiate the "update incomplete download" source first, and then * we instantiate the "update complete download", but when we process, we * will do the complete download bit first. * * We do this, because in the "update incomplete download" section, completed * downloads are enabled for it. And the reason it is, is because this will * allow the code to behave properly if move on completion is not enabled. * * Complete downloads apply to this bit, just in case the "move on completion" * section isn't active. */ source = new SourceSpecification(); source.updateSettings(COMPLETION_DETAILS.source.getSettings()); source.setBoolean("default dir", true); mi_1 = new MovementInformation(source, COMPLETION_DETAILS.target, COMPLETION_DETAILS.transfer, "Update completed download"); // Now incomplete downloads. We have to define completely new settings for // it, since we've never defined it before. source = new SourceSpecification(); source.setBoolean("default dir", true); // Must be in default directory to update. source.setBoolean("default subdir", SUBDIR_PARAM); source.setBoolean("incomplete dl", true); dest = new TargetSpecification(); dest.setBoolean("enabled", true); dest.setString("target", "Default save path"); trans = new TransferSpecification(); trans.setBoolean("torrent", false); // Rest of the settings are the same. mi_2 = new MovementInformation(source, dest, trans, "Update incomplete download"); UPDATE_FOR_MOVE_DETAILS = new MovementInformation[] {mi_1, mi_2}; } private static MovementInformation getTagMovementInformation( DownloadManager dm, MovementInformation def_mi ) { List<Tag> dm_tags = TagManagerFactory.getTagManager().getTagsForTaggable( dm ); if ( dm_tags == null || dm_tags.size() == 0 ){ return( def_mi ); } List<Tag> applicable_tags = new ArrayList<Tag>(); for ( Tag tag: dm_tags ){ if ( tag.getTagType().hasTagTypeFeature( TagFeature.TF_FILE_LOCATION )){ TagFeatureFileLocation fl = (TagFeatureFileLocation)tag; if ( fl.supportsTagMoveOnComplete()){ File move_to = fl.getTagMoveOnCompleteFolder(); if ( move_to != null ){ if ( !move_to.exists()){ move_to.mkdirs(); } if ( move_to.isDirectory() && move_to.canWrite()){ applicable_tags.add( tag ); }else{ logInfo( "Ignoring invalid tag move-to location: " + move_to, dm ); } } } } } if ( applicable_tags.size() == 0 ){ return( def_mi ); }else if ( applicable_tags.size() > 1 ){ Collections.sort( applicable_tags, new Comparator<Tag>() { public int compare( Tag o1, Tag o2) { return( o1.getTagID() - o2.getTagID()); } }); String str = ""; for ( Tag tag: applicable_tags ){ str += (str.length()==0?"":", ") + tag.getTagName( true ); } logInfo( "Multiple applicable tags found: " + str + " - selecting first", dm ); } Tag tag_target = applicable_tags.get(0); TagFeatureFileLocation fl = (TagFeatureFileLocation)tag_target; File move_to = fl.getTagMoveOnCompleteFolder(); if ( move_to != null ){ SourceSpecification source = new SourceSpecification(); source.setBoolean( "default dir", "Move Only When In Default Save Dir" ); source.setBoolean( "default subdir", SUBDIR_PARAM ); source.setBoolean( "incomplete dl", false ); TargetSpecification dest = new TargetSpecification(); dest.setBoolean( "enabled", true ); dest.setString( "target_raw", move_to.getAbsolutePath()); dest.setContext( "Tag '" + tag_target.getTagName( true ) + "' move-on-complete directory" ); TransferSpecification trans = new TransferSpecification(); trans.setBoolean("torrent", "Move Torrent When Done"); MovementInformation tag_mi = new MovementInformation(source, dest, trans, "Tag Move on Completion"); return( tag_mi ); } return( def_mi ); } private static interface ContextDescriptor { public String getContext(); } private static String normaliseRelativePathPart(String name) { name = name.trim(); if (name.length() == 0) {return "";} if (name.equals(".") || name.equals("..")) { return null; } return FileUtil.convertOSSpecificChars(name, false).trim(); } public static File normaliseRelativePath(File path) { if (path.isAbsolute()) {return null;} File parent = path.getParentFile(); String child_name = normaliseRelativePathPart(path.getName()); if (child_name == null) { return null; } // Simple one-level path. if (parent == null) { return new File(child_name); } ArrayList parts = new ArrayList(); parts.add(child_name); String filepart = null; while (parent != null) { filepart = normaliseRelativePathPart(parent.getName()); if (filepart == null) {return null;} else if (filepart.length()==0) {/* continue */} else {parts.add(0, filepart);} parent = parent.getParentFile(); } StringBuffer sb = new StringBuffer((String)parts.get(0)); for (int i=1; i<parts.size(); i++) { sb.append(File.separatorChar); sb.append(parts.get(i)); } return new File(sb.toString()); } private static File[] getDefaultDirs() { List results = new ArrayList(); File location = null; TargetSpecification ts = null; for (int i=0; i<DEFAULT_DIRS.length; i++) { ts = DEFAULT_DIRS[i]; location = ts.getTarget(null, ts); if (location != null) { results.add(location); } } return (File[])results.toArray(new File[results.size()]); } /** * This does the guts of determining appropriate file paths. */ private static SaveLocationChange determinePaths(DownloadManager dm, MovementInformation mi, boolean check_source, boolean is_test) { boolean proceed = !check_source || mi.source.matchesDownload(dm, mi, is_test ); if (!proceed) { logInfo("Cannot consider " + describe(dm, mi) + " - does not match source criteria.", dm); return null; } File target_path = mi.target.getTarget(dm, mi); if (target_path == null) { logInfo("Unable to determine an appropriate target for " + describe(dm, mi) + ".", dm); return null; } logInfo("Determined path for " + describe(dm, mi) + ".", dm); return mi.transfer.getTransferDetails(dm, mi, target_path); } static boolean isInDefaultDownloadDir(DownloadManager dm) { // We don't create this object properly, but just enough to get it // to be usable. SourceSpecification source = new SourceSpecification(); source.setBoolean("default subdir", SUBDIR_PARAM); return source.checkDefaultDir(dm.getSaveLocation().getParentFile(), getDefaultDirs()); } private static class MovementInformation implements ContextDescriptor { final SourceSpecification source; final TargetSpecification target; final TransferSpecification transfer; final String title; MovementInformation(SourceSpecification source, TargetSpecification target, TransferSpecification transfer, String title) { this.source = source; this.target = target; this.transfer = transfer; this.title = title; } public String getContext() {return title;} } private abstract static class ParameterHelper implements ContextDescriptor { private Map settings = new HashMap(); private String context = null; protected boolean getBoolean(String key) { Object result = this.settings.get(key); if (result == null) {throw new RuntimeException("bad key: " + key);} if (result instanceof Boolean) {return ((Boolean)result).booleanValue();} return COConfigurationManager.getBooleanParameter((String)result); } protected void setBoolean(String key, boolean value) { settings.put(key, Boolean.valueOf(value)); } protected void setBoolean(String key, String param) { settings.put(key, param); } protected void setString(String key, String param) { settings.put(key, param); } protected String getStringRaw(String key) { return((String)this.settings.get(key)); } protected String getString(String key) { String result = (String)this.settings.get(key); if (result == null) {throw new RuntimeException("bad key: " + key);} // This try-catch should be removed, it's only here for debugging purposes. return COConfigurationManager.getStringParameter(result); } public Map getSettings() {return this.settings;} public void updateSettings(Map settings) {this.settings.putAll(settings);} public String getContext() {return this.context;} public void setContext(String context) {this.context = context;} } private static class SourceSpecification extends ParameterHelper { public boolean matchesDownload(DownloadManager dm, ContextDescriptor context, boolean ignore_completeness ) { if (this.getBoolean("default dir")) { logInfo("Checking if " + describe(dm, context) + " is inside default dirs.", dm); File[] default_dirs = getDefaultDirs(); File current_location = dm.getSaveLocation().getParentFile(); /** * Very very rare, but I have seen this on fscked up downloads which don't appear * to have a blank / malformed download path. */ if (current_location == null) { logWarn(describe(dm, context) + " appears to have a malformed save directory, skipping.", dm); return false; } if (!this.checkDefaultDir(current_location, default_dirs)) { logWarn(describe(dm, context) + " doesn't exist in any of the following default directories" + " (current dir: " + current_location + ", subdirectories checked: " + this.getBoolean("default subdir") + ") - " + Arrays.asList(default_dirs), dm); return false; } logInfo(describe(dm, context) + " does exist inside default dirs.", dm); } // Does it work for incomplete downloads? if (!dm.isDownloadComplete(false)) { boolean can_move = ignore_completeness || this.getBoolean("incomplete dl"); String log_message = describe(dm, context) + " is incomplete which is " + ((can_move) ? "" : "not ") + "an appropriate state."; if (!can_move) { logInfo(log_message, dm); return false; } } return true; } public boolean checkDefaultDir(File location, File[] default_dirs) { location = FileUtil.canonise(location); boolean subdir = this.getBoolean("default subdir"); for (int i=0; i<default_dirs.length; i++) { if (subdir) { if (FileUtil.isAncestorOf(default_dirs[i], location)) {return true;} } else { if (default_dirs[i].equals(location)) {return true;} } } return false; } } private static class TargetSpecification extends ParameterHelper { public File getTarget(DownloadManager dm, ContextDescriptor cd) { //logInfo("Calculating target location for " + describe(dm, cd), lr); if (!this.getBoolean("enabled")) { logInfo("Target for " + describe(dm, cd) + " is not enabled.", dm); return null; } String location = getStringRaw( "target_raw" ); if ( location == null ){ location = this.getString("target").trim(); } if (location.length() == 0) { logInfo("No explicit target for " + describe(dm, cd) + ".", dm); return null; } File target = new File(FileUtil.getCanonicalFileName(location)); String relative_path = null; if( dm != null && dm.getDownloadState() != null ) { relative_path = dm.getDownloadState().getRelativeSavePath(); } if (relative_path != null && relative_path.length() > 0) { logInfo("Consider relative save path: " + relative_path, dm); // Doesn't matter if File.separator is required or not, it seems to // remove duplicate file separators. target = new File(target.getPath() + File.separator + relative_path); } return target; } } private static class TransferSpecification extends ParameterHelper { public SaveLocationChange getTransferDetails(DownloadManager dm, ContextDescriptor cd, File target_path) { if (target_path == null) {throw new NullPointerException();} SaveLocationChange result = new SaveLocationChange(); result.download_location = target_path; if (this.getBoolean("torrent")) { result.torrent_location = target_path; } return result; } } public static File getCompletionDirectory(DownloadManager dm) { return COMPLETION_DETAILS.target.getTarget(dm, null); } static String describe(DownloadManager dm, ContextDescriptor cs) { if (cs == null) {return describe(dm);} if (dm == null) { return "\"" + cs.getContext() + "\""; } return "\"" + dm.getDisplayName() + "\" with regard to \"" + cs.getContext() + "\""; } }