diff --git a/CKAN/CKAN/CKAN.csproj b/CKAN/CKAN/CKAN.csproj
index 7fc579b3fd..421cd19d1f 100644
--- a/CKAN/CKAN/CKAN.csproj
+++ b/CKAN/CKAN/CKAN.csproj
@@ -49,6 +49,10 @@
+
+
+
+
-
\ No newline at end of file
+
diff --git a/CKAN/CKAN/InstalledModule.cs b/CKAN/CKAN/InstalledModule.cs
new file mode 100644
index 0000000000..e9637b55c1
--- /dev/null
+++ b/CKAN/CKAN/InstalledModule.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace CKAN
+{
+ public class InstalledModuleFile
+ {
+ public string name;
+ public string sha1_sum;
+ }
+
+ public class InstalledModule
+ {
+ public InstalledModuleFile[] installed_files;
+ public Module source_module;
+ public DateTime install_time;
+
+ public InstalledModule (InstalledModuleFile[] installed_files, Module source_module, DateTime install_time)
+ {
+ this.installed_files = installed_files;
+ this.source_module = source_module;
+ this.install_time = install_time;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CKAN/CKAN/Module.cs b/CKAN/CKAN/Module.cs
index bac98d10fe..149935cd04 100644
--- a/CKAN/CKAN/Module.cs
+++ b/CKAN/CKAN/Module.cs
@@ -1,15 +1,6 @@
using System;
-using System.IO;
-using System.Net;
-using System.Linq;
-
using Newtonsoft.Json;
-using ICSharpCode.SharpZipLib.Core;
-using ICSharpCode.SharpZipLib.Zip;
-
-using System.Text.RegularExpressions;
-
///
/// Describes a CKAN module (ie, what's in the CKAN.schema file).
///
@@ -35,70 +26,61 @@ namespace CKAN {
public class Module {
[JsonProperty("name", Required = Required.Always)]
- public string _name;
+ public string name;
[JsonProperty("identifier", Required = Required.Always)]
- public string _identifier; // TODO: Strong type
+ public string identifier; // TODO: Strong type
// TODO: Change spec: abstract -> description
[JsonProperty("abstract", Required = Required.Always)]
- public string _abstract;
+ public string @abstract;
[JsonProperty("comment")]
- public string _comment;
+ public string comment;
[JsonProperty("author")]
- public string[] _author;
+ public string[] author;
[JsonProperty("download", Required = Required.Always)]
- public Uri _download;
+ public Uri download;
[JsonProperty("license", Required= Required.Always)]
- public dynamic _license; // TODO: Strong type
+ public dynamic license; // TODO: Strong type
[JsonProperty("version", Required = Required.Always)]
- public string _version; // TODO: Strong type
+ public string version; // TODO: Strong type
[JsonProperty("release_status")]
- public string _release_status; // TODO: Strong type
+ public string release_status; // TODO: Strong type
[JsonProperty("min_ksp")]
- public string _min_ksp; // TODO: Type
+ public string min_ksp; // TODO: Type
[JsonProperty("max_ksp")]
- public string _max_ksp; // TODO: Type
+ public string max_ksp; // TODO: Type
[JsonProperty("requires")]
- public dynamic[] _requires;
+ public dynamic[] requires;
[JsonProperty("recommends")]
- public dynamic[] _recommends;
+ public dynamic[] recommends;
[JsonProperty("conflicts")]
- public dynamic[] _conflicts;
+ public dynamic[] conflicts;
[JsonProperty("resourcs")]
- public dynamic[] _resources;
+ public dynamic[] resources;
[JsonProperty("install", Required = Required.Always)]
- public dynamic[] _install;
+ public dynamic[] install;
[JsonProperty("bundles")]
- public dynamic[] _bundles;
-
- // Private record of which file we came from.
- string origCkanFile;
+ public dynamic[] bundles;
/// Generates a CKAN.Meta object given a filename
public static Module from_file(string filename) {
string json = System.IO.File.ReadAllText (filename);
-
- Module built = Module.from_string (json);
-
- // Attach which file this came from.
- built.origCkanFile = filename;
-
- return built;
+ return Module.from_string (json);
}
/// Generates a CKAN.META object from a string
@@ -106,155 +88,18 @@ public static Module from_string(string json) {
return JsonConvert.DeserializeObject (json);
}
- ///
- /// Download the given mod. Returns the filename it was saved to.
- ///
- /// If no filename is provided, the standard_name() will be used.
- ///
- ///
- /// Filename.
- public string download(string filename = null) {
-
- // Generate a temporary file if none is provided.
- if (filename == null) {
- filename = standard_name();
- }
-
- Console.WriteLine (" * Downloading " + filename + "...");
-
- WebClient agent = new WebClient ();
- agent.DownloadFile (_download, filename);
-
- return filename;
- }
-
///
/// Returns a standardised name for this module, in the form
/// "identifier-version.zip". For example, `RealSolarSystem-7.3.zip`
///
- public string standard_name() {
- return _identifier + "-" + _version + ".zip";
- }
-
- ///
- /// Install our mod from the filename supplied.
- /// If no file is supplied, we will fetch() it first.
- ///
-
- public void install(string filename = null) {
-
- Console.WriteLine (_identifier + ":\n");
-
- // Fetch our file if we don't already have it.
- if (filename == null) {
- filename = download ();
- }
-
- // Open our zip file for processing
- ZipFile zipfile = new ZipFile (File.OpenRead (filename));
-
- // Walk through our install instructions.
- foreach (dynamic stanza in _install) {
- install_component (stanza, zipfile);
-
- // TODO: We should just *serialise* our current state, not
- // copy the original file, because we can't always guarantee
- // there will be an original file.
-
- // TODO: This will just throw them in GameData.
- // We need a way to convert stanzas to install locations.
- // We really should make Stanza its own class.
-
- File.Copy (origCkanFile, KSP.gameData() + "/" + _identifier + ".ckan", true);
- }
-
- // Finish now if we have no bundled mods.
- if (_bundles == null) { return; }
-
- // Do the same with our bundled mods.
- foreach (dynamic stanza in _bundles) {
-
- // TODO: Check versions, so we don't double install.
-
- install_component (stanza, zipfile);
-
- // TODO: Generate CKAN metadata for the bundled component.
- }
-
- return;
-
- }
-
- void install_component(dynamic stanza, ZipFile zipfile) {
- string fileToInstall = stanza.file;
-
- Console.WriteLine (" * Installing " + fileToInstall);
-
- string[] path = fileToInstall.Split('/');
-
- // TODO: This will depend upon the `install_to` in the JSON file
- string installDir = KSP.gameData ();
-
- // This is what we strip off paths
- string stripDir = String.Join("/", path.Take(path.Count() - 1)) + "/";
-
- // Console.WriteLine("InstallDir is "+installDir);
- // Console.WriteLine ("StripDir is " + stripDir);
-
- // This is awful. There's got to be a better way to extract a tree?
- string filter = "^" + stanza.file + "(/|$)";
-
- // O(N^2) solution. Surely there's a better way...
- foreach (ZipEntry entry in zipfile) {
-
- // Skip things we don't want.
- if (! Regex.IsMatch (entry.Name, filter)) {
- continue;
- }
-
- // Get the full name of the file.
- string outputName = entry.Name;
-
- // Strip off the prefix (often GameData/)
- // TODO: The C# equivalent of "\Q stripDir \E" so we can't be caught by metacharacters.
- outputName = Regex.Replace (outputName, @"^" + stripDir, "");
-
- // Aww hell yes, let's write this file out!
-
- string fullPath = Path.Combine (installDir, outputName);
- // Console.WriteLine (fullPath);
-
- copyZipEntry (zipfile, entry, fullPath);
- }
-
- return;
+ public string standard_name ()
+ {
+ return identifier + "-" + version + ".zip";
}
- // TODO: Test that this actually throws exceptions if it can't do its job.
- void copyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPath) {
-
- if (entry.IsDirectory) {
- // Console.WriteLine ("Making directory " + fullPath);
- Directory.CreateDirectory (fullPath);
- }
- else {
- // Console.WriteLine ("Writing file " + fullPath);
-
- // It's a file! Prepare the streams
- Stream zipStream = zipfile.GetInputStream(entry);
- FileStream output = File.Create (fullPath);
-
- // Copy
- zipStream.CopyTo (output);
-
- // Tidy up.
- zipStream.Close();
- output.Close();
- }
-
- return;
-
+ public string serialise ()
+ {
+ return JsonConvert.SerializeObject (this);
}
}
-}
-
+}
\ No newline at end of file
diff --git a/CKAN/CKAN/ModuleDict.cs b/CKAN/CKAN/ModuleDict.cs
index 28e35258de..d70d0d75e4 100644
--- a/CKAN/CKAN/ModuleDict.cs
+++ b/CKAN/CKAN/ModuleDict.cs
@@ -39,8 +39,8 @@ public ModuleDict () {
string module = Regex.Replace (file, "^.*/([^.]+).*", "$1");
this [module] = new Module ();
- this [module]._version = "0"; // We can say it exists, but have no idea of other info.
- this [module]._identifier = module;
+ this [module].version = "0"; // We can say it exists, but have no idea of other info.
+ this [module].identifier = module;
// Console.WriteLine (this[module]._identifier + " ( " + file + " ) ");
}
@@ -59,7 +59,7 @@ public ModuleDict () {
foreach (string file in ckanFiles) {
Module module = Module.from_file (file);
- this [module._identifier] = module;
+ this [module.identifier] = module;
// Console.WriteLine (module._identifier + " " + module._version + " ( " + file + " ) ");
@@ -69,7 +69,7 @@ public ModuleDict () {
public void showInstalled() {
foreach(KeyValuePair entry in this) {
- Console.WriteLine (entry.Value._identifier + " " + entry.Value._version);
+ Console.WriteLine (entry.Value.identifier + " " + entry.Value.version);
}
return;
diff --git a/CKAN/CKAN/ModuleInstaller.cs b/CKAN/CKAN/ModuleInstaller.cs
new file mode 100644
index 0000000000..9f82ed8e89
--- /dev/null
+++ b/CKAN/CKAN/ModuleInstaller.cs
@@ -0,0 +1,189 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Linq;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using ICSharpCode.SharpZipLib.Core;
+using ICSharpCode.SharpZipLib.Zip;
+using System.Text.RegularExpressions;
+
+namespace CKAN
+{
+ public class ModuleInstaller
+ {
+ RegistryManager registry_manager = new RegistryManager("/tmp/ksp_registry");
+
+ public ModuleInstaller ()
+ {
+ }
+
+ ///
+ /// Download the given mod. Returns the filename it was saved to.
+ ///
+ /// If no filename is provided, the standard_name() will be used.
+ ///
+ ///
+ /// Filename.
+ public string download (Module module, string filename = null)
+ {
+
+ // Generate a temporary file if none is provided.
+ if (filename == null) {
+ filename = module.standard_name ();
+ }
+
+ Console.WriteLine (" * Downloading " + filename + "...");
+
+ WebClient agent = new WebClient ();
+ agent.DownloadFile (module.download, filename);
+
+ return filename;
+ }
+
+ ///
+ /// Install our mod from the filename supplied.
+ /// If no file is supplied, we will fetch() it first.
+ ///
+
+ public void install (Module module, string filename = null)
+ {
+ List module_files = new List ();
+
+ Console.WriteLine (module.identifier + ":\n");
+
+ // Fetch our file if we don't already have it.
+ if (filename == null) {
+ filename = download (module);
+ }
+
+ // Open our zip file for processing
+ ZipFile zipfile = new ZipFile (File.OpenRead (filename));
+
+ // Walk through our install instructions.
+ foreach (dynamic stanza in module.install) {
+ install_component (stanza, zipfile, module_files);
+
+ // TODO: We should just *serialise* our current state, not
+ // copy the original file, because we can't always guarantee
+ // there will be an original file.
+
+ // TODO: This will just throw them in GameData.
+ // We need a way to convert stanzas to install locations.
+ // We really should make Stanza its own class.
+
+ File.WriteAllText (KSP.gameData () + module.identifier + ".ckan", module.serialise());
+ }
+
+ // Finish now if we have no bundled mods.
+ if (module.bundles == null) {
+ return;
+ }
+
+ // Do the same with our bundled mods.
+ foreach (dynamic stanza in module.bundles) {
+
+ // TODO: Check versions, so we don't double install.
+
+ install_component (stanza, zipfile, module_files);
+
+ // TODO: Generate CKAN metadata for the bundled component.
+ }
+
+ Registry registry = registry_manager.load_or_create ();
+ registry_manager.save(
+ registry.append (new InstalledModule (module_files.ToArray(), module, DateTime.Now)));
+
+ return;
+
+ }
+
+ string sha1_sum (string path)
+ {
+ SHA1 hasher = new SHA1CryptoServiceProvider();
+
+ try {
+ return BitConverter.ToString(hasher.ComputeHash (File.OpenRead (path)));
+ }
+ catch {
+ return null;
+ };
+ }
+
+ void install_component (dynamic stanza, ZipFile zipfile, List module_files)
+ {
+ string fileToInstall = stanza.file;
+
+ Console.WriteLine (" * Installing " + fileToInstall);
+
+ string[] path = fileToInstall.Split ('/');
+
+ // TODO: This will depend upon the `install_to` in the JSON file
+ string installDir = KSP.gameData ();
+
+ // This is what we strip off paths
+ string stripDir = String.Join ("/", path.Take (path.Count () - 1)) + "/";
+
+ // Console.WriteLine("InstallDir is "+installDir);
+ // Console.WriteLine ("StripDir is " + stripDir);
+
+ // This is awful. There's got to be a better way to extract a tree?
+ string filter = "^" + stanza.file + "(/|$)";
+
+ // O(N^2) solution. Surely there's a better way...
+ foreach (ZipEntry entry in zipfile) {
+
+ // Skip things we don't want.
+ if (! Regex.IsMatch (entry.Name, filter)) {
+ continue;
+ }
+
+ // Get the full name of the file.
+ string outputName = entry.Name;
+
+ // Strip off the prefix (often GameData/)
+ // TODO: The C# equivalent of "\Q stripDir \E" so we can't be caught by metacharacters.
+ outputName = Regex.Replace (outputName, @"^" + stripDir, "");
+
+ // Aww hell yes, let's write this file out!
+
+ string fullPath = Path.Combine (installDir, outputName);
+ // Console.WriteLine (fullPath);
+
+ copyZipEntry (zipfile, entry, fullPath);
+
+ module_files.Add (new InstalledModuleFile {
+ sha1_sum = sha1_sum (fullPath),
+ name = outputName,
+ });
+ }
+
+ return;
+ }
+ // TODO: Test that this actually throws exceptions if it can't do its job.
+ void copyZipEntry (ZipFile zipfile, ZipEntry entry, string fullPath)
+ {
+
+ if (entry.IsDirectory) {
+ // Console.WriteLine ("Making directory " + fullPath);
+ Directory.CreateDirectory (fullPath);
+ } else {
+ // Console.WriteLine ("Writing file " + fullPath);
+
+ // It's a file! Prepare the streams
+ Stream zipStream = zipfile.GetInputStream (entry);
+ FileStream output = File.Create (fullPath);
+
+ // Copy
+ zipStream.CopyTo (output);
+
+ // Tidy up.
+ zipStream.Close ();
+ output.Close ();
+ }
+
+ return;
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/CKAN/CKAN/Program.cs b/CKAN/CKAN/Program.cs
index bb14144a6c..3b68ed7eea 100644
--- a/CKAN/CKAN/Program.cs
+++ b/CKAN/CKAN/Program.cs
@@ -72,8 +72,9 @@ public static int install(InstallOptions options) {
Console.WriteLine ("Installing " + ckanFilename + " from " + zipFilename);
// Aha! We've been called as ckan -f somefile.zip somefile.ckan
Module module = Module.from_file (ckanFilename);
+ ModuleInstaller installer = new ModuleInstaller ();
- module.install (zipFilename);
+ installer.install (module, zipFilename);
return EXIT_OK;
}
@@ -81,8 +82,8 @@ public static int install(InstallOptions options) {
foreach (string filename in options.Files) {
Module module = Module.from_file (filename);
-
- module.install ();
+ ModuleInstaller installer = new ModuleInstaller ();
+ installer.install (module);
}
Console.WriteLine ("\nDone!\n");
diff --git a/CKAN/CKAN/Registry.cs b/CKAN/CKAN/Registry.cs
new file mode 100644
index 0000000000..f2b60b5939
--- /dev/null
+++ b/CKAN/CKAN/Registry.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+
+namespace CKAN
+{
+ class RegistryVersionNotSupportedException : Exception
+ {
+ public int requested_version;
+
+ public RegistryVersionNotSupportedException (int v)
+ {
+ requested_version = v;
+ }
+ }
+
+ public class Registry
+ {
+ const int LATEST_REGISTRY_VERSION = 0;
+ public int registry_version;
+ public InstalledModule[] installed_modules;
+
+ public Registry (int version, InstalledModule[] mods)
+ {
+ /* TODO: support more than just the latest version */
+ if (version != LATEST_REGISTRY_VERSION) {
+ throw new RegistryVersionNotSupportedException (version);
+ }
+
+ installed_modules = mods;
+ }
+
+ public static Registry empty ()
+ {
+ return new Registry (LATEST_REGISTRY_VERSION, new InstalledModule[] {});
+ }
+
+ public Registry append (InstalledModule mod)
+ {
+ /* UGH! I wish we could easily use 4.5's immutable collections */
+ InstalledModule[] new_modules = new InstalledModule[installed_modules.Length + 1];
+ installed_modules.CopyTo (new_modules, 0);
+ new_modules.SetValue (mod, installed_modules.Length);
+ return new Registry (registry_version, new_modules);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CKAN/CKAN/RegistryManager.cs b/CKAN/CKAN/RegistryManager.cs
new file mode 100644
index 0000000000..7a05ce3178
--- /dev/null
+++ b/CKAN/CKAN/RegistryManager.cs
@@ -0,0 +1,42 @@
+using System;
+using Newtonsoft.Json;
+
+namespace CKAN
+{
+ public class RegistryManager
+ {
+ string path;
+
+ public RegistryManager (string path)
+ {
+ this.path = path;
+ }
+
+ public Registry load() {
+ string json = System.IO.File.ReadAllText(path);
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public Registry load_or_create() {
+ try {
+ return load ();
+ }
+ catch (System.IO.FileNotFoundException) {
+ create ();
+ return load ();
+ }
+ }
+
+ void create() {
+ save (Registry.empty ());
+ }
+
+ public string serialise (Registry registry) {
+ return JsonConvert.SerializeObject (registry);
+ }
+
+ public void save (Registry registry) {
+ System.IO.File.WriteAllText(path, serialise (registry));
+ }
+ }
+}