Skip to content

Commit e8512c1

Browse files
authored
SF-3159 Upgrade to SharpZipLib (#2967)
1 parent cf5cabe commit e8512c1

File tree

5 files changed

+75
-32
lines changed

5 files changed

+75
-32
lines changed

src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
</PropertyGroup>
2929
<ItemGroup>
3030
<PackageReference Include="CsvHelper" Version="33.0.1" />
31-
<PackageReference Include="DotNetZip" Version="1.16.0" />
3231
<PackageReference Include="Duende.AccessTokenManagement" Version="3.2.0" />
3332
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
3433
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
@@ -42,6 +41,7 @@
4241
InternetSettings.xml file. Also update server config scriptureforge.org_v2.yml. -->
4342
<PackageReference Include="ParatextData" Version="9.5.0.8" />
4443
<PackageReference Include="Serval.Client" Version="1.9.0" />
44+
<PackageReference Include="SharpZipLib" Version="1.4.2" />
4545
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
4646
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.2" />
4747
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />

src/SIL.XForge.Scripture/Services/ParatextService.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ SyncMetrics syncMetrics
274274
+ $"{paratextId}"
275275
);
276276
}
277-
EnsureProjectReposExists(userSecret, ptProject, source);
277+
await EnsureProjectReposExistsAsync(userSecret, ptProject, source);
278278
if (ptProject is not ParatextResource)
279279
{
280280
StartProgressReporting(progress);
@@ -2480,7 +2480,7 @@ private void InstallStyles()
24802480
/// <summary>
24812481
/// Ensure the target project repository exists on the local SF server, cloning if necessary.
24822482
/// </summary>
2483-
private void EnsureProjectReposExists(
2483+
private async Task EnsureProjectReposExistsAsync(
24842484
UserSecret userSecret,
24852485
ParatextProject target,
24862486
IInternetSharedRepositorySource repositorySource
@@ -2492,7 +2492,7 @@ IInternetSharedRepositorySource repositorySource
24922492
if (target is ParatextResource resource)
24932493
{
24942494
// If the target is a resource, install it
2495-
InstallResource(username, resource, target.ParatextId, targetNeedsCloned);
2495+
await InstallResourceAsync(username, resource, target.ParatextId, targetNeedsCloned);
24962496
}
24972497
else if (targetNeedsCloned)
24982498
{
@@ -2515,7 +2515,7 @@ IInternetSharedRepositorySource repositorySource
25152515
/// <remarks>
25162516
/// <paramref name="targetParatextId" /> is required because the resource may be a source or target.
25172517
/// </remarks>
2518-
private void InstallResource(
2518+
async private Task InstallResourceAsync(
25192519
string username,
25202520
ParatextResource resource,
25212521
string targetParatextId,
@@ -2553,7 +2553,7 @@ bool needsToBeCloned
25532553
{
25542554
string path = LocalProjectDir(targetParatextId);
25552555
_fileSystemService.CreateDirectory(path);
2556-
resource.InstallableResource.ExtractToDirectory(path);
2556+
await resource.InstallableResource.ExtractToDirectoryAsync(path);
25572557
MigrateResourceIfRequired(username, targetParatextId, overrideLanguageId);
25582558
}
25592559
}
@@ -2903,7 +2903,8 @@ CancellationToken token
29032903
_exceptionHandler,
29042904
_dblServerUri
29052905
);
2906-
IReadOnlyDictionary<string, int> resourceRevisions = SFInstallableDblResource.GetInstalledResourceRevisions();
2906+
IReadOnlyDictionary<string, int> resourceRevisions =
2907+
await SFInstallableDblResource.GetInstalledResourceRevisionsAsync();
29072908
return resources
29082909
.OrderBy(r => r.FullName)
29092910
.Select(r =>

src/SIL.XForge.Scripture/Services/SFInstallableDblResource.cs

+37-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
using System.IO;
44
using System.Linq;
55
using System.Net;
6-
using Ionic.Zip;
6+
using System.Threading.Tasks;
7+
using ICSharpCode.SharpZipLib.Zip;
78
using Newtonsoft.Json;
89
using Newtonsoft.Json.Linq;
910
using Paratext.Data;
@@ -376,7 +377,7 @@ public static IEnumerable<SFInstallableDblResource> GetInstallableDblResources(
376377
/// <remarks>
377378
/// After the resource is extracted, it can be a source or target.
378379
/// </remarks>
379-
public void ExtractToDirectory(string path)
380+
async public Task ExtractToDirectoryAsync(string path)
380381
{
381382
// Check parameters
382383
if (string.IsNullOrWhiteSpace(path))
@@ -398,11 +399,34 @@ public void ExtractToDirectory(string path)
398399
this.DBLEntryUid,
399400
ProjectFileManager.resourceFileExtension
400401
);
401-
if (RobustFile.Exists(resourceFile))
402+
if (_fileSystemService.FileExists(resourceFile))
402403
{
403-
using var zipFile = ZipFile.Read(resourceFile);
404-
zipFile.Password = this._passwordProvider?.GetPassword();
405-
zipFile.ExtractAll(path, ExtractExistingFileAction.DoNotOverwrite);
404+
await using Stream stream = _fileSystemService.OpenFile(resourceFile, FileMode.Open);
405+
using ZipFile zipFile = new ZipFile(stream);
406+
zipFile.Password = _passwordProvider?.GetPassword();
407+
await ExtractAllAsync(zipFile, path);
408+
}
409+
}
410+
411+
private async Task ExtractAllAsync(ZipFile zip, string path)
412+
{
413+
foreach (ZipEntry entry in zip)
414+
{
415+
if (!entry.IsFile)
416+
continue; // Skip directories
417+
418+
string entryPath = Path.Combine(path, entry.Name);
419+
420+
if (_fileSystemService.FileExists(entryPath))
421+
continue; // Don't overwrite
422+
423+
// Ensure directories in the ZIP entry are created
424+
_fileSystemService.CreateDirectory(Path.GetDirectoryName(entryPath));
425+
426+
// Extract the file
427+
await using Stream zipStream = zip.GetInputStream(entry);
428+
await using Stream output = _fileSystemService.CreateFile(entryPath);
429+
await zipStream.CopyToAsync(output);
406430
}
407431
}
408432

@@ -475,7 +499,7 @@ public override bool Install()
475499
/// <returns>
476500
/// A dictionary where the resource id is the key, and the revision is the value.
477501
/// </returns>
478-
internal static IReadOnlyDictionary<string, int> GetInstalledResourceRevisions()
502+
internal static async Task<IReadOnlyDictionary<string, int>> GetInstalledResourceRevisionsAsync()
479503
{
480504
// Initialize variables
481505
Dictionary<string, int> resourceRevisions = [];
@@ -510,9 +534,8 @@ internal static IReadOnlyDictionary<string, int> GetInstalledResourceRevisions()
510534
// See if this a zip file, and if it contains the correct ID
511535
try
512536
{
513-
// This only uses DotNetZip because ParatextData uses DotNetZip
514-
// You could use System.IO.Compression if you wanted to
515-
using var zipFile = ZipFile.Read(resourceFile);
537+
await using var stream = new FileStream(resourceFile, FileMode.Open);
538+
using var zipFile = new ZipFile(stream);
516539
// Zip files use forward slashes, even on Windows
517540
const string idSearchPath = DblFolderName + "/id/";
518541
const string revisionSearchPath = DblFolderName + "/revision/";
@@ -524,19 +547,19 @@ internal static IReadOnlyDictionary<string, int> GetInstalledResourceRevisions()
524547
if (
525548
string.IsNullOrWhiteSpace(fileId)
526549
&& !entry.IsDirectory
527-
&& entry.FileName.StartsWith(idSearchPath, StringComparison.OrdinalIgnoreCase)
550+
&& entry.Name.StartsWith(idSearchPath, StringComparison.OrdinalIgnoreCase)
528551
)
529552
{
530-
fileId = entry.FileName.Split('/', StringSplitOptions.RemoveEmptyEntries).Last();
553+
fileId = entry.Name.Split('/', StringSplitOptions.RemoveEmptyEntries).Last();
531554
}
532555
else if (
533556
revision == 0
534557
&& !entry.IsDirectory
535-
&& entry.FileName.StartsWith(revisionSearchPath, StringComparison.OrdinalIgnoreCase)
558+
&& entry.Name.StartsWith(revisionSearchPath, StringComparison.OrdinalIgnoreCase)
536559
)
537560
{
538561
string revisionFilename = entry
539-
.FileName.Split('/', StringSplitOptions.RemoveEmptyEntries)
562+
.Name.Split('/', StringSplitOptions.RemoveEmptyEntries)
540563
.Last();
541564
if (!int.TryParse(revisionFilename, out revision))
542565
{

src/SIL.XForge.Scripture/packages.lock.json

-11
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@
88
"resolved": "33.0.1",
99
"contentHash": "fev4lynklAU2A9GVMLtwarkwaanjSYB4wUqO2nOJX5hnzObORzUqVLe+bDYCUyIIRQM4o5Bsq3CcyJR89iMmEQ=="
1010
},
11-
"DotNetZip": {
12-
"type": "Direct",
13-
"requested": "[1.16.0, )",
14-
"resolved": "1.16.0",
15-
"contentHash": "CS9sjjXF23FLIbwlLT5k/jXBYxVnLMn01lyAvN/hxFM+UDvSicjTPB9ZMA+Wp/QlvBPbD2pzkAXUp1O50gP/Lw==",
16-
"dependencies": {
17-
"System.Security.Permissions": "4.7.0",
18-
"System.Text.Encoding.CodePages": "4.7.1"
19-
}
20-
},
2111
"Duende.AccessTokenManagement": {
2212
"type": "Direct",
2313
"requested": "[3.2.0, )",
@@ -164,7 +154,6 @@
164154
"resolved": "9.5.0.8",
165155
"contentHash": "TFRH8KBM77q5seEI6W8iOENk5XY5KXRIxJnllEVO6PT9D37nDHIqDKvq5Y3ilpsPZjhZj/F/lVciw1dOkZ9gzQ==",
166156
"dependencies": {
167-
"DotNetZip": "1.16.0",
168157
"Icu4c.Win.Min": "59.1.7",
169158
"JetBrains.Annotations": "2021.2.0",
170159
"Microsoft.CSharp": "4.7.0",

test/SIL.XForge.Scripture.Tests/Services/ParatextServiceTests.cs

+30
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using System.Xml;
1212
using System.Xml.Linq;
13+
using ICSharpCode.SharpZipLib.Zip;
1314
using Microsoft.AspNetCore.Hosting;
1415
using Microsoft.Extensions.Logging;
1516
using Microsoft.Extensions.Options;
@@ -4370,6 +4371,10 @@ public async Task SendReceiveAsync_SourceResource_MigratesLdml()
43704371
)
43714372
.Returns(resourceScrText);
43724373
env.MockFileSystemService.FileExists(Arg.Is<string>(p => p.EndsWith(".p8z"))).Returns(true);
4374+
await using var zipStream = await TestEnvironment.CreateZipStubAsync();
4375+
env.MockFileSystemService.OpenFile(Arg.Is<string>(p => p.EndsWith(".p8z")), FileMode.Open).Returns(zipStream);
4376+
await using var stream = new MemoryStream();
4377+
env.MockFileSystemService.CreateFile(Arg.Any<string>()).Returns(stream);
43734378

43744379
// Set up the Resource ScrText when it is installed on disk
43754380
using MockScrText scrText = env.GetScrText(associatedPtUser, resourceId);
@@ -4420,6 +4425,10 @@ public async Task SendReceiveAsync_SourceResource_DblLanguageDifferent()
44204425
)
44214426
.Returns(resourceScrText);
44224427
env.MockFileSystemService.FileExists(Arg.Is<string>(p => p.EndsWith(".p8z"))).Returns(true);
4428+
await using var zipStream = await TestEnvironment.CreateZipStubAsync();
4429+
env.MockFileSystemService.OpenFile(Arg.Is<string>(p => p.EndsWith(".p8z")), FileMode.Open).Returns(zipStream);
4430+
await using var stream = new MemoryStream();
4431+
env.MockFileSystemService.CreateFile(Arg.Any<string>()).Returns(stream);
44234432

44244433
// Set up the Resource ScrText when it is installed on disk
44254434
using MockScrText scrText = env.GetScrText(associatedPtUser, resourceId);
@@ -4483,6 +4492,10 @@ public async Task SendReceiveAsync_SourceResource_DblLanguageMissing()
44834492
bool resourceDownloaded = false;
44844493
env.MockFileSystemService.When(f => f.CreateDirectory(Arg.Any<string>())).Do(_ => resourceDownloaded = true);
44854494
env.MockFileSystemService.FileExists(Arg.Is<string>(p => p.EndsWith(".p8z"))).Returns(_ => resourceDownloaded);
4495+
await using var zipStream = await TestEnvironment.CreateZipStubAsync();
4496+
env.MockFileSystemService.OpenFile(Arg.Is<string>(p => p.EndsWith(".p8z")), FileMode.Open).Returns(zipStream);
4497+
await using var stream = new MemoryStream();
4498+
env.MockFileSystemService.CreateFile(Arg.Any<string>()).Returns(stream);
44864499

44874500
// Set up the Resource ScrText when it is installed on disk
44884501
using MockScrText scrText = env.GetScrText(associatedPtUser, resourceId);
@@ -7295,6 +7308,23 @@ string[] dataIds
72957308
public static async Task<IDocument<NoteThread>> GetNoteThreadDocAsync(IConnection connection, string dataId) =>
72967309
await connection.FetchAsync<NoteThread>("project01:" + dataId);
72977310

7311+
public static async Task<Stream> CreateZipStubAsync()
7312+
{
7313+
var outputMemStream = new MemoryStream();
7314+
await using (var zipStream = new ZipOutputStream(outputMemStream))
7315+
{
7316+
ZipEntry newEntry = new ZipEntry("test.txt");
7317+
await zipStream.PutNextEntryAsync(newEntry);
7318+
await zipStream.CloseEntryAsync(CancellationToken.None);
7319+
7320+
// Stop ZipStream.Dispose() from also closing the underlying stream.
7321+
zipStream.IsStreamOwner = false;
7322+
}
7323+
7324+
outputMemStream.Position = 0;
7325+
return outputMemStream;
7326+
}
7327+
72987328
public string SetupProject(string baseId, ParatextUser associatedPtUser, bool hasEditPermission = true)
72997329
{
73007330
string ptProjectId = PTProjectIds[baseId].Id;

0 commit comments

Comments
 (0)