Skip to content

Commit 4887d10

Browse files
Merge pull request #30 from SwiftPackageIndex/use-zipfoundation-for-unzip
Use zipfoundation for unzip
2 parents 2882b7c + 91d2f35 commit 4887d10

File tree

10 files changed

+59
-176
lines changed

10 files changed

+59
-176
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,15 @@
251251
"revision" : "67fa55813b9e7b3b9acee9c0ae501def28746d76",
252252
"version" : "2.1.2"
253253
}
254+
},
255+
{
256+
"identity" : "zipfoundation",
257+
"kind" : "remoteSourceControl",
258+
"location" : "https://github.com/weichsel/ZIPFoundation.git",
259+
"state" : {
260+
"revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0",
261+
"version" : "0.9.19"
262+
}
254263
}
255264
],
256265
"version" : 2

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ let package = Package(
4141
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.1"),
4242
.package(url: "https://github.com/soto-project/soto-s3-file-transfer.git", from: "1.2.0"),
4343
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"),
44+
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.19"),
4445
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.0.0")
4546
],
4647
targets: [
@@ -57,6 +58,7 @@ let package = Package(
5758
),
5859
.target(name: "DocUploadBundle", dependencies: [
5960
.product(name: "Zip", package: "Zip"),
61+
.product(name: "ZIPFoundation", package: "zipfoundation"),
6062
.product(name: "Dependencies", package: "swift-dependencies")
6163
]),
6264
.testTarget(name: "DocUploadBundleTests", dependencies: ["DocUploadBundle"], exclude: ["Fixtures"]),

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Instead, use the `dev` environment to validate a new release as follows:
6060
```
6161
Use the doc bundle included in this repo or download a `dev-` doc bundle from `spi-docs-inbox` on S3.
6262
63-
- Check the [`DocUploaderLambda-Test` CloudWatch log group](https://us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logsV2:log-groups/log-group/$252Faws$252Flambda$252FDocUploaderLambda-Test-UploadFunction-3D3w0QTh1l6H) to confirm the new version has been triggered and processed the file without errors.
63+
- Check the [`DocUploaderLambda-Test` CloudWatch log group](https://us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logsV2:log-groups/log-group/$252Faws$252Flambda$252FDocUploaderLambda-Test-UploadFunction-A0zLSxC2sCV5) to confirm the new version has been triggered and processed the file without errors.
6464
6565
```bash
6666
open 'https://us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logsV2:log-groups/log-group/$252Faws$252Flambda$252FDocUploaderLambda-Test-UploadFunction-3D3w0QTh1l6H'

Sources/DocUploadBundle/DocUploadBundle.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,12 @@ public struct DocUploadBundle {
105105
)
106106
}
107107

