Recursively walk a folder tree to count files and bytes. Compare your own recursion with FileManager.enumerator, and name the hazards.

29.2.6 · L2 · Interview

Reported at: Google

Predict first

What do the two ways of counting give on a temporary folder with three files?

import Foundation
func countFiles(at url: URL) -> (files: Int, bytes: Int) {
    let fm = FileManager.default
    guard let items = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey]) else { return (0, 0) }
    var files = 0, bytes = 0
    for item in items {
        let v = try? item.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
        if v?.isDirectory == true {
            let sub = countFiles(at: item)                   // recurse into the folder
            files += sub.files; bytes += sub.bytes
        } else {
            files += 1; bytes += v?.fileSize ?? 0
        }
    }
    return (files, bytes)
}
func countWithEnumerator(at url: URL) -> (files: Int, bytes: Int) {
    var files = 0, bytes = 0
    let keys: [URLResourceKey] = [.isRegularFileKey, .fileSizeKey]
    if let e = FileManager.default.enumerator(at: url, includingPropertiesForKeys: keys) {
        for case let u as URL in e {
            let v = try? u.resourceValues(forKeys: Set(keys))
            if v?.isRegularFile == true { files += 1; bytes += v?.fileSize ?? 0 }
        }
    }
    return (files, bytes)
}
let fm = FileManager.default
let root = fm.temporaryDirectory.appendingPathComponent("walk-\(UUID().uuidString)")
try fm.createDirectory(at: root.appendingPathComponent("sub/deep"), withIntermediateDirectories: true)
try fm.createDirectory(at: root.appendingPathComponent("empty"), withIntermediateDirectories: true)
try "abc".write(to: root.appendingPathComponent("a.txt"), atomically: true, encoding: .utf8)
try "hello".write(to: root.appendingPathComponent("sub/b.txt"), atomically: true, encoding: .utf8)
try "x".write(to: root.appendingPathComponent("sub/deep/c.txt"), atomically: true, encoding: .utf8)
let r = countFiles(at: root), e = countWithEnumerator(at: root)
print("recursive: \(r.files) files, \(r.bytes) bytes")
print("enumerator: \(e.files) files, \(e.bytes) bytes")
print("missing folder:", countFiles(at: root.appendingPathComponent("nope")))
try fm.removeItem(at: root)
  • recursive: 3 files, 9 bytes / enumerator: 3 files, 9 bytes / missing folder: (files: 0, bytes: 0)
  • recursive: 4 files, 9 bytes / enumerator: 3 files, 9 bytes / error
  • recursive: 3 files, 9 bytes / enumerator: 2 files, 9 bytes / (files: 0, bytes: 0)

Mental model. Counting the books in a library: you count the loose books on this shelf, and for every cabinet you open it and do the same inside it.

List a folder with contentsOfDirectory(at:includingPropertiesForKeys:), ask each item for .isDirectoryKey and .fileSizeKey, recurse into directories and add up the rest. Asking for the keys up front is much cheaper than a stat call per item. Handle failure: the call throws for an unreadable or missing folder, so use try? or do/catch (a missing folder returns (0, 0) in the check). The recursion uses stack proportional to the depth of the tree, which is fine for real folders.

The standard alternative is FileManager.enumerator(at:includingPropertiesForKeys:), a deep, lazy iterator, so you write one loop and filter with .isRegularFileKey; the program runs both on a temporary tree and both report 3 files and 9 bytes. Hazards to name: symbolic links (a link to a parent folder can create an endless loop in hand-written recursion, so do not follow links), hidden files, packages (bundles that look like files), permission errors partway through, and doing the walk on the main thread, which freezes the UI for large trees (run it in a background task). On iOS, remember the sandbox: you can only walk your own container. Reported at Google (anonymous, unverified).

Both methods see the files in nested folders, empty folders add nothing, and a missing folder gives zero instead of crashing.

Common traps

  • Following symbolic links in a hand-written recursion. A link back to a parent loops forever.
  • Walking on the main thread. A deep tree stalls scrolling and touch handling.

They might follow up with

  • How would you stop at a depth limit or skip a folder? Pass the depth in recursion, or call skipDescendants() on the enumerator.
  • How do you find the largest file? Track a maximum as you go, or a min-heap if you need the top k.

Related questions

  • 29.2.5 Print the boundary of an N-ary tree counterclockwise: root, left edge, leaves left to right, right edge going…
  • 29.2.7 Given function entry and exit events with timestamps, print the time spent in each function when it exits.…
  • 29.2.1 Reverse a singly linked list in Swift, iteratively and recursively. What can go wrong with long lists?

← Print the boundary of an N-ary tree counterclockwise: root, left…   Given function entry and exit events with timestamps, print the time… →