public static void InteractiveDownload(string productSelected = null, string hash = null, bool autoYes = true, bool verbose = true, bool quiet = false) { Product selected = null; Manifest manifest = null; using (HttpClient client = new HttpClient()) { while (selected == null || hash == null) { while (selected == null) { for (int i = 0; i < Products.Length; ++i) { Product product = Products[i]; if (!quiet) { Console.WriteLine($"[{i}] {product.ProductId} {product.FriendlyProductName ?? product.ProductName}"); } } if (string.IsNullOrEmpty(productSelected) && !quiet) { if (Products.Length != 0) { Console.Write($"Enter product number (0-{Products.Length - 1}): "); } else { Console.WriteLine("Enter product ID: "); } } if (string.IsNullOrWhiteSpace(productSelected)) { productSelected = Console.ReadLine().Trim('\r', '\n', ' '); } if (int.TryParse(productSelected, out int productId)) { if (productId < Products.Length) { selected = Products[productId]; } else { selected = Products.FirstOrDefault(c => c.ProductId.Equals(productId.ToString())); } if (selected == null) { if (!quiet && verbose) { Console.WriteLine("Unknown product ID, attempting to download info"); } try { string details = client.GetStringAsync($"http://nexon.ws/api/gms/products/{productSelected}").Result; selected = JsonConvert.DeserializeObject <Product>(details); } catch (Exception ex) { if (!quiet) { Console.WriteLine("Couldn't get product info"); Console.WriteLine(ex.Message); } } } } productSelected = null; if (!quiet) { if (selected == null) { Console.WriteLine("Invalid product selected."); } else { Console.WriteLine($"{selected.FriendlyProductName ?? selected.ProductName} selected."); } } } if (verbose) { Console.WriteLine("Downloading possible manifest hashes"); } KeyValuePair <string, Tuple <string, string> >[] hashes = selected.Details.Branches .SelectMany(c => c.Value) .Append(new KeyValuePair <string, string>("Main", selected.Details.ManifestURL)) .Where(c => c.Value != null) .Select(c => { string manifestHash = null; try { manifestHash = client.GetStringAsync(c.Value).Result; } catch (Exception ex) { if (!quiet) { Console.WriteLine("Error downloading manifest's hash, skipping"); } manifestHash = null; } return(new KeyValuePair <string, Tuple <string, string> >(c.Key, new Tuple <string, string>(manifestHash, c.Value))); }) .Where(c => c.Value.Item1 != null) .ToArray(); while (hash == null) { for (int i = 0; i < hashes.Length; ++i) { KeyValuePair <string, Tuple <string, string> > manifestHash = hashes[i]; if (!quiet) { Console.WriteLine($"[{i}] {manifestHash.Key} {manifestHash.Value.Item1} @ {manifestHash.Value.Item2}"); } } if (!quiet) { if (hashes.Length == 0) { Console.Write("No manifests found, enter manifest hash to attempt to download: "); } else { Console.Write($"Select manifest (0-{hashes.Length - 1}): "); } } hash = Console.ReadLine().Trim('\r', '\n', ' '); if (int.TryParse(hash, out int hashIndex)) { hash = hashes[hashIndex].Value.Item1; } else if (string.IsNullOrEmpty(hash)) { hash = null; selected = null; if (!quiet) { Console.WriteLine("Returning to product selection"); } break; } else if (hash.Length != 40) { hash = null; } if (hash == null) { if (!quiet) { Console.WriteLine("Invalid hash selected"); } } else { if (!quiet) { Console.WriteLine("Downloading manifest"); } try { byte[] ManifestCompressed = client.GetByteArrayAsync($"https://download2.nexon.net/Game/nxl/games/{selected.ProductId}/{hash}").Result; // Parse the manifest manifest = Manifest.Parse(ManifestCompressed); } catch (Exception ex) { if (!quiet) { Console.WriteLine("Error getting manifest"); Console.WriteLine(ex.Message); } hash = null; } } } if (manifest != null && !autoYes && !quiet) { Console.WriteLine($"Selected {manifest.BuiltAt} of {manifest.Product}"); Console.WriteLine($"This download will be {manifest.TotalCompressedSize} and will take up {manifest.TotalUncompressedSize} disk space"); Console.Write("Continue? [Y]/N: "); if (Console.ReadLine().Trim('\r', '\n', ' ').Equals("N", StringComparison.CurrentCultureIgnoreCase)) { hash = null; manifest = null; Console.WriteLine("Returning to hash selection"); } } } } Download(manifest, selected, hash, quiet); }
public static void Download(Manifest manifest, Product selected, string selectedHash, bool quiet = false) { bool running = true; // Handle the console messages in its own thread so as to prevent any locking or messages being written at the same time Thread consoleQueue = new Thread(() => { string message = null; while (running || consoleMessages.TryDequeue(out message)) { do { if (message != null) { Console.WriteLine(message); } Thread.Sleep(1); } while (consoleMessages.TryDequeue(out message)); } }); consoleQueue.Start(); string output = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), selected.ProductId, selectedHash); if (!Directory.Exists(output)) { Directory.CreateDirectory(output); } Dictionary <string, FileEntry> FileNames = manifest.RealFileNames; // Build the directory tree before we start downloading KeyValuePair <string, FileEntry>[] directories = FileNames.Where(c => c.Value.ChunkHashes.Count > 0 && c.Value.ChunkHashes.First().Equals("__DIR__")).ToArray(); foreach (KeyValuePair <string, FileEntry> directory in directories) { string subDirectory = Path.Combine(output, directory.Key); if (File.Exists(subDirectory)) { File.Delete(subDirectory); } if (!Directory.Exists(subDirectory)) { Directory.CreateDirectory(subDirectory); } } long toDownload = manifest.TotalUncompressedSize; long downloaded = 0; foreach (KeyValuePair <string, FileEntry> file in FileNames.Where(c => !directories.Contains(c) && c.Value.ChunkHashes.Count > 0)) { string filePath = Path.Combine(output, file.Key); if (!quiet) { Log($"Starting download of {file.Key}"); } long position = 0; long existingFileSize = 0; long firstChunkSize = file.Value.ChunkSizes.First(); // Get all of the chunks in their own threads long writtenSize = file.Value.ChunkHashes.Select(hash => { Tuple <long, byte[]> chunk = null; int index = file.Value.ChunkHashes.IndexOf(hash); // Which chunk are we downloading long size = file.Value.ChunkSizes[index]; // How big is the chunk long realSize = 0; do { byte[] existing; SHA1 sha1 = SHA1.Create(); string sha1Hash; if (!File.Exists(filePath)) { File.Create(filePath).Dispose(); } else // Otherwise check the hashes { using (FileStream fileOut = File.OpenRead(filePath)) // Reusing the same FileStream seems to cause memory issues, so make a new one { existingFileSize = fileOut.Length; if (!quiet) { Log($"Verifying existing data at {position} ({size} / {firstChunkSize})"); } if (fileOut.Length >= position + size) { existing = new byte[size]; fileOut.Position = position; int read = 0; while ((read += fileOut.Read(existing, read, (int)(size - read))) != size) { ; // Usually less than int.max so this should be okay } sha1Hash = string.Join("", sha1.ComputeHash(existing).Select(c => c.ToString("x2"))); if (sha1Hash.Equals(hash)) { if (!quiet) { Log("Hash check passed, skipping downloading"); } realSize = size; break; } else if (fileOut.Length >= position + firstChunkSize && firstChunkSize != size) { existing = new byte[firstChunkSize]; fileOut.Position = position; read = 0; while ((read += fileOut.Read(existing, read, (int)(firstChunkSize - read))) != firstChunkSize) { ; // Usually less than int.max so this should be okay } sha1Hash = string.Join("", sha1.ComputeHash(existing).Select(c => c.ToString("x2"))); if (sha1Hash.Equals(hash)) { if (!quiet) { Log("Hash check passed, skipping downloading"); } realSize = firstChunkSize; break; } } else if (!quiet) { Log("Chunk didn't match hash"); } } if (file.Value.ChunkHashes.Count == 1) { existing = new byte[fileOut.Length]; int read = 0; fileOut.Position = 0; while ((read += fileOut.Read(existing, read, (int)(fileOut.Length - read))) != fileOut.Length) { ; // Usually less than int.max so this should be okay } sha1Hash = string.Join("", sha1.ComputeHash(existing).Select(c => c.ToString("x2"))); if (sha1Hash.Equals(hash)) { if (!quiet) { Log("Hash check passed, skipping downloading"); } realSize = fileOut.Length; break; } else if (!quiet) { Log("File didn't match hash"); } byte[] compressed = Program.Compress(existing); sha1Hash = string.Join("", sha1.ComputeHash(compressed).Select(c => c.ToString("x2"))); if (sha1Hash.Equals(hash)) { if (!quiet) { Log("Hash check passed, skipping downloading"); } realSize = compressed.Length; break; } else if (!quiet) { Log("Compressed file didn't match hash"); } } } } using (FileStream fileOut = File.OpenWrite(filePath)) { if (!quiet) { Log($"Downloading {hash}"); } chunk = FileEntry.DownloadChunk(manifest.Product, hash, size, position); // Download the chunk realSize = chunk.Item2.Length; fileOut.Position = chunk.Item1; // The chunk's offset fileOut.Write(chunk.Item2, 0, chunk.Item2.Length); // Write the chunk data to the file if (!quiet) { Log($"Wrote 0x{chunk.Item2.Length.ToString("X")} at 0x{chunk.Item1.ToString("X")} to {file.Key}"); } fileOut.Flush(); // Flush it out and dispose of the FileStream } } while (chunk == null); position += realSize; downloaded += realSize; if (!quiet) { Log($"Downloaded: {((((position * 100f) / file.Value.FileSize))).ToString("0.00")}% {position} / {file.Value.FileSize} total: {((((downloaded * 100f) / toDownload))).ToString("0.00")}% {downloaded} / {toDownload}"); } chunk = null; GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); // Try to GC if possible return(realSize); }).Sum(); // Get the sum of the chunked data if (writtenSize != file.Value.FileSize) { if (!quiet) { Log($"ERROR, mismatch written and expected size"); } } if (file.Value.FileSize != existingFileSize && file.Value.FileSize != writtenSize) { if (!quiet) { Log($"Existing file size does not match expected size, trimming to {file.Value.FileSize}"); } using (FileStream fileOut = File.OpenWrite(filePath)) // Ensure no trailing excess data fileOut.SetLength(file.Value.FileSize); } if (!quiet) { Log($"{file.Key} Total: {writtenSize} Expected: {file.Value.FileSize}"); } } // Exit out of the console message processor running = false; consoleQueue.Join(); // Wait for console processor to exit }