Skip to content
Open
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
36 changes: 35 additions & 1 deletion Sources/Reporters/DirectoryTree/DirectoryTreeFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ struct DirectoryTreeFactory {
/// The file manager to use for operations.
var fileManager: FileManager = .default

/// The extraction method to use.
var nameExtraction: Directory.NameExtraction = .localized

/// Closure to determine if a directory's contents should be skipped.
var skipDirectoryContents: @Sendable (_ path: String) -> Bool = { _ in false }

func make() throws -> DirectoryTreeNode {
guard let rootNode = try nodeFrom(path: path, depth: 0) else {
throw Error.rootNodeCreationFailed
Expand All @@ -43,12 +49,25 @@ struct DirectoryTreeFactory {
private func nodeFrom(path: String, depth: Int) throws -> DirectoryTreeNode? {
guard depth < maxDepth else { return nil }

let name = fileManager.displayName(atPath: path)
let name = switch nameExtraction {
case .localized: fileManager.displayName(atPath: path)
case .raw: (path as NSString).lastPathComponent
}

guard includeHiddenFiles || !name.starts(with: ".") else {
return nil
}

// Skip expensive operations for directories we don't want to traverse (especially beneficial on iCloud folders).
if isDirectory(atPath: path) && skipDirectoryContents(path) {
if isSymbolicLink(atPath: path) {
guard includeSymbolicLinks else { return nil }
return .symbolLink(path, name)
} else {
return .directory(path, name, [])
}
}

let type = try fileType(atPath: path)

switch type {
Expand Down Expand Up @@ -83,4 +102,19 @@ struct DirectoryTreeFactory {
}
return FileAttributeType(rawValue: type)
}

/// Checks if a path is a symbolic link
private func isSymbolicLink(atPath path: String) -> Bool {
(try? fileManager.destinationOfSymbolicLink(atPath: path)) != nil
}

/// Checks if a path points to a directory. Uses fileExists which is faster than checking attributes, especially on iCloud folders.
private func isDirectory(atPath path: String) -> Bool {
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue {
return true
}

return false
}
}
23 changes: 21 additions & 2 deletions Sources/Reporters/DirectoryTree/DirectoryTreeReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,24 @@
import Foundation

public struct Directory: Sendable {
/// How to get file and folder names.
public enum NameExtraction: Sendable {
/// Uses the system's display name. Localized but can be slow on iCloud folders.
case localized
/// Gets the name directly from the path. Fast but not localized.
case raw
}

let url: URL
let customisedName: String?
let maxDepth: Int
let maxLength: Int
let includeHiddenFiles: Bool
let includeSymbolicLinks: Bool
let printFullPath: Bool
let nameExtraction: NameExtraction
let skipDirectoryContents: @Sendable (String) -> Bool


/// Directory/Group to be diagnosed
/// - Parameters:
Expand All @@ -25,20 +36,26 @@ public struct Directory: Sendable {
/// - includeHiddenFiles: Whether hidden files should be captured. Defaults to `false`.
/// - includeSymbolicLinks: Whether symbolic links should be captured. Defaults to `false`.
/// - printFullPath: Whether the full path of the node should be printed or just the name. Defaults to `false`.
/// - nameExtraction: How to extract node names. Defaults to `.localized`.
/// - skipDirectoryContents: Closure to determine if a directory's contents should be skipped. Defaults to never skip.
public init(url: URL,
customisedName: String? = nil,
maxDepth: Int = .max,
maxLength: Int = 10,
includeHiddenFiles: Bool = false,
includeSymbolicLinks: Bool = false,
printFullPath: Bool = false) {
printFullPath: Bool = false,
nameExtraction: NameExtraction = .localized,
skipDirectoryContents: @escaping @Sendable (String) -> Bool = { _ in false }) {
self.url = url
self.customisedName = customisedName
self.maxDepth = maxDepth
self.maxLength = maxLength
self.includeHiddenFiles = includeHiddenFiles
self.includeSymbolicLinks = includeSymbolicLinks
self.printFullPath = printFullPath
self.nameExtraction = nameExtraction
self.skipDirectoryContents = skipDirectoryContents
}
}

Expand Down Expand Up @@ -93,7 +110,9 @@ public struct DirectoryTreesReporter: DiagnosticsReporting {
maxDepth: trunk.maxDepth,
maxLength: trunk.maxLength,
includeHiddenFiles: trunk.includeHiddenFiles,
includeSymbolicLinks: trunk.includeSymbolicLinks
includeSymbolicLinks: trunk.includeSymbolicLinks,
nameExtraction: trunk.nameExtraction,
skipDirectoryContents: trunk.skipDirectoryContents
).make()

diagnostics.append("""
Expand Down