File: ReleaseFile.cs
Web Access
Project: src\src\deployment-tools\src\Microsoft.Deployment.DotNet.Releases\src\Microsoft.Deployment.DotNet.Releases.csproj (Microsoft.Deployment.DotNet.Releases)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;

namespace Microsoft.Deployment.DotNet.Releases
{
    /// <summary>
    /// Represents a single file associated with a release component such as an SDK or runtime.
    /// </summary>
    public class ReleaseFile : IEquatable<ReleaseFile>
    {
        private static readonly SHA512 s_defaultHashAlgorithm = SHA512.Create();

        private Uri _address;
        private string _addressString;

        /// <summary>
        /// The URL from where to download the file.
        /// </summary>
        public Uri Address
        {
            get => _address ??= _addressString != null ? new Uri(_addressString) : null;
            private set
            {
                _address = value;
                _addressString = value?.OriginalString;
            }
        }

        /// <summary>
        /// The filename and extension of this <see cref="ReleaseFile"/>.
        /// </summary>
        public string FileName => Path.GetFileName(Address.LocalPath);

        /// <summary>
        /// The <see cref="SHA512"/> hash of the file.
        /// </summary>
        public string Hash
        {
            get;
            private set;
        }

        /// <summary>
        /// The version agnostic name and extension of the file.
        /// </summary>
        public string Name
        {
            get;
            private set;
        }

        /// <summary>
        /// The runtime identifier associated with the file.
        /// </summary>
        public string Rid
        {
            get;
            private set;
        }

        /// <summary>
        /// Creates a new <see cref="ReleaseFile"/> instance from a <see cref="JsonElement"/>.
        /// </summary>
        /// <param name="fileElement">The <see cref="JsonElement"/> to deserialize.</param>
        internal ReleaseFile(JsonElement fileElement)
        {
            _addressString = fileElement.GetStringOrDefault("url");
            Hash = fileElement.GetStringOrDefault("hash");
            Name = fileElement.GetStringOrDefault("name");
            Rid = fileElement.GetStringOrDefault("rid");
        }

        /// <summary>
        /// Creates a new <see cref="ReleaseFile"/> instance.
        /// </summary>
        /// <param name="address">The URL of the file.</param>
        /// <param name="hash">A string containing the SHA512 hash of the file.</param>
        /// <param name="name">The name and extension of the file.</param>
        /// <param name="rid">The RID associated with the file.</param>
        internal ReleaseFile(Uri address, string hash, string name, string rid)
        {
            Address = address;
            Hash = hash;
            Name = name;
            Rid = rid;
        }

        /// <summary>
        /// Download this file to the specified local file and verify the file hash. If the destination file exists, the new copy is
        /// downloaded to a temporary file before verifying its hash. If the hash check fails, the temporary file is deleted. Otherwise,
        /// the temporary file is copied to the destination path. If the destination file does not exist, the file is downloaded and
        /// the hash is verified. If the hash check fails, the destination file is deleted.
        /// </summary>
        /// <param name="destinationPath">The path, including the filename of the local file. The file will be
        /// overwritten if it already exists if the hash check passed.</param>
        /// <exception cref="InvalidDataException">Thrown if the downloaded file's hash does to match the 
        /// expected hash.</exception>
        public async Task DownloadAsync(string destinationPath)
        {
            if (destinationPath is null)
            {
                throw new ArgumentNullException(nameof(destinationPath));
            }

            if (destinationPath == string.Empty)
            {
                throw new ArgumentException(ReleasesResources.ValueCannotBeEmpty, nameof(destinationPath));
            }

            // If the destination file doesn't exist we can skip using an actual temporary file.
            string tempPath = !File.Exists(destinationPath) ? destinationPath : Path.GetTempFileName();
            await Utils.DownloadFileAsync(Address, tempPath).ConfigureAwait(false);

            // Most of the files are large since they represent full installations of .NET/.NET Core. They can
            // easily be 100MB+ so we won't verify the hash in memory.
            string actualHash = Utils.GetFileHash(tempPath, s_defaultHashAlgorithm);

            if (!string.Equals(Hash, actualHash, StringComparison.OrdinalIgnoreCase))
            {
                File.Delete(tempPath);
                throw new InvalidDataException(string.Format(ReleasesResources.HashMismatch, Hash, actualHash, destinationPath));
            }

            // Replace the destination file if the hash verified successfully and we used an actual temporary file.
            if (!string.Equals(destinationPath, tempPath))
            {
                File.Delete(destinationPath);
                File.Move(tempPath, destinationPath);
            }
        }

        /// <summary>
        /// Determines whether the specified object is equal to this instance.
        /// </summary>
        /// <param name="obj">The object to compare to the current object.</param>
        /// <returns><see langword="true"/> if the specified object is equal to the current object; <see langword="false"/> otherwise.</returns>
        public override bool Equals(object obj)
        {
            return Equals((ReleaseFile)obj);
        }

        /// <summary>
        /// Determines whether the specified <see cref="ReleaseFile"/> is equal to this instance.
        /// </summary>
        /// <param name="other">The <see cref="ReleaseFile"/> to compare to this instance.</param>
        /// <returns><see langword="true"/> if the specified <see cref="ReleaseFile"/> is equal to this instance; <see langword="false"/> otherwise.</returns>
        public bool Equals(ReleaseFile other)
        {
            return ReferenceEquals(this, other) ||
                Name == other.Name &&
                Rid == other.Rid &&
                Hash == other.Hash &&
                _addressString == other._addressString;
        }

        /// <summary>
        /// Returns the hash code for this release file.
        /// </summary>
        /// <returns>A hash code for the current object.</returns>
        public override int GetHashCode() =>
            Hash.GetHashCode();

        internal static ReleaseFile Create(string hash, string name, string rid, string address) =>
            new ReleaseFile(new Uri(address), hash, name, rid);
    }
}