Skip to content

FileManager: Implement missing Path and URL Enumerator functionality. #1548

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

Merged
merged 3 commits into from
May 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 45 additions & 16 deletions Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -943,9 +943,6 @@ open class FileManager : NSObject {
*/
// Note: Because the error handler is an optional block, the compiler treats it as @escaping by default. If that behavior changes, the @escaping will need to be added back.
open func enumerator(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: DirectoryEnumerationOptions = [], errorHandler handler: (/* @escaping */ (URL, Error) -> Bool)? = nil) -> DirectoryEnumerator? {
if mask.contains(.skipsPackageDescendants) || mask.contains(.skipsHiddenFiles) {
NSUnimplemented("Enumeration options not yet implemented")
}
return NSURLDirectoryEnumerator(url: url, options: mask, errorHandler: handler)
}

Expand Down Expand Up @@ -1307,19 +1304,25 @@ extension FileManager {
internal class NSPathDirectoryEnumerator: DirectoryEnumerator {
let baseURL: URL
let innerEnumerator : DirectoryEnumerator
private var _currentItemPath: String?

override var fileAttributes: [FileAttributeKey : Any]? {
NSUnimplemented()
guard let currentItemPath = _currentItemPath else {
return nil
}
return try? FileManager.default.attributesOfItem(atPath: baseURL.appendingPathComponent(currentItemPath).path)
}

override var directoryAttributes: [FileAttributeKey : Any]? {
NSUnimplemented()
return try? FileManager.default.attributesOfItem(atPath: baseURL.path)
}

override var level: Int {
NSUnimplemented()
return innerEnumerator.level
}

override func skipDescendants() {
NSUnimplemented()
innerEnumerator.skipDescendants()
}

init?(path: String) {
Expand All @@ -1337,9 +1340,9 @@ extension FileManager {
return nil
}
let path = url.path.replacingOccurrences(of: baseURL.path+"/", with: "")
_currentItemPath = path
return path
}

}

internal class NSURLDirectoryEnumerator : DirectoryEnumerator {
Expand Down Expand Up @@ -1378,32 +1381,58 @@ extension FileManager {
}

override func nextObject() -> Any? {

func match(filename: String, to options: DirectoryEnumerationOptions, isDir: Bool) -> (Bool, Bool) {
var showFile = true
var skipDescendants = false

if isDir {
if options.contains(.skipsSubdirectoryDescendants) {
skipDescendants = true
}
// Ignore .skipsPackageDescendants
}
if options.contains(.skipsHiddenFiles) && (filename[filename._startOfLastPathComponent] == ".") {
showFile = false
skipDescendants = true
}

return (showFile, skipDescendants)
}


if let stream = _stream {

if !_gotRoot {
_gotRoot = true

// Skip the root.
_current = fts_read(stream)

}

_current = fts_read(stream)
while let current = _current {
let filename = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject

switch Int32(current.pointee.fts_info) {
case FTS_D:
if _options.contains(.skipsSubdirectoryDescendants) {
let (showFile, skipDescendants) = match(filename: filename, to: _options, isDir: true)
if skipDescendants {
fts_set(_stream, _current, FTS_SKIP)
}
fallthrough
if showFile {
return URL(fileURLWithPath: filename)
}

case FTS_DEFAULT, FTS_F, FTS_NSOK, FTS_SL, FTS_SLNONE:
let str = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject
return URL(fileURLWithPath: str)
let (showFile, _) = match(filename: filename, to: _options, isDir: false)
if showFile {
return URL(fileURLWithPath: filename)
}
case FTS_DNR, FTS_ERR, FTS_NS:
let keepGoing : Bool
let keepGoing: Bool
if let handler = _errorHandler {
let str = NSString(bytes: current.pointee.fts_path, length: Int(strlen(current.pointee.fts_path)), encoding: String.Encoding.utf8.rawValue)!._swiftObject
keepGoing = handler(URL(fileURLWithPath: str), _NSErrorWithErrno(current.pointee.fts_errno, reading: true))
keepGoing = handler(URL(fileURLWithPath: filename), _NSErrorWithErrno(current.pointee.fts_errno, reading: true))
} else {
keepGoing = true
}
Expand Down
186 changes: 119 additions & 67 deletions TestFoundation/TestFileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,93 +340,153 @@ class TestFileManager : XCTestCase {
try fm.createDirectory(atPath: basePath, withIntermediateDirectories: false, attributes: nil)
try fm.createDirectory(atPath: basePath2, withIntermediateDirectories: false, attributes: nil)

let _ = fm.createFile(atPath: itemPath, contents: Data(), attributes: nil)
let _ = fm.createFile(atPath: itemPath2, contents: Data(), attributes: nil)
let _ = fm.createFile(atPath: itemPath, contents: Data(count: 123), attributes: nil)
let _ = fm.createFile(atPath: itemPath2, contents: Data(count: 456), attributes: nil)

} catch _ {
XCTFail()
}


var item1FileAttributes: [FileAttributeKey: Any]!
var item2FileAttributes: [FileAttributeKey: Any]!
if let e = FileManager.default.enumerator(atPath: basePath) {
let attrs = e.directoryAttributes
XCTAssertNotNil(attrs)
XCTAssertEqual(attrs?[.type] as? FileAttributeType, .typeDirectory)

var foundItems = Set<String>()
while let item = e.nextObject() as? String {
foundItems.insert(item)
if item == "item" {
item1FileAttributes = e.fileAttributes
} else if item == "path2/item" {
item2FileAttributes = e.fileAttributes
}
}
XCTAssertEqual(foundItems, Set(["item", "path2", "path2/item"]))
} else {
XCTFail()
}

XCTAssertNotNil(item1FileAttributes)
if let size = item1FileAttributes[.size] as? NSNumber {
XCTAssertEqual(size.int64Value, 123)
} else {
XCTFail("Cant get file size for 'item'")
}

XCTAssertNotNil(item2FileAttributes)
if let size = item2FileAttributes[.size] as? NSNumber {
XCTAssertEqual(size.int64Value, 456)
} else {
XCTFail("Cant get file size for 'path2/item'")
}

if let e2 = FileManager.default.enumerator(atPath: basePath) {
var foundItems = Set<String>()
while let item = e2.nextObject() as? String {
foundItems.insert(item)
if item == "path2" {
e2.skipDescendants()
XCTAssertEqual(e2.level, 1)
XCTAssertNotNil(e2.fileAttributes)
}
}
XCTAssertEqual(foundItems, Set(["item", "path2"]))
} else {
XCTFail()
}
}

func test_directoryEnumerator() {
let fm = FileManager.default
let testDirName = "testdir\(NSUUID().uuidString)"
let path = NSTemporaryDirectory() + "\(testDirName)"
let itemPath = NSTemporaryDirectory() + "\(testDirName)/item"

ignoreError { try fm.removeItem(atPath: path) }

do {
try fm.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
let _ = fm.createFile(atPath: itemPath, contents: Data(), attributes: nil)
} catch _ {
XCTFail()
let basePath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)/"
let subDirs1 = basePath + "subdir1/subdir2/.hiddenDir/subdir3/"
let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/subdir7.ext./"
let itemPath1 = basePath + "itemFile1"
let itemPath2 = subDirs1 + "itemFile2."
let itemPath3 = subDirs1 + "itemFile3.ext."
let hiddenItem1 = basePath + ".hiddenFile1"
let hiddenItem2 = subDirs1 + ".hiddenFile2"
let hiddenItem3 = subDirs2 + ".hiddenFile3"
let hiddenItem4 = subDirs2 + ".hiddenFile4.ext"

var fileLevels: [String: Int] = [
"itemFile1": 1,
".hiddenFile1": 1,
"subdir1": 1,
"subdir2": 2,
"subdir4.app": 3,
"subdir5.": 4,
".subdir6.ext": 5,
"subdir7.ext.": 6,
".hiddenFile4.ext": 7,
".hiddenFile3": 7,
".hiddenDir": 3,
"subdir3": 4,
"itemFile3.ext.": 5,
"itemFile2.": 5,
".hiddenFile2": 5
]

func directoryItems(options: FileManager.DirectoryEnumerationOptions) -> [String: Int]? {
if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath), includingPropertiesForKeys: nil, options: options, errorHandler: nil) {
var foundItems = [String:Int]()
while let item = e.nextObject() as? URL {
foundItems[item.lastPathComponent] = e.level
}
return foundItems
} else {
return nil
}
}

if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
var foundItems = [String:Int]()
while let item = e.nextObject() as? URL {
foundItems[item.path] = e.level
ignoreError { try fm.removeItem(atPath: basePath) }
defer { ignoreError { try fm.removeItem(atPath: basePath) } }

XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs1, withIntermediateDirectories: true, attributes: nil))
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs2, withIntermediateDirectories: true, attributes: nil))
for filename in [itemPath1, itemPath2, itemPath3, hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
}

if let foundItems = directoryItems(options: []) {
XCTAssertEqual(foundItems.count, fileLevels.count)
for (name, level) in foundItems {
XCTAssertEqual(fileLevels[name], level, "File level for \(name) is wrong")
}
XCTAssertEqual(foundItems[itemPath], 1)
} else {
XCTFail()
XCTFail("Cant enumerate directory at \(basePath) with options: []")
}

let subDirPath = NSTemporaryDirectory() + "\(testDirName)/testdir2"
let subDirItemPath = NSTemporaryDirectory() + "\(testDirName)/testdir2/item"
do {
try fm.createDirectory(atPath: subDirPath, withIntermediateDirectories: false, attributes: nil)
let _ = fm.createFile(atPath: subDirItemPath, contents: Data(), attributes: nil)
} catch _ {
XCTFail()

if let foundItems = directoryItems(options: [.skipsHiddenFiles]) {
XCTAssertEqual(foundItems.count, 5)
} else {
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles]")
}

if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
var foundItems = [String:Int]()
while let item = e.nextObject() as? URL {
foundItems[item.path] = e.level
}
XCTAssertEqual(foundItems[itemPath], 1)
XCTAssertEqual(foundItems[subDirPath], 1)
XCTAssertEqual(foundItems[subDirItemPath], 2)

if let foundItems = directoryItems(options: [.skipsSubdirectoryDescendants]) {
XCTAssertEqual(foundItems.count, 3)
} else {
XCTFail()
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsSubdirectoryDescendants]")
}

if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants], errorHandler: nil) {
var foundItems = [String:Int]()
while let item = e.nextObject() as? URL {
foundItems[item.path] = e.level
}
XCTAssertEqual(foundItems[itemPath], 1)
XCTAssertEqual(foundItems[subDirPath], 1)

if let foundItems = directoryItems(options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) {
XCTAssertEqual(foundItems.count, 2)
} else {
XCTFail()
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]")
}

