Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Interactions between On-Disk Files and In-Memory Archives #160

Open
wants to merge 38 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
290c512
add method for unzipping in-memory archive to disk
WowbaggersLiquidLunch Mar 21, 2020
2b9b0d7
add tests for the new method that unzips in-memory archive to disk un…
WowbaggersLiquidLunch Mar 21, 2020
1211d20
refactor unzipItem(at:to:skipCRC32:progress:preferredEncoding:) to ca…
WowbaggersLiquidLunch Mar 21, 2020
57e4ace
add missing check of destinationURL for unzipItem(at:to:skipCRC32:pro…
WowbaggersLiquidLunch Mar 21, 2020
3895b23
add test testUnzipArchiveErrorConditions(); it fails at archive(for:m…
WowbaggersLiquidLunch Mar 21, 2020
355fcd5
fix documentation comment for unzip(_:destinationURL:skipCRC32:progre…
WowbaggersLiquidLunch Mar 21, 2020
96f30bf
add a method for zipping a file on disk to an archive that's not nece…
WowbaggersLiquidLunch Mar 21, 2020
4ca1827
add tests for the method that zips an item on disk to an archive that…
WowbaggersLiquidLunch Mar 21, 2020
b51417f
refactor zipItem(at sourceURL: URL, to destinationURL: URL) to call z…
WowbaggersLiquidLunch Mar 21, 2020
6e1f57d
catch up with upstream
WowbaggersLiquidLunch Mar 21, 2020
a2c866d
fix check for destinationURL
WowbaggersLiquidLunch Mar 21, 2020
792bbe5
add redundant check for sourceURL; otherwise test fails
WowbaggersLiquidLunch Mar 21, 2020
64caeba
let it pass if the destination is unwritable
WowbaggersLiquidLunch Mar 21, 2020
56a3e08
add test for in-memory archives if Swift >= 5.0
WowbaggersLiquidLunch Mar 21, 2020
8139ba6
add a method that zips an item and returns an archive
WowbaggersLiquidLunch Mar 21, 2020
9709030
add documentation comment for itemZipped(from:shouldKeepParent:compre…
WowbaggersLiquidLunch Mar 21, 2020
a945a2c
add 2 test for itemZipped(from:shouldKeepParent:compressionMethod:pro…
WowbaggersLiquidLunch Mar 21, 2020
6ea94c4
add test resources
WowbaggersLiquidLunch Mar 21, 2020
43d5e50
styling fix
WowbaggersLiquidLunch Jun 16, 2020
577f785
enable parallel and randomly ordered testing
WowbaggersLiquidLunch Jun 16, 2020
1e80409
create test plan from existing scheme
WowbaggersLiquidLunch Jun 16, 2020
8aa94ad
enable sanitisers and random test ordering
WowbaggersLiquidLunch Jun 16, 2020
318b14c
reorder test plan relative to other test files
WowbaggersLiquidLunch Jun 16, 2020
f216f18
more styling changes
WowbaggersLiquidLunch Jun 16, 2020
2b15e78
expand and update test platforms
WowbaggersLiquidLunch Jun 16, 2020
f96932b
move matrix strategy declaration inside each Linux job
WowbaggersLiquidLunch Jun 16, 2020
cdaf215
rename job Swift_latest to Linux_Swift_latest
WowbaggersLiquidLunch Jun 16, 2020
69f2e27
use guard on archive unwrapping in itemAipped(from:shouldKeepParent:c…
WowbaggersLiquidLunch Jun 16, 2020
25f6033
Merge branch 'development' of https://github.com/WowbaggersLiquidLunc…
WowbaggersLiquidLunch Jun 16, 2020
a9f6e6b
use guard for unwrapping newly created empty in-memory archives for t…
WowbaggersLiquidLunch Jun 16, 2020
84c12f2
force-unwrap itemZipped(from:)'s return value in do-catch
WowbaggersLiquidLunch Jun 16, 2020
3e677e3
minor styling fix
WowbaggersLiquidLunch Jun 16, 2020
c633d75
Revert "Merge branch 'development' of https://github.com/WowbaggersLi…
WowbaggersLiquidLunch Jun 16, 2020
1c124f7
Revert "reorder test plan relative to other test files"
WowbaggersLiquidLunch Jun 16, 2020
2b39fdd
Revert "enable sanitisers and random test ordering"
WowbaggersLiquidLunch Jun 16, 2020
b5fe190
Revert "create test plan from existing scheme"
WowbaggersLiquidLunch Jun 16, 2020
ea955a4
Revert "enable parallel and randomly ordered testing"
WowbaggersLiquidLunch Jun 16, 2020
f471786
add 'return' after 'XCFail'
WowbaggersLiquidLunch Jun 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 117 additions & 13 deletions Sources/ZIPFoundation/FileManager+ZIP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ extension FileManager {
/// By default, `zipItem` will create uncompressed archives.
/// - progress: A progress object that can be used to track or cancel the zip operation.
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
public func zipItem(at sourceURL: URL, to destinationURL: URL,
shouldKeepParent: Bool = true, compressionMethod: CompressionMethod = .none,
progress: Progress? = nil) throws {
public func zipItem(
at sourceURL: URL,
to destinationURL: URL,
shouldKeepParent: Bool = true,
compressionMethod: CompressionMethod = .none,
progress: Progress? = nil
) throws {
let fileManager = FileManager()
// FIXME: Somehow testZipItemErrorConditions() fails, if the method doesn't check for source's existance here.
guard fileManager.itemExists(at: sourceURL) else {
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
}
Expand All @@ -42,6 +47,42 @@ extension FileManager {
guard let archive = Archive(url: destinationURL, accessMode: .create) else {
throw Archive.ArchiveError.unwritableArchive
}
try zipItem(
at: sourceURL,
to: archive,
shouldKeepParent: shouldKeepParent,
compressionMethod: compressionMethod,
progress: progress
)
}

/// Zips the file or direcory contents at the specified source URL to the given archive.
///
/// If the item at the source URL is a directory, the directory itself will be
/// represented within the ZIP `Archive`. Calling this method with a directory URL
/// `file:///path/directory/` will create an archive with a `directory/` entry at the root level.
/// You can override this behavior by passing `false` for `shouldKeepParent`. In that case, the contents
/// of the source directory will be placed at the root of the archive.
/// - Parameters:
/// - sourceURL: The file URL pointing to an existing file or directory.
/// - archive: The file URL that identifies the destination of the zip operation.
/// - shouldKeepParent: Indicates that the directory name of a source item should be used as root element
/// within the archive. Default is `true`.
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied.
/// By default, `zipItem` will create uncompressed archives.
/// - progress: A progress object that can be used to track or cancel the zip operation.
/// - Throws: Throws an error if the source item does not exist.
public func zipItem(
at sourceURL: URL,
to archive: Archive,
shouldKeepParent: Bool = true,
compressionMethod: CompressionMethod = .none,
progress: Progress? = nil
) throws {
let fileManager = FileManager()
guard fileManager.itemExists(at: sourceURL) else {
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
}
let isDirectory = try FileManager.typeForItem(at: sourceURL) == .directory
if isDirectory {
let subPaths = try self.subpathsOfDirectory(atPath: sourceURL.path)
Expand All @@ -54,9 +95,9 @@ extension FileManager {
})
progress.totalUnitCount = totalUnitCount
}

// If the caller wants to keep the parent directory, we use the lastPathComponent of the source URL
// as common base for all entries (similar to macOS' Archive Utility.app)
// If the caller wants to keep the parent directory,
// we use the lastPathComponent of the source URL as common base for all entries
// (similar to macOS' Archive Utility.app)
let directoryPrefix = sourceURL.lastPathComponent
for entryPath in subPaths {
let finalEntryPath = shouldKeepParent ? directoryPrefix + "/" + entryPath : entryPath
Expand All @@ -80,6 +121,41 @@ extension FileManager {
}
}

#if swift(>=5)
/// Zips the file or direcory contents at the specified source URL and returns an archive containing them.
///
/// If the item at the source URL is a directory, the directory itself will be represented within the ZIP `Archive`.
/// Calling this method with a directory URL `file:///path/directory/` will create an archive with a `directory/` entry at the root level.
/// You can override this behavior by passing `false` for `shouldKeepParent`.
/// In that case, the contents of the source directory will be placed at the root of the archive.
/// - Parameters:
/// - sourceURL: The file URL pointing to an existing file or directory.
/// - shouldKeepParent: Indicates that the directory name of a source item should be used as root element within the archive.
/// Default is `true`.
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied.
/// By default, `zipItem` will create uncompressed archives.
/// - progress: A progress object that can be used to track or cancel the zip operation.
/// - Returns: If successful at creating an in-memory Archive, an archive containing the zipped file or direcory contents at the specified source URL;
/// if not, `nil`.
/// - Throws: Throws an error if the source item does not exist.
public func itemZipped(
from sourceURL: URL,
shouldKeepParent: Bool = true,
compressionMethod: CompressionMethod = .none,
progress: Progress? = nil
) throws -> Archive? {
guard let archive = Archive(accessMode: .create) else { return nil }
try zipItem(
at: sourceURL,
to: archive,
shouldKeepParent: shouldKeepParent,
compressionMethod: compressionMethod,
progress: progress
)
return archive
}
#endif

/// Unzips the contents at the specified source URL to the destination URL.
///
/// - Parameters:
Expand All @@ -89,15 +165,44 @@ extension FileManager {
/// - progress: A progress object that can be used to track or cancel the unzip operation.
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
public func unzipItem(at sourceURL: URL, to destinationURL: URL, skipCRC32: Bool = false,
progress: Progress? = nil, preferredEncoding: String.Encoding? = nil) throws {
public func unzipItem(
at sourceURL: URL,
to destinationURL: URL,
skipCRC32: Bool = false,
progress: Progress? = nil,
preferredEncoding: String.Encoding? = nil
) throws {
let fileManager = FileManager()
guard fileManager.itemExists(at: sourceURL) else {
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
}
guard let archive = Archive(url: sourceURL, accessMode: .read, preferredEncoding: preferredEncoding) else {
throw Archive.ArchiveError.unreadableArchive
}
try unzip(
archive,
to: destinationURL,
skipCRC32: skipCRC32,
progress: progress,
preferredEncoding: preferredEncoding
)
}

/// Unzips the contents of the specified archive to the destination URL.
///
/// - Parameters:
/// - archive: The archive to be unzipped.
/// - destinationURL: The file URL that identifies the destination directory of the unzip operation.
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
/// - progress: A progress object that can be used to track or cancel the unzip operation.
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
/// - Throws: Throws an error if destination URL is not writable.
public func unzip(_ archive: Archive, to destinationURL: URL, skipCRC32: Bool = false,
progress: Progress? = nil, preferredEncoding: String.Encoding? = nil) throws {
let fileManager = FileManager()
guard fileManager.isWritableFile(atPath: destinationURL.path) else {
throw CocoaError(.fileWriteNoPermission, userInfo: [NSFilePathErrorKey: destinationURL.path])
}
// Defer extraction of symlinks until all files & directories have been created.
// This is necessary because we can't create links to files that haven't been created yet.
let sortedEntries = archive.sorted { (left, right) -> Bool in
Expand All @@ -113,7 +218,6 @@ extension FileManager {
totalUnitCount = sortedEntries.reduce(0, { $0 + archive.totalUnitCountForReading($1) })
progress.totalUnitCount = totalUnitCount
}

for entry in sortedEntries {
let path = preferredEncoding == nil ? entry.path : entry.path(using: preferredEncoding!)
let destinationEntryURL = destinationURL.appendingPathComponent(path)
Expand All @@ -136,10 +240,10 @@ extension FileManager {
func itemExists(at url: URL) -> Bool {
// Use `URL.checkResourceIsReachable()` instead of `FileManager.fileExists()` here
// because we don't want implicit symlink resolution.
// As per documentation, `FileManager.fileExists()` traverses symlinks and therefore a broken symlink
// would throw a `.fileReadNoSuchFile` false positive error.
// For ZIP files it may be intended to archive "broken" symlinks because they might be
// resolvable again when extracting the archive to a different destination.
// As per documentation, `FileManager.fileExists()` traverses symlinks
// and therefore a broken symlink would throw a `.fileReadNoSuchFile` false positive error.
// For ZIP files it may be intended to archive "broken" symlinks
// because they might be resolvable again when extracting the archive to a different destination.
return (try? url.checkResourceIsReachable()) == true
}

Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading