├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md ├── TOFileSystemObserver.podspec ├── TOFileSystemObserver ├── Categories │ ├── NSFileManager+TOFileSystemDirectoryEnumerator.h │ ├── NSFileManager+TOFileSystemDirectoryEnumerator.m │ ├── NSIndexPath+AppKitAdditions.h │ ├── NSIndexPath+AppKitAdditions.m │ ├── NSURL+TOFileSystemAttributes.h │ ├── NSURL+TOFileSystemAttributes.m │ ├── NSURL+TOFileSystemUUID.h │ └── NSURL+TOFileSystemUUID.m ├── Entities │ ├── Changes │ │ ├── TOFileSystemChanges+Private.h │ │ ├── TOFileSystemChanges.h │ │ ├── TOFileSystemChanges.m │ │ ├── TOFileSystemItemListChanges+Private.h │ │ ├── TOFileSystemItemListChanges.h │ │ └── TOFileSystemItemListChanges.m │ ├── Collections │ │ ├── TOFileSystemItemMapTable.h │ │ ├── TOFileSystemItemMapTable.m │ │ ├── TOFileSystemItemURLDictionary.h │ │ └── TOFileSystemItemURLDictionary.m │ ├── FilePaths │ │ ├── TOFileSystemPath.h │ │ └── TOFileSystemPath.m │ ├── Items │ │ ├── TOFileSystemItem+Private.h │ │ ├── TOFileSystemItem.h │ │ ├── TOFileSystemItem.m │ │ ├── TOFileSystemItemList+Private.h │ │ ├── TOFileSystemItemList.h │ │ └── TOFileSystemItemList.m │ └── Notifications │ │ ├── TOFileSystemNotificationToken+Private.h │ │ ├── TOFileSystemNotificationToken.h │ │ └── TOFileSystemNotificationToken.m ├── Info.plist ├── Scanning │ ├── TOFileSystemPresenter.h │ ├── TOFileSystemPresenter.m │ ├── TOFileSystemScanOperation.h │ └── TOFileSystemScanOperation.m ├── TOFileSystemObserver.h ├── TOFileSystemObserver.m ├── Utilities │ ├── TOFileSystemObserver+AppKit.h │ ├── TOFileSystemObserver+UIKit.h │ └── TOFileSystemObserverConstants.h └── include │ ├── NSFileManager+TOFileSystemDirectoryEnumerator.h │ ├── NSIndexPath+AppKitAdditions.h │ ├── NSURL+TOFileSystemAttributes.h │ ├── NSURL+TOFileSystemUUID.h │ ├── TOFileSystemChanges+Private.h │ ├── TOFileSystemChanges.h │ ├── TOFileSystemItem+Private.h │ ├── TOFileSystemItem.h │ ├── TOFileSystemItemList+Private.h │ ├── TOFileSystemItemList.h │ ├── TOFileSystemItemListChanges+Private.h │ ├── TOFileSystemItemListChanges.h │ ├── TOFileSystemItemMapTable.h │ ├── TOFileSystemItemURLDictionary.h │ ├── TOFileSystemNotificationToken+Private.h │ ├── TOFileSystemNotificationToken.h │ ├── TOFileSystemObserver+AppKit.h │ ├── TOFileSystemObserver+UIKit.h │ ├── TOFileSystemObserver.h │ ├── TOFileSystemObserverConstants.h │ ├── TOFileSystemPath.h │ ├── TOFileSystemPresenter.h │ └── TOFileSystemScanOperation.h ├── TOFileSystemObserverExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── TOFileSystemObserverExample.xcscheme │ ├── TOFileSystemObserverMacExample.xcscheme │ └── TOFileSystemObserverTests.xcscheme ├── TOFileSystemObserverExample ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── TOAppDelegate.h ├── TOAppDelegate.m ├── TOImages.h ├── TOImages.m ├── TOViewController.h ├── TOViewController.m └── main.m ├── TOFileSystemObserverMacExample ├── ARAppDelegate.h ├── ARAppDelegate.m ├── ARImages.h ├── ARImages.m ├── ARViewController.h ├── ARViewController.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── TOFileSystemObserverMacExample.entitlements └── main.m ├── TOFileSystemObserverTests ├── Categories │ ├── TOFileSystemEnumeratorTests.m │ ├── TOFileSystemFileAttributesTests.m │ └── TOFileSystemUUIDTests.m ├── Entities │ ├── TOFileSystemChangesTests.m │ ├── TOFileSystemItemDictionaryTests.m │ ├── TOFileSystemItemListChangesTests.m │ ├── TOFileSystemItemMapTableTests.m │ └── TOFileSystemItemURLDictionaryTests.m └── Info.plist └── screenshot.jpg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: timoliver 2 | custom: https://tim.dev/paypal 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Run all of the library unit tests. 13 | run: '(curl -s -L https://tim.dev/install_ios_oss_ci | bash -s arg1 arg2) && bundle exec fastlane test' 14 | env: 15 | TEST_SCHEME: "TOFileSystemObserverTests" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots/**/*.png 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | .DS_Store 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | x.y.z Release Notes (yyyy-MM-dd) 2 | ============================================================= 3 | 4 | 0.0.4 Release Notes (2022-01-23) 5 | ============================================================= 6 | 7 | ### Enhancements 8 | 9 | * Replaced dispatch barriers in `TOFileSystemItem` with threading locks to minimize GCD queue creation. 10 | * General internal code/commenting cleanup and refinement. 11 | 12 | ### Fixed 13 | 14 | * An incorrect casting breaking the visible number of items inside of a directory. 15 | * A crash that can occur if querying for the number of files in a deleted folder. 16 | * A linking issue when importing the library via SPM on macOS. 17 | 18 | 0.0.3 Release Notes (2020-02-24) 19 | ============================================================= 20 | 21 | ### Added 22 | 23 | * Public, thread-safe API access for accessing the UUID string of an observed item, 24 | and/or its parent directory. 25 | 26 | ### Fixed 27 | 28 | * A thread coordinating issue where file UUIDs might not have 29 | been set yet upon first access. 30 | * An issue where proper UUIDs were not being generated before 31 | adding items as children to list objects. 32 | * An issue where deleting an item in Files.app would make the 33 | `.Trash` folder become treated like an official item. 34 | 35 | 0.0.2 Release Notes (2020-02-11) 36 | ============================================================= 37 | 38 | ### Enhancements 39 | 40 | * Exposed full system scans as a property on `TOFileSystemChanges` in order to let 41 | observing objects defer work until the scan is complete. 42 | 43 | ### Fixed 44 | 45 | * A bug where files moved below the sub-directory level limit weren't treated as deleted. 46 | * A bug where specifying a sub-directory limit would result in an infinite loop. 47 | * A bug where renaming/moving a file wouldn't be properly updated in the main graph. 48 | * A bug where items marked as 'skipped' weren't being handled as such. 49 | * A bug where files created during the initial scan would be permanently left in the 'copying' state. 50 | * A bug where files of the incorrect sub-directory level limit were still being accessed. 51 | 52 | 0.0.1 Release Notes (2020-01-30) 53 | ============================================================= 54 | 55 | * Initial Release! 🎉 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Tim Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TOFileSystemObserver", 7 | platforms: [ 8 | .macOS(.v10_12), 9 | .iOS(.v8) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "TOFileSystemObserver", 15 | type: .dynamic, 16 | targets: ["TOFileSystemObserver"]), 17 | ], 18 | dependencies: [ ], 19 | targets: [ 20 | .target( 21 | name: "TOFileSystemObserver", 22 | dependencies: [], 23 | path: "TOFileSystemObserver", 24 | cSettings: [.define("TARGET_OS_OSX", .when(platforms: [.macOS]))] 25 | ), 26 | ] 27 | ) 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOFileSystemObserver 2 | 3 | TOFileSystemObserver 4 | 5 | [![CI](https://github.com/TimOliver/TOFileSystemObserver/workflows/CI/badge.svg)](https://github.com/TimOliver/TOFileSystemObserver/actions?query=workflow%3ACI) 6 | [![Version](https://img.shields.io/cocoapods/v/TOFileSystemObserver.svg?style=flat)](http://cocoadocs.org/docsets/TOFileSystemObserver) 7 | [![Platform](https://img.shields.io/cocoapods/p/TOFileSystemObserver.svg?style=flat)](http://cocoadocs.org/docsets/TOFileSystemObserver) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/TimOliver/TOFileSystemObserver/master/LICENSE) 9 | [![PayPal](https://img.shields.io/badge/paypal-donate-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M4RKULAVKV7K8) 10 | [![Twitch](https://img.shields.io/badge/twitch-timXD-6441a5.svg)](http://twitch.tv/timXD) 11 | 12 | `TOFileSystemObserver` is a bullet-proof mechanism (hopefully) for detecting any user-initiated changes made to the contents of an iOS / iPadOS app's sandbox while the app is open. 13 | 14 | Since iOS 11, the Files app has given the option to allow apps to expose the contents of their Documents directories to the users, letting them manipulate the files, either while the app is closed, suspended, or even running side-by-side with iPad multitasking. 15 | 16 | For document-based apps that display a list of available files from the Documents library, this library aims to let you detect and respond to all file events that your app will need to adjust its UI and caches for. 17 | 18 | # Features 19 | * A single class that runs for the duration of the app session, and processes file events from the system. 20 | * Can detect file changes on *any* subdirectory level. 21 | * Can detect when the user imports a new file, and is able to detect when large files have finished copying. 22 | * Can detect when the user moves a file, including between directory levels. 23 | * Can detect when a user deletes a file, either from the Files app, or directly via iTunes. 24 | * Can detect when a user renames a file. 25 | * Can detect when a user duplicates the same file via the Files app. 26 | * Provides "live" objects representing the contents of directories, and files which will update in conjunction with the file system. 27 | 28 | # Example 29 | 30 | ```objc 31 | #import "TOFileSystemObserver.h" 32 | 33 | // Create a new instance (Targets the Documents directory by default) 34 | TOFileSystemObserver *observer = [[TOFileSystemObserver alloc] init]; 35 | 36 | // Start observing the target directory. 37 | [observer start]; 38 | 39 | // Register a notification token to receive events from the observer 40 | TOFileSystemNotificationToken *observerToken = [self.observer addNotificationBlock: 41 | ^(TOFileSystemObserver *observer, 42 | TOFileSystemObserverNotificationType type, 43 | TOFileSystemChanges *changes) 44 | { 45 | // At the start of the session, the observer will perform a full system scan. 46 | // This event will give observers a chance to set up before the scan. 47 | if (type == TOFileSystemObserverNotificationTypeWillBeginFullScan) { 48 | NSLog(@"Scan Will Start!"); 49 | return; 50 | } 51 | 52 | // At the start of the session, the observer will perform a full system scan. 53 | // This event will give observers a chance to clean up after the scan. 54 | if (type == TOFileSystemObserverNotificationTypeDidCompleteFullScan) { 55 | NSLog(@"Scan Complete!"); 56 | return; 57 | } 58 | 59 | NSLog(@"%@", changes); 60 | }]; 61 | ``` 62 | 63 | Please check the sample app for more examples on the features of this library. 64 | 65 | # Requirements 66 | 67 | `TOFileSystemObserver` will work with iOS 8.0 and above. While it's been written in Objective-C, it will also work with Swift (But the Swift interface may need some more work.) 68 | 69 | ## Manual Installation 70 | 71 | Copy the contents of the `TOFileSystemObserver` folder to your app project. 72 | 73 | ## CocoaPods 74 | 75 | ``` 76 | pod 'TOFileSystemObserver' 77 | ``` 78 | 79 | ## Carthage and SPM 80 | 81 | I only need CocoaPods for my current plans with this library, so Carthage and SPM are low priority for now. If you would like Carthage or SPM support, please submit a PR. 82 | 83 | # How Does it Work? 84 | 85 | Observing files on the file system consists of a variety of problems that each need to be solved to work. 86 | 87 | ### Receiving System Events for File Changes 88 | 89 | Historically, [Apple staff have recommended](https://forums.developer.apple.com/thread/90531) using `DispatchSource` for detecting file changes. However, since this doesn't support subdirectories, it wasn't suitable here. Instead, `TOFileSystemObserver` uses [`NSFilePresenter`](https://developer.apple.com/documentation/foundation/nsfilepresenter) a component of coordinated file access to detect when a file has changed. 90 | 91 | ### Tracking Files Uniquely on Disk 92 | 93 | Since it's very easy for file names to change, and there's no guarantee they'll be unique (eg, multiple `Chapter1.zip` files in different folders), it was necessary to assign each file an ID that the user cannot easily modify and would be unique. 94 | 95 | To that end, `TOFileSystemObserver` uses the [Extended File Attributes](https://nshipster.com/extended-file-attributes/) feature of APFS to attach a unique UUID string to each file it tracks. The observer then keeps an in-memory graph of every file's UUID and the URL of their last location, in order to determine when a file is moved or renamed. 96 | 97 | ### Determining When a File is Copying 98 | 99 | `NSFilePresenter` will trigger 2 times for a file being copied in: once at the start, and again at the end. Since most file imports need to happen only when the file has finished copying, a way to check that the file has finished copying was necessary. I sadly lost the original Stack Overflow post, but an extremely bright person discovered that when a file is still copying, its reported modification date will be equal to the current date. In this way, we can check if the file is still copying or not. 100 | 101 | # Credits 102 | 103 | `TOFileSystemObserver` was created by [Tim Oliver](http://twitter.com/TimOliverAU) as a component of [iComics](http://icomics.co). 104 | 105 | A huge thank you to [Jeffrey Bergier](https://twitter.com/jeffburg) whose [`JSBFileSystem`](https://github.com/jeffreybergier/JSBFilesystem) served as the base inspiration for this library, and for his help in letting me bounce ideas off him (such as eschewing having an on-disk store) during the start of this project. 106 | 107 | iOS device mockup art by [Pixeden](http://pixeden.com). 108 | 109 | # License 110 | 111 | `TOFileSystemObserver` is available under the MIT license. Please see the [LICENSE](LICENSE) file for more information. ![analytics](https://ga-beacon.appspot.com/UA-5643664-16/TOFileSystemObserver/README.md?pixel) 112 | -------------------------------------------------------------------------------- /TOFileSystemObserver.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'TOFileSystemObserver' 3 | s.version = '0.0.4' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'A bullet-proof mechanism for detecting any changes made to the contents of a folder in iOS & iPadOS.' 6 | s.homepage = 'https://github.com/TimOliver/TOFileSystemObserver' 7 | s.author = 'Tim Oliver' 8 | s.source = { :git => 'https://github.com/TimOliver/TOFileSystemObserver.git', :tag => s.version } 9 | s.platforms = { :ios => "8.0", :osx => "10.12" } 10 | s.source_files = 'TOFileSystemObserver/**/*.{h,m}' 11 | s.exclude_files = 'TOFileSystemObserver/include/**' 12 | s.osx.exclude_files = 'TOFileSystemObserver/Utilities/TOFileSystemObserver+UIKit.h' 13 | s.ios.exclude_files = ['TOFileSystemObserver/Utilities/TOFileSystemObserver+AppKit.h','TOFileSystemObserver/Categories/NSIndexPath+AppKitAdditions.{h,m}'] 14 | s.requires_arc = true 15 | end 16 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSFileManager+TOFileSystemDirectoryEnumerator.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+TOFileSystemDirectoryEnumerator.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A convenience category that creates and pre-configures 29 | directory enumerators with the parameters we need. 30 | */ 31 | @interface NSFileManager (TOFileSystemDirectoryEnumerator) 32 | 33 | /** Create a new directory enumerator with the attributes we're interested configured. */ 34 | - (NSDirectoryEnumerator *)to_fileSystemEnumeratorForDirectoryAtURL:(NSURL *)url; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSFileManager+TOFileSystemDirectoryEnumerator.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+TOFileSystemDirectoryEnumerator.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "NSFileManager+TOFileSystemDirectoryEnumerator.h" 24 | 25 | @implementation NSFileManager (TOFileSystemDirectoryEnumerator) 26 | 27 | - (NSDirectoryEnumerator *)to_fileSystemEnumeratorForDirectoryAtURL:(NSURL *)url 28 | { 29 | // Set the keys for the properties we wish to capture 30 | NSArray *keys = @[NSURLIsDirectoryKey, 31 | NSURLFileSizeKey, 32 | NSURLCreationDateKey, 33 | NSURLContentModificationDateKey]; 34 | 35 | // Set the flags for the enumerator 36 | NSDirectoryEnumerationOptions options = NSDirectoryEnumerationSkipsHiddenFiles | 37 | NSDirectoryEnumerationSkipsSubdirectoryDescendants; 38 | 39 | // Create the enumerator 40 | NSDirectoryEnumerator *urlEnumerator = [self enumeratorAtURL:url 41 | includingPropertiesForKeys:keys 42 | options:options 43 | errorHandler:nil]; 44 | return urlEnumerator; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSIndexPath+AppKitAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSIndexPath+AppKitAdditions.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver, Anatoly Rosencrantz. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #if TARGET_OS_OSX 24 | 25 | #import 26 | #import 27 | 28 | @interface NSIndexPath(UIKitAdditions) 29 | 30 | - (NSInteger)row; 31 | + (NSIndexPath*)indexPathForRow:(NSInteger)row inSection:(NSInteger)section; 32 | 33 | @end 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSIndexPath+AppKitAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSIndexPath+AppKitAdditions.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver, Anatoly Rosencrantz. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #if TARGET_OS_OSX 24 | 25 | #import "NSIndexPath+AppKitAdditions.h" 26 | 27 | @implementation NSIndexPath(UIKitAdditions) 28 | 29 | - (NSInteger)row { return self.item; } 30 | 31 | + (NSIndexPath *)indexPathForRow:(NSInteger)row inSection:(NSInteger)section { 32 | return [self indexPathForItem:row inSection:section]; 33 | } 34 | 35 | @end 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+TOFileSystemAttributes.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A convenience wrapper for fetching specific attributes 29 | about the item on disk that this URL represents 30 | */ 31 | @interface NSURL (TOFileSystemAttributes) 32 | 33 | /** Whether the file is currently being copied or not. */ 34 | @property (nonatomic, readonly) BOOL to_isCopying; 35 | 36 | /** Whether the item is a directory or file. */ 37 | @property (nonatomic, readonly) BOOL to_isDirectory; 38 | 39 | /** The file size of the item (0 for directories) */ 40 | @property (nonatomic, readonly) long long to_size; 41 | 42 | /** The creation date of the item. */ 43 | @property (nonatomic, readonly) NSDate *to_creationDate; 44 | 45 | /** The modification date of the item. */ 46 | @property (nonatomic, readonly) NSDate *to_modificationDate; 47 | 48 | /** The number of sub-items in this directory. */ 49 | @property (nonatomic, readonly) NSInteger to_numberOfSubItems; 50 | 51 | @end 52 | 53 | NS_ASSUME_NONNULL_END 54 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+TOFileSystemAttributes.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "NSURL+TOFileSystemAttributes.h" 24 | #import "TOFileSystemObserverConstants.h" 25 | #include 26 | 27 | @implementation NSURL (TOFileSystemAttributes) 28 | 29 | - (BOOL)to_isCopying 30 | { 31 | // When files are still being copied, their 32 | // modification date is equal to the current device time. 33 | return [self.to_modificationDate timeIntervalSinceDate:[NSDate date]] 34 | > (-kTOFileSystemObserverCopyingTimeDelay - FLT_EPSILON); 35 | } 36 | 37 | - (BOOL)to_isDirectory 38 | { 39 | NSNumber *isDirectory; 40 | [self getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; 41 | return isDirectory.boolValue; 42 | } 43 | 44 | - (long long)to_size 45 | { 46 | NSNumber *fileSize; 47 | [self getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil]; 48 | return fileSize.longLongValue; 49 | } 50 | 51 | - (NSDate *)to_creationDate 52 | { 53 | NSDate *creationDate; 54 | [self getResourceValue:&creationDate forKey:NSURLCreationDateKey error:nil]; 55 | return creationDate; 56 | } 57 | 58 | - (NSDate *)to_modificationDate 59 | { 60 | [self removeCachedResourceValueForKey:NSURLContentModificationDateKey]; 61 | NSDate *modificationDate; 62 | [self getResourceValue:&modificationDate forKey:NSURLContentModificationDateKey error:nil]; 63 | return modificationDate; 64 | } 65 | 66 | - (NSInteger)to_numberOfSubItems 67 | { 68 | NSInteger numberOfItems = 0; 69 | DIR *directory; 70 | struct dirent *entry; 71 | 72 | // Do it using POSIX APIs to avoid needing to load in all of the file names 73 | const char *path = [self.path cStringUsingEncoding:NSUTF8StringEncoding]; 74 | directory = opendir(path); 75 | if (directory == NULL) { return 0; } 76 | while ((entry = readdir(directory)) != NULL) { 77 | if (entry->d_name[0] == '.') { continue; } 78 | if (entry->d_type == DT_REG || entry->d_type == DT_DIR) { 79 | numberOfItems++; 80 | } 81 | } 82 | closedir(directory); 83 | 84 | return numberOfItems; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+TOFileSystemUUID.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A convenience category that wraps the ability 29 | to assign a specific extended attribute 30 | to the file, so we can uniquely track it. 31 | 32 | Thanks to NSHipster for highlighting this technique. 33 | https://nshipster.com/extended-file-attributes/ 34 | */ 35 | @interface NSURL (TOFileSystemUUID) 36 | 37 | /** Changes the prefix of the key name under which the UUID is saved.*/ 38 | + (void)to_setKeyNamePrefix:(NSString *)prefix; 39 | 40 | /** Returns the unique UUID value assigned to this file. */ 41 | - (nullable NSString *)to_fileSystemUUID; 42 | 43 | /** Sets a predetermined UUID to be the value of the file. */ 44 | - (void)to_setFileSystemUUID:(NSString *)uuid; 45 | 46 | /** Regardless if one exists, generate and save a new UUID. */ 47 | - (NSString *)to_generateFileSystemUUID; 48 | 49 | @end 50 | 51 | NS_ASSUME_NONNULL_END 52 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+TOFileSystemUUID.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "NSURL+TOFileSystemUUID.h" 24 | #import 25 | 26 | static NSString *kTOFileSystemAttributeKey = @"dev.tim.fileSystemObserver.UUID"; 27 | 28 | @implementation NSURL (TOFileSystemUUID) 29 | 30 | + (void)to_setKeyNamePrefix:(NSString *)prefix 31 | { 32 | kTOFileSystemAttributeKey = [NSString stringWithFormat:@"%@.fileSystemObserver.UUID", prefix]; 33 | } 34 | 35 | - (NSString *)to_fileSystemUUID 36 | { 37 | const char *filePath = [self.path fileSystemRepresentation]; 38 | const char *keyName = kTOFileSystemAttributeKey.UTF8String; 39 | 40 | // Allocate a buffer for the value (UUID values are always 36 characters) 41 | char value[36]; 42 | 43 | // Fetch the value from disk 44 | getxattr(filePath, keyName, value, 36, 0, 0); 45 | 46 | // Convert to a string, and return if successful 47 | NSString *uuid = [[NSString alloc] initWithBytes:value length:36 encoding:NSUTF8StringEncoding]; 48 | if (uuid.length == 0) { 49 | return nil; 50 | } 51 | 52 | // Verify to make sure the provided value is a valid UUID string 53 | NSString *uuidPattern = @"\\A[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}\\Z"; 54 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:uuidPattern 55 | options:NSRegularExpressionCaseInsensitive 56 | error:nil]; 57 | NSRange range = [regex rangeOfFirstMatchInString:uuid options:0 range:NSMakeRange(0, uuid.length)]; 58 | 59 | // A valid regex was found. 60 | if (range.location == NSNotFound) { 61 | return nil; 62 | } 63 | 64 | return uuid; 65 | } 66 | 67 | - (void)to_setFileSystemUUID:(NSString *)uuid 68 | { 69 | if (uuid.length > 0 && uuid.length != 36) { 70 | @throw [NSException exceptionWithName:NSInternalInconsistencyException 71 | reason:@"UUID must be 36 characters long!" 72 | userInfo:nil]; 73 | } 74 | 75 | // Determine the file path and destination key 76 | const char *filePath = [self.path fileSystemRepresentation]; 77 | const char *keyName = kTOFileSystemAttributeKey.UTF8String; 78 | 79 | // Convert the string to a C byte string 80 | const char *uuidString = [uuid cStringUsingEncoding:NSUTF8StringEncoding]; 81 | 82 | // Save it to this file 83 | setxattr(filePath, keyName, uuidString, strlen(uuidString), 0, 0); 84 | } 85 | 86 | - (NSString *)to_generateFileSystemUUID 87 | { 88 | NSString *uuid = [NSUUID UUID].UUIDString; 89 | [self to_setFileSystemUUID:uuid]; 90 | return uuid; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemChanges+Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemChanges.m 3 | // 4 | // Copyright 2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | #import "TOFileSystemChanges.h" 26 | 27 | NS_ASSUME_NONNULL_BEGIN 28 | 29 | @interface TOFileSystemChanges () 30 | 31 | /** Create a new instance. */ 32 | - (instancetype)initWithFileSystemObserver:(TOFileSystemObserver *)fileSystemObserver; 33 | 34 | /** Sets that these changes came from the initial full system scan */ 35 | - (void)setIsFullScan; 36 | 37 | /** Add a new discovered item to the list. */ 38 | - (void)addDiscoveredItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL; 39 | 40 | /** Add a new discovered item to the list. */ 41 | - (void)addModifiedItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL; 42 | 43 | /** Add a new discovered item to the list. */ 44 | - (void)addDeletedItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL; 45 | 46 | /** Add a new discovered item to the list. */ 47 | - (void)addMovedItemWithUUID:(NSString *)uuid oldFileURL:(NSURL *)oldFileURL newFileURL:(NSURL *)newFileURL; 48 | 49 | @end 50 | 51 | NS_ASSUME_NONNULL_END 52 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemChanges.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemChanges.h 3 | // 4 | // Copyright 2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @class TOFileSystemObserver; 26 | 27 | NS_ASSUME_NONNULL_BEGIN 28 | 29 | /** 30 | Whenever a file system observer object detects a change 31 | on disk, it will broadcast an event to any objects that 32 | have subscribed. The specific changes that were detected 33 | will be contained in an instance of this class. 34 | */ 35 | NS_SWIFT_NAME(FileSystemChanges) 36 | @interface TOFileSystemChanges : NSObject 37 | 38 | /** The observer from where these changes were broadcasted. */ 39 | @property (nonatomic, weak, readonly) TOFileSystemObserver *fileSystemObserver; 40 | 41 | /** 42 | Whether the observer is performing the full scan at the start of the session or 43 | an explict list of files mid-session. If this is true, you might prefer to 44 | wait to defer any heavy work to the notification that will be sent along when 45 | this particular scan is complete. 46 | */ 47 | @property (nonatomic, assign, readonly) BOOL isFullScan; 48 | 49 | /** 50 | A dictionary of items that were discovered by the file system observer. 51 | These are either files that were already on disk and were just discovered for the 52 | first time in this session, or they are brand new files that were just added by the user. 53 | 54 | All files in your app will be discovered at least once during an app session. Use 55 | this method to compare the files against the current state of your cached information 56 | to see if it needs to be updated. 57 | 58 | The dictionary key is the unique UUID string assigned to the file on disk, and the value 59 | is the absolute file path URL to the file on disk. 60 | */ 61 | @property (nonatomic, readonly, nullable) NSDictionary *discoveredItems; 62 | 63 | /** 64 | A dictionary of items that were noted to have had changed during this app session. 65 | Sorts of changes include the file-name changing, or if it was still being copied and 66 | had just completed. 67 | 68 | The dictionary key is the unique UUID string assigned to the file on disk, and the value 69 | is the absolute file path URL to the file on disk. 70 | */ 71 | @property (nonatomic, readonly, nullable) NSDictionary *modifiedItems; 72 | 73 | /** 74 | A dictionary of items that were noted to have been deleted during this app session. 75 | 76 | The dictionary key is the unique UUID string assigned to the file on disk, and the value 77 | is the absolute file path URL to the file on disk. 78 | */ 79 | @property (nonatomic, readonly, nullable) NSDictionary *deletedItems; 80 | 81 | /** 82 | A dictionary of items that were noted to have moved during this app session. 83 | 84 | The dictionary key is the unique UUID string assigned to the file on disk, and the value 85 | is a 2-element array where the first value is the previous URL and the second is the new URL. 86 | */ 87 | @property (nonatomic, readonly, nullable) NSDictionary *movedItems; 88 | 89 | @end 90 | 91 | NS_ASSUME_NONNULL_END 92 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemChanges.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemChanges.m 3 | // 4 | // Copyright 2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemChanges.h" 24 | 25 | @interface TOFileSystemChanges () 26 | 27 | @property (nonatomic, weak, readwrite) TOFileSystemObserver *fileSystemObserver; 28 | @property (nonatomic, strong, readwrite) NSMutableDictionary *discoveredItems; 29 | @property (nonatomic, strong, readwrite) NSMutableDictionary *modifiedItems; 30 | @property (nonatomic, strong, readwrite) NSMutableDictionary *deletedItems; 31 | @property (nonatomic, strong, readwrite) NSMutableDictionary *movedItems; 32 | @property (nonatomic, assign, readwrite) BOOL isFullScan; 33 | @end 34 | 35 | @implementation TOFileSystemChanges 36 | 37 | - (instancetype)initWithFileSystemObserver:(TOFileSystemObserver *)fileSystemObserver 38 | { 39 | if (self = [super init]) { 40 | _fileSystemObserver = fileSystemObserver; 41 | } 42 | return self; 43 | } 44 | 45 | - (void)addDiscoveredItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL 46 | { 47 | if (_discoveredItems == nil) { 48 | _discoveredItems = [NSMutableDictionary dictionary]; 49 | } 50 | _discoveredItems[uuid] = fileURL; 51 | } 52 | 53 | - (void)addModifiedItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL 54 | { 55 | if (_modifiedItems == nil) { 56 | _modifiedItems = [NSMutableDictionary dictionary]; 57 | } 58 | _modifiedItems[uuid] = fileURL; 59 | } 60 | 61 | - (void)addDeletedItemWithUUID:(NSString *)uuid fileURL:(NSURL *)fileURL 62 | { 63 | if (_deletedItems == nil) { 64 | _deletedItems = [NSMutableDictionary dictionary]; 65 | } 66 | _deletedItems[uuid] = fileURL; 67 | } 68 | 69 | - (void)addMovedItemWithUUID:(NSString *)uuid 70 | oldFileURL:(NSURL *)oldFileURL 71 | newFileURL:(NSURL *)newFileURL 72 | { 73 | if (_movedItems == nil) { 74 | _movedItems = [NSMutableDictionary dictionary]; 75 | } 76 | _movedItems[uuid] = @[oldFileURL, newFileURL]; 77 | } 78 | 79 | - (void)setIsFullScan 80 | { 81 | self.isFullScan = YES; 82 | } 83 | 84 | - (NSString *)description 85 | { 86 | return [NSString stringWithFormat:@"Discovered items: %@\nModified items: %@\nDeleted Items: %@\nMoved items: %@\n", 87 | self.discoveredItems, self.modifiedItems, self.deletedItems, self.movedItems]; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemItemListChanges+Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemListChanges+Private.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "TOFileSystemItemListChanges.h" 25 | 26 | NS_ASSUME_NONNULL_BEGIN 27 | 28 | @interface TOFileSystemItemListChanges () 29 | 30 | /** Add the index of an item to be deleted. */ 31 | - (void)addDeletionIndex:(NSInteger)index; 32 | 33 | /** Add the index of an item to be inserted. */ 34 | - (void)addInsertionIndex:(NSInteger)index; 35 | 36 | /** Add the index of an item to be modified. */ 37 | - (void)addModificationIndex:(NSInteger)index; 38 | 39 | /** Add the source and dest index values for a row to be moved. */ 40 | - (void)addMovementWithSourceIndex:(NSInteger)sourceIndex 41 | destinationIndex:(NSInteger)destinationIndex; 42 | 43 | @end 44 | 45 | NS_ASSUME_NONNULL_END 46 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemItemListChanges.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemListChanges.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | This class stores sets of indices denoting which objects, 29 | if any, in an item list were changed, and any subsequent user 30 | interfaces need to be refreshed. 31 | 32 | Whenever an item list detects a change has happened, it will 33 | trigger a notification block and provide an instance of this class. 34 | */ 35 | 36 | NS_SWIFT_NAME(FileSystemListChanges) 37 | @interface TOFileSystemItemListChanges : NSObject 38 | 39 | /** State check to see if there are any pending movements. */ 40 | @property (nonatomic, readonly) BOOL hasItemMovements; 41 | 42 | /** State check if it has cell updates that aren't movements. */ 43 | @property (nonatomic, readonly) BOOL hasItemChanges; 44 | 45 | /** The indices of any objects that were deleted. */ 46 | @property (nonatomic, readonly, nullable) NSArray *deletions; 47 | 48 | /** The indices of any objects that were added. */ 49 | @property (nonatomic, readonly, nullable) NSArray *insertions; 50 | 51 | /** The indices of any objects that were modified. */ 52 | @property (nonatomic, readonly, nullable) NSArray *modificatons; 53 | 54 | /** The indices of any objects that have been moved in the list. */ 55 | @property (nonatomic, readonly, nullable) NSDictionary *movements; 56 | 57 | /** For table/collection view convenience, create an array of index paths 58 | for items that were deleted. */ 59 | - (NSArray *)indexPathsForDeletionsInSection:(NSInteger)section; 60 | 61 | /** For table/collection view convenience, create an array of index paths 62 | for items that were inserted. */ 63 | - (NSArray *)indexPathsForInsertionsInSection:(NSInteger)section; 64 | 65 | /** For table/collection view convenience, create an array of index paths 66 | for items that were modified. */ 67 | - (NSArray *)indexPathsForModificationsInSection:(NSInteger)section; 68 | 69 | /** For table/collection view convenience, create an array of index paths that 70 | items about to be moved were originally in. */ 71 | - (NSArray *)indexPathsForMovementSourcesInSection:(NSInteger)section; 72 | 73 | /** For table/collection view convenience, create an array of index paths that 74 | items about to be moved are now currently in, based off an array of source index */ 75 | - (NSArray *)indexPathsForMovementDestinationsWithSourceIndexPaths:(NSArray *)sourceIndexPaths; 76 | 77 | @end 78 | 79 | NS_ASSUME_NONNULL_END 80 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Changes/TOFileSystemItemListChanges.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemListChanges.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemItemListChanges.h" 24 | 25 | #if TARGET_OS_OSX 26 | #import "NSIndexPath+AppKitAdditions.h" 27 | #else 28 | #import 29 | #endif 30 | 31 | 32 | @interface TOFileSystemItemListChanges () 33 | 34 | /** The indices of any objects that were deleted. */ 35 | @property (nonatomic, strong, readwrite) NSMutableArray *deletions; 36 | 37 | /** The indices of any objects that were added. */ 38 | @property (nonatomic, strong, readwrite) NSMutableArray *insertions; 39 | 40 | /** The indices of any objects that were modified. */ 41 | @property (nonatomic, strong, readwrite) NSMutableArray *modificatons; 42 | 43 | /** The indices of any objects that have been moved in the list. */ 44 | @property (nonatomic, strong, readwrite) NSMutableDictionary *movements; 45 | 46 | @end 47 | 48 | @implementation TOFileSystemItemListChanges 49 | 50 | #pragma mark - Adding Index Values - 51 | 52 | - (void)addDeletionIndex:(NSInteger)index 53 | { 54 | if (self.deletions == nil) { 55 | self.deletions = [NSMutableArray array]; 56 | } 57 | 58 | [(NSMutableArray *)self.deletions addObject:@(index)]; 59 | } 60 | 61 | - (void)addInsertionIndex:(NSInteger)index 62 | { 63 | if (self.insertions == nil) { 64 | self.insertions = [NSMutableArray array]; 65 | } 66 | 67 | [(NSMutableArray *)self.insertions addObject:@(index)]; 68 | } 69 | 70 | - (void)addModificationIndex:(NSInteger)index 71 | { 72 | if (self.modificatons == nil) { 73 | self.modificatons = [NSMutableArray array]; 74 | } 75 | 76 | [(NSMutableArray *)self.modificatons addObject:@(index)]; 77 | } 78 | 79 | - (void)addMovementWithSourceIndex:(NSInteger)sourceIndex 80 | destinationIndex:(NSInteger)destinationIndex 81 | { 82 | if (self.movements == nil) { 83 | self.movements = [NSMutableDictionary dictionary]; 84 | } 85 | 86 | NSMutableDictionary *dict = (NSMutableDictionary *)self.movements; 87 | dict[@(sourceIndex)] = @(destinationIndex); 88 | } 89 | 90 | #pragma mark - Table/Collection View Converters - 91 | 92 | - (NSArray *)indexPathsForCollection:(nullable NSArray *)collection 93 | inSection:(NSInteger)section 94 | { 95 | if (!collection) { return [NSArray array]; } 96 | 97 | NSMutableArray *array = [NSMutableArray array]; 98 | for (NSNumber *number in collection) { 99 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:number.intValue inSection:section]; 100 | [array addObject:indexPath]; 101 | } 102 | 103 | return [NSArray arrayWithArray:array]; 104 | } 105 | 106 | - (NSArray *)indexPathsForDeletionsInSection:(NSInteger)section 107 | { 108 | return [self indexPathsForCollection:self.deletions inSection:section]; 109 | } 110 | 111 | - (NSArray *)indexPathsForInsertionsInSection:(NSInteger)section 112 | { 113 | return [self indexPathsForCollection:self.insertions inSection:section]; 114 | } 115 | 116 | - (NSArray *)indexPathsForModificationsInSection:(NSInteger)section 117 | { 118 | return [self indexPathsForCollection:self.modificatons inSection:section]; 119 | } 120 | 121 | - (NSArray *)indexPathsForMovementSourcesInSection:(NSInteger)section 122 | { 123 | return [self indexPathsForCollection:self.movements.allKeys inSection:section]; 124 | } 125 | 126 | - (NSArray *)indexPathsForMovementDestinationsWithSourceIndexPaths:(NSArray *)sourceIndexPaths 127 | { 128 | if (self.movements == nil) { return [NSArray array]; } 129 | 130 | NSMutableArray *array = [NSMutableArray array]; 131 | for (NSIndexPath *sourceIndexPath in sourceIndexPaths) { 132 | NSInteger row = self.movements[@(sourceIndexPath.row)].intValue; 133 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:sourceIndexPath.section]; 134 | [array addObject:indexPath]; 135 | } 136 | 137 | return [NSArray arrayWithArray:array]; 138 | } 139 | 140 | - (BOOL)hasItemMovements 141 | { 142 | return self.movements != nil; 143 | } 144 | 145 | - (BOOL)hasItemChanges 146 | { 147 | return (self.deletions.count || 148 | self.insertions.count || 149 | self.modificatons.count); 150 | } 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemMapTable.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A thread-safe wrapper for the NSMapTable objects 29 | used to store re-usable instances of item and list 30 | objects. 31 | */ 32 | @interface TOFileSystemItemMapTable : NSObject 33 | 34 | @property (nonatomic, readonly) NSInteger count; 35 | 36 | - (void)setItem:(id)object forUUID:(NSString *)uuid; 37 | - (id)itemForUUID:(NSString *)uuid; 38 | - (void)removeItemForUUID:(NSString *)uuid; 39 | 40 | /** Implementations for allowing dictionary style literal syntax. */ 41 | - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key; 42 | - (nullable id)objectForKeyedSubscript:(NSString *)key; 43 | 44 | @end 45 | 46 | NS_ASSUME_NONNULL_END 47 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemMapTable.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemItemMapTable.h" 24 | 25 | @interface TOFileSystemItemMapTable () 26 | 27 | /** The map table to hold the items */ 28 | @property (nonatomic, strong) NSMapTable *mapTable; 29 | 30 | /** The dispatch queue for synchronzing reads */ 31 | @property (nonatomic, strong) dispatch_queue_t dispatchQueue; 32 | 33 | @end 34 | 35 | @implementation TOFileSystemItemMapTable 36 | 37 | - (instancetype)init 38 | { 39 | if (self = [super init]) { 40 | _mapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory 41 | valueOptions:NSPointerFunctionsWeakMemory]; 42 | _dispatchQueue = dispatch_queue_create("TOFileSystemObserver.itemMapTable", DISPATCH_QUEUE_CONCURRENT); 43 | } 44 | 45 | return self; 46 | } 47 | 48 | - (NSInteger)count 49 | { 50 | __block NSInteger count = 0; 51 | dispatch_sync(self.dispatchQueue, ^{ 52 | count = self.mapTable.count; 53 | }); 54 | 55 | return count; 56 | } 57 | 58 | - (void)setItem:(id)object forUUID:(NSString *)uuid 59 | { 60 | dispatch_barrier_async(self.dispatchQueue, ^{ 61 | [self.mapTable setObject:object forKey:uuid]; 62 | }); 63 | } 64 | 65 | - (id)itemForUUID:(NSString *)uuid 66 | { 67 | __block id item = nil; 68 | dispatch_sync(self.dispatchQueue, ^{ 69 | @autoreleasepool { 70 | item = [self.mapTable objectForKey:uuid]; 71 | } 72 | }); 73 | 74 | return item; 75 | } 76 | 77 | - (void)removeItemForUUID:(NSString *)uuid 78 | { 79 | dispatch_barrier_async(self.dispatchQueue, ^{ 80 | [self.mapTable removeObjectForKey:uuid]; 81 | }); 82 | } 83 | 84 | - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key 85 | { 86 | [self setItem:object forUUID:key]; 87 | } 88 | 89 | - (nullable id)objectForKeyedSubscript:(NSString *)key 90 | { 91 | return [self itemForUUID:key]; 92 | } 93 | 94 | - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state 95 | objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer 96 | count:(NSUInteger)len 97 | { 98 | return [_mapTable countByEnumeratingWithState:state 99 | objects:buffer 100 | count:len]; 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Collections/TOFileSystemItemURLDictionary.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemDictionary.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A thread-safe dictionary that stores the file paths 29 | to items using their on-disk UUID string as the key. 30 | 31 | This is used to store an in-memory graph of all of the files 32 | as they were at the start of the app session, so that any 33 | detected changes to the file system can be compared. 34 | 35 | The URLs are converted to and stored as relative URLs to 36 | save memory, and are converted back to absolute URLs when retrieved. 37 | */ 38 | @interface TOFileSystemItemURLDictionary : NSObject 39 | 40 | /** The number of items currently in the dictionary. */ 41 | @property (nonatomic, readonly) NSUInteger count; 42 | 43 | /** Create a new instance with the base URL that all items will be relatively saved against. */ 44 | - (instancetype)initWithBaseURL:(NSURL *)baseURL; 45 | 46 | /** Adds an item URL to the dictionary. May be called from multiple threads. */ 47 | - (void)setItemURL:(nullable NSURL *)itemURL forUUID:(nullable NSString *)uuid; 48 | 49 | /** Retrieves an item URL from the dictionary. May be called from multiple threads. */ 50 | - (nullable NSURL *)itemURLForUUID:(nullable NSString *)uuid; 51 | 52 | /** Tries to retrieve a UUID value for a URL, if it exists */ 53 | - (nullable NSString *)uuidForItemWithURL:(NSURL *)itemURL; 54 | 55 | /** Get all UUID keys. */ 56 | - (nullable NSArray *)allUUIDs; 57 | 58 | /** Get all URL objects. */ 59 | - (nullable NSArray *)allURLs; 60 | 61 | /** Delete an entry from the store. */ 62 | - (void)removeItemURLForUUID:(NSString *)uuid; 63 | 64 | /** Remove all items. */ 65 | - (void)removeAllItems; 66 | 67 | /** Implementations for allowing dictionary style literal syntax. */ 68 | - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key; 69 | - (nullable id)objectForKeyedSubscript:(NSString *)key; 70 | 71 | - (instancetype)init NS_UNAVAILABLE; 72 | 73 | @end 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Collections/TOFileSystemItemURLDictionary.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemDictionary.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemItemURLDictionary.h" 24 | 25 | @interface TOFileSystemItemURLDictionary () 26 | 27 | /** The base URL against which all other URLs are saved. */ 28 | @property (nonatomic, strong) NSURL *baseURL; 29 | 30 | /** The dictionary that holds all of the item URLs */ 31 | @property (nonatomic, strong) NSMutableDictionary *uuidItems; 32 | 33 | /** A reverse dictionary tha stores UUIDs for URLs */ 34 | @property (nonatomic, strong) NSMutableDictionary *urlItems; 35 | 36 | /** The dispatch queue used to read and write safely to this dictionary. */ 37 | @property (nonatomic, strong) dispatch_queue_t itemQueue; 38 | 39 | @end 40 | 41 | @implementation TOFileSystemItemURLDictionary 42 | 43 | #pragma mark - Class Creation - 44 | 45 | - (instancetype)initWithBaseURL:(NSURL *)baseURL 46 | { 47 | if (self = [super init]) { 48 | _baseURL = baseURL.URLByDeletingLastPathComponent.URLByStandardizingPath; 49 | _uuidItems = [NSMutableDictionary dictionary]; 50 | _urlItems = [NSMutableDictionary dictionary]; 51 | _itemQueue = dispatch_queue_create("TOFileSystemObserver.itemDictionaryQueue", 52 | DISPATCH_QUEUE_CONCURRENT); 53 | } 54 | 55 | return self; 56 | } 57 | 58 | - (NSUInteger)count 59 | { 60 | __block NSInteger count = 0; 61 | dispatch_sync(self.itemQueue, ^{ 62 | count = self.uuidItems.count; 63 | }); 64 | 65 | return count; 66 | } 67 | 68 | - (void)setItemURL:(nullable NSURL *)itemURL forUUID:(nullable NSString *)uuid 69 | { 70 | if (uuid.length == 0) { return; } 71 | 72 | // If the item is nil, remove it from the store 73 | if (itemURL == nil) { 74 | dispatch_barrier_async(self.itemQueue, ^{ 75 | NSURL *url = self.uuidItems[uuid]; 76 | [self.urlItems removeObjectForKey:url]; 77 | [self.uuidItems removeObjectForKey:uuid]; 78 | }); 79 | return; 80 | } 81 | 82 | // Use dispatch barriers to block all reads when we mutate the dictionary 83 | dispatch_barrier_async(self.itemQueue, ^{ 84 | // Purge the previously saved entries as they may be stale 85 | NSURL *savedURL = self.uuidItems[uuid]; 86 | NSString *savedUUID = self.urlItems[savedURL]; 87 | if (savedUUID) { [self.uuidItems removeObjectForKey:savedUUID]; } 88 | if (savedURL) { [self.urlItems removeObjectForKey:savedURL]; } 89 | 90 | // Remove the un-needed absolute path to save memory 91 | NSURL *url = [self relativeURLForURL:itemURL]; 92 | self.uuidItems[uuid] = url; 93 | self.urlItems[url] = uuid; 94 | }); 95 | } 96 | 97 | - (nullable NSURL *)itemURLForUUID:(NSString *)uuid 98 | { 99 | if (uuid.length == 0) { return nil; } 100 | 101 | // Use dispatch barriers to allow asynchronouse reading 102 | __block NSURL *itemURL = nil; 103 | dispatch_sync(self.itemQueue, ^{ 104 | itemURL = self.uuidItems[uuid]; 105 | }); 106 | if (itemURL == nil) { return nil; } 107 | 108 | return [self.baseURL URLByAppendingPathComponent:itemURL.path].URLByStandardizingPath; 109 | } 110 | 111 | - (nullable NSString *)uuidForItemWithURL:(NSURL *)itemURL 112 | { 113 | // Convert the item URL to relative 114 | NSURL *url = [self relativeURLForURL:itemURL]; 115 | 116 | // Look up the URL in the dictionary 117 | __block NSString *uuid = nil; 118 | dispatch_sync(self.itemQueue, ^{ 119 | uuid = self.urlItems[url]; 120 | }); 121 | 122 | return uuid; 123 | } 124 | 125 | - (nullable NSArray *)allUUIDs 126 | { 127 | __block NSArray *uuids = nil; 128 | dispatch_sync(self.itemQueue, ^{ 129 | uuids = self.uuidItems.allKeys; 130 | }); 131 | return uuids; 132 | } 133 | 134 | - (nullable NSArray *)allURLs 135 | { 136 | // Loop through each item in the store, and restore its URL 137 | __block NSMutableArray *array = [NSMutableArray array]; 138 | dispatch_sync(self.itemQueue, ^{ 139 | for (NSString *uuid in self.uuidItems) { 140 | NSString *path = self.uuidItems[uuid].path; 141 | NSURL *url = [self.baseURL URLByAppendingPathComponent:path]; 142 | [array addObject:url.URLByStandardizingPath]; 143 | } 144 | }); 145 | 146 | // If the array was empty, return nil 147 | if (array.count == 0) { return nil; } 148 | 149 | // Return an immutable version 150 | return [NSArray arrayWithArray:array]; 151 | } 152 | 153 | - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key 154 | { 155 | [self setItemURL:object forUUID:key]; 156 | } 157 | 158 | - (void)removeItemURLForUUID:(NSString *)uuid 159 | { 160 | if (uuid == nil) { return; } 161 | 162 | dispatch_barrier_async(self.itemQueue, ^{ 163 | NSURL *url = self.uuidItems[uuid]; 164 | if (url == nil) { return; } 165 | [self.urlItems removeObjectForKey:url]; 166 | [self.uuidItems removeObjectForKey:uuid]; 167 | }); 168 | } 169 | 170 | - (void)removeAllItems 171 | { 172 | dispatch_barrier_async(self.itemQueue, ^{ 173 | [self.urlItems removeAllObjects]; 174 | [self.uuidItems removeAllObjects]; 175 | }); 176 | } 177 | 178 | - (nullable id)objectForKeyedSubscript:(NSString *)key 179 | { 180 | return [self itemURLForUUID:key]; 181 | } 182 | 183 | #pragma mark - URL Conversion - 184 | 185 | - (NSURL *)relativeURLForURL:(NSURL *)url 186 | { 187 | NSString *basePath = self.baseURL.path; 188 | NSString *itemPath = url.URLByStandardizingPath.path; 189 | NSString *relativePath = [itemPath stringByReplacingOccurrencesOfString:basePath withString:@""]; 190 | return [NSURL fileURLWithPath:relativePath]; 191 | } 192 | 193 | #pragma mark - Debugging - 194 | 195 | - (NSString *)description 196 | { 197 | NSString *descriptionString = @""; 198 | for (NSString *key in self.uuidItems.allKeys) { 199 | descriptionString = [descriptionString stringByAppendingFormat:@"%@ - %@\n", key, self.uuidItems[key]]; 200 | } 201 | 202 | return descriptionString; 203 | } 204 | 205 | @end 206 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/FilePaths/TOFileSystemPath.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemPath.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | A static class to centralize all file path 29 | manipulation logic. 30 | */ 31 | @interface TOFileSystemPath : NSObject 32 | 33 | /** The path to the application sandbox. */ 34 | + (NSURL *)applicationSandboxURL; 35 | 36 | /** The path to the application documents directory. */ 37 | + (NSURL *)documentsDirectoryURL; 38 | 39 | /** Takes an absolute URL, and strips off the sandbox portion, making it relative. */ 40 | + (NSString *)relativePathWithPath:(NSURL *)fileURL; 41 | 42 | /** Takes a flat array of URLs, and organizes them into a 43 | dictionary where each key is the parent directory URL, and the value 44 | is an array of all items in that directory. */ 45 | + (NSDictionary *)directoryDictionaryWithItemURLs:(NSArray *)itemURLs; 46 | 47 | @end 48 | 49 | NS_ASSUME_NONNULL_END 50 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/FilePaths/TOFileSystemPath.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemPath.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemPath.h" 24 | 25 | @implementation TOFileSystemPath 26 | 27 | + (NSURL *)applicationSandboxURL 28 | { 29 | return [NSURL fileURLWithPath:NSHomeDirectory()]; 30 | } 31 | 32 | + (NSURL *)documentsDirectoryURL 33 | { 34 | NSFileManager *fileManager = [NSFileManager defaultManager]; 35 | return [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject; 36 | } 37 | 38 | + (NSString *)relativePathWithPath:(NSURL *)fileURL 39 | { 40 | NSString *sandboxPath = [TOFileSystemPath applicationSandboxURL].path; 41 | NSString *path = fileURL.path; 42 | 43 | // Replace the sandbox portion with an empty string. 44 | path = [path stringByReplacingOccurrencesOfString:sandboxPath withString:@""]; 45 | 46 | // Remove leading slashes 47 | if ([[path substringToIndex:1] isEqualToString:@"/"]) { 48 | path = [path substringFromIndex:1]; 49 | } 50 | 51 | // Remove trailing slashes 52 | if ([[path substringFromIndex:path.length - 1] isEqualToString:@"/"]) { 53 | path = [path substringToIndex:path.length - 2]; 54 | } 55 | 56 | return path; 57 | } 58 | 59 | + (NSDictionary *)directoryDictionaryWithItemURLs:(NSArray *)itemURLs 60 | { 61 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; 62 | 63 | return dictionary; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Items/TOFileSystemItem+Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItem+Private.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "TOFileSystemItem.h" 25 | 26 | @class TOFileSystemObserver; 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | /** Private interface for creating item objects */ 31 | @interface TOFileSystemItem () 32 | 33 | /** Creates a new instance of an item for the target item. */ 34 | - (instancetype)initWithItemAtFileURL:(NSURL *)fileURL 35 | fileSystemObserver:(TOFileSystemObserver *)observer; 36 | 37 | /** Adds this item as a child of a list. */ 38 | - (void)addToList:(TOFileSystemItemList *)list; 39 | 40 | /** Remove this item from a list. */ 41 | - (void)removeFromList; 42 | 43 | /** Forces a refresh of the UUID (in cases where the file seems to have been duplicated) */ 44 | - (void)regenerateUUID; 45 | 46 | /** Notify this object that it should re-fetch all its properties from disk. 47 | Returns true if there were changes. */ 48 | - (BOOL)refreshWithURL:(nullable NSURL *)itemURL; 49 | 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Items/TOFileSystemItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItem.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemObserverConstants.h" 24 | 25 | @class TOFileSystemObserver; 26 | @class TOFileSystemItemList; 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | /** 31 | An object that represents either a file 32 | or folder on disk. 33 | */ 34 | NS_SWIFT_NAME(FileSystemItem) 35 | @interface TOFileSystemItem : NSObject 36 | 37 | /** The absolute URL path to this item. */ 38 | @property (nonatomic, readonly) NSURL *fileURL; 39 | 40 | /** The type of the item (either a file or folder) */ 41 | @property (nonatomic, readonly) TOFileSystemItemType type; 42 | 43 | /** The unique UUID that was assigned to the file by this library. */ 44 | @property (nonatomic, readonly) NSString *uuid; 45 | 46 | /** The name on disk of the item. */ 47 | @property (nonatomic, readonly) NSString *name; 48 | 49 | /** The size (in bytes) of this item. (0 for directories). */ 50 | @property (nonatomic, readonly) long long size; 51 | 52 | /** The creation date of the item. */ 53 | @property (nonatomic, readonly) NSDate *creationDate; 54 | 55 | /** The last modification date of the item. */ 56 | @property (nonatomic, readonly) NSDate *modificationDate; 57 | 58 | /** If a directory, the number of files/subdirectories inside this item. */ 59 | @property (nonatomic, readonly) NSInteger numberOfSubItems; 60 | 61 | /** Whether the item is still being copied into the app container. */ 62 | @property (nonatomic, readonly) BOOL isCopying; 63 | 64 | /** Whether the item on disk represented by this object no longer exists. */ 65 | @property (nonatomic, readonly) BOOL isDeleted; 66 | 67 | /** The file system observer backing this object. */ 68 | @property (nonatomic, weak, readonly) TOFileSystemObserver *fileSystemObserver; 69 | 70 | /** Returns the list this item belongs to (if a list has separately been created.) */ 71 | @property (nonatomic, weak, nullable, readonly) TOFileSystemItemList *list; 72 | 73 | @end 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Items/TOFileSystemItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItem.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemItem.h" 24 | #import "TOFileSystemItemList+Private.h" 25 | #import "TOFileSystemPath.h" 26 | #import "TOFileSystemObserver.h" 27 | #import "TOFileSystemPresenter.h" 28 | #import "NSURL+TOFileSystemAttributes.h" 29 | #import "NSURL+TOFileSystemUUID.h" 30 | #import "TOFileSystemObserverConstants.h" 31 | 32 | #import 33 | #import 34 | 35 | /** Private interface to expose the file presenter for coordinated writes. */ 36 | @interface TOFileSystemObserver () 37 | @property (nonatomic, readonly) TOFileSystemPresenter *fileSystemPresenter; 38 | @end 39 | 40 | @interface TOFileSystemItem () 41 | 42 | /** The list that this item belongs to. */ 43 | @property (nonatomic, weak, nullable, readwrite) TOFileSystemItemList *list; 44 | 45 | /** Internal writing overrides for public properties */ 46 | @property (nonatomic, strong, readwrite) NSURL *fileURL; 47 | @property (nonatomic, assign, readwrite) TOFileSystemItemType type; 48 | @property (nonatomic, copy, readwrite) NSString *uuid; 49 | @property (nonatomic, copy, readwrite) NSString *name; 50 | @property (nonatomic, assign, readwrite) long long size; 51 | @property (nonatomic, strong, readwrite) NSDate *creationDate; 52 | @property (nonatomic, strong, readwrite) NSDate *modificationDate; 53 | @property (nonatomic, assign, readwrite) BOOL isCopying; 54 | @property (nonatomic, assign, readwrite) NSInteger numberOfSubItems; 55 | 56 | /** Thread safe locks */ 57 | #pragma clang diagnostic push 58 | #pragma clang diagnostic ignored "-Weverything" 59 | @property (nonatomic, assign) os_unfair_lock unfairLock; 60 | @property (nonatomic, assign) pthread_mutex_t pthreadMutexLock; 61 | #pragma clang diagnostic pop 62 | 63 | @end 64 | 65 | @implementation TOFileSystemItem 66 | 67 | #pragma mark - Class Creation - 68 | 69 | - (instancetype)initWithItemAtFileURL:(NSURL *)fileURL 70 | fileSystemObserver:(TOFileSystemObserver *)observer 71 | { 72 | if (self = [super init]) { 73 | _fileURL = fileURL; 74 | _fileSystemObserver = observer; 75 | 76 | // Initialize the lock 77 | if (@available(iOS 10.0, *)) { 78 | self.unfairLock = OS_UNFAIR_LOCK_INIT; 79 | } else { 80 | pthread_mutex_init(&_pthreadMutexLock, NULL); 81 | } 82 | 83 | // If this item represents a deleted file, skip gathering the data 84 | if (!self.isDeleted) { 85 | [self performWithLock:^{ 86 | [self configureUUIDForceRefresh:NO]; 87 | [self refreshFromItemAtURL:fileURL]; 88 | }]; 89 | } 90 | } 91 | 92 | return self; 93 | } 94 | 95 | #pragma mark - Update Properties - 96 | 97 | - (void)configureUUIDForceRefresh:(BOOL)forceRefresh 98 | { 99 | TOFileSystemPresenter *presenter = self.fileSystemObserver.fileSystemPresenter; 100 | _uuid = [presenter uuidForItemAtURL:_fileURL]; 101 | } 102 | 103 | - (BOOL)refreshFromItemAtURL:(NSURL *)url 104 | { 105 | BOOL hasChanges = NO; 106 | 107 | // Copy the new URL to this item 108 | if (url) { 109 | _fileURL = url; 110 | } 111 | 112 | // Copy the name of the item 113 | NSString *name = [_fileURL lastPathComponent]; 114 | if (_name.length == 0 || ![name isEqualToString:_name]) { 115 | _name = name; 116 | hasChanges = YES; 117 | } 118 | 119 | // Check if it is a file or directory 120 | TOFileSystemItemType type = _fileURL.to_isDirectory ? TOFileSystemItemTypeDirectory : 121 | TOFileSystemItemTypeFile; 122 | if (type != _type) { 123 | _type = type; 124 | hasChanges = YES; 125 | } 126 | 127 | // Get its creation date 128 | NSDate *creationDate = _fileURL.to_creationDate; 129 | if (![_creationDate isEqualToDate:creationDate]) { 130 | _creationDate = creationDate; 131 | hasChanges = YES; 132 | } 133 | 134 | // Get its modification date 135 | NSDate *modificationDate = _fileURL.to_modificationDate; 136 | if (![_modificationDate isEqualToDate:modificationDate]) { 137 | _modificationDate = modificationDate; 138 | hasChanges = YES; 139 | } 140 | 141 | // If the type is a file 142 | if (_type == TOFileSystemItemTypeFile) { 143 | // Fetch the item file size 144 | long long fileSize = _fileURL.to_size; 145 | if (fileSize != _size) { 146 | _size = fileSize; 147 | hasChanges = YES; 148 | } 149 | 150 | // Check to see if it is copying 151 | BOOL isCopying = _fileURL.to_isCopying; 152 | if (isCopying != _isCopying) { 153 | _isCopying = isCopying; 154 | hasChanges = YES; 155 | } 156 | } 157 | else { 158 | // Else, it's a directory, count the number of items inside 159 | NSInteger numberOfChildItems = [_fileURL to_numberOfSubItems]; 160 | if (_numberOfSubItems != numberOfChildItems) { 161 | _numberOfSubItems = numberOfChildItems; 162 | hasChanges = YES; 163 | } 164 | } 165 | 166 | return hasChanges; 167 | } 168 | 169 | - (BOOL)isDeleted 170 | { 171 | return ![[NSFileManager defaultManager] fileExistsAtPath:self.fileURL.path]; 172 | } 173 | 174 | - (void)regenerateUUID 175 | { 176 | [self performWithLock:^{ 177 | [self configureUUIDForceRefresh:YES]; 178 | }]; 179 | } 180 | 181 | #pragma mark - Lists - 182 | 183 | - (BOOL)refreshWithURL:(nullable NSURL *)itemURL 184 | { 185 | // Perform a re-fetch of all of the properties of the 186 | // item from disk, and re-populate all of the properties. 187 | 188 | // A lock needs to be used as this operation will ideally be done 189 | // in the background due to how heavy it could potentially be 190 | __block BOOL hasChanges = NO; 191 | [self performWithLock:^{ 192 | hasChanges = [self refreshFromItemAtURL:itemURL]; 193 | }]; 194 | 195 | // If it was detected one or more of the properties were 196 | // different, if the item is a member of a list, inform 197 | // the list that the UI state of this item will need to be updated. 198 | if (hasChanges) { 199 | [NSOperationQueue.mainQueue addOperationWithBlock:^{ 200 | if (self.list == nil) { return; } 201 | [self.list itemDidRefreshWithUUID:self.uuid]; 202 | }]; 203 | } 204 | 205 | return hasChanges; 206 | } 207 | 208 | - (void)addToList:(TOFileSystemItemList *)list 209 | { 210 | self.list = list; 211 | } 212 | 213 | - (void)removeFromList 214 | { 215 | self.list = nil; 216 | } 217 | 218 | #pragma mark - Equality - 219 | 220 | - (BOOL)isEqual:(id)object 221 | { 222 | if (self == object) { return YES; } 223 | if (![object isKindOfClass:TOFileSystemItem.class]) { return NO; } 224 | 225 | TOFileSystemItem *item = (TOFileSystemItem *)object; 226 | return [item.uuid isEqualToString:self.uuid]; 227 | } 228 | 229 | - (NSUInteger)hash 230 | { 231 | return self.uuid.hash; 232 | } 233 | 234 | #pragma mark - Thread-Safe Accessors - 235 | 236 | // To ensure thread safety, fetch the value of an object 237 | // on the barrier queue 238 | - (id)fetchValueForObject:(NSString *)objectName 239 | { 240 | __block id objectValue = nil; 241 | [self performWithLock:^{ 242 | objectValue = [self valueForKey:objectName]; 243 | }]; 244 | 245 | return objectValue; 246 | } 247 | 248 | - (long long)fetchValueForInteger:(NSString *)integerName 249 | { 250 | __block long long intValue = 0; 251 | [self performWithLock:^{ 252 | intValue = [[self valueForKey:integerName] longLongValue]; 253 | }]; 254 | 255 | return intValue; 256 | } 257 | 258 | - (NSURL *)fileURL { return (NSURL *)[self fetchValueForObject:@"_fileURL"]; } 259 | - (NSString *)uuid { return (NSString *)[self fetchValueForObject:@"_uuid"]; } 260 | - (NSString *)name { return (NSString *)[self fetchValueForObject:@"_name"]; } 261 | - (long long)size { return (long long)[self fetchValueForInteger:@"_size"]; } 262 | - (NSDate *)creationDate { return (NSDate *)[self fetchValueForObject:@"_creationDate"]; } 263 | - (NSDate *)modificationDate { return (NSDate *)[self fetchValueForObject:@"_modificationDate"]; } 264 | - (BOOL)isCopying { return (BOOL)[self fetchValueForInteger:@"_isCopying"]; } 265 | - (NSInteger)numberOfSubItems { return (NSInteger)[self fetchValueForInteger:@"_numberOfSubItems"]; } 266 | 267 | #pragma mark - Thread Safe Access - 268 | 269 | - (void)performWithLock:(void (^)(void))block; 270 | { 271 | // Lock the current thread 272 | if (@available(iOS 10.0, *)) { 273 | os_unfair_lock_lock(&_unfairLock); 274 | } else { 275 | pthread_mutex_lock(&_pthreadMutexLock); 276 | } 277 | 278 | block(); 279 | 280 | // Unlock the thread 281 | if (@available(iOS 10.0, *)) { 282 | os_unfair_lock_unlock(&_unfairLock); 283 | } else { 284 | pthread_mutex_unlock(&_pthreadMutexLock); 285 | } 286 | } 287 | 288 | #pragma mark - Debugging - 289 | 290 | - (NSString *)description 291 | { 292 | NSString *description = @"TOFileSystem Item - \n" 293 | @"Name: %@\n" 294 | @"UUID: %@\n" 295 | @"Type: %@\n" 296 | @"Size: %d\n" 297 | @"Created: %@\n" 298 | @"Modified: %@\n"; 299 | 300 | return [NSString stringWithFormat:description, 301 | self.name, 302 | self.uuid, 303 | self.type != 0 ? @"Folder" : @"File", 304 | self.size, 305 | self.creationDate, 306 | self.modificationDate]; 307 | } 308 | 309 | @end 310 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Items/TOFileSystemItemList+Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemList+Private.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "TOFileSystemItemList.h" 25 | #import "TOFileSystemItem.h" 26 | #import "TOFileSystemItemListChanges.h" 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | @class TOFileSystemObserver; 31 | 32 | /** Private interface for creating item objects */ 33 | @interface TOFileSystemItemList () 34 | 35 | /** Creates a new instance of an item for the target item. */ 36 | - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL 37 | fileSystemObserver:(TOFileSystemObserver *)observer; 38 | 39 | /** Add a new item to the list. */ 40 | - (void)addItemWithUUID:(NSString *)uuid itemURL:(NSURL *)url; 41 | 42 | /** Triggered when an item's properties have changed. */ 43 | - (void)itemDidRefreshWithUUID:(NSString *)uuid; 44 | 45 | /** Remove an object from the list (It was deleted or moved away). */ 46 | - (void)removeItemWithUUID:(NSString *)uuid fileURL:(NSURL *)url; 47 | 48 | /** If the folder was moved, update it's own reference to its file path. */ 49 | - (BOOL)refreshWithURL:(nullable NSURL *)directoryURL; 50 | 51 | /** Loop through every item on disk, and delete any items that are no longer there. */ 52 | - (void)synchronizeWithDisk; 53 | 54 | @end 55 | 56 | NS_ASSUME_NONNULL_END 57 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Items/TOFileSystemItemList.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemList.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "TOFileSystemObserverConstants.h" 25 | 26 | @class TOFileSystemObserver; 27 | @class TOFileSystemItem; 28 | @class TOFileSystemItemListChanges; 29 | @class TOFileSystemNotificationToken; 30 | @class TOFileSystemItemList; 31 | 32 | NS_ASSUME_NONNULL_BEGIN 33 | 34 | /** 35 | This class represents a list of files and 36 | directories located within the directroy that was 37 | specified. 38 | 39 | It is backed by an observer object, that will ensure 40 | that it is kept up-to-date with any changes that occur on 41 | the file system. 42 | */ 43 | NS_SWIFT_NAME(FileSystemItemList) 44 | @interface TOFileSystemItemList : NSObject 45 | 46 | /** The unique UUID string saved in the attributes of this directory. */ 47 | @property (nonatomic, readonly) NSString *uuid; 48 | 49 | /** The observer object backing this list object. */ 50 | @property (nonatomic, weak, readonly) TOFileSystemObserver *fileSystemObserver; 51 | 52 | /** The type of ordering of the items. */ 53 | @property (nonatomic, assign) TOFileSystemItemListOrder listOrder; 54 | 55 | /** Whether the list is ascending or descending. (Default is ascending). */ 56 | @property (nonatomic, assign) BOOL isDescending; 57 | 58 | /** The number of items in this list. */ 59 | @property (nonatomic, readonly) NSUInteger count; 60 | 61 | /** The absolute URL to this directory containing these items. */ 62 | @property (nonatomic, readonly) NSURL *directoryURL; 63 | 64 | /** 65 | Registers a new notification block that will be 66 | triggered each time the data in the list changes. 67 | 68 | The returned notification token must be strongly retained 69 | by your code for the duration you wish to receive notifications. 70 | */ 71 | - (TOFileSystemNotificationToken *)addNotificationBlock:(TOFileSystemItemListNotificationBlock)block; 72 | 73 | /** Retrieves the item at the requested index. */ 74 | - (TOFileSystemItem *)objectAtIndex:(NSUInteger)index; 75 | 76 | /** Allows array-style lookup of items at specific indexes. */ 77 | - (TOFileSystemItem *)objectAtIndexedSubscript:(NSUInteger)index; 78 | 79 | - (instancetype)init NS_UNAVAILABLE; 80 | 81 | @end 82 | 83 | NS_ASSUME_NONNULL_END 84 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Notifications/TOFileSystemNotificationToken+Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemNotificationToken+Private.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "TOFileSystemNotificationToken.h" 25 | 26 | NS_ASSUME_NONNULL_BEGIN 27 | 28 | /** A protocol denoting that the object can serve notification tokens. */ 29 | @protocol TOFileSystemNotifying 30 | 31 | @required 32 | /** Removes the notification from the observing object. */ 33 | - (void)removeNotificationToken:(TOFileSystemNotificationToken *)token; 34 | 35 | @end 36 | 37 | @interface TOFileSystemNotificationToken () 38 | 39 | /** The object for which this token was generated from. */ 40 | @property (nonatomic, weak, readwrite) id observingObject; 41 | 42 | /** The block that will be triggered each time an event occurs. */ 43 | @property (nonatomic, copy, readwrite) id notificationBlock; 44 | 45 | /** Create a new instance with the observer and the block */ 46 | + (instancetype)tokenWithObservingObject:(id)observingObject 47 | block:(id)block; 48 | 49 | @end 50 | 51 | NS_ASSUME_NONNULL_END 52 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Notifications/TOFileSystemNotificationToken.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemNotificationToken.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | An opaque token object used to track registered notification blocks. 29 | 30 | It is your responsibility to maintain a strong reference to this 31 | object for the duration that you wish to receive events. It will 32 | remove itself from the observing object when deallocated. 33 | */ 34 | NS_SWIFT_NAME(FileSystemNotificationToken) 35 | @interface TOFileSystemNotificationToken : NSObject 36 | 37 | /** 38 | Stops all notifications being made to the block, and removes it 39 | from the file system observer. 40 | */ 41 | - (void)invalidate; 42 | 43 | - (instancetype)init NS_UNAVAILABLE; 44 | 45 | @end 46 | 47 | NS_ASSUME_NONNULL_END 48 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Entities/Notifications/TOFileSystemNotificationToken.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemNotificationToken.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemNotificationToken.h" 24 | #import "TOFileSystemNotificationToken+Private.h" 25 | 26 | @implementation TOFileSystemNotificationToken 27 | 28 | #pragma mark - Class Creation - 29 | 30 | + (instancetype)tokenWithObservingObject:(id)observingObject 31 | block:(id)block 32 | { 33 | TOFileSystemNotificationToken *token = [[TOFileSystemNotificationToken alloc] init]; 34 | token.observingObject = observingObject; 35 | token.notificationBlock = block; 36 | return token; 37 | } 38 | 39 | - (void)dealloc 40 | { 41 | [self invalidate]; 42 | } 43 | 44 | - (void)invalidate 45 | { 46 | [self.observingObject removeNotificationToken:self]; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Scanning/TOFileSystemPresenter.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemPresenter.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | /** 28 | This file presenter object handles coordinating state 29 | and events with the file system. It uses `NSFileCoordinator` 30 | to receive events from the system, when files change, 31 | and also performs coordinated reads and writes for retrieving 32 | UUIDs from the files it manages. 33 | */ 34 | @interface TOFileSystemPresenter : NSObject 35 | 36 | /** The directory that will be observed by this presenter object */ 37 | @property (nonatomic, strong) NSURL *directoryURL; 38 | 39 | /** The presenter is actively listening for events. */ 40 | @property (nonatomic, readonly) BOOL isRunning; 41 | 42 | /** 43 | Since multiple events can come through, a timer is used to 44 | coalesce batches of events and trigger an update periodically. 45 | (Default is 100 miliseconds) 46 | */ 47 | @property (nonatomic, assign) NSTimeInterval timerInterval; 48 | 49 | /** 50 | A block that will be called with all of the collected events. 51 | It will be called on the same operation queue as managed by this class, 52 | so the logic contained should be thread-safe. 53 | */ 54 | @property (nonatomic, copy) void (^itemsDidChangeHandler)(NSArray *itemURLs); 55 | 56 | /** Start listening for file events in the target directory. */ 57 | - (void)start; 58 | 59 | /** Perform a synchronous coordinated read on a file. */ 60 | - (void)performCoordinatedRead:(void (^)(void))block; 61 | 62 | /** Perform a synchronous write operation on a file */ 63 | - (void)performCoordinatedWrite:(void (^)(void))block; 64 | 65 | /** Stop listening and cancel any pending timer events. */ 66 | - (void)stop; 67 | 68 | /** Coordinates reading (and writing if need be) a UUID string for the supplied item */ 69 | - (nullable NSString *)uuidForItemAtURL:(NSURL *)itemURL; 70 | 71 | @end 72 | 73 | NS_ASSUME_NONNULL_END 74 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Scanning/TOFileSystemPresenter.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemPresenter.m 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import "TOFileSystemPresenter.h" 24 | #import "NSURL+TOFileSystemUUID.h" 25 | 26 | @interface TOFileSystemPresenter () 27 | 28 | /** The presenter is actively listening for events. */ 29 | @property (nonatomic, assign, readwrite) BOOL isRunning; 30 | 31 | /** The operation queue that will receive all of the file events*/ 32 | @property (nonatomic, strong) NSOperationQueue *eventsOperationQueue; 33 | 34 | /** The list of items currently detected. */ 35 | @property (nonatomic, strong) NSMutableArray *items; 36 | 37 | /** A serial queue for managing access to the list (including the timer) */ 38 | @property (nonatomic, strong) dispatch_queue_t itemListAccessQueue; 39 | 40 | /** Whether a timer has been set yet or not */ 41 | @property (nonatomic, assign) BOOL isTiming; 42 | 43 | /** A concurrent queue used to coordinate writing UUIDs to files. */ 44 | @property (nonatomic, readonly) dispatch_queue_t fileCoordinatorQueue; 45 | 46 | @end 47 | 48 | @implementation TOFileSystemPresenter 49 | 50 | #pragma mark - Class Lifecycle - 51 | 52 | - (instancetype)init 53 | { 54 | if (self = [super init]) { 55 | [self commonInit]; 56 | } 57 | 58 | return self; 59 | } 60 | 61 | - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL 62 | { 63 | if (self = [super init]) { 64 | _directoryURL = directoryURL; 65 | [self commonInit]; 66 | } 67 | 68 | return self; 69 | } 70 | 71 | - (dispatch_queue_t)fileCoordinatorQueue 72 | { 73 | // In case we have multiple file observers, we must share this 74 | // coordinator amongst all of them in case two separate instances 75 | // try and write to the same file. 76 | static dispatch_queue_t _fileCoordinatorQueue = NULL; 77 | static dispatch_once_t onceToken; 78 | dispatch_once(&onceToken, ^{ 79 | _fileCoordinatorQueue = dispatch_queue_create("TOFileSystemObserver.fileCoordinatorQueue", 80 | DISPATCH_QUEUE_CONCURRENT); 81 | }); 82 | return _fileCoordinatorQueue; 83 | } 84 | 85 | - (void)commonInit 86 | { 87 | // Create the queue to receive events 88 | _eventsOperationQueue = [[NSOperationQueue alloc] init]; 89 | _eventsOperationQueue.qualityOfService = NSQualityOfServiceBackground; 90 | 91 | // Create the array to hold the items detected 92 | _items = [NSMutableArray array]; 93 | 94 | // Create the dispatch queue for the items 95 | _itemListAccessQueue = dispatch_queue_create("TOFileSystemObserver.itemListAccessQueue", DISPATCH_QUEUE_SERIAL); 96 | 97 | // Default time interval 98 | _timerInterval = 0.1f; 99 | } 100 | 101 | - (void)dealloc 102 | { 103 | [self stop]; 104 | } 105 | 106 | #pragma mark - Timer Handling - 107 | 108 | - (void)beginTimer 109 | { 110 | // When the timer finishes, create a copy of the items, 111 | // and then flush what we currently have in the main item list 112 | id completionBlock = ^{ 113 | if (!self.isRunning) { return; } 114 | self.isTiming = NO; 115 | 116 | @autoreleasepool { 117 | NSArray *items = [self.items copy]; 118 | [self.items removeAllObjects]; 119 | if (items.count == 0) { return; } 120 | 121 | if (self.itemsDidChangeHandler) { 122 | self.itemsDidChangeHandler(items); 123 | } 124 | } 125 | }; 126 | 127 | id timerBlock = ^{ 128 | // Cancel if timing has already been started 129 | if (self.isTiming) { return; } 130 | self.isTiming = YES; 131 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 132 | (int64_t)(self.timerInterval * NSEC_PER_SEC)), 133 | self.itemListAccessQueue, 134 | completionBlock); 135 | }; 136 | 137 | dispatch_async(self.itemListAccessQueue, timerBlock); 138 | } 139 | 140 | #pragma mark - Item Handling - 141 | 142 | - (void)addItemToList:(NSURL *)itemURL 143 | { 144 | // Add the new item to the items list in a barrier queue access. 145 | dispatch_async(self.itemListAccessQueue, ^{ 146 | [self.items addObject:itemURL]; 147 | }); 148 | } 149 | 150 | #pragma mark - Public Control - 151 | 152 | - (void)start 153 | { 154 | if (self.isRunning) { return; } 155 | [NSFileCoordinator addFilePresenter:self]; 156 | self.isRunning = YES; 157 | } 158 | 159 | - (void)performCoordinatedRead:(void (^)(void))block 160 | { 161 | dispatch_sync(self.fileCoordinatorQueue, ^{ 162 | @autoreleasepool { 163 | if (block) { block(); } 164 | } 165 | }); 166 | } 167 | 168 | - (void)performCoordinatedWrite:(void (^)(void))block 169 | { 170 | dispatch_barrier_sync(self.fileCoordinatorQueue, ^{ 171 | @autoreleasepool { 172 | if (block) { block(); } 173 | } 174 | }); 175 | } 176 | 177 | - (void)stop 178 | { 179 | if (!self.isRunning) { return; } 180 | [NSFileCoordinator removeFilePresenter:self]; 181 | self.isRunning = NO; 182 | } 183 | 184 | - (nullable NSString *)uuidForItemAtURL:(NSURL *)itemURL 185 | { 186 | __block NSString *uuid = nil; 187 | 188 | // If the file exists, but it's not in the store yet, 189 | // attempt to access it from disk 190 | [self performCoordinatedRead:^{ 191 | uuid = [itemURL to_fileSystemUUID]; 192 | }]; 193 | if (uuid.length) { return uuid; } 194 | 195 | // If even that failed, then it's necessary to generate a new one 196 | [self performCoordinatedWrite:^{ 197 | // Try again in case a previous operation already generated one 198 | uuid = [itemURL to_fileSystemUUID]; 199 | if (uuid.length == 0) { 200 | uuid = [itemURL to_generateFileSystemUUID]; 201 | } 202 | }]; 203 | 204 | return uuid; 205 | } 206 | 207 | #pragma mark - NSFilePresenter Delegate Events - 208 | 209 | - (void)presentedSubitemDidChangeAtURL:(NSURL *)url 210 | { 211 | [self addItemToList:url]; 212 | [self beginTimer]; 213 | } 214 | 215 | - (NSURL *)presentedItemURL 216 | { 217 | return self.directoryURL; 218 | } 219 | 220 | - (NSOperationQueue *)presentedItemOperationQueue 221 | { 222 | return self.eventsOperationQueue; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Scanning/TOFileSystemScanOperation.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemScanOperation.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @class TOFileSystemPresenter; 26 | @class TOFileSystemItemURLDictionary; 27 | @class TOFileSystemScanOperation; 28 | 29 | NS_ASSUME_NONNULL_BEGIN 30 | 31 | @protocol TOFileSystemScanOperationDelegate 32 | 33 | @required 34 | 35 | /** 36 | Called when a new item is discovered is discovered during a scan. 37 | This is called every time for full-system scans, but will then only be called on items not previously found before 38 | in subsequent scans. 39 | */ 40 | - (void)scanOperation:(TOFileSystemScanOperation *)scanOperation didDiscoverItemAtURL:(NSURL *)itemURL withUUID:(NSString *)uuid; 41 | 42 | /** Called when the properties of an object have been changed (eg, renamed etc) */ 43 | - (void)scanOperation:(TOFileSystemScanOperation *)scanOperation itemDidChangeAtURL:(NSURL *)itemURL withUUID:(NSString *)uuid; 44 | 45 | /** Called when the file has been moved to another part of the sandbox. */ 46 | - (void)scanOperation:(TOFileSystemScanOperation *)scanOperation itemWithUUID:(NSString *)uuid 47 | didMoveFromURL:(NSURL *)previousURL 48 | toURL:(NSURL *)url; 49 | 50 | /** Called when the file has been deleted. */ 51 | - (void)scanOperation:(TOFileSystemScanOperation *)scanOperation didDeleteItemAtURL:(NSURL *)itemURL withUUID:(NSString *)uuid; 52 | 53 | /** Called before a full directory scan has started to allow any delegates to prepare in advance. */ 54 | - (void)scanOperationWillBeginFullScan:(TOFileSystemScanOperation *)scanOperation; 55 | 56 | /** Called when a full directory scan has been completed so we can do some final clean-up. */ 57 | - (void)scanOperationDidCompleteFullScan:(TOFileSystemScanOperation *)scanOperation; 58 | 59 | @end 60 | 61 | /** 62 | An operation that will either scan all 63 | child items of a directory, or a list of items 64 | and update their snapshot if they have changed. 65 | */ 66 | @interface TOFileSystemScanOperation : NSOperation 67 | 68 | /** Whether this a complete deep scan or not. */ 69 | @property (nonatomic, readonly) BOOL isFullScan; 70 | 71 | /** A delegate object that will be called upon any detected change events. */ 72 | @property (nonatomic, weak) id delegate; 73 | 74 | /** When scanning hierarchies, the numbers deep to scan (-1 is all of them) */ 75 | @property (nonatomic, assign) NSInteger subDirectoryLevelLimit; 76 | 77 | /** Create a new instance that will scan all of the child items of the provided directory */ 78 | - (instancetype)initForFullScanWithDirectoryAtURL:(NSURL *)directoryURL 79 | skippingItems:(NSArray *)skippedItems 80 | allItemsDictionary:(nonnull TOFileSystemItemURLDictionary *)allItems 81 | filePresenter:(TOFileSystemPresenter *)filePresenter; 82 | 83 | /** Create a new instance that will scan all of the files/folders provided. */ 84 | - (instancetype)initForItemScanWithItemURLs:(NSArray *)itemURLs 85 | baseURL:(NSURL *)baseURL 86 | skippingItems:(NSArray *)skippedItems 87 | allItemsDictionary:(nonnull TOFileSystemItemURLDictionary *)allItems 88 | filePresenter:(TOFileSystemPresenter *)filePresenter; 89 | 90 | @end 91 | 92 | NS_ASSUME_NONNULL_END 93 | -------------------------------------------------------------------------------- /TOFileSystemObserver/TOFileSystemObserver.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemObserver.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | #import "TOFileSystemItemList.h" 26 | #import "TOFileSystemItem.h" 27 | #import "TOFileSystemNotificationToken.h" 28 | #import "TOFileSystemItemListChanges.h" 29 | #import "TOFileSystemChanges.h" 30 | #import "TOFileSystemObserverConstants.h" 31 | 32 | NS_ASSUME_NONNULL_BEGIN 33 | 34 | @class TOFileSystemObserver; 35 | 36 | NS_SWIFT_NAME(FileSystemObserver) 37 | @interface TOFileSystemObserver : NSObject 38 | 39 | /** Whether the observer is currently active and observing its target directory. */ 40 | @property (nonatomic, readonly) BOOL isRunning; 41 | 42 | /** 43 | The absolute file path to the directory that is being observed. 44 | It may only be set while the observer isn't running. (Default is the Documents directory). 45 | */ 46 | @property (nonatomic, strong) NSURL *directoryURL; 47 | 48 | /** 49 | Optionally, a list of relative file paths from `directoryURL` to directories that 50 | will not be monitored. By default, this includes the 'Inbox' directory in the 51 | Documents directory. 52 | */ 53 | @property (nonatomic, strong, nullable) NSArray *excludedItems; 54 | 55 | /** 56 | Optionally, the number of directory levels down that the observer will recursively traverse from the base target. 57 | This is useful to avoid performance overhead in instances such as if your app only has one level of directories. (Default is 0). 58 | */ 59 | @property (nonatomic, assign) NSInteger includedDirectoryLevels; 60 | 61 | /** 62 | The item that represents the base directory that was set to be observed 63 | by this file system observer. 64 | 65 | (This item will be refetched from the database each time it is called, 66 | so it is completely thread-safe. Like most multi-threading cases, please ensure it 67 | is kept in an autorelease pool in dispatch queues.) 68 | */ 69 | @property (nonatomic, readonly) TOFileSystemItem *directoryItem; 70 | 71 | /** 72 | Turns on system-wide `NSNotification` broadcast events whenever a change is 73 | detected. This is YES for the singleton instance by default, but NO for all other instances. 74 | */ 75 | @property (nonatomic, assign) BOOL broadcastsNotifications; 76 | 77 | /** Create a new instance of the observer with the base URL that will be observed. */ 78 | - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL; 79 | 80 | /** A singleton instance that can be accessed globally. It is created the first time this is called. */ 81 | + (instancetype)sharedObserver; 82 | 83 | /** If desired, promotes a locally created and configured observer to the singleton. */ 84 | + (void)setSharedObserver:(TOFileSystemObserver *)observer; 85 | 86 | /** 87 | Starts the file system observer monitoring the target directory for changes. 88 | Upon starting, the observer will perform a full file system scan, and will post a 'did discover' 89 | event for every item it finds. This can be used in order to ensure any caches, or previously 90 | expected locations of files can be updated. 91 | */ 92 | - (void)start; 93 | 94 | /** Completely stops the file observer from running and resets all of the internal state. When 95 | calling 'start' from after this state, another full file system scan will be performed. */ 96 | - (void)stop; 97 | 98 | /** 99 | Returns a list of directories and files inside the directory specified. 100 | While the file observer is running, this list is live, and will be automatically 101 | updated whenever any of the underlying files on disk are changed. 102 | 103 | @param directoryURL The URL to target. Use `nil` for the observer's base directory. 104 | */ 105 | - (nullable TOFileSystemItemList *)itemListForDirectoryAtURL:(nullable NSURL *)directoryURL; 106 | 107 | /** 108 | Returns an item object representing the file or directory at the URL specified. 109 | While the file observer is running, this object is live, and will be automatically 110 | updated whenever the system detects that the file has changed. 111 | 112 | @param fileURL The URL to target. 113 | */ 114 | - (nullable TOFileSystemItem *)itemForFileAtURL:(NSURL *)fileURL; 115 | 116 | /** 117 | Returns the unique UUID string that's been associated with the file at the provided URL from disk. 118 | This will attempt to retrieve the UUID while avoiding performing a file read if it can help it. 119 | If the file has not yet had a UUID string assigned, it will generate a new one in a thread-safe manner. 120 | 121 | @param itemURL The url of the item whose UUID that will be accessed. 122 | @return The UUID string associated with the file on disk. Returns nil if the URL is invalid. 123 | */ 124 | - (nullable NSString *)uuidForItemAtURL:(NSURL *)itemURL; 125 | 126 | /** 127 | Returns the unique UUID for the parent directory of the item at the provided URL. 128 | This will attempt to retrieve the UUID while avoiding performing a file read if it can help it. 129 | If the item doesn't yet have a UUID, it will create one in a thread safe manner. 130 | */ 131 | - (nullable NSString *)uuidForParentOfItemAtURL:(NSURL *)itemURL; 132 | 133 | /** 134 | Registers a new notification block that will be triggered each time an update is detected. 135 | It is your responsibility to strongly retain the token object, and release it only 136 | when you wish to stop receiving notifications. 137 | 138 | @param block A block that will be called each time a file system event is detected. 139 | */ 140 | - (TOFileSystemNotificationToken *)addNotificationBlock:(TOFileSystemNotificationBlock)block; 141 | 142 | /** 143 | When a notification token is no longer needed, it can be removed from the observer 144 | and freed from memory. This method will automatically be called if `invalidate` is 145 | called from the token. 146 | 147 | @param token The token that will be removed from the observer and deallocated. 148 | */ 149 | - (void)removeNotificationToken:(TOFileSystemNotificationToken *)token; 150 | 151 | @end 152 | 153 | FOUNDATION_EXPORT double TOFileSystemObserverVersionNumber; 154 | FOUNDATION_EXPORT const unsigned char TOFileSystemObserverVersionString[]; 155 | 156 | NS_ASSUME_NONNULL_END 157 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Utilities/TOFileSystemObserver+AppKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemObserver+UIKit.h 3 | // 4 | // Copyright 2019-2022 Anatoly Rosencrantz, Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | // 24 | // TOFileSystemObserver+UIKit.h 25 | // TOFileSystemObserverMacExample 26 | // 27 | // Created by Anatoly Rosencrantz on 26/03/2020. 28 | // Copyright © 2020 Tim Oliver. All rights reserved. 29 | // 30 | 31 | #if TARGET_OS_OSX 32 | 33 | #import 34 | #import 35 | #import "TOFileSystemItemListChanges.h" 36 | 37 | /// A convenience function to update a standard `NSTableView` with a new set 38 | /// of changes reported by a file system observer. 39 | /// @param tableView The `NSTableView` to update 40 | /// @param changes The changes that were returned from the file system obserrver. 41 | /// @param section The section in the table view that the changes should be applied to. 42 | static inline void TOFileSystemItemListUpdateTableView(NSTableView * _Nonnull tableView, 43 | TOFileSystemItemListChanges * _Nonnull changes, 44 | NSInteger section) 45 | { 46 | // Perform any cell re-ordering in its own batch operation first 47 | if (changes.hasItemMovements) { 48 | [tableView beginUpdates]; 49 | { 50 | NSArray *sourceMovements = [changes indexPathsForMovementSourcesInSection:section]; 51 | NSArray *destinationMovements = [changes indexPathsForMovementDestinationsWithSourceIndexPaths:sourceMovements]; 52 | for (NSInteger i = 0; i < sourceMovements.count; i++) { 53 | [tableView moveRowAtIndex:sourceMovements[i].item toIndex:destinationMovements[i].item]; 54 | } 55 | } 56 | [tableView endUpdates]; 57 | } 58 | 59 | // Perform all additional cell update operations afterwards 60 | if (changes.hasItemChanges) { 61 | [tableView beginUpdates]; 62 | { 63 | NSMutableIndexSet *changesList = [[NSMutableIndexSet alloc] init]; 64 | [[changes indexPathsForDeletionsInSection:section] enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 65 | [changesList addIndex:(NSUInteger)obj.item]; 66 | }]; 67 | [tableView removeRowsAtIndexes:changesList withAnimation:NSTableViewAnimationEffectFade]; 68 | 69 | NSMutableIndexSet *insertsList = [[NSMutableIndexSet alloc] init]; 70 | [[changes indexPathsForInsertionsInSection:section] enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 71 | [insertsList addIndex:(NSUInteger)obj.item]; 72 | }]; 73 | [tableView insertRowsAtIndexes:insertsList withAnimation:NSTableViewAnimationEffectFade]; 74 | 75 | NSMutableIndexSet *reloadsList = [[NSMutableIndexSet alloc] init]; 76 | [[changes indexPathsForModificationsInSection:section] enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 77 | [reloadsList addIndex:(NSUInteger)obj.item]; 78 | }]; 79 | [tableView reloadDataForRowIndexes:reloadsList columnIndexes:[NSIndexSet indexSetWithIndex:0]]; 80 | } 81 | [tableView endUpdates]; 82 | } 83 | } 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Utilities/TOFileSystemObserver+UIKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemObserver+UIKit.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #if TARGET_OS_IOS 24 | 25 | #import 26 | #import 27 | #import "TOFileSystemItemListChanges.h" 28 | 29 | /// A convenience function to update a standard `UITableView` with a new set 30 | /// of changes reported by a file system observer. 31 | /// @param tableView The `UITableView` to update 32 | /// @param changes The changes that were returned from the file system obserrver. 33 | /// @param section The section in the table view that the changes should be applied to. 34 | static inline void TOFileSystemItemListUpdateTableView(UITableView * _Nonnull tableView, 35 | TOFileSystemItemListChanges * _Nonnull changes, 36 | NSInteger section) 37 | { 38 | // Perform any cell re-ordering in its own batch operation first 39 | if (changes.hasItemMovements) { 40 | [tableView beginUpdates]; 41 | { 42 | NSArray *sourceMovements = [changes indexPathsForMovementSourcesInSection:section]; 43 | NSArray *destinationMovements = [changes indexPathsForMovementDestinationsWithSourceIndexPaths:sourceMovements]; 44 | for (NSInteger i = 0; i < sourceMovements.count; i++) { 45 | [tableView moveRowAtIndexPath:sourceMovements[i] toIndexPath:destinationMovements[i]]; 46 | } 47 | } 48 | [tableView endUpdates]; 49 | } 50 | 51 | // Perform all additional cell update operations afterwards 52 | if (changes.hasItemChanges) { 53 | [tableView beginUpdates]; 54 | { 55 | [tableView deleteRowsAtIndexPaths:[changes indexPathsForDeletionsInSection:section] 56 | withRowAnimation:UITableViewRowAnimationAutomatic]; 57 | [tableView insertRowsAtIndexPaths:[changes indexPathsForInsertionsInSection:section] 58 | withRowAnimation:UITableViewRowAnimationAutomatic]; 59 | [tableView reloadRowsAtIndexPaths:[changes indexPathsForModificationsInSection:section] 60 | withRowAnimation:UITableViewRowAnimationAutomatic]; 61 | } 62 | [tableView endUpdates]; 63 | } 64 | } 65 | 66 | /// A convenience function to update a standard `UICollectionView` with a new set 67 | /// of changes reported by a file system observer. 68 | /// @param collectionView The `UICollectionView` to update 69 | /// @param changes The changes that were returned from the file system obserrver. 70 | /// @param section The section in the table view that the changes should be applied to. 71 | static inline void TOFileSystemItemListUpdateCollectionView(UICollectionView * _Nonnull collectionView, 72 | TOFileSystemItemListChanges * _Nonnull changes, 73 | NSInteger section) 74 | { 75 | // Perform any cell re-ordering in its own batch operation first 76 | [collectionView performBatchUpdates:^{ 77 | NSArray *sourceMovements = [changes indexPathsForMovementSourcesInSection:section]; 78 | NSArray *destinationMovements = [changes indexPathsForMovementDestinationsWithSourceIndexPaths:sourceMovements]; 79 | for (NSInteger i = 0; i < sourceMovements.count; i++) { 80 | [collectionView moveItemAtIndexPath:sourceMovements[i] toIndexPath:destinationMovements[i]]; 81 | } 82 | } completion:nil]; 83 | 84 | // Perform all additional cell update operations afterwards 85 | [collectionView performBatchUpdates:^{ 86 | [collectionView deleteItemsAtIndexPaths:[changes indexPathsForDeletionsInSection:section]]; 87 | [collectionView insertItemsAtIndexPaths:[changes indexPathsForInsertionsInSection:section]]; 88 | [collectionView reloadItemsAtIndexPaths:[changes indexPathsForModificationsInSection:section]]; 89 | } completion:nil]; 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /TOFileSystemObserver/Utilities/TOFileSystemObserverConstants.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemObserverConstants.h 3 | // 4 | // Copyright 2019-2022 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | @class TOFileSystemObserver; 26 | @class TOFileSystemChanges; 27 | @class TOFileSystemItemList; 28 | @class TOFileSystemItemListChanges; 29 | 30 | NS_ASSUME_NONNULL_BEGIN 31 | 32 | /** When calling a notification block, these are the types of events available. */ 33 | typedef NS_ENUM(NSInteger, TOFileSystemObserverNotificationType) { 34 | TOFileSystemObserverNotificationTypeDidChange, // The scanner detected a file change 35 | TOFileSystemObserverNotificationTypeWillBeginFullScan, // The scanner is about to perform a full-scan 36 | TOFileSystemObserverNotificationTypeDidCompleteFullScan // The scanner just completed a full scan 37 | } NS_SWIFT_NAME(FileSystemObserver.NotificationType); 38 | 39 | /** The different options for ordering item lists */ 40 | typedef NS_ENUM(NSInteger, TOFileSystemItemListOrder) { 41 | TOFileSystemItemListOrderAlphanumeric, // Alphanumeric ordering 42 | TOFileSystemItemListOrderDate, // Creation date 43 | TOFileSystemItemListOrderSize // File size 44 | } NS_SWIFT_NAME(FileSystemItemList.Order); 45 | 46 | // The different types of items stored in the file system 47 | typedef NS_ENUM(NSInteger, TOFileSystemItemType) { 48 | TOFileSystemItemTypeFile, // A standard file 49 | TOFileSystemItemTypeDirectory // A folder 50 | } NS_SWIFT_NAME(FileSystemItem.Type); 51 | 52 | /** Whenever a scan event occurs, all registered notification blocks will be called with the change information. */ 53 | typedef void (^TOFileSystemNotificationBlock)(TOFileSystemObserver * _Nonnull observer, 54 | TOFileSystemObserverNotificationType type, 55 | TOFileSystemChanges * _Nullable changes) 56 | NS_SWIFT_NAME(FileSystemNotificationBlock); 57 | 58 | /** A block that may be registered in order to observe when items in the list change. */ 59 | typedef void (^TOFileSystemItemListNotificationBlock)(TOFileSystemItemList * _Nonnull itemList, 60 | TOFileSystemItemListChanges * _Nullable changes) 61 | NS_SWIFT_NAME(FileSystemItemListNotificationBlock); 62 | 63 | /** 64 | A notification that will be broadcast before a full file system scan starts. 65 | It will be called on the same thread as the scan operation. 66 | */ 67 | extern NSNotificationName const TOFileSystemObserverWillBeginFullScanNotification 68 | NS_SWIFT_NAME(FileSystemObserverWillBeginFullScanNotification); 69 | 70 | /** 71 | A notification that will be broadcast once a full scan has completed. 72 | It will be called on the same thread as the scan operation. 73 | */ 74 | extern NSNotificationName const TOFileSystemObserverDidCompleteFullScanNotification 75 | NS_SWIFT_NAME(FileSystemObserverWillDidCompleteScanNotification); 76 | 77 | /** 78 | A notification that will be broadcast when a file change was observed. 79 | 80 | It will be called on the same thread as the scan operation. 81 | */ 82 | extern NSNotificationName const TOFileSystemObserverDidChangeNotification 83 | NS_SWIFT_NAME(FileSystemObserverDidChangeNotification); 84 | 85 | /** 86 | When a notification is broadcasted from an observer, a reference to that 87 | observer object will be included in `userInfo` under this key. 88 | */ 89 | extern NSString * const TOFileSystemObserverUserInfoKey 90 | NS_SWIFT_NAME(FileSystemObserverUserInfoKey); 91 | 92 | /** 93 | When a notification is broadcasted, all of the changes that were detected 94 | will be provided in the notification `userInfo` dictionary under this key. 95 | */ 96 | extern NSString * const TOFileSystemObserverChangesUserInfoKey 97 | NS_SWIFT_NAME(FileSystemObserverChangesUserInfoKey); 98 | 99 | /** 100 | When checking for if a file is still copying, a little bit of a delay is used when 101 | comparing the file's modification date to the current date since there will always be 102 | around a second of drift. 103 | */ 104 | static NSTimeInterval const kTOFileSystemObserverCopyingTimeDelay = 3.0f; 105 | 106 | NS_ASSUME_NONNULL_END 107 | -------------------------------------------------------------------------------- /TOFileSystemObserver/include/NSFileManager+TOFileSystemDirectoryEnumerator.h: -------------------------------------------------------------------------------- 1 | ../Categories/NSFileManager+TOFileSystemDirectoryEnumerator.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/NSIndexPath+AppKitAdditions.h: -------------------------------------------------------------------------------- 1 | ../Categories/NSIndexPath+AppKitAdditions.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/NSURL+TOFileSystemAttributes.h: -------------------------------------------------------------------------------- 1 | ../Categories/NSURL+TOFileSystemAttributes.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/NSURL+TOFileSystemUUID.h: -------------------------------------------------------------------------------- 1 | ../Categories/NSURL+TOFileSystemUUID.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemChanges+Private.h: -------------------------------------------------------------------------------- 1 | ../Entities/Changes/TOFileSystemChanges+Private.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemChanges.h: -------------------------------------------------------------------------------- 1 | ../Entities/Changes/TOFileSystemChanges.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItem+Private.h: -------------------------------------------------------------------------------- 1 | ../Entities/Items/TOFileSystemItem+Private.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItem.h: -------------------------------------------------------------------------------- 1 | ../Entities/Items/TOFileSystemItem.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemList+Private.h: -------------------------------------------------------------------------------- 1 | ../Entities/Items/TOFileSystemItemList+Private.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemList.h: -------------------------------------------------------------------------------- 1 | ../Entities/Items/TOFileSystemItemList.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemListChanges+Private.h: -------------------------------------------------------------------------------- 1 | ../Entities/Changes/TOFileSystemItemListChanges+Private.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemListChanges.h: -------------------------------------------------------------------------------- 1 | ../Entities/Changes/TOFileSystemItemListChanges.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemMapTable.h: -------------------------------------------------------------------------------- 1 | ../Entities/Collections/TOFileSystemItemMapTable.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemItemURLDictionary.h: -------------------------------------------------------------------------------- 1 | ../Entities/Collections/TOFileSystemItemURLDictionary.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemNotificationToken+Private.h: -------------------------------------------------------------------------------- 1 | ../Entities/Notifications/TOFileSystemNotificationToken+Private.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemNotificationToken.h: -------------------------------------------------------------------------------- 1 | ../Entities/Notifications/TOFileSystemNotificationToken.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemObserver+AppKit.h: -------------------------------------------------------------------------------- 1 | ../Utilities/TOFileSystemObserver+AppKit.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemObserver+UIKit.h: -------------------------------------------------------------------------------- 1 | ../Utilities/TOFileSystemObserver+UIKit.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemObserver.h: -------------------------------------------------------------------------------- 1 | ../TOFileSystemObserver.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemObserverConstants.h: -------------------------------------------------------------------------------- 1 | ../Utilities/TOFileSystemObserverConstants.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemPath.h: -------------------------------------------------------------------------------- 1 | ../Entities/FilePaths/TOFileSystemPath.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemPresenter.h: -------------------------------------------------------------------------------- 1 | ../Scanning/TOFileSystemPresenter.h -------------------------------------------------------------------------------- /TOFileSystemObserver/include/TOFileSystemScanOperation.h: -------------------------------------------------------------------------------- 1 | ../Scanning/TOFileSystemScanOperation.h -------------------------------------------------------------------------------- /TOFileSystemObserverExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample.xcodeproj/xcshareddata/xcschemes/TOFileSystemObserverExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample.xcodeproj/xcshareddata/xcschemes/TOFileSystemObserverMacExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample.xcodeproj/xcshareddata/xcschemes/TOFileSystemObserverTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 64 | 65 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TOFileSystemObserverExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TOFileSystemObserverExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | LSSupportsOpeningDocumentsInPlace 24 | 25 | UIFileSharingEnabled 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/TOAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 28/9/19. 6 | // Copyright © 2019 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TOAppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/TOAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 28/9/19. 6 | // Copyright © 2019 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "TOAppDelegate.h" 10 | #import "TOViewController.h" 11 | #import "TOFileSystemPath.h" 12 | 13 | @implementation TOAppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 16 | self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; 17 | 18 | [self setUpDefaultData]; 19 | 20 | TOViewController *viewController = [[TOViewController alloc] init]; 21 | UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; 22 | self.window.rootViewController = navigationController; 23 | [self.window makeKeyAndVisible]; 24 | 25 | return YES; 26 | } 27 | 28 | - (void)setUpDefaultData 29 | { 30 | NSURL *documentsDirectory = [TOFileSystemPath documentsDirectoryURL]; 31 | NSFileManager *fileManager = [NSFileManager defaultManager]; 32 | NSInteger numberOfItems = [fileManager contentsOfDirectoryAtURL:documentsDirectory 33 | includingPropertiesForKeys:nil 34 | options:0 35 | error:nil].count; 36 | if (numberOfItems > 1) { return; } 37 | 38 | // Create 5 folders 39 | for (NSInteger i = 0; i < 5; i++) { 40 | NSString *folderName = [NSString stringWithFormat:@"Folder %d", (int)i+1]; 41 | NSURL *folderURL = [documentsDirectory URLByAppendingPathComponent:folderName]; 42 | [fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:nil]; 43 | } 44 | 45 | // Create 5 arbitrary files 46 | NSArray *sizes = @[@(5), @(12), @(33), @(15), @(20)]; 47 | for (NSInteger i = 0; i < sizes.count; i++) { 48 | NSString *fileName = [NSString stringWithFormat:@"File-%d.dat", (int)i+1]; 49 | NSURL *fileURL = [documentsDirectory URLByAppendingPathComponent:fileName]; 50 | FILE *file = fopen([fileURL.path cStringUsingEncoding:NSUTF8StringEncoding], "wb"); 51 | 52 | NSInteger byteCount = [sizes[i] intValue] * 1000000; 53 | for (NSInteger j = 0; j < byteCount; j++) { 54 | fwrite("0", 1, 1, file); 55 | } 56 | fclose(file); 57 | } 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/TOImages.h: -------------------------------------------------------------------------------- 1 | // 2 | // TOImages.h 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 2020/01/27. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TOImages : UIImage 14 | 15 | + (UIImage *)downloadIcon; 16 | 17 | + (UIImage *)documentPickerDefaultFolderForStyle:(BOOL)darkMode; 18 | 19 | + (UIImage *)documentPickerFolderIconWithSize:(CGSize)size 20 | backgroundColor:(UIColor *)backgroundColor 21 | foregroundBottomColor:(UIColor *)foregroundBottomColor 22 | foregroundTopColor:(UIColor *)foregroundTopColor; 23 | 24 | + (UIImage *)documentPickerDefaultFileIconWithExtension:(NSString *)extension 25 | tintColor:(UIColor *)tintColor 26 | style:(BOOL)darkMode; 27 | 28 | + (UIImage *)documentPickerIconWithSize:(CGSize)size 29 | outlineColor:(UIColor *)outlineColor 30 | backgroundColor:(UIColor *)backgroundColor 31 | cornerColor:(UIColor *)cornerColor 32 | formatNameString:(NSString *)formatNameString 33 | formatNameFont:(UIFont *)formatNameFont 34 | formatNameColor:(UIColor *)formatNameColor; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/TOViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 28/9/19. 6 | // Copyright © 2019 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TOViewController : UITableViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/TOViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 28/9/19. 6 | // Copyright © 2019 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "TOViewController.h" 10 | 11 | #import "TOImages.h" 12 | #import "TOFileSystemObserver.h" 13 | #import "TOFileSystemObserver+UIKit.h" 14 | 15 | @interface TOViewController () 16 | 17 | @property (nonatomic, strong) TOFileSystemObserver *observer; 18 | @property (nonatomic, strong) TOFileSystemItemList *fileItemList; 19 | @property (nonatomic, strong) TOFileSystemNotificationToken *listToken; 20 | @property (nonatomic, strong) TOFileSystemNotificationToken *observerToken; 21 | 22 | @property (nonatomic, strong) UIImage *folderIcon; 23 | @property (nonatomic, strong) UIImage *fileIcon; 24 | 25 | @end 26 | 27 | @implementation TOViewController 28 | 29 | - (void)viewDidLoad { 30 | [super viewDidLoad]; 31 | 32 | self.title = @"TOFileSystemObserver"; 33 | self.tableView.rowHeight = 74; 34 | 35 | self.folderIcon = [TOImages documentPickerDefaultFolderForStyle:NO]; 36 | self.fileIcon = [TOImages documentPickerDefaultFileIconWithExtension:@"" tintColor:self.view.tintColor style:NO]; 37 | 38 | // Create a file system observer and start it 39 | self.observer = [[TOFileSystemObserver alloc] init]; 40 | [self.observer start]; 41 | 42 | // Create a live list of the base folder for this controller 43 | self.fileItemList = [self.observer itemListForDirectoryAtURL:nil]; 44 | 45 | __weak typeof(self) weakSelf = self; 46 | self.listToken = [self.fileItemList addNotificationBlock:^(TOFileSystemItemList *itemList, 47 | TOFileSystemItemListChanges *changes) 48 | { 49 | TOFileSystemItemListUpdateTableView(weakSelf.tableView, changes, 0); 50 | }]; 51 | 52 | self.observerToken = [self.observer addNotificationBlock:^(TOFileSystemObserver *observer, 53 | TOFileSystemObserverNotificationType type, 54 | TOFileSystemChanges *changes) 55 | { 56 | if (type == TOFileSystemObserverNotificationTypeWillBeginFullScan) { 57 | NSLog(@"Scan Will Start!"); 58 | return; 59 | } 60 | 61 | if (type == TOFileSystemObserverNotificationTypeDidCompleteFullScan) { 62 | NSLog(@"Scan Complete!"); 63 | return; 64 | } 65 | 66 | NSLog(@"%@", changes); 67 | }]; 68 | 69 | // Add test button for flipping direction 70 | UIBarButtonItem *leftButton = [[UIBarButtonItem alloc] initWithTitle:@"Asc" 71 | style:UIBarButtonItemStylePlain 72 | target:self 73 | action:@selector(leftButtonTapped:)]; 74 | self.navigationItem.leftBarButtonItem = leftButton; 75 | 76 | // Add test button for rotating list order 77 | UIBarButtonItem *rightButton = [[UIBarButtonItem alloc] initWithTitle:@"Name" 78 | style:UIBarButtonItemStylePlain 79 | target:self 80 | action:@selector(rightButtonTapped:)]; 81 | self.navigationItem.rightBarButtonItem = rightButton; 82 | } 83 | 84 | - (void)leftButtonTapped:(id)sender 85 | { 86 | BOOL descending = !self.fileItemList.isDescending; 87 | UIBarButtonItem *item = (UIBarButtonItem *)sender; 88 | item.title = descending ? @"Desc" : @"Asc"; 89 | self.fileItemList.isDescending = descending; 90 | } 91 | 92 | - (void)rightButtonTapped:(id)sender 93 | { 94 | TOFileSystemItemListOrder order = self.fileItemList.listOrder + 1; 95 | if (order > TOFileSystemItemListOrderSize) { order = 0; } 96 | 97 | UIBarButtonItem *item = (UIBarButtonItem *)sender; 98 | switch (order) { 99 | case TOFileSystemItemListOrderSize: 100 | item.title = @"Size"; 101 | break; 102 | case TOFileSystemItemListOrderDate: 103 | item.title = @"Date"; 104 | break; 105 | default: 106 | item.title = @"Name"; 107 | break; 108 | } 109 | 110 | self.fileItemList.listOrder = order; 111 | } 112 | 113 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 114 | { 115 | return 1; 116 | } 117 | 118 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 119 | { 120 | return self.fileItemList.count; 121 | } 122 | 123 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 124 | { 125 | static NSString *cellIdentifier = @"Cell"; 126 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 127 | if (cell == nil) { 128 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier]; 129 | } 130 | 131 | TOFileSystemItem *fileItem = self.fileItemList[indexPath.row]; 132 | cell.textLabel.text = fileItem.name; 133 | 134 | if (fileItem.isCopying) { 135 | cell.detailTextLabel.text = @"Copying"; 136 | } 137 | else { 138 | if (fileItem.type == TOFileSystemItemTypeDirectory) { 139 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 140 | if (fileItem.numberOfSubItems == 1) { 141 | cell.detailTextLabel.text = @"1 item"; 142 | } 143 | else { 144 | cell.detailTextLabel.text = [NSString stringWithFormat:@"%ld items", (long)fileItem.numberOfSubItems]; 145 | } 146 | cell.imageView.image = self.folderIcon; 147 | 148 | UIEdgeInsets insets = cell.layoutMargins; 149 | insets.left = 16; 150 | cell.layoutMargins = insets; 151 | } 152 | else { 153 | cell.accessoryType = UITableViewCellAccessoryNone; 154 | cell.detailTextLabel.text = [NSByteCountFormatter stringFromByteCount:fileItem.size 155 | countStyle:NSByteCountFormatterCountStyleFile]; 156 | cell.imageView.image = self.fileIcon; 157 | 158 | UIEdgeInsets insets = cell.layoutMargins; 159 | insets.left = 27; 160 | cell.layoutMargins = insets; 161 | } 162 | } 163 | 164 | return cell; 165 | } 166 | 167 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 168 | { 169 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 170 | } 171 | 172 | @end 173 | -------------------------------------------------------------------------------- /TOFileSystemObserverExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Tim Oliver on 28/9/19. 6 | // Copyright © 2019 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TOAppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | NSString * appDelegateClassName; 14 | @autoreleasepool { 15 | // Setup code that might create autoreleased objects goes here. 16 | appDelegateClassName = NSStringFromClass([TOAppDelegate class]); 17 | } 18 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 19 | } 20 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ARAppDelegate : NSObject 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "ARAppDelegate.h" 10 | #import "TOFileSystemPath.h" 11 | 12 | @interface ARAppDelegate () 13 | 14 | @end 15 | 16 | @implementation ARAppDelegate 17 | 18 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 19 | [self setUpDefaultData]; 20 | } 21 | 22 | 23 | - (void)setUpDefaultData 24 | { 25 | NSURL *documentsDirectory = [TOFileSystemPath documentsDirectoryURL]; 26 | NSFileManager *fileManager = [NSFileManager defaultManager]; 27 | NSInteger numberOfItems = [fileManager contentsOfDirectoryAtURL:documentsDirectory 28 | includingPropertiesForKeys:nil 29 | options:0 30 | error:nil].count; 31 | if (numberOfItems > 1) { return; } 32 | 33 | // Create 5 folders 34 | for (NSInteger i = 0; i < 5; i++) { 35 | NSString *folderName = [NSString stringWithFormat:@"Folder %d", (int)i+1]; 36 | NSURL *folderURL = [documentsDirectory URLByAppendingPathComponent:folderName]; 37 | [fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:nil]; 38 | } 39 | 40 | // Create 5 arbitrary files 41 | NSArray *sizes = @[@(5), @(12), @(33), @(15), @(20)]; 42 | for (NSInteger i = 0; i < sizes.count; i++) { 43 | NSString *fileName = [NSString stringWithFormat:@"File-%d.dat", (int)i+1]; 44 | NSURL *fileURL = [documentsDirectory URLByAppendingPathComponent:fileName]; 45 | FILE *file = fopen([fileURL.path cStringUsingEncoding:NSUTF8StringEncoding], "wb"); 46 | 47 | NSInteger byteCount = [sizes[i] intValue] * 1000000; 48 | for (NSInteger j = 0; j < byteCount; j++) { 49 | fwrite("0", 1, 1, file); 50 | } 51 | fclose(file); 52 | } 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARImages.h: -------------------------------------------------------------------------------- 1 | // 2 | // ARImages.h 3 | // TOFileSystemObserverExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ARImages : NSImage 14 | 15 | + (NSImage *)documentPickerDefaultFolderForStyle:(BOOL)darkMode; 16 | 17 | + (NSImage *)documentPickerDefaultFileIconWithExtension:(NSString *)extension 18 | tintColor:(NSColor *)tintColor 19 | style:(BOOL)darkMode; 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | 24 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARImages.m: -------------------------------------------------------------------------------- 1 | // 2 | // ARImages.m 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ARImages.h" 11 | 12 | @implementation ARImages 13 | + (NSURL *)documentsDirectoryURL 14 | { 15 | NSFileManager *fileManager = [NSFileManager defaultManager]; 16 | return [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject; 17 | } 18 | 19 | + (NSImage *)documentPickerDefaultFolderForStyle:(BOOL)darkMode 20 | { 21 | NSURL* url = [[self documentsDirectoryURL] URLByAppendingPathComponent:@".tmp"]; 22 | [[NSFileManager defaultManager] createDirectoryAtURL:url withIntermediateDirectories:false attributes:nil error:nil]; 23 | NSImage *icon = [[url resourceValuesForKeys:@[NSURLEffectiveIconKey] error:nil] objectForKey:NSURLEffectiveIconKey]; 24 | [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; 25 | return icon; 26 | } 27 | 28 | + (NSImage *)documentPickerDefaultFileIconWithExtension:(NSString *)extension 29 | tintColor:(NSColor *)tintColor 30 | style:(BOOL)darkMode 31 | { 32 | NSURL* url = [[self documentsDirectoryURL] URLByAppendingPathComponent:@".tmp"]; 33 | NSData* data = [[NSData alloc] init]; 34 | [[NSFileManager defaultManager] createFileAtPath:url.path contents:data attributes:nil]; 35 | NSImage *icon = [[url resourceValuesForKeys:@[NSURLEffectiveIconKey] error:nil] objectForKey:NSURLEffectiveIconKey]; 36 | [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; 37 | return icon; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ARViewController : NSViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/ARViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import "ARViewController.h" 10 | #import "TOFileSystemObserver.h" 11 | #import "TOFileSystemObserver+AppKit.h" 12 | #import "ARImages.h" 13 | 14 | @interface ARViewController () 15 | 16 | @property (nonatomic, strong) TOFileSystemObserver *observer; 17 | @property (nonatomic, strong) TOFileSystemItemList *fileItemList; 18 | @property (nonatomic, strong) TOFileSystemNotificationToken *listToken; 19 | @property (nonatomic, strong) TOFileSystemNotificationToken *observerToken; 20 | 21 | @property (nonatomic, strong) NSImage *folderIcon; 22 | @property (nonatomic, strong) NSImage *fileIcon; 23 | 24 | @property (weak) IBOutlet NSTableView *tableView; 25 | 26 | @end 27 | 28 | @implementation ARViewController 29 | 30 | - (void)viewDidLoad { 31 | [super viewDidLoad]; 32 | 33 | self.folderIcon = [ARImages documentPickerDefaultFolderForStyle:NO]; 34 | self.fileIcon = [ARImages documentPickerDefaultFileIconWithExtension:@"" tintColor:[NSColor colorForControlTint:[NSColor currentControlTint]] style:NO]; 35 | 36 | // Create a file system observer and start it 37 | self.observer = [[TOFileSystemObserver alloc] init]; 38 | [self.observer start]; 39 | 40 | // Create a live list of the base folder for this controller 41 | self.fileItemList = [self.observer itemListForDirectoryAtURL:nil]; 42 | 43 | __weak typeof(self) weakSelf = self; 44 | self.listToken = [self.fileItemList addNotificationBlock:^(TOFileSystemItemList *itemList, 45 | TOFileSystemItemListChanges *changes) 46 | { 47 | TOFileSystemItemListUpdateTableView(weakSelf.tableView, changes, 0); 48 | }]; 49 | 50 | self.observerToken = [self.observer addNotificationBlock:^(TOFileSystemObserver *observer, 51 | TOFileSystemObserverNotificationType type, 52 | TOFileSystemChanges *changes) 53 | { 54 | if (type == TOFileSystemObserverNotificationTypeWillBeginFullScan) { 55 | NSLog(@"Scan Will Start!"); 56 | return; 57 | } 58 | 59 | if (type == TOFileSystemObserverNotificationTypeDidCompleteFullScan) { 60 | NSLog(@"Scan Complete!"); 61 | return; 62 | } 63 | 64 | NSLog(@"%@", changes); 65 | }]; 66 | } 67 | 68 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { 69 | return self.fileItemList.count; 70 | } 71 | 72 | - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { 73 | TOFileSystemItem *fileItem = self.fileItemList[row]; 74 | 75 | if ([tableColumn.identifier isEqualToString: @"NameColumn"]) { 76 | static NSString *cellIdentifier = @"NameCell"; 77 | NSTableCellView* cell = [tableView makeViewWithIdentifier:cellIdentifier owner:nil]; 78 | 79 | cell.textField.stringValue = fileItem.name; 80 | 81 | if (fileItem.type == TOFileSystemItemTypeDirectory) { 82 | cell.imageView.image = self.folderIcon; 83 | } 84 | else { 85 | cell.imageView.image = self.fileIcon; 86 | } 87 | 88 | return cell; 89 | } 90 | else if ([tableColumn.identifier isEqualToString: @"StatusColumn"]) { 91 | static NSString *cellIdentifier = @"StatusCell"; 92 | NSTableCellView* cell = [tableView makeViewWithIdentifier:cellIdentifier owner:nil]; 93 | 94 | if (fileItem.isCopying) { 95 | cell.textField.stringValue = @"Copying"; 96 | } 97 | else { 98 | if (fileItem.type == TOFileSystemItemTypeDirectory) { 99 | if (fileItem.numberOfSubItems == 1) { 100 | cell.textField.stringValue = @"1 item"; 101 | } 102 | else { 103 | cell.textField.stringValue = [NSString stringWithFormat:@"%ld items", (long)fileItem.numberOfSubItems]; 104 | } 105 | } 106 | else { 107 | cell.textField.stringValue = [NSByteCountFormatter stringFromByteCount:fileItem.size 108 | countStyle:NSByteCountFormatterCountStyleFile]; 109 | } 110 | } 111 | return cell; 112 | } 113 | 114 | return nil; 115 | } 116 | 117 | 118 | @end 119 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2020 Tim Oliver. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | NSSupportsAutomaticTermination 34 | 35 | NSSupportsSuddenTermination 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/TOFileSystemObserverMacExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TOFileSystemObserverMacExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TOFileSystemObserverMacExample 4 | // 5 | // Created by Anatoly Rosencrantz on 26/03/2020. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | @autoreleasepool { 13 | // Setup code that might create autoreleased objects goes here. 14 | } 15 | return NSApplicationMain(argc, argv); 16 | } 17 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Categories/TOFileSystemEnumeratorTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemEnumeratorTests.m 3 | // 4 | // Copyright 2019-2020 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "NSFileManager+TOFileSystemDirectoryEnumerator.h" 25 | 26 | @interface TOFileSystemEnumeratorTests : XCTestCase 27 | 28 | @property (nonatomic, strong) NSDirectoryEnumerator *enumerator; 29 | @property (nonatomic, strong) NSURL *folderURL; 30 | 31 | @end 32 | 33 | @implementation TOFileSystemEnumeratorTests 34 | 35 | - (void)setUp 36 | { 37 | NSURL *url = [NSURL fileURLWithPath:NSTemporaryDirectory()]; 38 | self.folderURL = [url URLByAppendingPathComponent:@"Folder"]; 39 | [NSFileManager.defaultManager createDirectoryAtURL:self.folderURL withIntermediateDirectories:YES attributes:nil error:nil]; 40 | 41 | self.enumerator = [NSFileManager.defaultManager to_fileSystemEnumeratorForDirectoryAtURL:url]; 42 | } 43 | 44 | - (void)tearDown 45 | { 46 | self.enumerator = nil; 47 | [NSFileManager.defaultManager removeItemAtURL:self.folderURL error:nil]; 48 | } 49 | 50 | - (void)testEnumerator 51 | { 52 | NSInteger i = 0; 53 | for (NSURL *url in self.enumerator) { 54 | if (!url) { break; } // Supress unused warning 55 | i++; 56 | } 57 | 58 | XCTAssertTrue(i > 0); 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Categories/TOFileSystemFileAttributesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemFileAttributesTests.m 3 | // 4 | // Copyright 2019-2020 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "NSURL+TOFileSystemAttributes.h" 25 | 26 | const NSInteger kTOFileSystemTestFileSize = 3 * 1000000; 27 | 28 | @interface TOFileSystemFileAttributesTests : XCTestCase 29 | 30 | @property (nonatomic, strong) NSURL *fileURL; 31 | @property (nonatomic, strong) NSURL *directoryURL; 32 | @property (nonatomic, strong) NSURL *subdirectoryURL; 33 | 34 | @end 35 | 36 | @implementation TOFileSystemFileAttributesTests 37 | 38 | - (void)setUp 39 | { 40 | //Generate a file we can use to test 41 | NSURL *tempDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory()]; 42 | 43 | // Create a file 44 | self.fileURL = [tempDirectory URLByAppendingPathComponent:@"File-1.dat"]; 45 | FILE *file = fopen([self.fileURL.path cStringUsingEncoding:NSUTF8StringEncoding], "wb"); 46 | NSInteger byteCount = kTOFileSystemTestFileSize; 47 | for (NSInteger j = 0; j < byteCount; j++) { 48 | fwrite("0", 1, 1, file); 49 | } 50 | fclose(file); 51 | 52 | // Create the folder 53 | self.directoryURL = [tempDirectory URLByAppendingPathComponent:@"TestFolder"]; 54 | [NSFileManager.defaultManager createDirectoryAtURL:self.directoryURL withIntermediateDirectories:YES attributes:nil error:nil]; 55 | 56 | self.subdirectoryURL = [self.directoryURL URLByAppendingPathComponent:@"SubFolder"]; 57 | [NSFileManager.defaultManager createDirectoryAtURL:self.subdirectoryURL withIntermediateDirectories:YES attributes:nil error:nil]; 58 | } 59 | 60 | - (void)tearDown 61 | { 62 | [NSFileManager.defaultManager removeItemAtURL:self.fileURL error:nil]; 63 | } 64 | 65 | - (void)testCopying 66 | { 67 | // Since this is executed right after creation, this will be true 68 | XCTAssertTrue([self.fileURL to_isCopying]); 69 | 70 | // Wait a second, and it should transition to "completed' 71 | XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Copying File Completed"]; 72 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 73 | if ([self.fileURL to_isCopying] == NO) { 74 | [expectation fulfill]; 75 | } 76 | }); 77 | 78 | [self waitForExpectations:@[expectation] timeout:4.0f]; 79 | } 80 | 81 | - (void)testIsDirectory 82 | { 83 | XCTAssertTrue(self.directoryURL.to_isDirectory); 84 | } 85 | 86 | - (void)testSize 87 | { 88 | XCTAssertTrue(self.fileURL.to_size == kTOFileSystemTestFileSize); 89 | } 90 | 91 | - (void)testSubItemCount 92 | { 93 | XCTAssertTrue(self.directoryURL.to_numberOfSubItems == 1); 94 | } 95 | 96 | - (void)testCreationDate 97 | { 98 | XCTAssertNotNil(self.fileURL.to_creationDate); 99 | } 100 | 101 | - (void)testModificationDate 102 | { 103 | XCTAssertNotNil(self.fileURL.to_modificationDate); 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Categories/TOFileSystemUUIDTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemUUIDTests.m 3 | // 4 | // Copyright 2019-2020 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | #import "NSURL+TOFileSystemUUID.h" 25 | 26 | @interface TOFileSystemUUIDTests : XCTestCase 27 | @property (nonatomic, strong) NSURL *itemURL; 28 | @property (nonatomic, strong) NSURL *childItemURL; 29 | @end 30 | 31 | @implementation TOFileSystemUUIDTests 32 | 33 | - (void)setUp { 34 | // Create a temp folder to test 35 | NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"Folder"]; 36 | self.itemURL = [NSURL fileURLWithPath:filePath]; 37 | [NSFileManager.defaultManager createDirectoryAtURL:self.itemURL 38 | withIntermediateDirectories:YES 39 | attributes:nil 40 | error:nil]; 41 | 42 | // Create a temp child folder 43 | self.childItemURL = [self.itemURL URLByAppendingPathComponent:@"ChildFolder"]; 44 | [NSFileManager.defaultManager createDirectoryAtURL:self.childItemURL 45 | withIntermediateDirectories:YES 46 | attributes:nil 47 | error:nil]; 48 | } 49 | 50 | - (void)tearDown { 51 | // Delete the folder so we can start fresh 52 | [[NSFileManager defaultManager] removeItemAtURL:self.itemURL error:nil]; 53 | } 54 | 55 | - (void)testCreatingUUID 56 | { 57 | // Confirm it's nil at the start 58 | XCTAssert([self.itemURL to_fileSystemUUID].length == 0); 59 | 60 | // Generate a UUID for the item 61 | NSString *uuid = [self.itemURL to_generateFileSystemUUID]; 62 | 63 | // Compare to the one on disk now 64 | XCTAssert([[self.itemURL to_fileSystemUUID] isEqualToString:uuid]); 65 | } 66 | 67 | - (void)testRepairingUUID 68 | { 69 | // Confirm it's nil at the start 70 | XCTAssertNil([self.itemURL to_fileSystemUUID]); 71 | 72 | // Set a non-uuid value to the file 73 | [self.itemURL to_setFileSystemUUID:@"000000000000000000000000000000000000"]; 74 | 75 | // Regenerate a new uuid 76 | NSString *newUUID = [self.itemURL to_fileSystemUUID]; 77 | 78 | // Sanity check it's not matching the dummy 79 | XCTAssertNil(newUUID); 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Entities/TOFileSystemChangesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemChangesTests.m 3 | // TOFileSystemObserverTests 4 | // 5 | // Created by Tim Oliver on 2020/01/28. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "TOFileSystemObserver.h" 12 | #import "TOFileSystemChanges+Private.h" 13 | 14 | @interface TOFileSystemChangesTests : XCTestCase 15 | 16 | @property (nonatomic, strong) NSURL *url; 17 | @property (nonatomic, copy) NSString *uuid; 18 | 19 | @property (nonatomic, strong) TOFileSystemObserver *observer; 20 | @property (nonatomic, strong) TOFileSystemChanges *changes; 21 | 22 | @end 23 | 24 | @implementation TOFileSystemChangesTests 25 | 26 | - (void)setUp 27 | { 28 | // Create test data 29 | self.url = [NSURL fileURLWithPath:@"/Documents"]; 30 | self.uuid = @"0256d425-f081-4bc3-8db5-bcb158568abb"; 31 | 32 | // Create test objects 33 | self.observer = [[TOFileSystemObserver alloc] init]; 34 | self.changes = [[TOFileSystemChanges alloc] initWithFileSystemObserver:self.observer]; 35 | } 36 | 37 | - (void)tearDown 38 | { 39 | self.url = nil; 40 | self.uuid = nil; 41 | self.observer = nil; 42 | self.changes = nil; 43 | } 44 | 45 | - (void)testObserverInChanges 46 | { 47 | XCTAssertEqual(self.changes.fileSystemObserver, self.observer); 48 | } 49 | 50 | - (void)testDiscoveries 51 | { 52 | // Test we can properly retrieve discovered items that were added 53 | [self.changes addDiscoveredItemWithUUID:self.uuid fileURL:self.url]; 54 | XCTAssert(self.changes.discoveredItems[self.uuid] == self.url); 55 | } 56 | 57 | - (void)testModifications 58 | { 59 | // Test we can properly retrieve modified items that were added 60 | [self.changes addModifiedItemWithUUID:self.uuid fileURL:self.url]; 61 | XCTAssert(self.changes.modifiedItems[self.uuid] == self.url); 62 | } 63 | 64 | - (void)testDeletions 65 | { 66 | // Test we can properly retrieve deleted items that were added 67 | [self.changes addDeletedItemWithUUID:self.uuid fileURL:self.url]; 68 | XCTAssert(self.changes.deletedItems[self.uuid] == self.url); 69 | } 70 | 71 | - (void)testMovedItems 72 | { 73 | // Test we can properly retrieve moved items that were added 74 | NSURL *newURL = [NSURL fileURLWithPath:@"/Documents/Folder"]; 75 | [self.changes addMovedItemWithUUID:self.uuid oldFileURL:self.url newFileURL:newURL]; 76 | XCTAssert(self.changes.movedItems[self.uuid].firstObject == self.url); 77 | XCTAssert(self.changes.movedItems[self.uuid].lastObject == newURL); 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Entities/TOFileSystemItemDictionaryTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemDictionaryTests.m 3 | // 4 | // Copyright 2019-2020 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | #import "TOFileSystemItemURLDictionary.h" 26 | 27 | @interface TOFileSystemItemDictionaryTests : XCTestCase 28 | 29 | @property (nonatomic, strong) NSString *tempDirectory; 30 | @property (nonatomic, strong) NSURL *baseURL; 31 | 32 | @end 33 | 34 | @implementation TOFileSystemItemDictionaryTests 35 | 36 | - (void)setUp 37 | { 38 | self.tempDirectory = @"/Users/XD/Documents"; 39 | self.baseURL = [NSURL fileURLWithPath:self.tempDirectory]; 40 | } 41 | 42 | - (void)testCreation 43 | { 44 | TOFileSystemItemURLDictionary *dict = [[TOFileSystemItemURLDictionary alloc] initWithBaseURL:self.baseURL]; 45 | XCTAssertNotNil(dict); 46 | } 47 | 48 | - (void)testInsertionAndDeletion 49 | { 50 | TOFileSystemItemURLDictionary *dict = [[TOFileSystemItemURLDictionary alloc] initWithBaseURL:self.baseURL]; 51 | 52 | // Test first insertion 53 | NSString *folder1URL = [NSString stringWithFormat:@"%@/Folder1", self.tempDirectory]; 54 | NSURL *url = [NSURL fileURLWithPath:folder1URL].URLByStandardizingPath; 55 | dict[@"folder1"] = url; 56 | XCTAssert([url isEqual:dict[@"folder1"]]); 57 | 58 | // Test second insertion 59 | NSString *folder2URL = [NSString stringWithFormat:@"%@/Folder2", self.tempDirectory]; 60 | url = [NSURL fileURLWithPath:folder2URL].URLByStandardizingPath; 61 | dict[@"folder2"] = url; 62 | XCTAssert([url isEqual:dict[@"folder2"]]); 63 | 64 | // Test deletion 65 | dict[@"folder1"] = nil; 66 | XCTAssertNil(dict[@"folder1"]); 67 | } 68 | 69 | - (void)testConcurrentReads 70 | { 71 | TOFileSystemItemURLDictionary *dict = [[TOFileSystemItemURLDictionary alloc] initWithBaseURL:self.baseURL]; 72 | 73 | NSString *folder1URL = [NSString stringWithFormat:@"%@/Folder1", self.tempDirectory]; 74 | NSURL *url = [NSURL fileURLWithPath:folder1URL].URLByStandardizingPath; 75 | dict[@"folder1"] = url; 76 | 77 | // Check read is working on the main thread 78 | XCTAssert([url isEqual:dict[@"folder1"]]); 79 | 80 | // Create expectation to test concurrent execution 81 | XCTestExpectation *expectation = [[XCTestExpectation alloc] 82 | initWithDescription:@"Dictionary Reads Succeeded"]; 83 | 84 | // Create dispatch group so we can call a trigger when both queues finish executing 85 | dispatch_group_t dispatchGroup = dispatch_group_create(); 86 | 87 | // Kickstart a read on the first thread 88 | dispatch_queue_t firstQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 89 | dispatch_group_async(dispatchGroup, firstQueue, ^ { 90 | XCTAssert([url isEqual:dict[@"folder1"]]); 91 | }); 92 | 93 | // Kickstart a read on the second thread 94 | dispatch_queue_t secondQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 95 | dispatch_group_async(dispatchGroup, secondQueue, ^ { 96 | XCTAssert([url isEqual:dict[@"folder1"]]); 97 | }); 98 | 99 | // Upon completion of both reads, ensure the dictionary contains both values 100 | dispatch_queue_t mainQueue = dispatch_get_main_queue(); 101 | dispatch_group_notify(dispatchGroup, mainQueue, ^ { 102 | [expectation fulfill]; 103 | }); 104 | 105 | // Wait for the expectation 106 | [self waitForExpectations:@[expectation] timeout:1.0f]; 107 | } 108 | 109 | - (void)testConcurrentWrite 110 | { 111 | TOFileSystemItemURLDictionary *dict = [[TOFileSystemItemURLDictionary alloc] initWithBaseURL:self.baseURL]; 112 | 113 | // Create some test URLs to inject 114 | NSString *folder1URL = [NSString stringWithFormat:@"%@/Folder1", self.tempDirectory]; 115 | NSURL *firstUrl = [NSURL fileURLWithPath:folder1URL].URLByStandardizingPath; 116 | 117 | NSString *folder2URL = [NSString stringWithFormat:@"%@/Folder2", self.tempDirectory]; 118 | NSURL *secondUrl = [NSURL fileURLWithPath:folder2URL].URLByStandardizingPath; 119 | 120 | // Create expectation to test concurrent execution 121 | XCTestExpectation *expectation = [[XCTestExpectation alloc] 122 | initWithDescription:@"Dictionary Writes Succeeded"]; 123 | 124 | // Create dispatch group so we can call a trigger when both queues finish executing 125 | dispatch_group_t dispatchGroup = dispatch_group_create(); 126 | 127 | // Kickstart a write on the first thread 128 | dispatch_queue_t firstQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 129 | dispatch_group_async(dispatchGroup, firstQueue, ^ { 130 | [dict setItemURL:firstUrl forUUID:@"folder1"]; 131 | }); 132 | 133 | // Kickstart a write on the second thread 134 | dispatch_queue_t secondQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 135 | dispatch_group_async(dispatchGroup, secondQueue, ^ { 136 | [dict setItemURL:secondUrl forUUID:@"folder2"]; 137 | }); 138 | 139 | // Upon completion of both writes, ensure the dictionary contains both values 140 | dispatch_queue_t mainQueue = dispatch_get_main_queue(); 141 | dispatch_group_notify(dispatchGroup, mainQueue, ^ { 142 | XCTAssert([firstUrl isEqual:dict[@"folder1"]]); 143 | XCTAssert([secondUrl isEqual:dict[@"folder2"]]); 144 | XCTAssertNil([dict itemURLForUUID:@"folder3"]); 145 | [expectation fulfill]; 146 | }); 147 | 148 | // Wait for the expectation 149 | [self waitForExpectations:@[expectation] timeout:1.0f]; 150 | } 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Entities/TOFileSystemItemListChangesTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemListChangesTests.m 3 | // 4 | // Copyright 2019-2020 Timothy Oliver. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to 8 | // deal in the Software without restriction, including without limitation the 9 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | // sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 21 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | #import 24 | 25 | #if TARGET_OS_OSX 26 | #import "NSIndexPath+UIKitAdditions.h" 27 | #endif 28 | 29 | #import "TOFileSystemItemListChanges.h" 30 | #import "TOFileSystemItemListChanges+Private.h" 31 | 32 | @interface TOFileSystemItemListChangesTests : XCTestCase 33 | 34 | @end 35 | 36 | @implementation TOFileSystemItemListChangesTests 37 | 38 | - (void)testInsertionIndices 39 | { 40 | // Add an insertion index 41 | TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; 42 | [changes addInsertionIndex:1]; 43 | 44 | // Test that changes are recognized 45 | XCTAssertTrue(changes.hasItemChanges); 46 | 47 | // Check the insertion state 48 | XCTAssert([changes.insertions isEqualToArray:@[@1]]); 49 | 50 | // Check the index path mapping 51 | NSIndexPath *indexPath = [changes indexPathsForInsertionsInSection:1].firstObject; 52 | XCTAssertNotNil(indexPath); 53 | XCTAssertEqual(indexPath.row, 1); 54 | XCTAssertEqual(indexPath.section, 1); 55 | } 56 | 57 | - (void)testDeletionsIndices 58 | { 59 | // Add a deletion index 60 | TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; 61 | [changes addDeletionIndex:1]; 62 | 63 | // Test that changes are recognized 64 | XCTAssertTrue(changes.hasItemChanges); 65 | 66 | // Check the deletion state 67 | XCTAssert([changes.deletions isEqualToArray:@[@1]]); 68 | 69 | // Check the index path mapping 70 | NSIndexPath *indexPath = [changes indexPathsForDeletionsInSection:1].firstObject; 71 | XCTAssertNotNil(indexPath); 72 | XCTAssertEqual(indexPath.row, 1); 73 | XCTAssertEqual(indexPath.section, 1); 74 | } 75 | 76 | - (void)testModificationIndices 77 | { 78 | // Add a deletion index 79 | TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; 80 | [changes addModificationIndex:1]; 81 | 82 | // Test that changes are recognized 83 | XCTAssertTrue(changes.hasItemChanges); 84 | 85 | // Check the deletion state 86 | XCTAssert([changes.modificatons isEqualToArray:@[@1]]); 87 | 88 | // Check the index path mapping 89 | NSIndexPath *indexPath = [changes indexPathsForModificationsInSection:1].firstObject; 90 | XCTAssertNotNil(indexPath); 91 | XCTAssertEqual(indexPath.row, 1); 92 | XCTAssertEqual(indexPath.section, 1); 93 | } 94 | 95 | - (void)testIndexMovements 96 | { 97 | // Add a movement index 98 | TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; 99 | [changes addMovementWithSourceIndex:1 destinationIndex:2]; 100 | 101 | // Check that the state has been validated 102 | XCTAssertTrue(changes.hasItemMovements); 103 | 104 | // Check the movement state 105 | XCTAssert([changes.movements[@1] isEqualToNumber:@2]); 106 | 107 | // Check the index path mapping 108 | NSArray *sourceIndices = [changes indexPathsForMovementSourcesInSection:1]; 109 | NSArray *destIndices = [changes indexPathsForMovementDestinationsWithSourceIndexPaths:sourceIndices]; 110 | XCTAssert(sourceIndices.count == destIndices.count); 111 | 112 | // Check the index paths 113 | NSIndexPath *sourceIndexPath = sourceIndices.firstObject; 114 | XCTAssertEqual(sourceIndexPath.section, 1); 115 | XCTAssertEqual(sourceIndexPath.row, 1); 116 | 117 | NSIndexPath *destIndexPath = destIndices.firstObject; 118 | XCTAssertEqual(destIndexPath.section, 1); 119 | XCTAssertEqual(destIndexPath.row, 2); 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Entities/TOFileSystemItemMapTableTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemMapTableTests.m 3 | // TOFileSystemObserverTests 4 | // 5 | // Created by Tim Oliver on 2020/01/28. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "TOFileSystemItemMapTable.h" 12 | 13 | @interface TOFileSystemItemMapTableTests : XCTestCase 14 | 15 | @property (nonatomic, strong) TOFileSystemItemMapTable *mapTable; 16 | @property (nonatomic, copy) NSString *uuid; 17 | @property (nonatomic, strong) NSString *object; 18 | 19 | @end 20 | 21 | @implementation TOFileSystemItemMapTableTests 22 | 23 | - (void)setUp 24 | { 25 | @autoreleasepool { 26 | self.mapTable = [[TOFileSystemItemMapTable alloc] init]; 27 | self.uuid = @"0afec03c-ba74-4b87-9941-9c59bb97ccc4"; 28 | self.object = @"XD"; 29 | } 30 | 31 | // Insert the item 32 | [self.mapTable setItem:self.object forUUID:self.uuid]; 33 | } 34 | 35 | - (void)tearDown 36 | { 37 | self.mapTable = nil; 38 | self.uuid = nil; 39 | self.object = nil; 40 | } 41 | 42 | - (void)testInsertion 43 | { 44 | XCTAssertEqual(self.mapTable.count, 1); 45 | } 46 | 47 | - (void)testRetrieval 48 | { 49 | XCTAssert([[self.mapTable itemForUUID:self.uuid] isEqualToString:self.object]); 50 | } 51 | 52 | - (void)testDeletion 53 | { 54 | [self.mapTable removeItemForUUID:self.uuid]; 55 | XCTAssertEqual(self.mapTable.count, 0); 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Entities/TOFileSystemItemURLDictionaryTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // TOFileSystemItemURLDictionaryTests.m 3 | // TOFileSystemObserverTests 4 | // 5 | // Created by Tim Oliver on 2020/01/28. 6 | // Copyright © 2020 Tim Oliver. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "TOFileSystemPath.h" 12 | #import "TOFileSystemItemURLDictionary.h" 13 | 14 | 15 | @interface TOFileSystemItemURLDictionaryTests : XCTestCase 16 | 17 | @property (nonatomic, strong) NSURL *baseURL; 18 | @property (nonatomic, strong) TOFileSystemItemURLDictionary *dictionary; 19 | 20 | @property (nonatomic, strong) NSURL *url; 21 | @property (nonatomic, copy) NSString *uuid; 22 | 23 | @end 24 | 25 | @implementation TOFileSystemItemURLDictionaryTests 26 | 27 | - (void)setUp 28 | { 29 | // Create a test object 30 | self.baseURL = [TOFileSystemPath documentsDirectoryURL]; 31 | self.dictionary = [[TOFileSystemItemURLDictionary alloc] initWithBaseURL:self.baseURL]; 32 | 33 | // Create some basic test data 34 | self.url = [self.baseURL URLByAppendingPathComponent:@"Folder"]; 35 | self.uuid = @"f2a5bc6d-0eab-4970-8650-8629fdc3a866"; 36 | 37 | // Insert the test data 38 | [self.dictionary setItemURL:self.url forUUID:self.uuid]; 39 | } 40 | 41 | - (void)tearDown 42 | { 43 | self.baseURL = nil; 44 | self.dictionary = nil; 45 | self.url = nil; 46 | self.uuid = nil; 47 | } 48 | 49 | - (void)testInserting 50 | { 51 | // Verify the insertion was stored correctly 52 | XCTAssertEqual(self.dictionary.count, 1); 53 | } 54 | 55 | - (void)testRetrieval 56 | { 57 | // Retrieve the URL and verify it matches what was inserted 58 | XCTAssert([[self.dictionary itemURLForUUID:self.uuid] isEqual:self.url]); 59 | 60 | // Perform an inverse lookup 61 | XCTAssert([[self.dictionary uuidForItemWithURL:self.url] isEqualToString:self.uuid]); 62 | } 63 | 64 | - (void)testSubscripting 65 | { 66 | self.dictionary[self.uuid] = self.url; 67 | 68 | // Verify the insertion was stored correctly 69 | XCTAssertEqual(self.dictionary.count, 1); 70 | 71 | // Retrieve the URL and verify it matches what was inserted 72 | XCTAssert([self.dictionary[self.uuid] isEqual:self.url]); 73 | } 74 | 75 | - (void)testNullability 76 | { 77 | // Test nilling the value removes it 78 | self.dictionary[self.uuid] = nil; 79 | XCTAssertEqual(self.dictionary.count, 0); 80 | } 81 | 82 | - (void)testRemovingSpecificItem 83 | { 84 | // Test deleting a specific item works 85 | [self.dictionary removeItemURLForUUID:self.uuid]; 86 | XCTAssertEqual(self.dictionary.count, 0); 87 | } 88 | 89 | - (void)testRemovingAllItems 90 | { 91 | // Test deleting all items 92 | [self.dictionary removeAllItems]; 93 | XCTAssertEqual(self.dictionary.count, 0); 94 | } 95 | 96 | - (void)testConcurrency 97 | { 98 | dispatch_group_t group = dispatch_group_create(); 99 | 100 | XCTestExpectation *expectation = [[XCTestExpectation alloc] 101 | initWithDescription:@"Both queues executed as expected"]; 102 | 103 | // Blank the dictionary before we start 104 | [self.dictionary removeAllItems]; 105 | 106 | // First queue execution 107 | dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 108 | self.dictionary[self.uuid] = self.url; 109 | }); 110 | 111 | // Second queue execution 112 | dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 113 | NSURL *url = [self.baseURL URLByAppendingPathComponent:@"Folder2"]; 114 | NSString *uuid = @"3ccd0073-e57c-42c7-b3be-6410a051c900"; 115 | self.dictionary[uuid] = url; 116 | }); 117 | 118 | // Wait for completion 119 | dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 120 | XCTAssertEqual(self.dictionary.count, 2); 121 | [expectation fulfill]; 122 | }); 123 | 124 | [self waitForExpectations:@[expectation] timeout:0.5f]; 125 | } 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /TOFileSystemObserverTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimOliver/TOFileSystemObserver/c2d49bd5ef51188df96245a22c0dbadad412c5b2/screenshot.jpg --------------------------------------------------------------------------------