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)); + } + } +}