File: ExtractArchiveToDirectory.cs
Web Access
Project: ..\..\..\src\Tasks\sdk-tasks\sdk-tasks.csproj (sdk-tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
#if !NETFRAMEWORK
using System.Formats.Tar;
#endif
using System.IO.Compression;
 
namespace Microsoft.DotNet.Build.Tasks
{
    /// <summary>
    /// Extracts a .zip or .tar.gz file to a directory.
    /// </summary>
    public sealed class ExtractArchiveToDirectory : ToolTask
    {
        /// <summary>
        /// The path to the archive to extract.
        /// </summary>
        [Required]
        public string SourceArchive { get; set; }
 
        /// <summary>
        /// The path of the directory to which the archive should be extracted.
        /// </summary>
        [Required]
        public string DestinationDirectory { get; set; }
 
        /// <summary>
        /// Indicates if the destination directory should be cleaned if it already exists.
        /// </summary>
        public bool CleanDestination { get; set; }
 
        /// <summary>
        /// A list of directories, relative to the root of the archive to include. If empty all directories will be copied.
        /// </summary>
        public ITaskItem[] DirectoriesToCopy { get; set; }
 
        protected override bool ValidateParameters()
        {
            base.ValidateParameters();
 
            var retVal = true;
 
            if (Directory.Exists(DestinationDirectory) && CleanDestination == true)
            {
                Log.LogMessage(MessageImportance.Low, "'{0}' already exists, trying to delete before unzipping...", DestinationDirectory);
                Directory.Delete(DestinationDirectory, recursive: true);
            }
 
            if (!File.Exists(SourceArchive))
            {
                Log.LogError($"SourceArchive '{SourceArchive} does not exist.");
 
                retVal = false;
            }
 
            if (retVal)
            {
                Log.LogMessage($"Creating Directory {DestinationDirectory}");
                Directory.CreateDirectory(DestinationDirectory);
            }
            
            return retVal;
        }
 
        public override bool Execute()
        {
            bool retVal = true;
            bool isZipArchive = Path.GetExtension(SourceArchive).Equals(".zip", StringComparison.OrdinalIgnoreCase);
            bool isTarballArchive = SourceArchive.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase);
 
            //  Inherits from ToolTask in order to shell out to tar for complete extraction
            //  If the file is a .zip, then don't call the base Execute method, just run as a normal task
            //  If the file is a .tar.gz, and DirectoriesToCopy isn't empty, also run a normal task.
            if (isZipArchive || isTarballArchive)
            {
                if (ValidateParameters())
                {
                    if (DirectoriesToCopy != null && DirectoriesToCopy.Length != 0)
                    {
                        // Partial archive extraction
                        if (isZipArchive)
                        {
                            using var zip = new ZipArchive(File.OpenRead(SourceArchive));
                            string fullDestDirPath = GetFullDirectoryPathWithSeperator(DestinationDirectory);
 
                            foreach (var entry in zip.Entries)
                            {
                                if (ShouldExtractItem(entry.FullName))
                                {
                                    string destinationPath = Path.GetFullPath(Path.Combine(DestinationDirectory, entry.FullName));
 
                                    CheckDestinationPath(destinationPath, fullDestDirPath);
 
                                    Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
 
                                    Log.LogMessage(Path.GetDirectoryName(entry.FullName));
                                    entry.ExtractToFile(destinationPath);
                                }
                            }
                        }
                        else
                        {
#if NETFRAMEWORK
                            // Run the base tool, which uses external 'tar' command
                            retVal = base.Execute();
#else
                            // Decompress GZip content
                            using FileStream compressedFileStream = File.OpenRead(SourceArchive);
                            using var decompressor = new GZipStream(compressedFileStream, CompressionMode.Decompress);
                            using var decompressedStream = new MemoryStream();
                            decompressor.CopyTo(decompressedStream);
                            decompressedStream.Seek(0, SeekOrigin.Begin);
                            string fullDestDirPath = GetFullDirectoryPathWithSeperator(DestinationDirectory);
 
                            // Extract Tar content
                            using TarReader tr = new TarReader(decompressedStream);
                            while (tr.GetNextEntry() is TarEntry tarEntry)
                            {
                                if (tarEntry.EntryType != TarEntryType.Directory)
                                {
                                    string entryName = tarEntry.Name;
                                    entryName = entryName.StartsWith("./") ? entryName[2..] : entryName;
                                    if (ShouldExtractItem(entryName))
                                    {
                                        string destinationPath = Path.GetFullPath(Path.Combine(DestinationDirectory, entryName));
 
                                        CheckDestinationPath(destinationPath, fullDestDirPath);
                                        Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
 
                                        Log.LogMessage(entryName);
                                        tarEntry.ExtractToFile(destinationPath, overwrite: true);
                                    }
                                }
                            }
#endif
                        }
                    }
                    else
                    {
                        // Complete archive extraction
                        if (isZipArchive)
                        {
#if NETFRAMEWORK
                            //  .NET Framework doesn't have overload to overwrite files
                            ZipFile.ExtractToDirectory(SourceArchive, DestinationDirectory);
#else
 
                            ZipFile.ExtractToDirectory(SourceArchive, DestinationDirectory, overwriteFiles: true);
#endif
                        }
                        else
                        {
                            // Run the base tool, which uses external 'tar' command
                            retVal = base.Execute();
                        }
                    }
                }
                else
                {
                    retVal = false;
                }
            }
            else
            {
                retVal = base.Execute();
            }
 
            if (!retVal)
            {
                Log.LogMessage($"Deleting Directory {DestinationDirectory}");
                Directory.Delete(DestinationDirectory);
            }
 
            return retVal;
        }
 
        private string GetFullDirectoryPathWithSeperator(string directory)
        {
            string fullDirectoryPath = Path.GetFullPath(directory);
 
            if (!fullDirectoryPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
            {
                fullDirectoryPath = string.Concat(fullDirectoryPath, Path.DirectorySeparatorChar);
            }
 
            return fullDirectoryPath;
        }
 
        private void CheckDestinationPath(string destinationFileName, string fullDestDirPath)
        {
            if (!destinationFileName.StartsWith(fullDestDirPath, StringComparison.Ordinal))
            {
                throw new System.InvalidOperationException("Entry is outside the target dir: " + destinationFileName);
            }
        }
 
        private bool ShouldExtractItem(string path) => DirectoriesToCopy?.Any(p => path.StartsWith(p.ItemSpec)) ?? false;
 
        protected override string ToolName => "tar";
 
        protected override MessageImportance StandardOutputLoggingImportance => MessageImportance.High;
 
        protected override string GenerateFullPathToTool() => "tar";
 
        protected override string GenerateCommandLineCommands() => $"xf {SourceArchive} -C {DestinationDirectory}";
    }
}