Skip to content

Commit 81b8512

Browse files
committed
added support for expanding tar.gz archives
1 parent 60575d1 commit 81b8512

File tree

7 files changed

+100
-109
lines changed

7 files changed

+100
-109
lines changed

src/ArchiveFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal static bool TryGetArchiveFormatFromExtension(string path, out ArchiveFo
3434
".zip" => ArchiveFormat.Zip,
3535
".tar" => ArchiveFormat.Tar,
3636
".gz" => path.EndsWith(".tar.gz") ? ArchiveFormat.Tgz : null,
37+
".tgz" => ArchiveFormat.Tgz,
3738
_ => null
3839
};
3940
return archiveFormat is not null;

src/Cmdlets/ExpandArchiveCommand.cs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -324,23 +324,14 @@ private string DetermineDestinationPath(IArchive archive)
324324
var workingDirectory = SessionState.Path.CurrentFileSystemLocation.ProviderPath;
325325
string? destinationDirectory = null;
326326

327-
// If the archive has a single top-level directory only, the destination will be: "working directory"
328-
// This makes it easier for the cmdlet to expand the directory without needing addition checks
329-
if (archive.HasTopLevelDirectory())
327+
var filename = System.IO.Path.GetFileName(archive.Path);
328+
// If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory
329+
if (System.IO.Path.GetExtension(filename) != string.Empty)
330330
{
331-
destinationDirectory = workingDirectory;
332-
}
333-
// Otherwise, the destination path will be: "working directory/archive file name"
334-
else
335-
{
336-
var filename = System.IO.Path.GetFileName(archive.Path);
337-
// If filename does have an exension, remove the extension and set the filename minus extension as destinationDirectory
338-
if (System.IO.Path.GetExtension(filename) != string.Empty)
339-
{
340-
int indexOfLastPeriod = filename.LastIndexOf('.');
341-
destinationDirectory = filename.Substring(0, indexOfLastPeriod);
342-
}
331+
int indexOfLastPeriod = filename.LastIndexOf('.');
332+
destinationDirectory = filename.Substring(0, indexOfLastPeriod);
343333
}
334+
344335

