Skip to content

Commit 2c97b73

Browse files
committed
Refactor ArchiveWriter to expose archiving each path independently
This PR refactors `ArchiveWriter` to add an API `archive(_ paths: base:)`. This API is used to archive the contents at each URL independently, similar to doing `tar -cvf archive.tar foo.bin /bar/baz.txt`.
1 parent df125a2 commit 2c97b73

2 files changed

Lines changed: 346 additions & 74 deletions

File tree

Sources/ContainerizationArchive/ArchiveWriter.swift

Lines changed: 111 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,94 @@ extension ArchiveWriter {
179179
}
180180

181181
extension ArchiveWriter {
182+
private func archive(_ relativePath: FilePath, dirPath: FilePath) throws {
183+
let fm = FileManager.default
184+
185+
let fullPath = dirPath.appending(relativePath.string)
186+
187+
var statInfo = stat()
188+
guard lstat(fullPath.string, &statInfo) == 0 else {
189+
let errNo = errno
190+
let err = POSIXErrorCode(rawValue: errNo) ?? .EINVAL
191+
throw ArchiveError.failedToCreateArchive("lstat failed for '\(fullPath)': \(POSIXError(err))")
192+
}
193+
194+
let mode = statInfo.st_mode
195+
let uid = statInfo.st_uid
196+
let gid = statInfo.st_gid
197+
var size: Int64 = 0
198+
let type: URLFileResourceType
199+
200+
if (mode & S_IFMT) == S_IFREG {
201+
type = .regular
202+
size = Int64(statInfo.st_size)
203+
} else if (mode & S_IFMT) == S_IFDIR {
204+
type = .directory
205+
} else if (mode & S_IFMT) == S_IFLNK {
206+
type = .symbolicLink
207+
} else {
208+
return
209+
}
210+
211+
#if os(macOS)
212+
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctimespec.tv_sec))
213+
let access = Date(timeIntervalSince1970: Double(statInfo.st_atimespec.tv_sec))
214+
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtimespec.tv_sec))
215+
#else
216+
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctim.tv_sec))
217+
let access = Date(timeIntervalSince1970: Double(statInfo.st_atim.tv_sec))
218+
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtim.tv_sec))
219+
#endif
220+
221+
let entry = WriteEntry()
222+
if type == .symbolicLink {
223+
let targetPath = try fm.destinationOfSymbolicLink(atPath: fullPath.string)
224+
// Resolve the target relative to the symlink's parent, not the archive root.
225+
let symlinkParent = fullPath.removingLastComponent()
226+
let resolvedFull = symlinkParent.appending(targetPath).lexicallyNormalized()
227+
guard resolvedFull.starts(with: dirPath) else {
228+
return
229+
}
230+
entry.symlinkTarget = targetPath
231+
}
232+
233+
entry.path = relativePath.string
234+
entry.size = size
235+
entry.creationDate = created
236+
entry.modificationDate = modified
237+
entry.contentAccessDate = access
238+
entry.fileType = type
239+
entry.group = gid
240+
entry.owner = uid
241+
entry.permissions = mode
242+
if type == .regular {
243+
let buf = UnsafeMutableRawBufferPointer.allocate(byteCount: Self.chunkSize, alignment: 1)
244+
guard let baseAddress = buf.baseAddress else {
245+
throw ArchiveError.failedToCreateArchive("cannot create temporary buffer of size \(Self.chunkSize)")
246+
}
247+
defer { buf.deallocate() }
248+
let fd = Foundation.open(fullPath.string, O_RDONLY)
249+
guard fd >= 0 else {
250+
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
251+
throw ArchiveError.failedToCreateArchive("cannot open file \(fullPath.string) for reading: \(err)")
252+
}
253+
defer { close(fd) }
254+
try self.writeHeader(entry: entry)
255+
while true {
256+
let n = read(fd, baseAddress, Self.chunkSize)
257+
if n == 0 { break }
258+
if n < 0 {
259+
let err = POSIXErrorCode(rawValue: errno) ?? .EIO
260+
throw ArchiveError.failedToCreateArchive("failed to read from file \(fullPath.string): \(err)")
261+
}
262+
try self.writeData(data: UnsafeRawBufferPointer(start: baseAddress, count: n))
263+
}
264+
try self.finishEntry()
265+
} else {
266+
try self.writeEntry(entry: entry, data: nil)
267+
}
268+
}
269+
182270
/// Recursively archives the content of a directory. Regular files, symlinks and directories are added into the archive.
183271
/// Note: Symlinks are added to the archive if both the source and target for the symlink are both contained in the top level directory.
184272
public func archiveDirectory(_ dir: URL) throws {
@@ -214,88 +302,37 @@ extension ArchiveWriter {
214302
try self.writeHeader(entry: rootEntry)
215303

216304
for case let relativePath as String in enumerator {
217-
let fullPath = dirPath.appending(relativePath)
305+
try archive(FilePath(relativePath), dirPath: dirPath)
306+
}
307+
}
218308

219-
var statInfo = stat()
220-
guard lstat(fullPath.string, &statInfo) == 0 else {
221-
let errNo = errno
222-
let err = POSIXErrorCode(rawValue: errNo) ?? .EINVAL
223-
throw ArchiveError.failedToCreateArchive("lstat failed for '\(fullPath)': \(POSIXError(err))")
224-
}
309+
public func archive(_ paths: [FilePath], base: FilePath) throws {
310+
let fm = FileManager.default
311+
let base = base.lexicallyNormalized()
225312

226-
let mode = statInfo.st_mode
227-
let uid = statInfo.st_uid
228-
let gid = statInfo.st_gid
229-
var size: Int64 = 0
230-
let type: URLFileResourceType
231-
232-
if (mode & S_IFMT) == S_IFREG {
233-
type = .regular
234-
size = Int64(statInfo.st_size)
235-
} else if (mode & S_IFMT) == S_IFDIR {
236-
type = .directory
237-
} else if (mode & S_IFMT) == S_IFLNK {
238-
type = .symbolicLink
239-
} else {
240-
continue
313+
for path in paths {
314+
guard path.starts(with: base) else {
315+
throw ArchiveError.failedToCreateArchive("'\(path.string)' is not under '\(base.string)'")
241316
}
242317

243-
#if os(macOS)
244-
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctimespec.tv_sec))
245-
let access = Date(timeIntervalSince1970: Double(statInfo.st_atimespec.tv_sec))
246-
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtimespec.tv_sec))
247-
#else
248-
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctim.tv_sec))
249-
let access = Date(timeIntervalSince1970: Double(statInfo.st_atim.tv_sec))
250-
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtim.tv_sec))
251-
#endif
252-
253-
let entry = WriteEntry()
254-
if type == .symbolicLink {
255-
let targetPath = try fm.destinationOfSymbolicLink(atPath: fullPath.string)
256-
// Resolve the target relative to the symlink's parent, not the archive root.
257-
let symlinkParent = fullPath.removingLastComponent()
258-
let resolvedFull = symlinkParent.appending(targetPath).lexicallyNormalized()
259-
guard resolvedFull.starts(with: dirPath) else {
260-
continue
261-
}
262-
entry.symlinkTarget = targetPath
263-
}
318+
let relativePath = path.components.dropFirst(base.components.count)
319+
.reduce(into: FilePath("")) { $0.append($1) }
264320

