public static async Task DownloadAppAsync(uint appId, uint depotId, ulong manifestId, CancellationToken cancellationToken)
        {
            // Load our configuration data containing the depots currently installed
            string configPath = Config.InstallDirectory;

            if (string.IsNullOrWhiteSpace(configPath))
            {
                configPath = DEFAULT_DOWNLOAD_DIR;
            }

            Directory.CreateDirectory(Path.Combine(configPath, CONFIG_DIR));
            DepotConfigStore.LoadFromFile(Path.Combine(configPath, CONFIG_DIR, "depot.config"));

            var depotIDs = new List <uint>()
            {
                depotId
            };
            KeyValue depots = await GetSteam3AppSectionAsync(appId, EAppInfoSection.Depots).ConfigureAwait(false);

            var infos = new List <DepotDownloadInfo>();

            foreach (var depot in depotIDs)
            {
                var info = await GetDepotInfoAsync(depot, appId, manifestId, "Public").ConfigureAwait(false);

                if (info != null)
                {
                    infos.Add(info);
                }

                cancellationToken.ThrowIfCancellationRequested();
            }

            cancellationToken.ThrowIfCancellationRequested();

            try
            {
                await DownloadSteam3Async(appId, infos, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("App {0} was not completely downloaded.", appId);
                throw;
            }
            finally
            {
                DepotConfigStore.Instance = null; // unload config store
            }
        }
예제 #2
0
        public static void LoadFromFile(string filename)
        {
            if (Loaded)
            {
                throw new Exception("Config already loaded");
            }

            if (File.Exists(filename))
            {
                using (FileStream fs = File.Open(filename, FileMode.Open))
                    using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
                        Instance = Serializer.Deserialize <DepotConfigStore>(ds);
            }
            else
            {
                Instance = new DepotConfigStore();
            }

            Instance.FileName = filename;
        }
        private static async Task DownloadSteam3Async(uint appId, List <DepotDownloadInfo> depots, CancellationToken cancellationToken)
        {
            ulong TotalBytesCompressed   = 0;
            ulong TotalBytesUncompressed = 0;

            foreach (var depot in depots)
            {
                ulong DepotBytesCompressed   = 0;
                ulong DepotBytesUncompressed = 0;

                Console.WriteLine("Downloading depot {0} - {1}", depot.Id, depot.ContentName);

                CancellationTokenSource cts = new CancellationTokenSource();
                cdnPool.ExhaustedToken = cts;

                ProtoManifest oldProtoManifest = null;
                ProtoManifest newProtoManifest = null;
                string        configDir        = Path.Combine(depot.InstallDir, CONFIG_DIR);

                ulong lastManifestId = INVALID_MANIFEST_ID;
                DepotConfigStore.Instance.InstalledManifestIDs.TryGetValue(depot.Id, out lastManifestId);

                // In case we have an early exit, this will force equiv of verifyall next run.
                DepotConfigStore.Instance.InstalledManifestIDs[depot.Id] = INVALID_MANIFEST_ID;
                DepotConfigStore.Save();

                if (lastManifestId != INVALID_MANIFEST_ID)
                {
                    var oldManifestFileName = Path.Combine(configDir, string.Format("{0}.bin", lastManifestId));

                    if (File.Exists(oldManifestFileName))
                    {
                        byte[] expectedChecksum, currentChecksum;

                        try
                        {
                            expectedChecksum = File.ReadAllBytes(oldManifestFileName + ".sha");
                        }
                        catch (IOException)
                        {
                            expectedChecksum = null;
                        }

                        oldProtoManifest = ProtoManifest.LoadFromFile(oldManifestFileName, out currentChecksum);

                        if (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum))
                        {
                            // We only have to show this warning if the old manifest ID was different
                            if (lastManifestId != depot.ManifestId)
                            {
                                Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", lastManifestId);
                            }
                            oldProtoManifest = null;
                        }
                    }
                }

                if (lastManifestId == depot.ManifestId && oldProtoManifest != null)
                {
                    newProtoManifest = oldProtoManifest;
                    Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.Id);
                }
                else
                {
                    var newManifestFileName = Path.Combine(configDir, string.Format("{0}_{1}.bin", depot.Id, depot.ManifestId));
                    if (newManifestFileName != null)
                    {
                        byte[] expectedChecksum, currentChecksum;

                        try
                        {
                            expectedChecksum = File.ReadAllBytes(newManifestFileName + ".sha");
                        }
                        catch (IOException)
                        {
                            expectedChecksum = null;
                        }

                        newProtoManifest = ProtoManifest.LoadFromFile(newManifestFileName, out currentChecksum);

                        if (newProtoManifest != null && (expectedChecksum == null || !expectedChecksum.SequenceEqual(currentChecksum)))
                        {
                            Console.WriteLine("Manifest {0} on disk did not match the expected checksum.", depot.ManifestId);
                            newProtoManifest = null;
                        }
                    }

                    if (newProtoManifest != null)
                    {
                        Console.WriteLine("Already have manifest {0} for depot {1}.", depot.ManifestId, depot.Id);
                    }
                    else
                    {
                        Console.Write("Downloading depot manifest...");

                        DepotManifest depotManifest = await DownloadManifest(appId, depot.Id, depot.ManifestId, depot.DepotKey).ConfigureAwait(false);

                        byte[] checksum;

                        newProtoManifest = new ProtoManifest(depotManifest, depot.ManifestId);
                        newProtoManifest.SaveToFile(newManifestFileName, out checksum);
                        File.WriteAllBytes(newManifestFileName + ".sha", checksum);

                        Console.WriteLine(" Done!");
                    }
                }

                newProtoManifest.Files.Sort((x, y) => string.Compare(x.FileName, y.FileName, StringComparison.Ordinal));

                Console.WriteLine("Manifest {0} ({1})", depot.ManifestId, newProtoManifest.CreationTime);

                ulong  complete_download_size = 0;
                ulong  size_downloaded        = 0;
                var    downloadChanges        = new ConcurrentQueue <(DateTime, ulong)>();
                object downloadChangesLock    = new object();

                void reportProgress(string currentFilePath)
                {
                    lock (downloadChangesLock)
                    {
                        while (downloadChanges.Count > 50) // Only average last 50 deltas
                        {
                            downloadChanges.TryDequeue(out _);
                        }
                    }

                    ulong bytesPerSecond = 0;

                    if (downloadChanges.Count > 0)
                    {
                        lock (downloadChangesLock)
                        {
                            TimeSpan timeSpan   = downloadChanges.Last().Item1 - downloadChanges.First().Item1;
                            ulong    totalBytes = downloadChanges.Aggregate(0ul, (a, b) => a + b.Item2);
                            double   rate       = totalBytes / timeSpan.TotalSeconds;

                            if (double.IsNaN(rate) || double.IsInfinity(rate))
                            {
                                rate = 0;
                            }

                            bytesPerSecond = (ulong)rate;
                        }
                    }

                    Globals.UiDispatcher.Invoke(() =>
                    {
                        Globals.AppState.DownloadState.DownloadPercentageComplete = ((double)size_downloaded / complete_download_size) * 100;
                        Globals.AppState.DownloadState.DownloadCurrentFile        = currentFilePath;
                        Globals.AppState.DownloadState.DownloadedBytes            = size_downloaded;
                        Globals.AppState.DownloadState.BytesPerSecond             = bytesPerSecond;
                    });
                }

                string stagingDir = Path.Combine(depot.InstallDir, STAGING_DIR);

                // Delete files that are removed in new manifest
                if (oldProtoManifest != null)
                {
                    var directoriesToCheck = new HashSet <string>(); // Directories to check if they're empty and delete

                    foreach (ProtoManifest.FileData oldFile in oldProtoManifest.Files)
                    {
                        if (!newProtoManifest.Files.Any(newFile => newFile.FileName == oldFile.FileName))
                        {
                            Console.WriteLine($"Deleting file: {oldFile.FileName}");
                            var fileFinalPath = Path.Combine(depot.InstallDir, oldFile.FileName);

                            if (File.Exists(fileFinalPath))
                            {
                                File.Delete(fileFinalPath);
                                directoriesToCheck.Add(Path.GetDirectoryName(fileFinalPath));
                            }
                        }
                    }

                    // Delete the now empty directories
                    foreach (string directoryPath in directoriesToCheck)
                    {
                        if (Directory.GetDirectories(directoryPath).Length == 0 && Directory.GetFiles(directoryPath).Length == 0)
                        {
                            Directory.Delete(directoryPath);
                        }
                    }
                }

                // Pre-process
                newProtoManifest.Files.ForEach(file =>
                {
                    var fileFinalPath   = Path.Combine(depot.InstallDir, file.FileName);
                    var fileStagingPath = Path.Combine(stagingDir, file.FileName);

                    if (file.Flags.HasFlag(EDepotFileFlag.Directory))
                    {
                        Directory.CreateDirectory(fileFinalPath);
                        Directory.CreateDirectory(fileStagingPath);
                    }
                    else
                    {
                        // Some manifests don't explicitly include all necessary directories
                        Directory.CreateDirectory(Path.GetDirectoryName(fileFinalPath));
                        Directory.CreateDirectory(Path.GetDirectoryName(fileStagingPath));

                        complete_download_size += file.TotalSize;
                    }
                });

                Globals.UiDispatcher.Invoke(() =>
                {
                    Globals.AppState.DownloadState.TotalBytes = complete_download_size;
                });

                cancellationToken.ThrowIfCancellationRequested();

                var semaphore = new SemaphoreSlim(Config.MaxDownloads);
                var files     = newProtoManifest.Files.Where(f => !f.Flags.HasFlag(EDepotFileFlag.Directory)).ToArray();
                var tasks     = new Task[files.Length];
                for (var i = 0; i < files.Length; i++)
                {
                    var file = files[i];
                    var task = Task.Run(async() =>
                    {
                        cts.Token.ThrowIfCancellationRequested();

                        try
                        {
                            await semaphore.WaitAsync().ConfigureAwait(false);
                            cts.Token.ThrowIfCancellationRequested();
                            cancellationToken.ThrowIfCancellationRequested();

                            string fileFinalPath   = Path.Combine(depot.InstallDir, file.FileName);
                            string fileStagingPath = Path.Combine(stagingDir, file.FileName);

                            // This may still exist if the previous run exited before cleanup
                            if (File.Exists(fileStagingPath))
                            {
                                File.Delete(fileStagingPath);
                            }

                            FileStream fs = null;
                            List <ProtoManifest.ChunkData> neededChunks;
                            FileInfo fi = new FileInfo(fileFinalPath);
                            if (!fi.Exists)
                            {
                                // create new file. need all chunks
                                fs = File.Create(fileFinalPath);
                                fs.SetLength((long)file.TotalSize);
                                neededChunks = new List <ProtoManifest.ChunkData>(file.Chunks);
                            }
                            else
                            {
                                // open existing
                                ProtoManifest.FileData oldManifestFile = null;
                                if (oldProtoManifest != null)
                                {
                                    oldManifestFile = oldProtoManifest.Files.SingleOrDefault(f => f.FileName == file.FileName);
                                }

                                if (oldManifestFile != null)
                                {
                                    neededChunks = new List <ProtoManifest.ChunkData>();

                                    if (Config.VerifyAll || !oldManifestFile.FileHash.SequenceEqual(file.FileHash))
                                    {
                                        // we have a version of this file, but it doesn't fully match what we want

                                        var matchingChunks = new List <ChunkMatch>();

                                        foreach (var chunk in file.Chunks)
                                        {
                                            var oldChunk = oldManifestFile.Chunks.FirstOrDefault(c => c.ChunkID.SequenceEqual(chunk.ChunkID));
                                            if (oldChunk != null)
                                            {
                                                matchingChunks.Add(new ChunkMatch(oldChunk, chunk));
                                            }
                                            else
                                            {
                                                neededChunks.Add(chunk);
                                            }
                                        }

                                        File.Move(fileFinalPath, fileStagingPath);

                                        fs = File.Open(fileFinalPath, FileMode.Create);
                                        fs.SetLength((long)file.TotalSize);

                                        using (var fsOld = File.Open(fileStagingPath, FileMode.Open))
                                        {
                                            foreach (var match in matchingChunks)
                                            {
                                                fsOld.Seek((long)match.OldChunk.Offset, SeekOrigin.Begin);

                                                byte[] tmp = new byte[match.OldChunk.UncompressedLength];
                                                fsOld.Read(tmp, 0, tmp.Length);

                                                byte[] adler = Util.AdlerHash(tmp);
                                                if (!adler.SequenceEqual(match.OldChunk.Checksum))
                                                {
                                                    neededChunks.Add(match.NewChunk);
                                                }
                                                else
                                                {
                                                    fs.Seek((long)match.NewChunk.Offset, SeekOrigin.Begin);
                                                    fs.Write(tmp, 0, tmp.Length);
                                                }
                                            }
                                        }

                                        File.Delete(fileStagingPath);
                                    }
                                }
                                else
                                {
                                    // No old manifest or file not in old manifest. We must validate.

                                    fs = File.Open(fileFinalPath, FileMode.Open);
                                    if ((ulong)fi.Length != file.TotalSize)
                                    {
                                        fs.SetLength((long)file.TotalSize);
                                    }

                                    neededChunks = Util.ValidateSteam3FileChecksums(fs, file.Chunks.OrderBy(x => x.Offset).ToArray());
                                }

                                if (neededChunks.Count() == 0)
                                {
                                    size_downloaded += file.TotalSize;
                                    reportProgress(file.FileName);

                                    if (fs != null)
                                    {
                                        fs.Dispose();
                                    }
                                    return;
                                }
                                else
                                {
                                    ulong sizeDelta  = (file.TotalSize - (ulong)neededChunks.Select(x => (long)x.UncompressedLength).Sum());
                                    size_downloaded += sizeDelta;
                                    reportProgress(file.FileName);
                                }
                            }

                            foreach (var chunk in neededChunks)
                            {
                                if (cts.IsCancellationRequested)
                                {
                                    break;
                                }

                                string chunkID = Util.EncodeHexString(chunk.ChunkID);
                                CDNClient.DepotChunk chunkData = null;

                                while (!cts.IsCancellationRequested)
                                {
                                    Tuple <CDNClient.Server, string> connection;
                                    try
                                    {
                                        connection = await cdnPool.GetConnectionForDepot(appId, depot.Id, cts.Token);
                                    }
                                    catch (OperationCanceledException)
                                    {
                                        break;
                                    }

                                    DepotManifest.ChunkData data = new DepotManifest.ChunkData();
                                    data.ChunkID            = chunk.ChunkID;
                                    data.Checksum           = chunk.Checksum;
                                    data.Offset             = chunk.Offset;
                                    data.CompressedLength   = chunk.CompressedLength;
                                    data.UncompressedLength = chunk.UncompressedLength;

                                    try
                                    {
                                        chunkData = await cdnPool.CDNClient.DownloadDepotChunkAsync(depot.Id, data,
                                                                                                    connection.Item1, connection.Item2, depot.DepotKey).ConfigureAwait(false);
                                        cdnPool.ReturnConnection(connection);
                                        break;
                                    }
                                    catch (SteamKitWebRequestException e)
                                    {
                                        cdnPool.ReturnBrokenConnection(connection);

                                        if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
                                        {
                                            Console.WriteLine("Encountered 401 for chunk {0}. Aborting.", chunkID);
                                            cts.Cancel();
                                            break;
                                        }
                                        else
                                        {
                                            Console.WriteLine("Encountered error downloading chunk {0}: {1}", chunkID, e.StatusCode);
                                        }
                                    }
                                    catch (TaskCanceledException)
                                    {
                                        Console.WriteLine("Connection timeout downloading chunk {0}", chunkID);
                                    }
                                    catch (Exception e)
                                    {
                                        cdnPool.ReturnBrokenConnection(connection);
                                        Console.WriteLine("Encountered unexpected error downloading chunk {0}: {1}", chunkID, e.Message);
                                    }
                                }

                                if (chunkData == null)
                                {
                                    Console.WriteLine("Failed to find any server with chunk {0} for depot {1}. Aborting.", chunkID, depot.Id);
                                    cts.Cancel();
                                }

                                // Throw the cancellation exception if requested so that this task is marked failed
                                cts.Token.ThrowIfCancellationRequested();
                                cancellationToken.ThrowIfCancellationRequested();

                                TotalBytesCompressed   += chunk.CompressedLength;
                                DepotBytesCompressed   += chunk.CompressedLength;
                                TotalBytesUncompressed += chunk.UncompressedLength;
                                DepotBytesUncompressed += chunk.UncompressedLength;

                                fs.Seek((long)chunk.Offset, SeekOrigin.Begin);
                                fs.Write(chunkData.Data, 0, chunkData.Data.Length);

                                size_downloaded += chunk.UncompressedLength;
                                downloadChanges.Enqueue((DateTime.Now, chunk.CompressedLength));
                                reportProgress(file.FileName);
                            }

                            fs.Dispose();
                            reportProgress(file.FileName);
                        }
                        finally
                        {
                            semaphore.Release();
                        }
                    });

                    tasks[i] = task;
                }

                await Task.WhenAll(tasks).ConfigureAwait(false);

                DepotConfigStore.Instance.InstalledManifestIDs[depot.Id] = depot.ManifestId;
                DepotConfigStore.Save();

                Console.WriteLine("Depot {0} - Downloaded {1} bytes ({2} bytes uncompressed)", depot.Id, DepotBytesCompressed, DepotBytesUncompressed);
                reportProgress(null);
            }

            Console.WriteLine("Total downloaded: {0} bytes ({1} bytes uncompressed) from {2} depots", TotalBytesCompressed, TotalBytesUncompressed, depots.Count);
        }