345336
if (destinationDirectory is null)
346337
{

src/Formats/GzipArchive.cs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ namespace Microsoft.PowerShell.Archive
1212
{
1313
internal class GzipArchive : IArchive
1414
{
15-
protected bool _disposedValue;
15+
private bool _disposedValue;
1616

17-
protected readonly ArchiveMode _mode;
17+
private readonly ArchiveMode _mode;
1818

19-
protected readonly string _path;
19+
private readonly string _path;
2020

21-
protected readonly FileStream _fileStream;
21+
private readonly FileStream _fileStream;
2222

23-
protected readonly CompressionLevel _compressionLevel;
23+
private readonly CompressionLevel _compressionLevel;
2424

2525
private bool _addedFile;
2626

27-
protected bool _didCallGetNextEntry;
27+
private bool _didCallGetNextEntry;
2828

2929
ArchiveMode IArchive.Mode => _mode;
3030

@@ -38,7 +38,7 @@ public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, Compres
3838
_compressionLevel = compressionLevel;
3939
}
4040

41-
public virtual void AddFileSystemEntry(ArchiveAddition entry)
41+
public void AddFileSystemEntry(ArchiveAddition entry)
4242
{
4343
if (_mode == ArchiveMode.Extract)
4444
{
@@ -61,7 +61,7 @@ public virtual void AddFileSystemEntry(ArchiveAddition entry)
6161
_addedFile = true;
6262
}
6363

64-
IEntry? IArchive.GetNextEntry()
64+
public IEntry? GetNextEntry()
6565
{
6666
// Gzip has no concept of entries
6767
if (!_didCallGetNextEntry) {
@@ -71,7 +71,7 @@ public virtual void AddFileSystemEntry(ArchiveAddition entry)
7171
return null;
7272
}
7373

74-
protected virtual void Dispose(bool disposing)
74+
private void Dispose(bool disposing)
7575
{
7676
if (!_disposedValue)
7777
{
@@ -94,11 +94,6 @@ public void Dispose()
9494
GC.SuppressFinalize(this);
9595
}
9696

97-
bool IArchive.HasTopLevelDirectory()
98-
{
99-
throw new NotSupportedException();
100-
}
101-
10297
internal class GzipArchiveEntry : IEntry {
10398

10499
private GzipArchive _gzipArchive;
@@ -116,7 +111,7 @@ public GzipArchiveEntry(GzipArchive gzipArchive)
116111

117112
void IEntry.ExpandTo(string destinationPath)
118113
{
119-
using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
114+
using var destinationFileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None);
120115
using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress);
121116
gzipDecompressor.CopyTo(destinationFileStream);
122117
}

src/Formats/TarArchive.cs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Formats.Tar;
76
using System.IO;
87
using System.Diagnostics;
@@ -66,7 +65,7 @@ public void AddFileSystemEntry(ArchiveAddition entry)
6665
_tarWriter.WriteEntry(fileName: entry.FileSystemInfo.FullName, entryName: entryName);
6766
}
6867

69-
IEntry? IArchive.GetNextEntry()
68+
public IEntry? GetNextEntry()
7069
{
7170
// If _tarReader is null, create it
7271
if (_tarReader is null)
@@ -147,36 +146,6 @@ public void Dispose()
147146
GC.SuppressFinalize(this);
148147
}
149148

150-
bool IArchive.HasTopLevelDirectory()
151-
{
152-
// Go through each entry and see if it is a top-level entry
153-
_tarReader = new TarReader(_fileStream, leaveOpen: true);
154-
155-
int topLevelDirectoriesCount = 0;
156-
var entry = _tarReader.GetNextEntry();
157-
while (entry is not null) {
158-
159-
if (entry.EntryType == TarEntryType.Directory)
160-
{
161-
topLevelDirectoriesCount++;
162-
if (topLevelDirectoriesCount > 1)
163-
{
164-
break;
165-
}
166-
} else
167-
{
168-
_tarReader.Dispose();
169-
_tarReader = null;
170-
return false;
171-
}
172-
entry = _tarReader.GetNextEntry();
173-
}
174-
175-
_tarReader.Dispose();
176-
_tarReader = null;
177-
return topLevelDirectoriesCount == 1;
178-
}
179-
180149
internal class TarArchiveEntry : IEntry {
181150

182151
// Underlying object is System.Formats.Tar.TarEntry

src/Formats/TarGzArchive.cs

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,129 @@
22
// Licensed under the MIT License.
33

44
using System;
5-
using System.Collections.Generic;
6-
using System.Formats.Tar;
75
using System.IO;
86
using System.IO.Compression;
97
using System.Diagnostics;
108

119
namespace Microsoft.PowerShell.Archive
1210
{
13-
internal class TarGzArchive : GzipArchive
11+
internal class TarGzArchive : IArchive
1412
{
1513

1614
// Use a tar archive because .tar.gz file is a compressed tar file
1715
private TarArchive? _tarArchive;
1816

17+
private FileStream? _tarFileStream;
18+
1919
private string? _tarFilePath;
2020

21-
private FileStream? _tarFileStream;
21+
private string _path;
22+
23+
private bool _disposedValue;
24+
25+
private bool _didCallGetNextEntry;
26+
27+
private readonly ArchiveMode _mode;
28+
29+
private readonly FileStream _fileStream;
30+
31+
private readonly CompressionLevel _compressionLevel;
32+
33+
public string Path => _path;
2234

23-
public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel) : base(path, mode, fileStream, compressionLevel)
35+
ArchiveMode IArchive.Mode => _mode;
36+
37+
string IArchive.Path => _path;
38+
39+
public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel)
2440
{
41+
_path = path;
42+
_mode = mode;
43+
_fileStream = fileStream;
44+
_compressionLevel = compressionLevel;
2545
}
2646

27-
public override void AddFileSystemEntry(ArchiveAddition entry)
47+
public void AddFileSystemEntry(ArchiveAddition entry)
2848
{
29-
if (_mode == ArchiveMode.Extract || _mode == ArchiveMode.Update) {
49+
if (_mode == ArchiveMode.Extract || _mode == ArchiveMode.Update)
50+
{
3051
throw new ArgumentException("Adding entries to the archive is not supported in extract or update mode");
3152
}
3253

3354
if (_tarArchive is null)
3455
{
35-
var outputDirectory = Path.GetDirectoryName(_path);
36-
var tarFilename = Path.GetRandomFileName();
37-
_tarFilePath = Path.Combine(outputDirectory, tarFilename);
38-
_tarFileStream = new FileStream(_tarFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
56+
// This will create a temp file and return the path
57+
_tarFilePath = System.IO.Path.GetTempFileName();
58+
// When creating the stream, the file already exists
59+
_tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
3960
_tarArchive = new TarArchive(_tarFilePath, ArchiveMode.Create, _tarFileStream);
4061

4162
}
4263
_tarArchive.AddFileSystemEntry(entry);
4364
}
4465

45-
protected override void Dispose(bool disposing)
66+
public IEntry? GetNextEntry()
67+
{
68+
if (_mode == ArchiveMode.Create)
69+
{
70+
throw new ArgumentException("Getting next entry is not supported when the archive is in Create mode");
71+
}
72+
73+
if (_tarArchive is null)
74+
{
75+
// Create a Gzip archive
76+
using var gzipArchive = new GzipArchive(_path, _mode, _fileStream, _compressionLevel);
77+
// Where to put the tar file when expanding the tar.gz archive
78+
_tarFilePath = System.IO.Path.GetTempFileName();
79+
// Expand the gzip portion
80+
var entry = gzipArchive.GetNextEntry();
81+
Debug.Assert(entry is not null);
82+
entry.ExpandTo(_tarFilePath);
83+
// Create a TarArchive pointing to the newly expanded out tar file from the tar.gz file
84+
FileStream tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
85+
_tarArchive = new TarArchive(_tarFilePath, ArchiveMode.Extract, tarFileStream);
86+
}
87+
return _tarArchive?.GetNextEntry();
88+
}
89+
90+
public void Dispose()
91+
{
92+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
93+
Dispose(disposing: true);
94+
GC.SuppressFinalize(this);
95+
}
96+
97+
// Performs gzip compression on _path
98+
private void CompressArchive() {
99+
Debug.Assert(_tarFilePath is not null);
100+
_tarFileStream = new FileStream(_tarFilePath, FileMode.Open, FileAccess.Read);
101+
using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, true);
102+
_tarFileStream.CopyTo(gzipCompressor);
103+
_tarFileStream.Dispose();
104+
}
105+
106+
private void Dispose(bool disposing)
46107
{
47108
if (!_disposedValue)
48109
{
49110
if (disposing)
50111
{
51-
// TODO: dispose managed state (managed objects)
112+
// Do this before compression because disposing a tar archive will add necessary EOF markers
113+
_tarArchive?.Dispose();
114+
if (_mode == ArchiveMode.Create) {
115+
CompressArchive();
116+
}
52117
_fileStream.Dispose();
53-
CompressArchive();
118+
if (_tarFilePath is not null) {
119+
// Delete the tar file created in the process of created the tar.gz file
120+
File.Delete(_tarFilePath);
121+
}
54122
}
55123

56124
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
57125
// TODO: set large fields to null
58126
_disposedValue = true;
59127
}
60128
}
61-
62-
// Performs gzip compression on _path
63-
private void CompressArchive() {
64-
Debug.Assert(_tarFileStream is not null);
65-
_tarFileStream.Position = 0;
66-
using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, true);
67-
_tarFileStream.CopyTo(gzipCompressor);
68-
}
69129
}
70130
}

src/Formats/ZipArchive.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,6 @@ public void Dispose()
151151
GC.SuppressFinalize(this);
152152
}
153153

154-
bool IArchive.HasTopLevelDirectory()
155-
{
156-
int topLevelDirectoriesCount = 0;
157-
foreach (var entry in _zipArchive.Entries)
158-
{
159-
if (entry.FullName.EndsWith(ZipArchiveDirectoryPathTerminator) &&
160-
entry.FullName.LastIndexOf(ZipArchiveDirectoryPathTerminator, entry.FullName.Length - 2) == -1)
161-
{
162-
topLevelDirectoriesCount++;
163-
if (topLevelDirectoriesCount > 1)
164-
{
165-
break;
166-
}
167-
} else
168-
{
169-
return false;
170-
}
171-
}
172-
173-
return topLevelDirectoriesCount == 1;
174-
}
175-
176154
internal class ZipArchiveEntry : IEntry
177155
{
178156
// Underlying object is System.IO.Compression.ZipArchiveEntry

src/IArchive.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,5 @@ interface IArchive: IDisposable
2121
public void AddFileSystemEntry(ArchiveAddition entry);
2222

2323
public IEntry? GetNextEntry();
24-
25-
// Does the archive have only a top-level directory?
26-
public bool HasTopLevelDirectory();
2724
}
2825
}

0 commit comments

Comments
 (0)