diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs
new file mode 100644
index 0000000000..8d0819d5cc
--- /dev/null
+++ b/Cmdline/Action/Import.cs
@@ -0,0 +1,106 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using log4net;
+
+namespace CKAN.CmdLine
+{
+
+ ///
+ /// Handler for "ckan import" command.
+ /// Imports manually downloaded ZIP files into the cache.
+ ///
+ public class Import : ICommand
+ {
+
+ ///
+ /// Initialize the command
+ ///
+ /// IUser object for user interaction
+ public Import(IUser user)
+ {
+ this.user = user;
+ }
+
+ ///
+ /// Execute an import command
+ ///
+ /// Game instance into which to import
+ /// Command line parameters from the user
+ ///
+ /// Process exit code
+ ///
+ public int RunCommand(CKAN.KSP ksp, object options)
+ {
+ try
+ {
+ ImportOptions opts = options as ImportOptions;
+ HashSet toImport = GetFiles(opts);
+ if (toImport.Count < 1)
+ {
+ user.RaiseMessage("Usage: ckan import path [path2, ...]");
+ return Exit.ERROR;
+ }
+ else
+ {
+ log.InfoFormat("Importing {0} files", toImport.Count);
+ List toInstall = new List();
+ ModuleInstaller inst = ModuleInstaller.GetInstance(ksp, user);
+ inst.ImportFiles(toImport, user, id => toInstall.Add(id), !opts.Headless);
+ if (toInstall.Count > 0)
+ {
+ inst.InstallList(
+ toInstall,
+ new RelationshipResolverOptions()
+ );
+ }
+ return Exit.OK;
+ }
+ }
+ catch (Exception ex)
+ {
+ user.RaiseError("Import error: {0}", ex.Message);
+ return Exit.ERROR;
+ }
+ }
+
+ private HashSet GetFiles(ImportOptions options)
+ {
+ HashSet files = new HashSet();
+ foreach (string filename in options.paths)
+ {
+ if (Directory.Exists(filename))
+ {
+ // Import everything in this folder
+ log.InfoFormat("{0} is a directory", filename);
+ foreach (string dirfile in Directory.EnumerateFiles(filename))
+ {
+ AddFile(files, dirfile);
+ }
+ }
+ else
+ {
+ AddFile(files, filename);
+ }
+ }
+ return files;
+ }
+
+ private void AddFile(HashSet files, string filename)
+ {
+ if (File.Exists(filename))
+ {
+ log.InfoFormat("Attempting import of {0}", filename);
+ files.Add(new FileInfo(filename));
+ }
+ else
+ {
+ user.RaiseMessage("File not found: {0}", filename);
+ }
+ }
+
+ private readonly IUser user;
+ private static readonly ILog log = LogManager.GetLogger(typeof(Import));
+ }
+
+}
diff --git a/Cmdline/CKAN-cmdline.csproj b/Cmdline/CKAN-cmdline.csproj
index 409b389768..2ca70b235e 100644
--- a/Cmdline/CKAN-cmdline.csproj
+++ b/Cmdline/CKAN-cmdline.csproj
@@ -57,6 +57,7 @@
+
diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs
index f2e19adde9..a35b916f92 100644
--- a/Cmdline/Main.cs
+++ b/Cmdline/Main.cs
@@ -189,6 +189,9 @@ private static int RunSimpleAction(Options cmdline, CommonOptions options, strin
Scan(GetGameInstance(manager), user, cmdline.action);
return (new Upgrade(user)).RunCommand(GetGameInstance(manager), cmdline.options);
+ case "import":
+ return (new Import(user)).RunCommand(GetGameInstance(manager), options);
+
case "clean":
return Clean(GetGameInstance(manager));
diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs
index e721506cb8..064553b015 100644
--- a/Cmdline/Options.cs
+++ b/Cmdline/Options.cs
@@ -67,6 +67,9 @@ internal class Actions : VerbCommandOptions
[VerbOption("remove", HelpText = "Remove an installed mod")]
public RemoveOptions Remove { get; set; }
+ [VerbOption("import", HelpText = "Import manually downloaded mods")]
+ public ImportOptions Import { get; set; }
+
[VerbOption("scan", HelpText = "Scan for manually installed KSP mods")]
public ScanOptions Scan { get; set; }
@@ -132,6 +135,9 @@ public string GetUsage(string verb)
case "compare":
ht.AddPreOptionsLine($"Usage: ckan {verb} [options] version1 version2");
break;
+ case "import":
+ ht.AddPreOptionsLine($"Usage: ckan {verb} [options] paths");
+ break;
// Now the commands with only --flag type options
case "gui":
@@ -433,6 +439,12 @@ internal class RemoveOptions : InstanceSpecificOptions
public bool rmall { get; set; }
}
+ internal class ImportOptions : InstanceSpecificOptions
+ {
+ [ValueList(typeof(List))]
+ public List paths { get; set; }
+ }
+
internal class ShowOptions : InstanceSpecificOptions
{
[ValueOption(0)] public string Modname { get; set; }
diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs
index b616cafff9..4f3ed0d43c 100644
--- a/Core/ModuleInstaller.cs
+++ b/Core/ModuleInstaller.cs
@@ -1092,7 +1092,8 @@ private void DownloadModules(IEnumerable mods, IDownloader downloade
/// Set of files to import
/// Object for user interaction
/// Function to call to mark a mod for installation
- public void ImportFiles(HashSet files, IUser user, Action installMod)
+ /// True to ask user whether to delete imported files, false to leave the files as is
+ public void ImportFiles(HashSet files, IUser user, Action installMod, bool allowDelete = true)
{
Registry registry = registry_manager.registry;
HashSet installable = new HashSet();
@@ -1134,7 +1135,7 @@ public void ImportFiles(HashSet files, IUser user, Action inst
}
++i;
}
- if (installable.Count > 0 && user.RaiseYesNoDialog($"Install {installable.Count} compatible imported mods?"))
+ if (installable.Count > 0 && user.RaiseYesNoDialog($"Install {installable.Count} compatible imported mods in game instance {ksp.Name} ({ksp.GameDir()})?"))
{
// Install the imported mods
foreach (string identifier in installable)
@@ -1142,7 +1143,7 @@ public void ImportFiles(HashSet files, IUser user, Action inst
installMod(identifier);
}
}
- if (user.RaiseYesNoDialog($"Import complete. Delete {deletable.Count} old files?"))
+ if (allowDelete && deletable.Count > 0 && user.RaiseYesNoDialog($"Import complete. Delete {deletable.Count} old files?"))
{
// Delete old files
foreach (FileInfo f in deletable)
diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs
index 7b298f1300..12c3cc3752 100644
--- a/Core/Net/NetModuleCache.cs
+++ b/Core/Net/NetModuleCache.cs
@@ -142,7 +142,7 @@ public string Store(CkanModule module, string path, string description = null, b
}
// If no exceptions, then everything is fine
- return cache.Store(module.download, path, description, move);
+ return cache.Store(module.download, path, description ?? module.StandardName(), move);
}
private NetFileCache cache;