From ff8f7c0dd28cc3967dd308e7c733d36e6b3b803a Mon Sep 17 00:00:00 2001 From: js6pak Date: Sat, 17 Jul 2021 01:00:51 +0200 Subject: [PATCH] Migrate to System.CommandLine --- DepotDownloader/ContentDownloader.cs | 11 +- DepotDownloader/DepotDownloader.csproj | 1 + DepotDownloader/Program.cs | 502 ++++++++++--------------- 3 files changed, 214 insertions(+), 300 deletions(-) diff --git a/DepotDownloader/ContentDownloader.cs b/DepotDownloader/ContentDownloader.cs index 7cee83f7..84914caf 100644 --- a/DepotDownloader/ContentDownloader.cs +++ b/DepotDownloader/ContentDownloader.cs @@ -341,6 +341,9 @@ namespace DepotDownloader public static bool InitializeSteam3(string username, string password) { + // capture the supplied password in case we need to re-use it after checking the login key + Config.SuppliedPassword = password; + string loginKey = null; if (username != null && Config.RememberPassword) @@ -458,7 +461,7 @@ namespace DepotDownloader File.Move(fileStagingPath, fileFinalPath); } - public static async Task DownloadAppAsync(uint appId, List<(uint depotId, ulong manifestId)> depotManifestIds, string branch, string os, string arch, string language, bool lv, bool isUgc) + public static async Task DownloadAppAsync(uint appId, List<(uint depotId, ulong manifestId)> depotManifestIds, string branch, string[] os, string[] arch, string[] language, bool lv, bool isUgc) { cdnPool = new CDNClientPool(steam3, appId); @@ -535,7 +538,7 @@ namespace DepotDownloader !string.IsNullOrWhiteSpace(depotConfig["oslist"].Value)) { var oslist = depotConfig["oslist"].Value.Split(','); - if (Array.IndexOf(oslist, os ?? Util.GetSteamOS()) == -1) + if (!os.Any(x => oslist.Contains(x))) continue; } @@ -543,7 +546,7 @@ namespace DepotDownloader !string.IsNullOrWhiteSpace(depotConfig["osarch"].Value)) { var depotArch = depotConfig["osarch"].Value; - if (depotArch != (arch ?? Util.GetSteamArch())) + if (!arch.Contains(depotArch)) continue; } @@ -552,7 +555,7 @@ namespace DepotDownloader !string.IsNullOrWhiteSpace(depotConfig["language"].Value)) { var depotLang = depotConfig["language"].Value; - if (depotLang != (language ?? "english")) + if (!language.Contains(depotLang)) continue; } diff --git a/DepotDownloader/DepotDownloader.csproj b/DepotDownloader/DepotDownloader.csproj index c9fbf234..ca34988f 100644 --- a/DepotDownloader/DepotDownloader.csproj +++ b/DepotDownloader/DepotDownloader.csproj @@ -13,5 +13,6 @@ + diff --git a/DepotDownloader/Program.cs b/DepotDownloader/Program.cs index e4ceb006..d12eed0d 100644 --- a/DepotDownloader/Program.cs +++ b/DepotDownloader/Program.cs @@ -1,6 +1,12 @@ +#nullable enable using System; using System.Collections.Generic; -using System.ComponentModel; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Help; +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.CommandLine.Parsing; using System.IO; using System.Linq; using System.Reflection; @@ -11,28 +17,156 @@ using SteamKit2; namespace DepotDownloader { - class Program + internal class Program { - static int Main(string[] args) - => MainAsync(args).GetAwaiter().GetResult(); + public static Task Main(string[] args) + { + var rootCommand = new RootCommand + { + new Option("--debug") { IsHidden = true }, + + new Option("--app", "The AppID to download") { IsRequired = true, ArgumentHelpName = "id" }, + + new Option("--depot", "The DepotID to download") { ArgumentHelpName = "id" }, + new Option("--manifest", "Manifest id of content to download (requires --depot, default: current for branch)"), + + new Option("--ugc", "The UGC ID to download"), + new Option("--pubfile", "The PublishedFileId to download (will automatically resolve to UGC id)"), + + new Option(new[] { "--branch", "--beta" }, "Download from specified branch if available"), + new Option(new[] { "--branch-password", "--betapassword" }, "Branch password if applicable"), + + new Option("--os", () => new[] { Util.GetSteamOS() }, "The operating system for which to download the game").FromAmong("all", "windows", "macos", "linux"), + new Option("--arch", () => new[] { Util.GetSteamArch() }, "The architecture for which to download the game").FromAmong("64", "32"), + new Option("--language", () => new[] { "english" }, "The language for which to download the game"), + new Option("--lowviolence", "Download low violence depots"), + + new Option("--username", "The username of the account to login to for restricted content"), + new Option("--password", "The password of the account to login to for restricted content"), + new Option("--remember-password", "If set, remember the password for subsequent logins of this user"), + + new Option(new[] { "--directory", "--dir" }, "The directory in which to place downloaded files"), + new Option("--filelist", "A list of files to download (from the manifest). Prefix file path with 'regex:' if you want to match with regex").ExistingOnly(), + new Option(new[] { "--validate", "--verify-all" }, "Include checksum verification of files already downloaded"), + new Option("--manifest-only", "Downloads a human readable manifest for any depots that would be downloaded"), + + new Option("--cellid", "The overridden CellID of the content server to download from"), + new Option("--max-servers", () => 20, "Maximum number of content servers to use"), + new Option("--max-downloads", () => 8, "Maximum number of chunks to download concurrently"), + new Option("--loginid", "A unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently"), + }; - static async Task MainAsync(string[] args) + rootCommand.Handler = CommandHandler.Create(DownloadAsync); + + return new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseHelpBuilder(ctx => new CustomHelpBuilder(ctx.Console)) + .Build().InvokeAsync(args); + } + + private sealed class CustomHelpBuilder : HelpBuilder { - if (args.Length == 0) + public CustomHelpBuilder(IConsole console) : base(console) { - PrintUsage(); - return 1; } - DebugLog.Enabled = false; + protected override void AddUsage(ICommand command) + { + if (command is not RootCommand) + return; + + Console.Out.WriteLine(@$"Examples: + - downloading one or all depots for an app: + {command.Name} --app [--depot [--manifest ]] [--username [--password ]] + + - downloading a workshop item using pubfile id: + {command.Name} --app --pubfile [--username [--password ]] + + - downloading a workshop item using ugc id: + {command.Name} --app --ugc [--username [--password ]]" + ); + + Console.Out.WriteLine(); + } + } + + public class InputModel + { + public InputModel(bool debug, uint app, uint[] depot, ulong[] manifest, ulong? ugc, ulong? pubfile, string? branch, string? branchPassword, string[] os, string[] arch, string[] language, bool lowViolence, string? username, string? password, bool rememberPassword, string directory, FileInfo? fileList, bool validate, bool manifestOnly, int? cellId, int maxServers, int maxDownloads, uint? loginId) + { + Debug = debug; + AppId = app; + Depots = depot; + Manifests = manifest; + UgcId = ugc; + Pubfile = pubfile; + Branch = EnsureNonEmpty(branch); + BranchPassword = EnsureNonEmpty(branchPassword); + OperatingSystems = os; + Architectures = arch; + Languages = language; + LowViolence = lowViolence; + Username = EnsureNonEmpty(username); + Password = EnsureNonEmpty(password); + RememberPassword = rememberPassword; + Directory = directory; + FileList = fileList; + Validate = validate; + ManifestOnly = manifestOnly; + CellId = cellId; + MaxServers = maxServers; + MaxDownloads = maxDownloads; + LoginId = loginId; + } + + // Workaround for https://github.com/dotnet/command-line-api/issues/1244 + private static string? EnsureNonEmpty(string? s) + { + return s == string.Empty ? null : s; + } + + public bool Debug { get; } + + public uint AppId { get; } + public uint[] Depots { get; } + public ulong[] Manifests { get; } + + public ulong? UgcId { get; } + public ulong? Pubfile { get; } + + public string? Branch { get; } + public string? BranchPassword { get; } + + public string[] OperatingSystems { get; } + public string[] Architectures { get; } + public string[] Languages { get; } + public bool LowViolence { get; } + + public string? Username { get; } + public string? Password { get; } + public bool RememberPassword { get; } + + public DirectoryInfo? Directory { get; } + public FileInfo? FileList { get; } + public bool Validate { get; } + public bool ManifestOnly { get; } + + public int? CellId { get; } + public int MaxServers { get; } + public int MaxDownloads { get; } + public uint? LoginId { get; } + } + + public static async Task DownloadAsync(InputModel input) + { AccountSettingsStore.LoadFromFile("account.config"); #region Common Options - if (HasParameter(args, "-debug")) + DebugLog.Enabled = input.Debug; + if (input.Debug) { - DebugLog.Enabled = true; DebugLog.AddListener((category, message) => { Console.WriteLine("[{0}] {1}", category, message); @@ -44,28 +178,15 @@ namespace DepotDownloader DebugLog.WriteLine("DepotDownloader", "Runtime: {0}", RuntimeInformation.FrameworkDescription); } - var username = GetParameter(args, "-username") ?? GetParameter(args, "-user"); - var password = GetParameter(args, "-password") ?? GetParameter(args, "-pass"); - ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password"); - - ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only"); + ContentDownloader.Config.RememberPassword = input.RememberPassword; + ContentDownloader.Config.DownloadManifestOnly = input.ManifestOnly; + ContentDownloader.Config.CellID = input.CellId ?? 0; - var cellId = GetParameter(args, "-cellid", -1); - if (cellId == -1) - { - cellId = 0; - } - - ContentDownloader.Config.CellID = cellId; - - var fileList = GetParameter(args, "-filelist"); - - if (fileList != null) + if (input.FileList != null) { try { - var fileListData = await File.ReadAllTextAsync(fileList); - var files = fileListData.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var files = await File.ReadAllLinesAsync(input.FileList.FullName); ContentDownloader.Config.UsingFileList = true; ContentDownloader.Config.FilesToDownload = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -75,8 +196,8 @@ namespace DepotDownloader { if (fileEntry.StartsWith("regex:")) { - var rgx = new Regex(fileEntry.Substring(6), RegexOptions.Compiled | RegexOptions.IgnoreCase); - ContentDownloader.Config.FilesToDownloadRegex.Add(rgx); + var regex = new Regex(fileEntry[6..], RegexOptions.Compiled | RegexOptions.IgnoreCase); + ContentDownloader.Config.FilesToDownloadRegex.Add(regex); } else { @@ -84,204 +205,101 @@ namespace DepotDownloader } } - Console.WriteLine("Using filelist: '{0}'.", fileList); + Console.WriteLine("Using filelist: '{0}'.", input.FileList); } catch (Exception ex) { - Console.WriteLine("Warning: Unable to load filelist: {0}", ex); + Console.WriteLine("Error: Unable to load filelist: {0}", ex); + return 1; } } - ContentDownloader.Config.InstallDirectory = GetParameter(args, "-dir"); - - ContentDownloader.Config.VerifyAll = HasParameter(args, "-verify-all") || HasParameter(args, "-verify_all") || HasParameter(args, "-validate"); - ContentDownloader.Config.MaxServers = GetParameter(args, "-max-servers", 20); - ContentDownloader.Config.MaxDownloads = GetParameter(args, "-max-downloads", 8); - ContentDownloader.Config.MaxServers = Math.Max(ContentDownloader.Config.MaxServers, ContentDownloader.Config.MaxDownloads); - ContentDownloader.Config.LoginID = HasParameter(args, "-loginid") ? GetParameter(args, "-loginid") : null; - - #endregion - - var appId = GetParameter(args, "-app", ContentDownloader.INVALID_APP_ID); - if (appId == ContentDownloader.INVALID_APP_ID) + if (input.Directory != null) { - Console.WriteLine("Error: -app not specified!"); - return 1; + ContentDownloader.Config.InstallDirectory = input.Directory.FullName; } - var pubFile = GetParameter(args, "-pubfile", ContentDownloader.INVALID_MANIFEST_ID); - var ugcId = GetParameter(args, "-ugc", ContentDownloader.INVALID_MANIFEST_ID); - if (pubFile != ContentDownloader.INVALID_MANIFEST_ID) - { - #region Pubfile Downloading + ContentDownloader.Config.VerifyAll = input.Validate; + ContentDownloader.Config.MaxDownloads = input.MaxDownloads; + ContentDownloader.Config.MaxServers = Math.Max(input.MaxServers, ContentDownloader.Config.MaxDownloads); + ContentDownloader.Config.LoginID = input.LoginId; - if (InitializeSteam(username, password)) - { - try - { - await ContentDownloader.DownloadPubfileAsync(appId, pubFile).ConfigureAwait(false); - } - catch (Exception ex) when ( - ex is ContentDownloaderException - || ex is OperationCanceledException) - { - Console.WriteLine(ex.Message); - return 1; - } - catch (Exception e) - { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); - throw; - } - finally - { - ContentDownloader.ShutdownSteam3(); - } - } - else - { - Console.WriteLine("Error: InitializeSteam failed"); - return 1; - } + #endregion - #endregion - } - else if (ugcId != ContentDownloader.INVALID_MANIFEST_ID) + if (InitializeSteam(input.Username, input.Password)) { - #region UGC Downloading - - if (InitializeSteam(username, password)) + try { - try + if (input.Pubfile != null) { - await ContentDownloader.DownloadUGCAsync(appId, ugcId).ConfigureAwait(false); + await ContentDownloader.DownloadPubfileAsync(input.AppId, input.Pubfile.Value).ConfigureAwait(false); } - catch (Exception ex) when ( - ex is ContentDownloaderException - || ex is OperationCanceledException) + else if (input.UgcId != null) { - Console.WriteLine(ex.Message); - return 1; + await ContentDownloader.DownloadUGCAsync(input.AppId, input.UgcId.Value).ConfigureAwait(false); } - catch (Exception e) - { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); - throw; - } - finally + else { - ContentDownloader.ShutdownSteam3(); - } - } - else - { - Console.WriteLine("Error: InitializeSteam failed"); - return 1; - } + ContentDownloader.Config.BetaPassword = input.BranchPassword; - #endregion - } - else - { - #region App downloading - - var branch = GetParameter(args, "-branch") ?? GetParameter(args, "-beta") ?? ContentDownloader.DEFAULT_BRANCH; - ContentDownloader.Config.BetaPassword = GetParameter(args, "-betapassword"); - - ContentDownloader.Config.DownloadAllPlatforms = HasParameter(args, "-all-platforms"); - var os = GetParameter(args, "-os"); - - if (ContentDownloader.Config.DownloadAllPlatforms && !String.IsNullOrEmpty(os)) - { - Console.WriteLine("Error: Cannot specify -os when -all-platforms is specified."); - return 1; - } - - var arch = GetParameter(args, "-osarch"); - - ContentDownloader.Config.DownloadAllLanguages = HasParameter(args, "-all-languages"); - var language = GetParameter(args, "-language"); - - if (ContentDownloader.Config.DownloadAllLanguages && !String.IsNullOrEmpty(language)) - { - Console.WriteLine("Error: Cannot specify -language when -all-languages is specified."); - return 1; - } + ContentDownloader.Config.DownloadAllPlatforms = input.OperatingSystems.Contains("all"); + ContentDownloader.Config.DownloadAllLanguages = input.Languages.Contains("all"); - var lv = HasParameter(args, "-lowviolence"); + var depotManifestIds = new List<(uint, ulong)>(); - var depotManifestIds = new List<(uint, ulong)>(); - var isUGC = false; + if (input.Manifests.Length > 0) + { + if (input.Depots.Length != input.Manifests.Length) + { + Console.WriteLine("Error: --manifest requires one id for every --depot specified"); + return 1; + } + + var zippedDepotManifest = input.Depots.Zip(input.Manifests, (depotId, manifestId) => (depotId, manifestId)); + depotManifestIds.AddRange(zippedDepotManifest); + } + else + { + depotManifestIds.AddRange(input.Depots.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); + } - var depotIdList = GetParameterList(args, "-depot"); - var manifestIdList = GetParameterList(args, "-manifest"); - if (manifestIdList.Count > 0) - { - if (depotIdList.Count != manifestIdList.Count) - { - Console.WriteLine("Error: -manifest requires one id for every -depot specified"); - return 1; + await ContentDownloader.DownloadAppAsync(input.AppId, depotManifestIds, input.Branch ?? ContentDownloader.DEFAULT_BRANCH, input.OperatingSystems, input.Architectures, input.Languages, input.LowViolence, false).ConfigureAwait(false); } - - var zippedDepotManifest = depotIdList.Zip(manifestIdList, (depotId, manifestId) => (depotId, manifestId)); - depotManifestIds.AddRange(zippedDepotManifest); } - else + catch (Exception ex) when (ex is ContentDownloaderException or OperationCanceledException) { - depotManifestIds.AddRange(depotIdList.Select(depotId => (depotId, ContentDownloader.INVALID_MANIFEST_ID))); + Console.WriteLine(ex.Message); + return 1; } - - if (InitializeSteam(username, password)) + catch (Exception e) { - try - { - await ContentDownloader.DownloadAppAsync(appId, depotManifestIds, branch, os, arch, language, lv, isUGC).ConfigureAwait(false); - } - catch (Exception ex) when ( - ex is ContentDownloaderException - || ex is OperationCanceledException) - { - Console.WriteLine(ex.Message); - return 1; - } - catch (Exception e) - { - Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); - throw; - } - finally - { - ContentDownloader.ShutdownSteam3(); - } + Console.WriteLine("Download failed to due to an unhandled exception: {0}", e.Message); + throw; } - else + finally { - Console.WriteLine("Error: InitializeSteam failed"); - return 1; + ContentDownloader.ShutdownSteam3(); } - - #endregion + } + else + { + Console.WriteLine("Error: InitializeSteam failed"); + return 1; } return 0; } - static bool InitializeSteam(string username, string password) + private static bool InitializeSteam(string? username, string? password) { if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey(username))) { do { Console.Write("Enter account password for \"{0}\": ", username); - if (Console.IsInputRedirected) - { - password = Console.ReadLine(); - } - else - { - // Avoid console echoing of password - password = Util.ReadPassword(); - } + password = Console.IsInputRedirected + ? Console.ReadLine() + : Util.ReadPassword(); Console.WriteLine(); } while (string.Empty == password); @@ -291,115 +309,7 @@ namespace DepotDownloader Console.WriteLine("No username given. Using anonymous account with dedicated server subscription."); } - // capture the supplied password in case we need to re-use it after checking the login key - ContentDownloader.Config.SuppliedPassword = password; - return ContentDownloader.InitializeSteam3(username, password); } - - static int IndexOfParam(string[] args, string param) - { - for (var x = 0; x < args.Length; ++x) - { - if (args[x].Equals(param, StringComparison.OrdinalIgnoreCase)) - return x; - } - - return -1; - } - - static bool HasParameter(string[] args, string param) - { - return IndexOfParam(args, param) > -1; - } - - static T GetParameter(string[] args, string param, T defaultValue = default(T)) - { - var index = IndexOfParam(args, param); - - if (index == -1 || index == (args.Length - 1)) - return defaultValue; - - var strParam = args[index + 1]; - - var converter = TypeDescriptor.GetConverter(typeof(T)); - if (converter != null) - { - return (T)converter.ConvertFromString(strParam); - } - - return default(T); - } - - static List GetParameterList(string[] args, string param) - { - var list = new List(); - var index = IndexOfParam(args, param); - - if (index == -1 || index == (args.Length - 1)) - return list; - - index++; - - while (index < args.Length) - { - var strParam = args[index]; - - if (strParam[0] == '-') break; - - var converter = TypeDescriptor.GetConverter(typeof(T)); - if (converter != null) - { - list.Add((T)converter.ConvertFromString(strParam)); - } - - index++; - } - - return list; - } - - static void PrintUsage() - { - Console.WriteLine(); - Console.WriteLine("Usage - downloading one or all depots for an app:"); - Console.WriteLine("\tdepotdownloader -app [-depot [-manifest ]]"); - Console.WriteLine("\t\t[-username [-password ]] [other options]"); - Console.WriteLine(); - Console.WriteLine("Usage - downloading a workshop item using pubfile id"); - Console.WriteLine("\tdepotdownloader -app -pubfile [-username [-password ]]"); - Console.WriteLine("Usage - downloading a workshop item using ugc id"); - Console.WriteLine("\tdepotdownloader -app -ugc [-username [-password ]]"); - Console.WriteLine(); - Console.WriteLine("Parameters:"); - Console.WriteLine("\t-app <#>\t\t\t\t- the AppID to download."); - Console.WriteLine("\t-depot <#>\t\t\t\t- the DepotID to download."); - Console.WriteLine("\t-manifest \t\t\t- manifest id of content to download (requires -depot, default: current for branch)."); - Console.WriteLine("\t-beta \t\t\t- download from specified branch if available (default: Public)."); - Console.WriteLine("\t-betapassword \t\t- branch password if applicable."); - Console.WriteLine("\t-all-platforms\t\t\t- downloads all platform-specific depots when -app is used."); - Console.WriteLine("\t-os \t\t\t\t- the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)"); - Console.WriteLine("\t-osarch \t\t\t\t- the architecture for which to download the game (32 or 64, default: the host's architecture)"); - Console.WriteLine("\t-all-languages\t\t\t\t- download all language-specific depots when -app is used."); - Console.WriteLine("\t-language \t\t\t\t- the language for which to download the game (default: english)"); - Console.WriteLine("\t-lowviolence\t\t\t\t- download low violence depots when -app is used."); - Console.WriteLine(); - Console.WriteLine("\t-ugc <#>\t\t\t\t- the UGC ID to download."); - Console.WriteLine("\t-pubfile <#>\t\t\t- the PublishedFileId to download. (Will automatically resolve to UGC id)"); - Console.WriteLine(); - Console.WriteLine("\t-username \t\t- the username of the account to login to for restricted content."); - Console.WriteLine("\t-password \t\t- the password of the account to login to for restricted content."); - Console.WriteLine("\t-remember-password\t\t- if set, remember the password for subsequent logins of this user. (Use -username -remember-password as login credentials)"); - Console.WriteLine(); - Console.WriteLine("\t-dir \t\t- the directory in which to place downloaded files."); - Console.WriteLine("\t-filelist \t- a list of files to download (from the manifest). Prefix file path with 'regex:' if you want to match with regex."); - Console.WriteLine("\t-validate\t\t\t\t- Include checksum verification of files already downloaded"); - Console.WriteLine(); - Console.WriteLine("\t-manifest-only\t\t\t- downloads a human readable manifest for any depots that would be downloaded."); - Console.WriteLine("\t-cellid <#>\t\t\t\t- the overridden CellID of the content server to download from."); - Console.WriteLine("\t-max-servers <#>\t\t- maximum number of content servers to use. (default: 20)."); - Console.WriteLine("\t-max-downloads <#>\t\t- maximum number of chunks to download concurrently. (default: 8)."); - Console.WriteLine("\t-loginid <#>\t\t- a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently."); - } } }