108-
public func zip(to workDir: String, method: Zipper.Method = .library) throws -> String {
108+
public func zip(to workDir: String) throws -> String {
109109
let archiveURL = URL(fileURLWithPath: "\(workDir)/\(archiveName)")
110110
let metadataURL = URL(fileURLWithPath: "\(workDir)/metadata.json")
111111
try JSONEncoder().encode(metadata).write(to: metadataURL)
112112

113-
try Zipper.zip(paths: [metadataURL, URL(fileURLWithPath: sourcePath)], to: archiveURL, method: method)
113+
try Zipper.zip(paths: [metadataURL, URL(fileURLWithPath: sourcePath)], to: archiveURL)
114114

115115
return archiveURL.path
116116
}

Sources/DocUploadBundle/Zipper.swift

Lines changed: 9 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,76 +15,19 @@
1515
import Foundation
1616

1717
import Zip
18+
import ZIPFoundation
1819

1920

20-
public enum Zipper {
21-
public static func zip(paths inputPaths: [URL], to outputPath: URL, method: Method = .library) throws {
22-
switch method {
23-
case .library:
24-
do {
25-
try Zip.zipFiles(paths: inputPaths, zipFilePath: outputPath, password: nil, progress: nil)
26-
} catch let error as ZipError {
27-
switch error {
28-
case .fileNotFound: throw Error.fileNotFound
29-
case .unzipFail: throw Error.unzipFail
30-
case .zipFail: throw Error.zipFail
31-
}
32-
}
33-
catch {
34-
throw Error.generic(reason: "\(error)")
35-
}
36-
37-
case .zipTool:
38-
do {
39-
try withTempDir { tempDir in
40-
let tempURL = URL(fileURLWithPath: tempDir)
41-
// Copy inputs to tempDir
42-
for source in inputPaths {
43-
let target = tempURL.appendingPathComponent(source.lastPathComponent)
44-
try FileManager.default.copyItem(at: source, to: target)
45-
}
46-
47-
// Run zip
48-
let process = Process()
49-
process.executableURL = zip
50-
process.arguments = ["-q", "-r", outputPath.path] + inputPaths.map(\.lastPathComponent)
51-
process.currentDirectoryURL = tempURL
52-
try process.run()
53-
process.waitUntilExit()
54-
}
55-
} catch {
56-
throw Error.generic(reason: "\(error)")
57-
}
58-
}
59-
}
60-
61-
public static func unzip(from inputPath: URL, to outputPath: URL, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws {
62-
do {
63-
try Zip.unzipFile(inputPath, destination: outputPath, overwrite: true, password: nil, fileOutputHandler: fileOutputHandler)
64-
} catch let error as ZipError {
65-
switch error {
66-
case .fileNotFound: throw Error.fileNotFound
67-
case .unzipFail: throw Error.unzipFail
68-
case .zipFail: throw Error.zipFail
69-
}
70-
}
71-
catch {
72-
throw Error.generic(reason: "\(error)")
73-
}
74-
}
75-
76-
static let zip = URL(fileURLWithPath: "/usr/bin/zip")
77-
78-
public enum Method {
79-
case library
80-
case zipTool
21+
enum Zipper {
22+
static func zip(paths inputPaths: [URL], to outputPath: URL) throws {
23+
try Zip.zipFiles(paths: inputPaths, zipFilePath: outputPath, password: nil, progress: nil)
8124
}
8225

83-
public enum Error: Swift.Error {
84-
case generic(reason: String)
85-
case fileNotFound
86-
case unzipFail
87-
case zipFail
26+
static func unzip(from inputPath: URL, to outputPath: URL, fileOutputHandler: ((_ unzippedFile: URL) -> Void)? = nil) throws {
27+
// Use ZipFoundation to unzip because of an archive that can't be round-tripped with marmelroy/Zip
28+
// https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/3137
29+
try FileManager.default.createDirectory(at: outputPath, withIntermediateDirectories: true)
30+
try FileManager.default.unzipItem(at: inputPath, to: outputPath)
8831
}
8932
}
9033

Sources/DocUploader/DocUploader.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ public struct DocUploader: LambdaHandler {
8383
logger.info("record: \(record)")
8484

8585
try await run {
86-
let outputPath = "/tmp"
86+
// When Lambdas are run in quick succession they seem to be seeing the same /tmp file system and unzipping can fail with
87+
// lambda handler returned an error: Error Domain=NSCocoaErrorDomain Code=516 "A file with the same name already exists."
88+
let outputPath = "/tmp/\(UUID())"
89+
try Foundation.FileManager.default.createDirectory(at: URL(fileURLWithPath: outputPath), withIntermediateDirectories: true)
8790

8891
do {
8992
logger.info("Copying \(s3Key.url) to \(outputPath)...")

Tests/DocUploadBundleTests/DocUploadBundleTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,34 @@ final class DocUploadBundleTests: XCTestCase {
100100
.init(bucket: "spi-prod-docs", path: "owner/name/feature-2.0.0"))
101101
}
102102

103+
func test_issue_3069() async throws {
104+
// https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/3069
105+
try await withTempDir { tempDir in
106+
let url = fixtureUrl(for: "prod-apple-swift-metrics-main-e6a00d36.zip")
107+
XCTAssertNoThrow(
108+
try DocUploadBundle.unzip(bundle: url.path, outputPath: tempDir)
109+
)
110+
for pathComponent in ["metadata.json",
111+
"main/index.html",
112+
"main/index/index.json"] {
113+
let path = tempDir + "/" + pathComponent
114+
XCTAssertTrue(FileManager.default.fileExists(atPath: path), "does not exist: \(path)")
115+
}
116+
// test roundtrip, to ensure the zip library can zip/unzip its own product
117+
// zip
118+
let urls = [tempDir + "/metadata.json",
119+
tempDir + "/main"].map(URL.init(fileURLWithPath:))
120+
let zipped = URL(fileURLWithPath: tempDir + "/out.zip")
121+
try Zipper.zip(paths: urls, to: zipped)
122+
XCTAssertTrue(FileManager.default.fileExists(atPath: zipped.path))
123+
// unzip
124+
let out = URL(fileURLWithPath: tempDir + "/out")
125+
try Zipper.unzip(from: zipped, to: out)
126+
XCTAssertTrue(FileManager.default.fileExists(atPath: out.path))
127+
XCTAssertTrue(FileManager.default.fileExists(atPath: out.appendingPathComponent("metadata.json").path))
128+
XCTAssertTrue(FileManager.default.fileExists(atPath: out.appendingPathComponent("main/index.html").path))
129+
XCTAssertTrue(FileManager.default.fileExists(atPath: out.appendingPathComponent("main/index/index.json").path))
130+
}
131+
}
132+
103133
}
Binary file not shown.

Sources/DocUploadBundle/TempDir.swift renamed to Tests/DocUploadBundleTests/TempDir.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,3 @@ func withTempDir<T>(body: (String) async throws -> T) async throws -> T {
3232
let tmp = try TempDir()
3333
return try await body(tmp.path)
3434
}
35-
36-
func withTempDir<T>(body: (String) throws -> T) throws -> T {
37-
let tmp = try TempDir()
38-
return try body(tmp.path)
39-
}

Tests/DocUploadBundleTests/ZipTests.swift

Lines changed: 2 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class ZipTests: XCTestCase {
2121

2222
func test_unzip() async throws {
2323
// Test basic unzip behaviour we expect from the library we use
24-
try withTempDir { tempDir in
24+
try await withTempDir { tempDir in
2525
let tempURL = URL(fileURLWithPath: tempDir)
2626
let zipFile = fixtureUrl(for: "out.zip")
2727
let outDir = tempURL.appendingPathComponent("out")
@@ -41,7 +41,7 @@ final class ZipTests: XCTestCase {
4141

4242
func test_zip_roundtrip() async throws {
4343
// Test basic zip roundtrip
44-
try withTempDir { tempDir in
44+
try await withTempDir { tempDir in
4545
// temp
4646
let tempURL = URL(fileURLWithPath: tempDir)
4747

@@ -57,14 +57,6 @@ final class ZipTests: XCTestCase {
5757
let fileB = subdir.appendingPathComponent("b.txt")
5858
try "b".write(to: fileB, atomically: true, encoding: .utf8)
5959

60-
// temp/subdir/subsubdir
61-
let subsubdir = subdir.appendingPathComponent("subsubdir")
62-
try FileManager.default.createDirectory(at: subsubdir, withIntermediateDirectories: false)
63-
64-
// temp/subdir/subdir/c.txt
65-
let fileC = subsubdir.appendingPathComponent("c.txt")
66-
try "c".write(to: fileC, atomically: true, encoding: .utf8)
67-
6860
let zipFile = tempURL.appendingPathComponent("out.zip")
6961
try Zipper.zip(paths: [fileA, subdir], to: zipFile)
7062
XCTAssert(FileManager.default.fileExists(atPath: zipFile.path))
@@ -77,101 +69,10 @@ final class ZipTests: XCTestCase {
7769
// roundtrip/subdir/b.txt
7870
let fileA = roundtrip.appendingPathComponent("a.txt")
7971
let fileB = roundtrip.appendingPathComponent("subdir").appendingPathComponent("b.txt")
80-
let fileC = roundtrip.appendingPathComponent("subdir").appendingPathComponent("subsubdir").appendingPathComponent("c.txt")
8172
XCTAssert(FileManager.default.fileExists(atPath: fileA.path))
8273
XCTAssert(FileManager.default.fileExists(atPath: fileB.path))
83-
XCTAssert(FileManager.default.fileExists(atPath: fileC.path))
8474
XCTAssertEqual(try String(contentsOf: fileA), "a")
8575
XCTAssertEqual(try String(contentsOf: fileB), "b")
86-
XCTAssertEqual(try String(contentsOf: fileC), "c")
87-
}
88-
}
89-
}
90-
91-
func test_zip_roundtrip_shellTool() async throws {
92-
try XCTSkipIf(!FileManager.default.fileExists(atPath: Zipper.zip.path))
93-
94-
// Test basic zip roundtrip with the shellTool method
95-
try withTempDir { tempDir in
96-
// temp
97-
let tempURL = URL(fileURLWithPath: tempDir)
98-
99-
// temp/a.txt
100-
let fileA = tempURL.appendingPathComponent("a.txt")
101-
try "a".write(to: fileA, atomically: true, encoding: .utf8)
102-
103-
// temp/subdir/
104-
let subdir = tempURL.appendingPathComponent("subdir")
105-
try FileManager.default.createDirectory(at: subdir, withIntermediateDirectories: false)
106-
107-
// temp/subdir/b.txt
108-
let fileB = subdir.appendingPathComponent("b.txt")
109-
try "b".write(to: fileB, atomically: true, encoding: .utf8)
110-
111-
// temp/subdir/subsubdir
112-
let subsubdir = subdir.appendingPathComponent("subsubdir")
113-
try FileManager.default.createDirectory(at: subsubdir, withIntermediateDirectories: false)
114-
115-
// temp/subdir/subdir/c.txt
116-
let fileC = subsubdir.appendingPathComponent("c.txt")
117-
try "c".write(to: fileC, atomically: true, encoding: .utf8)
118-
119-
let zipFile = tempURL.appendingPathComponent("out.zip")
120-
try Zipper.zip(paths: [fileA, subdir], to: zipFile, method: .zipTool)
121-
XCTAssert(FileManager.default.fileExists(atPath: zipFile.path))
122-
123-
do { // unzip what we zipped and check results
124-
let roundtrip = tempURL.appendingPathComponent("roundtrip")
125-
try Zipper.unzip(from: zipFile, to: roundtrip)
126-
XCTAssert(FileManager.default.fileExists(atPath: roundtrip.path))
127-
// roundtrip/a.txt
128-
// roundtrip/subdir/b.txt
129-
let fileA = roundtrip.appendingPathComponent("a.txt")
130-
let fileB = roundtrip.appendingPathComponent("subdir").appendingPathComponent("b.txt")
131-
let fileC = roundtrip.appendingPathComponent("subdir").appendingPathComponent("subsubdir").appendingPathComponent("c.txt")
132-
XCTAssert(FileManager.default.fileExists(atPath: fileA.path))
133-
XCTAssert(FileManager.default.fileExists(atPath: fileB.path))
134-
XCTAssert(FileManager.default.fileExists(atPath: fileC.path))
135-
XCTAssertEqual(try String(contentsOf: fileA), "a")
136-
XCTAssertEqual(try String(contentsOf: fileB), "b")
137-
XCTAssertEqual(try String(contentsOf: fileC), "c")
138-
}
139-
}
140-
}
141-
142-
func test_zip_roundtrip_shellTool_relative_paths() async throws {
143-
try XCTSkipIf(!FileManager.default.fileExists(atPath: Zipper.zip.path))
144-
145-
// Test basic zip roundtrip with the shellTool method and relative paths
146-
try withTempDir { tempDir in
147-
// DocBundle components
148-
// metadataURL: tempDir/metadata.json
149-
// sourceURL: tempDir/.docs/owner/repo/ref
150-
// should be zipped as
151-
// - metadata.json
152-
// - ref
153-
// at the top level as relative paths.
154-
let tempURL = URL(fileURLWithPath: tempDir)
155-
let metadataURL = tempURL.appendingPathComponent("metadata.json")
156-
try "metadata".write(to: metadataURL, atomically: true, encoding: .utf8)
157-
let sourceURL = tempURL.appendingPathComponent("docs/owner/repo/ref")
158-
try FileManager.default.createDirectory(at: sourceURL, withIntermediateDirectories: true)
159-
let indexHTML = sourceURL.appendingPathComponent("index.html")
160-
try "index".write(to: indexHTML, atomically: true, encoding: .utf8)
161-
162-
// MUT
163-
let zipFile = tempURL.appendingPathComponent("out.zip")
164-
try Zipper.zip(paths: [metadataURL, sourceURL], to: zipFile, method: .zipTool)
165-
166-
do { // validate
167-
let unzipDir = tempURL.appendingPathComponent("unzip")
168-
try Zipper.unzip(from: zipFile, to: unzipDir)
169-
let metadataURL = unzipDir.appendingPathComponent("metadata.json")
170-
let indexHTML = unzipDir.appendingPathComponent("ref/index.html")
171-
XCTAssert(FileManager.default.fileExists(atPath: metadataURL.path))
172-
XCTAssert(FileManager.default.fileExists(atPath: indexHTML.path))
173-
XCTAssertEqual(try String(contentsOf: metadataURL), "metadata")
174-
XCTAssertEqual(try String(contentsOf: indexHTML), "index")
17576
}
17677
}
17778
}

0 commit comments

Comments
 (0)