|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.IO; |
| 4 | +using System.Linq; |
| 5 | + |
| 6 | +namespace Pine.Core.Files; |
| 7 | + |
| 8 | +/// <summary> |
| 9 | +/// Helpers for working with ZIP archives. |
| 10 | +/// <see href="https://en.wikipedia.org/wiki/ZIP_(file_format)"></see> |
| 11 | +/// </summary> |
| 12 | +public static class ZipArchive |
| 13 | +{ |
| 14 | + /// <summary> |
| 15 | + /// https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs#L206-L234 |
| 16 | + /// </summary> |
| 17 | + public static DateTimeOffset EntryLastWriteTimeDefault => new(1980, 1, 1, 0, 0, 0, TimeSpan.Zero); |
| 18 | + |
| 19 | + /// <summary> |
| 20 | + /// Creates a ZIP archive containing the specified file entries and returns its binary data as a byte array. |
| 21 | + /// </summary> |
| 22 | + /// <remarks>Each file entry is added to the archive using its provided name. The method does not preserve |
| 23 | + /// file metadata such as timestamps or permissions. The returned byte array can be saved to disk or transmitted as |
| 24 | + /// needed.</remarks> |
| 25 | + /// <param name="entries">A collection of file entries, each consisting of a file name and its content as a read-only memory buffer. Each |
| 26 | + /// entry will be added to the archive with the provided name and content.</param> |
| 27 | + /// <param name="compressionLevel">The compression level to apply to the archive. Defaults to <see |
| 28 | + /// cref="System.IO.Compression.CompressionLevel.Optimal"/> if not specified.</param> |
| 29 | + /// <returns>A byte array containing the ZIP archive data with all specified entries included.</returns> |
| 30 | + public static byte[] ZipArchiveFromFiles( |
| 31 | + IEnumerable<(string name, ReadOnlyMemory<byte> content)> entries, |
| 32 | + System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => |
| 33 | + ZipArchiveFromFiles( |
| 34 | + [.. entries.Select(entry => (entry.name, entry.content, EntryLastWriteTimeDefault))], |
| 35 | + compressionLevel); |
| 36 | + |
| 37 | + /// <summary> |
| 38 | + /// Creates a ZIP archive containing the specified files and returns its contents as a byte array. |
| 39 | + /// </summary> |
| 40 | + /// <remarks>Each file path is constructed by joining the path segments in the key with a forward slash |
| 41 | + /// ('/'). The method does not validate file names or contents; callers should ensure that paths and data are valid |
| 42 | + /// for their intended use.</remarks> |
| 43 | + /// <param name="entries">A dictionary mapping each file's path segments to its content. Each key represents the file path as a list of |
| 44 | + /// strings, and each value contains the file's data as a read-only memory buffer.</param> |
| 45 | + /// <param name="compressionLevel">The compression level to use when creating the ZIP archive. The default is CompressionLevel.Optimal.</param> |
| 46 | + /// <returns>A byte array containing the ZIP archive with all specified files. The array will be empty if no entries are |
| 47 | + /// provided.</returns> |
| 48 | + public static byte[] ZipArchiveFromFiles( |
| 49 | + IReadOnlyDictionary<IReadOnlyList<string>, ReadOnlyMemory<byte>> entries, |
| 50 | + System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) => |
| 51 | + ZipArchiveFromFiles( |
| 52 | + entries.Select(entry => (name: string.Join("/", entry.Key), content: entry.Value)), |
| 53 | + compressionLevel); |
| 54 | + |
| 55 | + /// <summary> |
| 56 | + /// Creates a ZIP archive containing the specified files and returns its contents as a byte array. |
| 57 | + /// </summary> |
| 58 | + /// <remarks>The returned ZIP archive is created in memory and is not written to disk. The method does not |
| 59 | + /// validate the uniqueness of file names; duplicate names may result in multiple entries with the same name in the |
| 60 | + /// archive. The last write time for each entry is set according to the provided value.</remarks> |
| 61 | + /// <param name="entries">A collection of file entries to include in the archive. Each entry specifies the file name, content as a |
| 62 | + /// read-only memory buffer, and the last write time to set for the file in the archive. The file name must be a |
| 63 | + /// valid ZIP entry name and cannot be null or empty.</param> |
| 64 | + /// <param name="compressionLevel">The compression level to use for each file in the archive. Defaults to <see |
| 65 | + /// cref="System.IO.Compression.CompressionLevel.Optimal"/> if not specified.</param> |
| 66 | + /// <returns>A byte array containing the complete ZIP archive. The array will be empty if no entries are provided.</returns> |
| 67 | + public static byte[] ZipArchiveFromFiles( |
| 68 | + IEnumerable<(string name, ReadOnlyMemory<byte> content, DateTimeOffset lastWriteTime)> entries, |
| 69 | + System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) |
| 70 | + { |
| 71 | + var stream = new MemoryStream(); |
| 72 | + |
| 73 | + using (var fclZipArchive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Create, true)) |
| 74 | + { |
| 75 | + foreach (var (entryName, entryContent, lastWriteTime) in entries) |
| 76 | + { |
| 77 | + var entry = fclZipArchive.CreateEntry(entryName, compressionLevel); |
| 78 | + |
| 79 | + entry.LastWriteTime = lastWriteTime; |
| 80 | + |
| 81 | + using var entryStream = entry.Open(); |
| 82 | + |
| 83 | + entryStream.Write(entryContent.Span); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + stream.Seek(0, SeekOrigin.Begin); |
| 88 | + |
| 89 | + var zipArchive = new byte[stream.Length]; |
| 90 | + |
| 91 | + stream.Read(zipArchive, 0, (int)stream.Length); |
| 92 | + stream.Dispose(); |
| 93 | + |
| 94 | + return zipArchive; |
| 95 | + } |
| 96 | + |
| 97 | + /// <summary> |
| 98 | + /// Enumerates the files contained in a ZIP archive, each represented by its (flat) path and content. |
| 99 | + /// </summary> |
| 100 | + /// <param name="zipArchive">A read-only memory buffer containing the ZIP archive data. Must be a valid ZIP file; otherwise, the behavior is |
| 101 | + /// undefined.</param> |
| 102 | + /// <returns>An enumerable collection of tuples, each containing the file name and its content as a read-only memory buffer. |
| 103 | + /// Only file entries are included; directory entries are excluded.</returns> |
| 104 | + public static IEnumerable<(string name, ReadOnlyMemory<byte> content)> FileEntriesFromZipArchive( |
| 105 | + ReadOnlyMemory<byte> zipArchive) => |
| 106 | + EntriesFromZipArchive( |
| 107 | + zipArchive: zipArchive, |
| 108 | + includeEntry: entry => !entry.FullName.Replace('\\', '/').EndsWith('/')); |
| 109 | + |
| 110 | + /// <summary> |
| 111 | + /// Extracts all entries from the specified ZIP archive and returns their names and contents. |
| 112 | + /// </summary> |
| 113 | + /// <remarks>The method returns all entries in the archive, including files and directories. |
| 114 | + /// The order of entries matches their order in the archive.</remarks> |
| 115 | + /// <param name="zipArchive">A read-only memory buffer containing the ZIP archive data. Must represent a valid ZIP file format.</param> |
| 116 | + /// <returns>An enumerable collection of tuples, each containing the entry name and its content as a read-only memory buffer. |
| 117 | + /// The collection is empty if the archive contains no entries.</returns> |
| 118 | + public static IEnumerable<(string name, ReadOnlyMemory<byte> content)> EntriesFromZipArchive(ReadOnlyMemory<byte> zipArchive) => |
| 119 | + EntriesFromZipArchive(zipArchive: zipArchive, includeEntry: _ => true); |
| 120 | + |
| 121 | + /// <summary> |
| 122 | + /// Enumerates entries from a ZIP archive and returns the name and content of each entry that matches the specified |
| 123 | + /// filter. |
| 124 | + /// </summary> |
| 125 | + /// <remarks>The method reads the entire content of each included entry into memory. Use caution when |
| 126 | + /// processing large archives or entries to avoid excessive memory usage.</remarks> |
| 127 | + /// <param name="zipArchive">A read-only memory buffer containing the binary data of the ZIP archive to be read.</param> |
| 128 | + /// <param name="includeEntry">A predicate used to determine whether a given ZIP archive entry should be included in the results. The function |
| 129 | + /// receives each entry and should return <see langword="true"/> to include the entry; otherwise, <see |
| 130 | + /// langword="false"/>.</param> |
| 131 | + /// <returns>An enumerable collection of tuples, each containing the entry name and its content as a read-only memory buffer. |
| 132 | + /// Only entries for which <paramref name="includeEntry"/> returns <see langword="true"/> are included.</returns> |
| 133 | + /// <exception cref="Exception">Thrown if the number of bytes read from an entry does not match the expected entry length.</exception> |
| 134 | + public static IEnumerable<(string name, ReadOnlyMemory<byte> content)> EntriesFromZipArchive( |
| 135 | + ReadOnlyMemory<byte> zipArchive, |
| 136 | + Func<System.IO.Compression.ZipArchiveEntry, bool> includeEntry) |
| 137 | + { |
| 138 | + using var fclZipArchive = |
| 139 | + new System.IO.Compression.ZipArchive( |
| 140 | + new MemoryStream(zipArchive.ToArray()), |
| 141 | + System.IO.Compression.ZipArchiveMode.Read); |
| 142 | + |
| 143 | + foreach (var entry in fclZipArchive.Entries) |
| 144 | + { |
| 145 | + if (!includeEntry(entry)) |
| 146 | + continue; |
| 147 | + |
| 148 | + using var entryStream = entry.Open(); |
| 149 | + using var memoryStream = new MemoryStream(); |
| 150 | + |
| 151 | + entryStream.CopyTo(memoryStream); |
| 152 | + |
| 153 | + var entryContent = memoryStream.ToArray(); |
| 154 | + |
| 155 | + if (entryContent.Length != entry.Length) |
| 156 | + { |
| 157 | + throw new Exception( |
| 158 | + "Error trying to read entry '" + entry.FullName + "': got " + |
| 159 | + entryContent.Length + " bytes from entry instead of " + entry.Length); |
| 160 | + } |
| 161 | + |
| 162 | + yield return (entry.FullName, entryContent); |
| 163 | + } |
| 164 | + } |
| 165 | +} |
0 commit comments