2017-02-19 20:00:41 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
import RxSwift
|
|
|
|
|
2017-02-19 22:15:10 +03:00
|
|
|
class FileItemUtils {
|
2017-02-19 20:00:41 +03:00
|
|
|
|
2017-05-08 22:16:25 +03:00
|
|
|
static func flatFileItems(of url: URL,
|
2017-02-28 00:45:26 +03:00
|
|
|
ignorePatterns: Set<FileItemIgnorePattern>,
|
2017-02-19 20:00:41 +03:00
|
|
|
ignoreToken: Token,
|
2017-02-19 22:15:10 +03:00
|
|
|
root: FileItem) -> Observable<[FileItem]> {
|
2017-02-19 20:00:41 +03:00
|
|
|
guard url.isFileURL else {
|
|
|
|
return Observable.empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
guard FileUtils.fileExists(at: url) else {
|
|
|
|
return Observable.empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
let pathComponents = url.pathComponents
|
|
|
|
return Observable.create { observer in
|
|
|
|
let cancel = Disposables.create {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
|
|
|
|
scanDispatchQueue.async {
|
|
|
|
guard let targetItem = fileItem(for: pathComponents, root: root) else {
|
|
|
|
observer.onCompleted()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var flatNewFileItems: [FileItem] = []
|
|
|
|
|
|
|
|
var dirStack: [FileItem] = [targetItem]
|
|
|
|
while let curItem = dirStack.popLast() {
|
|
|
|
if cancel.isDisposed {
|
|
|
|
observer.onCompleted()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !curItem.childrenScanned || curItem.needsScanChildren {
|
|
|
|
scanChildren(curItem)
|
|
|
|
}
|
|
|
|
|
|
|
|
curItem.children
|
|
|
|
.filter { item in
|
|
|
|
if item.isHidden || item.isPackage {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// This item already has been fnmatch'ed, thus return the cached value.
|
|
|
|
if item.ignoreToken == ignoreToken {
|
|
|
|
return !item.ignore
|
|
|
|
}
|
|
|
|
|
|
|
|
item.ignoreToken = ignoreToken
|
|
|
|
item.ignore = false
|
|
|
|
|
|
|
|
let path = item.url.path
|
|
|
|
for pattern in ignorePatterns {
|
|
|
|
// We don't use `String.FnMatchOption.leadingDir` (`FNM_LEADING_DIR`) for directories since we do not
|
|
|
|
// scan ignored directories at all when filtering. For example "*/.git" would create a `FileItem`
|
|
|
|
// for `/some/path/.git`, but not scan its children when we filter.
|
|
|
|
if pattern.match(absolutePath: path) {
|
|
|
|
item.ignore = true
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
.forEach { $0.isDir ? dirStack.append($0) : flatNewFileItems.append($0) }
|
|
|
|
|
|
|
|
if flatNewFileItems.count >= emitChunkSize {
|
|
|
|
observer.onNext(flatNewFileItems)
|
|
|
|
flatNewFileItems = []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !cancel.isDisposed {
|
|
|
|
observer.onNext(flatNewFileItems)
|
|
|
|
observer.onCompleted()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cancel
|
|
|
|
}
|
|
|
|
}
|
2017-02-19 22:15:10 +03:00
|
|
|
|
|
|
|
static func item(for url: URL, root: FileItem, create: Bool = true) -> FileItem? {
|
|
|
|
return fileItem(for: url.pathComponents, root: root, create: create)
|
|
|
|
}
|
2017-02-24 00:15:35 +03:00
|
|
|
|
|
|
|
static func sortedChildren(for url: URL, root: FileItem) -> [FileItem] {
|
|
|
|
guard let fileItem = fileItem(for: url, root: root) else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
if !fileItem.childrenScanned || fileItem.needsScanChildren {
|
|
|
|
scanChildren(fileItem, sorted: true)
|
|
|
|
return fileItem.children
|
|
|
|
}
|
|
|
|
|
|
|
|
return fileItem.children.sorted()
|
|
|
|
}
|
2017-02-19 20:00:41 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// When at least this much of non-directory and visible files are scanned, they are emitted.
|
|
|
|
fileprivate let emitChunkSize = 1000
|
|
|
|
fileprivate let scanDispatchQueue = DispatchQueue.global(qos: .userInitiated)
|
2017-05-14 19:23:57 +03:00
|
|
|
fileprivate let lock = NSRecursiveLock()
|
2017-02-19 20:00:41 +03:00
|
|
|
|
2017-05-14 19:23:57 +03:00
|
|
|
fileprivate func synced<T>(_ fn: () -> T) -> T {
|
|
|
|
lock.lock()
|
|
|
|
defer { lock.unlock() }
|
|
|
|
return fn()
|
2017-02-19 20:00:41 +03:00
|
|
|
}
|
|
|
|
|
2017-02-19 22:15:10 +03:00
|
|
|
fileprivate func fileItem(for pathComponents: [String], root: FileItem, create: Bool = true) -> FileItem? {
|
2017-05-14 19:23:57 +03:00
|
|
|
return synced {
|
|
|
|
pathComponents.dropFirst().reduce(root) { (resultItem, childName) -> FileItem? in
|
|
|
|
guard let parent = resultItem else {
|
|
|
|
return nil
|
|
|
|
}
|
2017-02-19 20:00:41 +03:00
|
|
|
|
2017-05-14 19:23:57 +03:00
|
|
|
return child(withName: childName, ofParent: parent, create: true)
|
|
|
|
}
|
2017-02-19 20:00:41 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-24 00:15:35 +03:00
|
|
|
fileprivate func fileItem(for url: URL, root: FileItem, create: Bool = true) -> FileItem? {
|
|
|
|
return fileItem(for: url.pathComponents, root: root, create: create)
|
|
|
|
}
|
|
|
|
|
2017-02-19 20:00:41 +03:00
|
|
|
/// Even when the result is nil it does not mean that there's no child with the given name. It could well be that
|
|
|
|
/// it's not been scanned yet. However, if `create` parameter was true and `nil` is returned, the requested
|
|
|
|
/// child does not exist.
|
|
|
|
///
|
|
|
|
/// - parameters:
|
|
|
|
/// - name: name of the child to get.
|
|
|
|
/// - parent: parent of the child.
|
|
|
|
/// - create: whether to create the child `FileItem` if it's not scanned yet.
|
|
|
|
/// - returns: child `FileItem` or nil.
|
|
|
|
fileprivate func child(withName name: String, ofParent parent: FileItem, create: Bool = false) -> FileItem? {
|
|
|
|
let filteredChildren = parent.children.filter { $0.url.lastPathComponent == name }
|
|
|
|
|
|
|
|
if filteredChildren.isEmpty && create {
|
|
|
|
let childUrl = parent.url.appendingPathComponent(name)
|
|
|
|
|
|
|
|
guard FileUtils.fileExists(at: childUrl) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let child = FileItem(childUrl)
|
2017-05-14 19:23:57 +03:00
|
|
|
synced { parent.children.append(child) }
|
2017-02-19 20:00:41 +03:00
|
|
|
|
|
|
|
return child
|
|
|
|
}
|
|
|
|
|
|
|
|
return filteredChildren.first
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func scanChildren(_ item: FileItem, sorted: Bool = false) {
|
|
|
|
let children = FileUtils.directDescendants(of: item.url).map(FileItem.init)
|
2017-05-14 19:23:57 +03:00
|
|
|
synced {
|
2017-02-19 20:00:41 +03:00
|
|
|
if sorted {
|
|
|
|
item.children = children.sorted()
|
|
|
|
} else {
|
|
|
|
item.children = children
|
|
|
|
}
|
|
|
|
|
2017-05-14 19:23:57 +03:00
|
|
|
item.childrenScanned = true
|
|
|
|
item.needsScanChildren = false
|
|
|
|
}
|
2017-02-19 20:00:41 +03:00
|
|
|
}
|