if let e = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: [], errorHandler: nil) {
var foundItems = [String:Int]()
while let item = e.nextObject() as? URL {
foundItems[item.path] = e.level
}
XCTAssertEqual(foundItems[itemPath], 1)
XCTAssertEqual(foundItems[subDirPath], 1)

if let foundItems = directoryItems(options: [.skipsPackageDescendants]) {
#if DARWIN_COMPATIBILITY_TESTS
XCTAssertEqual(foundItems.count, 10) // Only native Foundation does not gnore .skipsPackageDescendants
#else
XCTAssertEqual(foundItems.count, 15)
#endif
} else {
XCTFail()
XCTFail("Cant enumerate directory at \(basePath) with options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]")
}

var didGetError = false
let handler : (URL, Error) -> Bool = { (URL, Error) in
didGetError = true
Expand All @@ -440,21 +500,13 @@ class TestFileManager : XCTestCase {
XCTAssertTrue(didGetError)

do {
let contents = try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil, options: []).map {
let contents = try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: basePath), includingPropertiesForKeys: nil, options: []).map {
return $0.path
}
XCTAssertEqual(contents.count, 2)
XCTAssertTrue(contents.contains(itemPath))
XCTAssertTrue(contents.contains(subDirPath))
XCTAssertEqual(contents.count, 3)
} catch {
XCTFail()
}

do {
try fm.removeItem(atPath: path)
} catch {
XCTFail("Failed to clean up files")
}
}

func test_contentsOfDirectoryAtPath() {
Expand Down