265-
entry.path = relativePath
266-
entry.size = size
267-
entry.creationDate = created
268-
entry.modificationDate = modified
269-
entry.contentAccessDate = access
270-
entry.fileType = type
271-
entry.group = gid
272-
entry.owner = uid
273-
entry.permissions = mode
274-
if type == .regular {
275-
let buf = UnsafeMutableRawBufferPointer.allocate(byteCount: Self.chunkSize, alignment: 1)
276-
guard let baseAddress = buf.baseAddress else {
277-
throw ArchiveError.failedToCreateArchive("cannot create temporary buffer of size \(Self.chunkSize)")
278-
}
279-
defer { buf.deallocate() }
280-
let fd = Foundation.open(fullPath.string, O_RDONLY)
281-
guard fd >= 0 else {
282-
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
283-
throw ArchiveError.failedToCreateArchive("cannot open file \(fullPath.string) for reading: \(err)")
321+
var isDir: ObjCBool = false
322+
_ = fm.fileExists(atPath: path.string, isDirectory: &isDir)
323+
if isDir.boolValue {
324+
guard let enumerator = fm.enumerator(atPath: path.string) else {
325+
throw POSIXError(.ENOTDIR)
284326
}
285-
defer { close(fd) }
286-
try self.writeHeader(entry: entry)
287-
while true {
288-
let n = read(fd, baseAddress, Self.chunkSize)
289-
if n == 0 { break }
290-
if n < 0 {
291-
let err = POSIXErrorCode(rawValue: errno) ?? .EIO
292-
throw ArchiveError.failedToCreateArchive("failed to read from file \(fullPath.string): \(err)")
293-
}
294-
try self.writeData(data: UnsafeRawBufferPointer(start: baseAddress, count: n))
327+
328+
try archive(relativePath, dirPath: base)
329+
for case let child as String in enumerator {
330+
let childPath = relativePath.appending(child)
331+
332+
try archive(childPath, dirPath: base)
295333
}
296-
try self.finishEntry()
297334
} else {
298-
try self.writeEntry(entry: entry, data: nil)
335+
try archive(relativePath, dirPath: base)
299336
}
300337
}
301338
}

0 commit comments

Comments
 (0)