tools/apiParser/Downloader/Program.cs (132 lines of code) (raw):

 using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; internal class Program { // app downloads // https://cloudmedia-docs.unity3d.com/docscloudstorage/<languagecode>/<version>/UnityDocumentation.zip // Language code: en, ja, kr, and cn // Versions: 2023.2, 6000.0, 6000.1, 6000.2, 6000.3 // like "https://cloudmedia-docs.unity3d.com/docscloudstorage/ja/2020.3/UnityDocumentation.zip" // after downloading all the zip-s, extract them to get the structure like below: // Documentation-(<version>) // Documentation // en // ja // kr // cn private static readonly string[] Languages = { "en", "ja", "kr", "cn" }; private static readonly string[] Versions = { "2023.2", "6000.0", "6000.1", "6000.2", "6000.3" }; private const string BaseUrl = "https://cloudmedia-docs.unity3d.com/docscloudstorage"; private static async Task<int> Main(string[] args) { try { var outRoot = args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) ? args[0] : Path.Combine(Environment.CurrentDirectory, "Docs"); Directory.CreateDirectory(outRoot); Console.WriteLine($"Output root: {outRoot}"); using var http = new HttpClient(); http.Timeout = TimeSpan.FromMinutes(30); foreach (var version in Versions) { var versionFolder = Path.Combine(outRoot, $"Documentation-{version}"); var documentationFolder = Path.Combine(versionFolder, "Documentation"); Directory.CreateDirectory(documentationFolder); foreach (var lang in Languages) { var langFolder = Path.Combine(documentationFolder, lang); if (IsAlreadyExtracted(langFolder)) { Console.WriteLine($"Skip: already extracted {version} {lang}"); continue; } // Ensure base folders exist Directory.CreateDirectory(versionFolder); Directory.CreateDirectory(documentationFolder); var url = $"{BaseUrl}/{lang}/{version}/UnityDocumentation.zip"; var zipPath = Path.Combine(versionFolder, $"UnityDocumentation-{version}-{lang}.zip"); Console.WriteLine($"Downloading {url}"); var ok = await DownloadWithRetryAsync(http, url, zipPath, retries: 3); if (!ok) { Console.WriteLine($"WARN: Failed to download {url}. Skipping."); SafeDeleteIfExists(zipPath); continue; } try { // Each zip already contains Documentation/<langCode>/... // Extract to version root so all languages merge into a single Documentation folder ZipFile.ExtractToDirectory(zipPath, versionFolder, overwriteFiles: true); Console.WriteLine($"Extracted {lang} for {version} into {documentationFolder}"); } catch (Exception e) { Console.WriteLine($"ERROR: Failed to extract {zipPath} to {versionFolder}: {e.Message}"); } finally { // Keep the zips by default; uncomment next line to remove after extraction // SafeDeleteIfExists(zipPath); } } } Console.WriteLine("Done"); return 0; } catch (Exception e) { Console.WriteLine($"Fatal error: {e}"); return 1; } } private static bool IsAlreadyExtracted(string langFolder) { try { if (!Directory.Exists(langFolder)) return false; // Heuristic: extracted folder should contain an index or at least some files return Directory.EnumerateFileSystemEntries(langFolder).GetEnumerator().MoveNext(); } catch { return false; } } private static async Task<bool> DownloadWithRetryAsync(HttpClient http, string url, string destination, int retries) { for (var attempt = 1; attempt <= retries; attempt++) { try { using var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Attempt {attempt}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); continue; } await using var stream = await response.Content.ReadAsStreamAsync(); await using var file = File.Create(destination); await stream.CopyToAsync(file); return true; } catch (Exception e) { Console.WriteLine($"Attempt {attempt} failed: {e.Message}"); await Task.Delay(TimeSpan.FromSeconds(Math.Min(30, attempt * 3))); } } return false; } private static void SafeDeleteIfExists(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { // ignore } } private static void EmptyDirectory(string path) { if (!Directory.Exists(path)) return; foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) { try { File.Delete(file); } catch { } } foreach (var dir in Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories)) { try { Directory.Delete(dir, true); } catch { } } } }