Skip to content

Commit

Permalink
fix: Remove files when they are deleted
Browse files Browse the repository at this point in the history
This includes some preparatory refactoring to store content type in the database for configurable per-folder queries.
  • Loading branch information
jbmorley committed Feb 9, 2024
1 parent b9ba2f7 commit 99373c3
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 25 deletions.
12 changes: 9 additions & 3 deletions Folders/Extensions/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@
// SOFTWARE.

import Foundation
import UniformTypeIdentifiers

struct Details {
let url: URL
let contentType: UTType
}

extension FileManager {

func files(directoryURL: URL) throws -> [URL] {
func files(directoryURL: URL) throws -> [Details] {
let date = Date()
let resourceKeys = Set<URLResourceKey>([.nameKey, .isDirectoryKey, .contentTypeKey])
let directoryEnumerator = enumerator(at: directoryURL,
includingPropertiesForKeys: Array(resourceKeys),
options: [.skipsHiddenFiles, .producesRelativePathURLs])!

var files: [URL] = []
var files: [Details] = []
for case let fileURL as URL in directoryEnumerator {
// Get the file metadata.
let isDirectory = try fileURL
Expand All @@ -54,7 +60,7 @@ extension FileManager {
continue
}

files.append(fileURL)
files.append(Details(url: fileURL, contentType: contentType))
}

let duration = date.distance(to: Date())
Expand Down
25 changes: 19 additions & 6 deletions Folders/Models/ApplicationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ class ApplicationModel: ObservableObject {

let applicationSupportURL = FileManager.default.urls(for: .applicationSupportDirectory,
in: .userDomainMask).first!
let storeURL = applicationSupportURL.appendingPathComponent("store.sqlite")

// TODO: Remove the initialization into a separate state so we can capture errors and recover.

// Clean up older versions major versions (these contain breaking changes).
let fileManager = FileManager.default
for i in 0 ..< Store.majorVersion {
let storeURL = applicationSupportURL.appendingPathComponent("store_\(i).sqlite")
if fileManager.fileExists(atPath: storeURL.path) {
try! fileManager.removeItem(at: storeURL)
}
}

// Open the database, creating a new one if necessary.
let storeURL = applicationSupportURL.appendingPathComponent("store_\(Store.majorVersion).sqlite")
self.store = try! Store(databaseURL: storeURL)

// Start the scanners.
Expand All @@ -65,21 +78,21 @@ class ApplicationModel: ObservableObject {
}

func start(scanner: DirectoryScanner) {
scanner.start { [store] urls in
scanner.start { [store] details in
// TODO: Maybe allow this to rethrow and catch it at the top level to make the code cleaner?
do {
let insertStart = Date()
for url in urls {
try store.insertBlocking(url: url)
for file in details {
try store.insertBlocking(details: file)
}
let insertDuration = insertStart.distance(to: Date())
print("Insert took \(insertDuration.formatted()) seconds.")
} catch {
print("FAILED TO INSERT UPDATES WITH ERROR \(error).")
}
} onFileCreation: { [store] url in
} onFileCreation: { [store] details in
do {
try store.insertBlocking(url: url)
try store.insertBlocking(details: details)
} catch {
print("Failed to perform creation update with error \(error).")
}
Expand Down
53 changes: 43 additions & 10 deletions Folders/Utilities/DirectoryScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ import SwiftUI

import FSEventsWrapper

extension FileManager {

func details(for path: String) throws -> Details {
let url = URL(filePath: path)

guard let contentType = try url.resourceValues(forKeys: [.contentTypeKey]).contentType else {
throw FoldersError.general("Unable to get content type for file '\(path)'.")
}

return Details(url: url, contentType: contentType)
}

}

class DirectoryScanner {

let url: URL
Expand All @@ -34,8 +48,8 @@ class DirectoryScanner {
self.url = url
}

func start(callback: @escaping ([URL]) -> Void,
onFileCreation: @escaping (URL) -> Void,
func start(callback: @escaping ([Details]) -> Void,
onFileCreation: @escaping (Details) -> Void, // TODO: This should be Details
onFileDeletion: @escaping (URL) -> Void) {

// TODO: Consider creating this in the constructor.
Expand All @@ -49,24 +63,43 @@ class DirectoryScanner {
case .itemClonedAtPath:
return
case .itemCreated(path: let path, itemType: let itemType, eventId: _, fromUs: _):
// TODO: We want to support directories in the future.
guard itemType != .dir else {
return
}
let url = URL(filePath: path)
print("File created '\(url)'")
onFileCreation(url)
print("File created at path '\(path)'")
do {
onFileCreation(try FileManager.default.details(for: path))
} catch {
print("Failed to handle file creation with error \(error).")
}

case .itemRenamed(path: let path, itemType: let itemType, eventId: _, fromUs: _):
guard itemType != .dir else {
return
}
let url = URL(filePath: path)
// Helpfully, file renames can be additions or removals, so we check to see if the file exists at the
// new location to determine which.
if fileManager.fileExists(atPath: url.path) {
print("File added by rename '\(url)'")
onFileCreation(url)
} else {
print("File removed by rename '\(url)'")
do {
if fileManager.fileExists(atPath: url.path) {
print("File added by rename '\(url)'")
onFileCreation(try FileManager.default.details(for: path))
} else {
print("File removed by rename '\(url)'")
onFileDeletion(url)
}
} catch {
print("Failed to handle file deletion with error \(error).")
}

case .itemRemoved(path: let path, itemType: let itemType, eventId: _, fromUs: _):
guard itemType != .dir else {
return
}
let url = URL(filePath: path)
do {
print("File removed '\(url)'")
onFileDeletion(url)
}
default:
Expand Down
15 changes: 10 additions & 5 deletions Folders/Utilities/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ class Store {
static let files = Table("files")
static let url = Expression<String>("url")
static let name = Expression<String>("name")
static let contentType = Expression<String>("content_type")
}

static let majorVersion = 1

var observers: [StoreObserver] = []

let databaseURL: URL
Expand All @@ -52,6 +55,7 @@ class Store {
try connection.run(Schema.files.create(ifNotExists: true) { t in
t.column(Schema.url, primaryKey: true)
t.column(Schema.name)
t.column(Schema.contentType)
})
},
]
Expand Down Expand Up @@ -132,23 +136,24 @@ class Store {
}
}

func insertBlocking(url: URL) throws {
func insertBlocking(details: Details) throws {
return try runBlocking { [connection] in
try connection.transaction {

// Check to see if the URL exists already.
let existingURL = try connection.pluck(Schema.files.filter(Schema.url == url.path).limit(1))
let existingURL = try connection.pluck(Schema.files.filter(Schema.url == details.url.path).limit(1))
guard existingURL == nil else {
return
}

// If it does not, we insert it.
try connection.run(Schema.files.insert(or: .fail,
Schema.url <- url.path,
Schema.name <- url.displayName))
Schema.url <- details.url.path,
Schema.name <- details.url.displayName,
Schema.contentType <- details.contentType.identifier))
for observer in self.observers {
DispatchQueue.global(qos: .default).async {
observer.store(self, didInsertURL: url)
observer.store(self, didInsertURL: details.url)
}
}

Expand Down
7 changes: 6 additions & 1 deletion Folders/Views/GridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ class InnerGridView: NSView {

collectionView = InteractiveCollectionView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.collectionViewLayout = FixedItemSizeCollectionViewLayout(spacing: 16.0, size: CGSize(width: 300, height: 300), contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
collectionView.collectionViewLayout = FixedItemSizeCollectionViewLayout(spacing: 16.0,
size: CGSize(width: 300, height: 300),
contentInsets: NSDirectionalEdgeInsets(top: 0,
leading: 0,
bottom: 0,
trailing: 0))

super.init(frame: .zero)

Expand Down

0 comments on commit 99373c3

Please sign in to comment.