diff --git a/CKAN/CKAN/CKAN.csproj b/CKAN/CKAN/CKAN.csproj index 6df40f5106..dded15803d 100644 --- a/CKAN/CKAN/CKAN.csproj +++ b/CKAN/CKAN/CKAN.csproj @@ -51,6 +51,7 @@ + @@ -69,7 +70,6 @@ - diff --git a/CKAN/CKAN/Cache.cs b/CKAN/CKAN/Cache.cs deleted file mode 100644 index 4d28a9bfe2..0000000000 --- a/CKAN/CKAN/Cache.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.IO; - -namespace CKAN -{ - /// - /// Everything do with file caching operations. - /// - public class Cache - { - internal string cache_path; - - /// - /// Creates a new cache object, using the path provided for cached files. - /// If the directory does not exist, a DirectoryNotFoundKraken is thrown. - public Cache(string cache_path) - { - if (! Directory.Exists(cache_path)) - { - throw new DirectoryNotFoundKraken(cache_path); - } - this.cache_path = cache_path; - } - - /// - /// Returns true if the given module is present in our cache. - /// - // - // TODO: Update this if we start caching by URL (GH #111) - public bool IsCached(CkanModule module) - { - return IsCached(module.StandardName()); - } - - /// - /// Returns true if the given file is in our cache. - /// - public bool IsCached(string filename) - { - // It's cached if we can find it on a cache lookup. - return CachedFile(filename) != null; - } - - /// - /// Returns the path to the cached copy of the file, or null if it's not cached. - /// - public string CachedFile(string file) - { - string full_path = CachePath(file); - if (File.Exists(full_path)) - { - return full_path; - } - return null; - } - - /// - /// Returns the path to the cached copy of the module, or null if it's not cached. - /// - public string CachedFile(CkanModule module) - { - return CachedFile(module.StandardName()); - } - - /// - /// Returns where the given file is cached, or would be cached if it we had it. - /// - public string CachePath(string file) - { - return Path.Combine(cache_path, file); - } - - public string CachePath(CkanModule module) - { - return CachePath(module.StandardName()); - } - - /// - /// Returns the path used by this cache. - /// - public string CachePath() - { - return cache_path; - } - - } -} - diff --git a/CKAN/CKAN/KSP.cs b/CKAN/CKAN/KSP.cs index 853a766149..84b7f81df0 100644 --- a/CKAN/CKAN/KSP.cs +++ b/CKAN/CKAN/KSP.cs @@ -18,8 +18,8 @@ public class KSP private string gamedir; private KSPVersion version; - private Cache _Cache; - public Cache Cache + private NetFileCache _Cache; + public NetFileCache Cache { get { @@ -36,7 +36,7 @@ public KSP(string directory) gamedir = directory; Init(); - _Cache = new Cache(DownloadCacheDir()); + _Cache = new NetFileCache(DownloadCacheDir()); } public string GameDir() diff --git a/CKAN/CKAN/ModuleInstaller.cs b/CKAN/CKAN/ModuleInstaller.cs index a022bb2bd0..573f08172b 100644 --- a/CKAN/CKAN/ModuleInstaller.cs +++ b/CKAN/CKAN/ModuleInstaller.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Threading; @@ -41,7 +42,7 @@ public class ModuleInstaller private bool installCanceled = false; // Used for inter-thread communication. // Our own cache is that of the KSP instance we're using. - public Cache Cache + public NetFileCache Cache { get { @@ -90,13 +91,13 @@ public string Download(Uri url, string filename) /// /// Downloads the given mod to the cache. Returns the filename it was saved to. /// - public static string Download(Uri url, string filename, Cache cache) + public static string Download(Uri url, string filename, NetFileCache cache) { log.Info("Downloading " + filename); - string full_path = cache.CachePath(filename); + string tmp_file = Net.Download(url); - return Net.Download(url, full_path); + return cache.Store(url, tmp_file, move: true); } /// @@ -130,39 +131,42 @@ public string CachedOrDownload(string identifier, Version version, Uri url, stri /// If no filename is provided, the module's standard name will be used. /// Chcecks provided cache first. /// - public static string CachedOrDownload(string identifier, Version version, Uri url, Cache cache, string filename = null) + public static string CachedOrDownload(string identifier, Version version, Uri url, NetFileCache cache, string filename = null) { if (filename == null) { filename = CkanModule.StandardName(identifier, version); } - string full_path = cache.CachedFile(filename); - - if (full_path != null) + string full_path; + if (!cache.IsCached(url, out full_path)) { - log.DebugFormat("Using {0} (cached)", filename); - return full_path; + return Download(url, filename, cache); } - return Download(url, filename, cache); + log.DebugFormat("Using {0} (cached)", filename); + return full_path; } /// /// Downloads all the modules specified to the cache. + /// Even if modules share download URLs, they will only be downloaded once. /// public NetAsyncDownloader DownloadAsync(CkanModule[] modules) { - var urls = new Uri[modules.Length]; - var fullPaths = new string[modules.Length]; + var unique_downloads = new Dictionary(); - for (int i = 0; i < modules.Length; i++) + // Walk through all our modules, but only keep the first of each + // one that has a unique download path. + foreach (var module in modules) { - fullPaths[i] = ksp.Cache.CachePath(modules[i]); - urls[i] = modules[i].download; + if (!unique_downloads.ContainsKey(module.download)) + { + unique_downloads[module.download] = module; + } } - downloader = new NetAsyncDownloader(urls, fullPaths); + downloader = new NetAsyncDownloader(unique_downloads.Keys.ToArray()); if (onReportProgress != null) { @@ -172,21 +176,11 @@ public NetAsyncDownloader DownloadAsync(CkanModule[] modules) percent); } - downloader.onCompleted = (_uris, strings, errors) => OnDownloadsComplete(_uris, fullPaths, modules, errors); + downloader.onCompleted = (_uris, paths, errors) => OnDownloadsComplete(_uris, paths, unique_downloads.Values.ToArray(), errors); return downloader; } - /// - /// Downloads all the modules specified to the cache. - /// - public NetAsyncDownloader DownloadAsync(List modules) - { - var mod_array = new CkanModule[modules.Count]; - modules.CopyTo(mod_array); - return DownloadAsync(mod_array); - } - /// /// Installs all modules given a list of identifiers as a transaction. Resolves dependencies. /// This *will* save the registry at the end of operation. @@ -211,14 +205,15 @@ public void InstallList(List modules, RelationshipResolverOptions option foreach (CkanModule module in modsToInstall) { - if (Cache.IsCached(module)) + string filename; + if (!KSPManager.CurrentInstance.Cache.IsCached(module.download, out filename)) { - User.WriteLine(" * {0} (cached)", module); + User.WriteLine(" * {0}", module); + downloads.Add(module); } else { - User.WriteLine(" * {0}", module); - downloads.Add(module); + User.WriteLine(" * {0} (cached)", module); } } @@ -243,9 +238,10 @@ public void InstallList(List modules, RelationshipResolverOptions option if (downloads.Count > 0) { - downloader = DownloadAsync(downloads); + downloader = DownloadAsync(downloads.ToArray()); downloader.StartDownload(); + // Wait for our downloads to finish. lock (downloader) { Monitor.Wait(downloader); @@ -303,26 +299,44 @@ public void CancelInstall() installCanceled = true; } + /// + /// Stores all of our files in the cache once done. + /// Called by NetAsyncDownloader. + /// private void OnDownloadsComplete(Uri[] urls, string[] filenames, CkanModule[] modules, Exception[] errors) { - bool noErrors = false; + // XXX: What the hell should we be doing if we are called with nulls? if (urls != null) { - noErrors = true; - for (int i = 0; i < errors.Length; i++) { if (errors[i] != null) { - noErrors = false; User.Error("Failed to download \"{0}\" - error: {1}", urls[i], errors[i].Message); + // XXX: Shouldn't be be throwing an exception about now? + } + else + { + // Even if some of our downloads failed, we want to cache the + // ones which succeeded. + ksp.Cache.Store(urls[i], filenames[i], modules[i].StandardName()); + } } } - lastDownloadSuccessful = noErrors; + // Finally, remove all our temp files. + // We probably *could* have used Store's integrated move function above, but if we managed + // to somehow get two URLs the same in our download set, that could cause right troubles! + foreach (string tmpfile in filenames) + { + log.DebugFormat("Cleaing up {0}", tmpfile); + file_transaction.Delete(tmpfile); + } + + // Signal that we're done. lock (downloader) { Monitor.Pulse(downloader); @@ -338,9 +352,8 @@ private void OnDownloadsComplete(Uri[] urls, string[] filenames, CkanModule[] mo public List GetModuleContentsList(CkanModule module) { - string filename = Cache.CachedFile(module); - - if (filename == null) + string filename; + if (!KSPManager.CurrentInstance.Cache.IsCached(module.download, out filename)) { return null; } @@ -630,6 +643,16 @@ internal static string TransformOutputName(string file, string outputName, strin { string leadingPathToRemove = KSPPathUtils.GetLeadingPathElements(file); + // Special-casing, if stanza.file is just "GameData" or "Ships", strip it. + // TODO: Do we need to do anything special for tutorials or GameRoot? + if ( + leadingPathToRemove == string.Empty && + (file == "GameData" || file == "Ships") + ) + { + leadingPathToRemove = file; + } + // If there's a leading path to remove, then we have some extra work that // needs doing... if (leadingPathToRemove != string.Empty) @@ -646,7 +669,7 @@ internal static string TransformOutputName(string file, string outputName, strin // Strip off leading path name outputName = Regex.Replace(outputName, leadingRegEx, ""); } - + // Return our snipped, normalised, and ready to go output filename! return KSPPathUtils.NormalizePath( Path.Combine(installDir, outputName) diff --git a/CKAN/CKAN/NetFileCache.cs b/CKAN/CKAN/NetFileCache.cs new file mode 100644 index 0000000000..8096fe783c --- /dev/null +++ b/CKAN/CKAN/NetFileCache.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using ChinhDo.Transactions; +using log4net; + +namespace CKAN +{ + + + /* + * This class allows us to cache downloads by URL + * It works using two directories - one to store downloads in-progress, and one to commit finished downloads + * URLs are cached by hashing them by taking the first 8 chars of the url's SHA1 hash. + * + * To use this class the user would have to: + * - Obtain a temporary download path by calling GetTemporaryPathForURL(url) + * - Initiate his download in this temporary path + * - Call CommitDownload(url, desired_filename) which will move the temporary file to the final location + * - The final file will be named such as -.zip + * - The user can call IsCached(url) to check if a particular url exists in the cache + * and GetCachedFilename() to get its filename + */ + public class NetFileCache + { + private string cachePath; + private static readonly TxFileManager tx_file = new TxFileManager(); + private static readonly ILog log = LogManager.GetLogger(typeof (NetFileCache)); + + public NetFileCache(string _cachePath) + { + // Basic validation, our cache has to exist. + + if (!Directory.Exists(_cachePath)) + { + throw new DirectoryNotFoundKraken(_cachePath, "Cannot find cache directory"); + } + + cachePath = _cachePath; + } + + public string GetCachePath() + { + return cachePath; + } + + // returns true if a url is already in the cache + public bool IsCached(Uri url) + { + return GetCachedFilename(url) != null; + } + + // returns true if a url is already in the cache + // returns the filename in the outFilename parameter + public bool IsCached(Uri url, out string outFilename) + { + outFilename = GetCachedFilename(url); + + return outFilename != null; + } + + /// > + /// Returns the filename of an already cached url or null otherwise + /// + public string GetCachedFilename(Uri url) + { + log.DebugFormat("Checking cache for {0}", url); + + string hash = CreateURLHash(url); + + foreach (string file in Directory.GetFiles(cachePath)) + { + string filename = Path.GetFileName(file); + if (filename.StartsWith(hash)) + { + return file; + } + } + + return null; + } + + /// + /// Stores the results of a given URL in the cache. + /// Description is appended to the file hash when saving. If not present, the filename will be used. + /// If `move` is true, then the file will be moved; otherwise, it will be copied. + /// + /// Returns a path to the newly cached file. + /// + /// This method is filesystem transaction aware. + /// + public string Store(Uri url, string path, string description = null, bool move = false) + { + log.DebugFormat("Storing {0}", url); + + // Make sure we clear our cache entry first. + this.Remove(url); + + string hash = CreateURLHash(url); + + description = description ?? Path.GetFileName(path); + + string fullName = String.Format("{0}-{1}", hash, Path.GetFileName(description)); + string targetPath = Path.Combine(cachePath, fullName); + + log.DebugFormat("Storing {0} in {1}", path, targetPath); + + if (move) + { + tx_file.Move(path, targetPath); + } + else + { + tx_file.Copy(path, targetPath, overwrite: true); + } + + return targetPath; + } + + /// + /// Removes the given URL from the cache. + /// Returns true if any work was done, false otherwise. + /// This method is filesystem transaction aware. + /// + public bool Remove(Uri url) + { + string file = this.GetCachedFilename(url); + + if (file != null) + { + tx_file.Delete(file); + return true; + } + + return false; + } + + // returns the 8-byte hash for a given url + private static string CreateURLHash(Uri url) + { + using (var sha1 = new SHA1Managed()) + { + byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(url.ToString())); + + return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8); + } + } + } +} diff --git a/CKAN/GUI/MainModInfo.cs b/CKAN/GUI/MainModInfo.cs index a1028002bb..5d79423f27 100644 --- a/CKAN/GUI/MainModInfo.cs +++ b/CKAN/GUI/MainModInfo.cs @@ -200,19 +200,20 @@ private void _UpdateModContentsTreeRecursively(CkanModule module, string parentF private void _UpdateModContentsTree(CkanModule module) { - if (ModuleInstaller.Instance.Cache.IsCached(module)) - { - NotCachedLabel.Text = "Module is cached, preview available"; - ContentsDownloadButton.Enabled = false; - ContentsPreviewTree.Enabled = true; - } - else + string filename; + if (!KSPManager.CurrentInstance.Cache.IsCached(module.download, out filename)) { NotCachedLabel.Text = "This mod is not in the cache, click 'Download' to preview contents"; ContentsDownloadButton.Enabled = true; ContentsPreviewTree.Enabled = false; } - + else + { + NotCachedLabel.Text = "Module is cached, preview available"; + ContentsDownloadButton.Enabled = false; + ContentsPreviewTree.Enabled = true; + } + ContentsPreviewTree.Nodes.Clear(); ContentsPreviewTree.Nodes.Add(module.name); diff --git a/CKAN/LocalRepo/App.config b/CKAN/LocalRepo/App.config new file mode 100644 index 0000000000..8e15646352 --- /dev/null +++ b/CKAN/LocalRepo/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CKAN/LocalRepo/LocalRepo.csproj b/CKAN/LocalRepo/LocalRepo.csproj new file mode 100644 index 0000000000..e077d47cd4 --- /dev/null +++ b/CKAN/LocalRepo/LocalRepo.csproj @@ -0,0 +1,61 @@ + + + + + Debug + AnyCPU + {9E93DBA7-F67E-432F-9DDE-17672B5B39AC} + Exe + Properties + LocalRepo + LocalRepo + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\SharpZipLib.0.86.0-pjf-ckan_gh221\ICSharpCode.SharpZipLib.dll + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CKAN/LocalRepo/Program.cs b/CKAN/LocalRepo/Program.cs new file mode 100644 index 0000000000..a2d9311f1d --- /dev/null +++ b/CKAN/LocalRepo/Program.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip; + +namespace LocalRepo +{ + public class Program + { + + private static string repoPath = ""; + private static uint port = 8081; + + private static void Main(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("Usage:"); + Console.WriteLine("LocalRepo.exe "); + return; + } + + if (args.Length >= 2) + { + port = uint.Parse(args[1]); + } + + repoPath = args[0]; + + if (!Directory.Exists(repoPath)) + { + Console.WriteLine("Error: {0} doesn't exist or isn't a directory", repoPath); + return; + } + + Console.WriteLine("Running async server on port {0}", port); + Console.WriteLine("Serving metadata from {0}", repoPath); + new AsyncServer(port); + } + + public static void CreateZipFromRepo(string filename) + { + ZipOutputStream stream = new ZipOutputStream(File.Create(filename)); + + foreach(var file in Directory.GetFiles(repoPath)) + { + if (Path.GetExtension(file) == ".ckan") + { + var fileInfo = new FileInfo(file); + ZipEntry entry = new ZipEntry(Path.GetFileName(file)); + entry.Size = fileInfo.Length; + stream.PutNextEntry(entry); + + byte[] buffer = new byte[4096]; + using (FileStream streamReader = File.OpenRead(file)) + { + StreamUtils.Copy(streamReader, stream, buffer); + } + + stream.CloseEntry(); + } + } + + stream.Close(); + } + + } + + public class AsyncServer + { + public AsyncServer(uint port) + { + var listener = new HttpListener(); + listener.Prefixes.Add(String.Format("http://127.0.0.1:{0}/", port)); + + listener.Start(); + + while (true) + { + try + { + HttpListenerContext context = listener.GetContext(); + ThreadPool.QueueUserWorkItem(o => HandleRequest(context)); + } + catch (Exception) + { + // Ignored for this example + } + } + } + + private void HandleRequest(object state) + { + try + { + var context = (HttpListenerContext) state; + + context.Response.StatusCode = 200; + context.Response.SendChunked = true; + context.Response.ContentType = "application/zip"; + + var filename = Path.GetTempFileName(); + Program.CreateZipFromRepo(filename); + + var bytes = File.ReadAllBytes(filename); + context.Response.OutputStream.Write(bytes, 0, bytes.Length); + context.Response.OutputStream.Close(); + + File.Delete(filename); + } + catch (Exception) + { + // Client disconnected or some other error - ignored for this example + } + } + } +} \ No newline at end of file diff --git a/CKAN/LocalRepo/Properties/AssemblyInfo.cs b/CKAN/LocalRepo/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..48305bb47a --- /dev/null +++ b/CKAN/LocalRepo/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LocalRepo")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LocalRepo")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d94edd36-154a-439e-9153-3a6b41d6549e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CKAN/NetKAN/Github/GithubRelease.cs b/CKAN/NetKAN/Github/GithubRelease.cs index ef3eb3f114..8b0c641889 100644 --- a/CKAN/NetKAN/Github/GithubRelease.cs +++ b/CKAN/NetKAN/Github/GithubRelease.cs @@ -71,7 +71,7 @@ override public void InflateMetadata(JObject metadata, string filename, object c } - public string Download(string identifier, Cache cache) + public string Download(string identifier, NetFileCache cache) { log.DebugFormat("Downloading {0}", download); diff --git a/CKAN/NetKAN/KS/KSVersion.cs b/CKAN/NetKAN/KS/KSVersion.cs index 7e6b2d376e..05c8c5b526 100644 --- a/CKAN/NetKAN/KS/KSVersion.cs +++ b/CKAN/NetKAN/KS/KSVersion.cs @@ -24,7 +24,7 @@ private void DeSerialisationFixes(StreamingContext like_i_could_care) log.DebugFormat("Download path is {0}", download_path); } - public string Download(string identifier, Cache cache) + public string Download(string identifier, NetFileCache cache) { log.DebugFormat("Downloading {0}", download_path); diff --git a/CKAN/NetKAN/MainClass.cs b/CKAN/NetKAN/MainClass.cs index 4fa0e16dc6..82c5593205 100644 --- a/CKAN/NetKAN/MainClass.cs +++ b/CKAN/NetKAN/MainClass.cs @@ -46,7 +46,7 @@ public static int Main(string[] args) return EXIT_BADOPT; } - Cache cache = FindCache(options); + NetFileCache cache = FindCache(options); log.InfoFormat("Processing {0}", options.File); @@ -81,7 +81,12 @@ public static int Main(string[] args) } // Find our cached file, we'll need it later. - string file = cache.CachedFile(mod); + string file; + if (!cache.IsCached(mod.download, out file)) + { + log.FatalFormat("Error: Unable to find {0} in the cache", mod.identifier); + return EXIT_ERROR; + } // Make sure this would actually generate an install try @@ -131,7 +136,7 @@ public static int Main(string[] args) /// Fetch le things from le KerbalStuff. /// Returns a JObject that should be a fully formed CKAN file. /// - internal static JObject KerbalStuff(JObject orig_metadata, string remote_id, Cache cache) + internal static JObject KerbalStuff(JObject orig_metadata, string remote_id, NetFileCache cache) { // Look up our mod on KS by its ID. KSMod ks = KSAPI.Mod(Convert.ToInt32(remote_id)); @@ -167,7 +172,7 @@ internal static JObject KerbalStuff(JObject orig_metadata, string remote_id, Cac /// /// Fetch things from Github, returning a complete CkanModule document. /// - private static JObject GitHub(JObject orig_metadata, string repo, Cache cache) + private static JObject GitHub(JObject orig_metadata, string repo, NetFileCache cache) { // Find the release on github and download. GithubRelease release = GithubAPI.GetLatestRelease(repo); @@ -283,18 +288,18 @@ internal static JObject JsonFromFile(string filename) return JObject.Parse(File.ReadAllText(filename)); } - internal static Cache FindCache(CmdLineOptions options) + internal static NetFileCache FindCache(CmdLineOptions options) { if (options.CacheDir != null) { log.InfoFormat("Using user-supplied cache at {0}", options.CacheDir); - return new Cache(options.CacheDir); + return new NetFileCache(options.CacheDir); } try { KSP ksp = KSPManager.GetPreferredInstance(); - log.InfoFormat("Using CKAN cache at {0}",ksp.Cache.CachePath()); + log.InfoFormat("Using CKAN cache at {0}",ksp.Cache.GetCachePath()); return ksp.Cache; } catch @@ -305,7 +310,7 @@ internal static Cache FindCache(CmdLineOptions options) string tempdir = Path.GetTempPath(); log.InfoFormat("Using tempdir for cache: {0}", tempdir); - return new Cache(tempdir); + return new NetFileCache(tempdir); } } diff --git a/CKAN/Tests/CKAN/Cache.cs b/CKAN/Tests/CKAN/Cache.cs index 972c96e825..9d151a750f 100644 --- a/CKAN/Tests/CKAN/Cache.cs +++ b/CKAN/Tests/CKAN/Cache.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using System; using System.IO; -using CKAN; namespace CKANTests { @@ -10,13 +9,13 @@ public class Cache { private readonly string cache_dir = Path.Combine(Tests.TestData.DataDir(),"cache_test"); - private CKAN.Cache cache; + private CKAN.NetFileCache cache; [SetUp()] public void MakeCache() { Directory.CreateDirectory(cache_dir); - cache = new CKAN.Cache(cache_dir); + cache = new CKAN.NetFileCache(cache_dir); } [TearDown()] @@ -25,69 +24,99 @@ public void RemoveCache() Directory.Delete(cache_dir, true); } - [Test()] - public void CacheKraken() + [Test] + public void Sanity() { - string dir = "/this/path/better/not/exist"; - - try - { - new CKAN.Cache(dir); - } - catch (DirectoryNotFoundKraken kraken) - { - Assert.AreSame(dir,kraken.directory); - } + Assert.IsInstanceOf(cache); + Assert.IsTrue(Directory.Exists(cache.GetCachePath())); } - [Test()] - public void New() + [Test] + public void StoreRetrieve() { - // Not much to do here, our Setup() makes an object for us. - // Let's make sure it's actually there. - Assert.IsNotNull(cache); + Uri url = new Uri("http://example.com/"); + string file = Tests.TestData.DogeCoinFlagZip(); + + // Sanity check, our cache dir is there, right? + Assert.IsTrue(Directory.Exists(cache.GetCachePath())); + + // Our URL shouldn't be cached to begin with. + Assert.IsFalse(cache.IsCached(url)); + + // Store our file. + cache.Store(url, file); + + // Now it should be cached. + Assert.IsTrue(cache.IsCached(url)); + + // Check contents match. + string cached_file = cache.GetCachedFilename(url); + FileAssert.AreEqual(file, cached_file); } - [Test()] - public void IsCachedFile() + [Test] + public void NamingHints() { - string full_file = Tests.TestData.DogeCoinFlagZip(); - string short_file = Path.GetFileName(full_file); + Uri url = new Uri("http://example.com/"); + string file = Tests.TestData.DogeCoinFlagZip(); - Assert.IsFalse(cache.IsCached(short_file)); - Store(full_file); - Assert.IsTrue(cache.IsCached(short_file)); + Assert.IsFalse(cache.IsCached(url)); + cache.Store(url, file, "cheesy.zip"); + + StringAssert.EndsWith("cheesy.zip", cache.GetCachedFilename(url)); } - [Test()] - public void IsCachedModule() + [Test] + public void StoreRemove() { - string full_file = Tests.TestData.DogeCoinFlagZip(); - CKAN.CkanModule module = Tests.TestData.DogeCoinFlag_101_module(); + Uri url = new Uri("http://example.com/"); + string file = Tests.TestData.DogeCoinFlagZip(); + + Assert.IsFalse(cache.IsCached(url)); + cache.Store(url, file); + Assert.IsTrue(cache.IsCached(url)); - Assert.IsFalse(cache.IsCached(module)); - Store(full_file); - Assert.IsTrue(cache.IsCached(module)); + cache.Remove(url); + + Assert.IsFalse(cache.IsCached(url)); } [Test()] - public void CachePathModule() + public void CacheKraken() { - CKAN.CkanModule module = Tests.TestData.DogeCoinFlag_101_module(); + string dir = "/this/path/better/not/exist"; - Assert.AreEqual(Path.Combine(cache_dir, module.StandardName()), cache.CachePath(module)); + try + { + new CKAN.NetFileCache(dir); + } + catch (CKAN.DirectoryNotFoundKraken kraken) + { + Assert.AreSame(dir,kraken.directory); + } } - // Stores the file in our cache. - // This may be good to have in the actual Cache class itself. - private void Store(string file) + [Test] + public void DoubleCache() { - string dir = cache.CachePath(); - string short_file = Path.GetFileName(file); + // Store and flip files in our cache. We should always get + // the most recent file we store for any given URL. - File.Copy(file, Path.Combine(dir, short_file)); - } + Uri url = new Uri("http://Double.Rainbow.What.Does.It.Mean/"); + Assert.IsFalse(cache.IsCached(url)); + string file1 = Tests.TestData.DogeCoinFlagZip(); + string file2 = Tests.TestData.ModuleManagerZip(); + + cache.Store(url, file1); + FileAssert.AreEqual(file1, cache.GetCachedFilename(url)); + + cache.Store(url, file2); + FileAssert.AreEqual(file2, cache.GetCachedFilename(url)); + + cache.Store(url, file1); + FileAssert.AreEqual(file1, cache.GetCachedFilename(url)); + } } } diff --git a/CKAN/Tests/CKAN/ModuleInstaller.cs b/CKAN/Tests/CKAN/ModuleInstaller.cs index 07936ca1a6..4b2c2f56b5 100644 --- a/CKAN/Tests/CKAN/ModuleInstaller.cs +++ b/CKAN/Tests/CKAN/ModuleInstaller.cs @@ -237,6 +237,8 @@ public void TransformOutputName() { Assert.AreEqual("GameData/kOS/Plugins/kOS.dll", CKAN.ModuleInstaller.TransformOutputName("GameData/kOS", "GameData/kOS/Plugins/kOS.dll", "GameData")); Assert.AreEqual("GameData/kOS/Plugins/kOS.dll", CKAN.ModuleInstaller.TransformOutputName("kOS-1.1/GameData/kOS", "kOS-1.1/GameData/kOS/Plugins/kOS.dll", "GameData")); + Assert.AreEqual("GameData/ModuleManager.2.5.1.dll", CKAN.ModuleInstaller.TransformOutputName("ModuleManager.2.5.1.dll", "ModuleManager.2.5.1.dll", "GameData")); + Assert.AreEqual("SomeDir/Ships/SPH/FAR Firehound.craft", CKAN.ModuleInstaller.TransformOutputName("Ships", "Ships/SPH/FAR Firehound.craft", "SomeDir/Ships")); } private static string CopyDogeFromZip()