Skip to content

Commit 73613e4

Browse files
committed
added tests for path structure preservation, added gzip support, worked on tar.gz support
1 parent 95c9bfc commit 73613e4

File tree

5 files changed

+299
-5
lines changed

5 files changed

+299
-5
lines changed

Tests/Assertions/Should-BeZipArchiveOnlyContaining.psm1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ function Should-BeZipArchiveOnlyContaining {
4949

5050
# Get 7-zip to list the contents of the archive
5151
if ($IsWindows) {
52-
$output = 7z.exe l $ActualValue -ba
52+
$output = 7z.exe l $ActualValue -ba -tzip
5353
} else {
54-
$output = 7z l $ActualValue -ba
54+
$output = 7z l $ActualValue -ba -tzip
5555
}
5656

5757
# Check if the output is null

Tests/Compress-Archive.Tests.ps1

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,4 +867,64 @@ BeforeDiscovery {
867867
$destinationPath | Should -Not -Exist
868868
}
869869
}
870+
871+
Context "CompressionLevel tests" {
872+
BeforeAll {
873+
New-Item -Path TestDrive:/file1.txt -ItemType File
874+
"Hello, World!" | Out-File -FilePath TestDrive:/file1.txt
875+
}
876+
877+
It "Throws an error when an invalid value is supplied to CompressionLevel" {
878+
try {
879+
Compress-Archive -Path TestDrive:/file1.txt -DestinationPath TestDrive:/archive1.zip -CompressionLevel fakelevel
880+
} catch {
881+
$_.FullyQualifiedErrorId | Should -Be "InvalidArgument, ${CmdletClassName}"
882+
}
883+
}
884+
}
885+
886+
Context "Path Structure Preservation Tests" {
887+
BeforeAll {
888+
New-Item -Path TestDrive:/file1.txt -ItemType File
889+
"Hello, World!" | Out-File -FilePath TestDrive:/file1.txt
890+
891+
New-Item -Path TestDrive:/directory1 -ItemType Directory
892+
New-Item -Path TestDrive:/directory1/subdir1 -ItemType Directory
893+
New-Item -Path TestDrive:/directory1/subdir1/file.txt -ItemType File
894+
"Hello, World!" | Out-File -FilePath TestDrive:/file.txt
895+
}
896+
897+
It "Creates an archive containing only a file when the path to that file is not relative to the working directory" {
898+
$destinationPath = "TestDrive:/archive1.zip"
899+
900+
Push-Location TestDrive:/directory1
901+
902+
Compress-Archive -Path TestDrive:/file1.txt -DestinationPath $destinationPath
903+
$destinationPath | Should -BeArchiveOnlyContaining @("file1.txt")
904+
905+
Pop-Location
906+
}
907+
908+
It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" {
909+
$destinationPath = "TestDrive:/archive2.zip"
910+
911+
Push-Location TestDrive:/
912+
913+
Compress-Archive -Path directory1/subdir1/file.txt -DestinationPath $destinationPath
914+
$destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt")
915+
916+
Pop-Location
917+
}
918+
919+
It "Creates an archive containing a file and its parent directories when the path to the file and its parent directories are descendents of the working directory" {
920+
$destinationPath = "TestDrive:/archive3.zip"
921+
922+
Push-Location TestDrive:/
923+
924+
Compress-Archive -Path directory1 -DestinationPath $destinationPath
925+
$destinationPath | Should -BeArchiveOnlyContaining @("directory1/subdir1/file.txt")
926+
927+
Pop-Location
928+
}
929+
}
870930
}

src/GzipArchive.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Formats.Tar;
7+
using System.IO;
8+
using System.IO.Compression;
9+
using System.Diagnostics;
10+
11+
namespace Microsoft.PowerShell.Archive
12+
{
13+
internal class GzipArchive : IArchive
14+
{
15+
private bool _disposedValue;
16+
17+
private readonly ArchiveMode _mode;
18+
19+
private readonly string _path;
20+
21+
private readonly FileStream _fileStream;
22+
23+
private readonly CompressionLevel _compressionLevel;
24+
25+
private bool _addedFile;
26+
27+
private bool _didCallGetNextEntry;
28+
29+
ArchiveMode IArchive.Mode => _mode;
30+
31+
string IArchive.Path => _path;
32+
33+
public GzipArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel)
34+
{
35+
_mode = mode;
36+
_path = path;
37+
_fileStream = fileStream;
38+
_compressionLevel = compressionLevel;
39+
}
40+
41+
void IArchive.AddFileSystemEntry(ArchiveAddition entry)
42+
{
43+
if (_mode == ArchiveMode.Extract)
44+
{
45+
throw new ArgumentException("Adding entries to the archive is not supported on Extract mode.");
46+
}
47+
if (_mode == ArchiveMode.Update)
48+
{
49+
throw new ArgumentException("Updating a Gzip file in not supported.");
50+
}
51+
if (_addedFile)
52+
{
53+
throw new ArgumentException("Adding a Gzip file in not supported.");
54+
}
55+
if (entry.FileSystemInfo.Attributes.HasFlag(FileAttributes.Directory)) {
56+
throw new ArgumentException("Compressing directories is not supported");
57+
}
58+
using var gzipCompressor = new GZipStream(_fileStream, _compressionLevel, leaveOpen: true);
59+
using var fileToCopy = new FileStream(entry.FileSystemInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
60+
fileToCopy.CopyTo(gzipCompressor);
61+
_addedFile = true;
62+
}
63+
64+
IEntry? IArchive.GetNextEntry()
65+
{
66+
// Gzip has no concept of entries
67+
if (!_didCallGetNextEntry) {
68+
_didCallGetNextEntry = true;
69+
return new GzipArchiveEntry(this);
70+
}
71+
return null;
72+
}
73+
74+
protected virtual void Dispose(bool disposing)
75+
{
76+
if (!_disposedValue)
77+
{
78+
if (disposing)
79+
{
80+
// TODO: dispose managed state (managed objects)
81+
_fileStream.Dispose();
82+
}
83+
84+
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
85+
// TODO: set large fields to null
86+
_disposedValue = true;
87+
}
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+
bool IArchive.HasTopLevelDirectory()
98+
{
99+
throw new NotSupportedException();
100+
}
101+
102+
internal class GzipArchiveEntry : IEntry {
103+
104+
private GzipArchive _gzipArchive;
105+
106+
// Gzip has no concept of entries, so getting the entry name is not supported
107+
string IEntry.Name => throw new NotSupportedException();
108+
109+
// Gzip does not compress directories, so this is always false
110+
bool IEntry.IsDirectory => false;
111+
112+
public GzipArchiveEntry(GzipArchive gzipArchive)
113+
{
114+
_gzipArchive = gzipArchive;
115+
}
116+
117+
void IEntry.ExpandTo(string destinationPath)
118+
{
119+
using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
120+
using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress);
121+
gzipDecompressor.CopyTo(destinationFileStream);
122+
}
123+
}
124+
}
125+
}

src/TarArchive.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,15 @@ void IEntry.ExpandTo(string destinationPath)
203203
Directory.CreateDirectory(parentDirectory);
204204
}
205205

206+
var lastWriteTime = _entry.ModificationTime.LocalDateTime;
206207
if (_objectAsIEntry.IsDirectory)
207208
{
208-
System.IO.Directory.CreateDirectory(destinationPath);
209-
var lastWriteTime = _entry.ModificationTime;
210-
System.IO.Directory.SetLastWriteTime(destinationPath, lastWriteTime.DateTime);
209+
Directory.CreateDirectory(destinationPath);
210+
Directory.SetLastWriteTime(destinationPath, lastWriteTime);
211211
} else
212212
{
213213
_entry.ExtractToFile(destinationPath, overwrite: false);
214+
File.SetLastWriteTime(destinationPath, lastWriteTime);
214215
}
215216
}
216217

src/TarGzArchive.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Formats.Tar;
7+
using System.IO;
8+
using System.IO.Compression;
9+
using System.Diagnostics;
10+
11+
namespace Microsoft.PowerShell.Archive
12+
{
13+
internal class TarGzArchive : IArchive
14+
{
15+
private bool _disposedValue;
16+
17+
private readonly ArchiveMode _mode;
18+
19+
private readonly string _path;
20+
21+
private readonly FileStream _fileStream;
22+
23+
private readonly CompressionLevel _compressionLevel;
24+
25+
// Use a tar archive because .tar.gz file is a compressed tar file
26+
private TarArchive _tarArchive;
27+
28+
ArchiveMode IArchive.Mode => _mode;
29+
30+
string IArchive.Path => _path;
31+
32+
public TarGzArchive(string path, ArchiveMode mode, FileStream fileStream, CompressionLevel compressionLevel)
33+
{
34+
_mode = mode;
35+
_path = path;
36+
_fileStream = fileStream;
37+
_compressionLevel = compressionLevel;
38+
}
39+
40+
void IArchive.AddFileSystemEntry(ArchiveAddition entry)
41+
{
42+
if (_mode == ArchiveMode.Create) {
43+
44+
}
45+
}
46+
47+
IEntry? IArchive.GetNextEntry()
48+
{
49+
// Gzip has no concept of entries
50+
if (!_didCallGetNextEntry) {
51+
_didCallGetNextEntry = true;
52+
return new TarGzArchiveEntry(this);
53+
}
54+
return null;
55+
}
56+
57+
protected virtual void Dispose(bool disposing)
58+
{
59+
if (!_disposedValue)
60+
{
61+
if (disposing)
62+
{
63+
// TODO: dispose managed state (managed objects)
64+
_fileStream.Dispose();
65+
}
66+
67+
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
68+
// TODO: set large fields to null
69+
_disposedValue = true;
70+
}
71+
}
72+
73+
public void Dispose()
74+
{
75+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
76+
Dispose(disposing: true);
77+
GC.SuppressFinalize(this);
78+
}
79+
80+
bool IArchive.HasTopLevelDirectory()
81+
{
82+
throw new NotSupportedException();
83+
}
84+
85+
internal class TarGzArchiveEntry : IEntry {
86+
87+
private TarGzArchive _gzipArchive;
88+
89+
// Gzip has no concept of entries, so getting the entry name is not supported
90+
string IEntry.Name => throw new NotSupportedException();
91+
92+
// Gzip does not compress directories, so this is always false
93+
bool IEntry.IsDirectory => false;
94+
95+
public TarGzArchiveEntry(TarGzArchive gzipArchive)
96+
{
97+
_gzipArchive = gzipArchive;
98+
}
99+
100+
void IEntry.ExpandTo(string destinationPath)
101+
{
102+
using var destinationFileStream = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
103+
using var gzipDecompressor = new GZipStream(_gzipArchive._fileStream, CompressionMode.Decompress);
104+
gzipDecompressor.CopyTo(destinationFileStream);
105+
}
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)