Created
October 14, 2019 11:06
-
-
Save depth42/334a3fd901442b2f7dd793dacc777add to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PWFilePromise.h | |
// PWAppKit | |
// | |
// Created by Frank Illenberger on 09.01.18. | |
// Copyright © 2018 ProjectWizards. All rights reserved. | |
// | |
NS_ASSUME_NONNULL_BEGIN | |
// There is no robust mechanism to get notified when a drag source has finished fulfilling a file | |
// promise. One therefore needs to poll for the completion by checking for the existence of the | |
// promised items. A PWFilePromise instance represents a promised item. | |
@interface PWFilePromise : NSObject | |
- (instancetype) init NS_UNAVAILABLE; | |
- (instancetype) initWithURL:(NSURL*)URL | |
isContainer:(BOOL)isContainer; | |
// The URL of an initially non-existing item for whose creation we want to poll. | |
// If isContainer==YES, URL is a container directory and we want to poll for the creation of items inside it. | |
@property (nonatomic, readonly, strong) NSURL* URL; | |
// Mail.app does not create multiple pasteboard items when dragging e-mails onto Merlin. | |
// Instead, it returns the provided target folder as a promisedURL and creates the e-mails | |
// in it. This violates the contract defined in Pasteboard.h. To work around it, | |
// we detect this case and set the "isContainer" flag. | |
@property (nonatomic, readonly) BOOL isContainer; | |
// Returns an empty array if the promise was not fulfilled. | |
@property (nonatomic, readonly, copy) NSArray<NSURL*>* fulfilledItems; | |
// The completion handler gets called when all promises are fulfilled or if the timeout | |
// interval has passed. An error is only returned, when a file operation error did occur. | |
// In the completionHandler, -fulfilledItems can be checked to determine if a promise | |
// has been fulfilled. | |
// The completionHandler is called on a private dispatch queue. | |
+ (void)pollForFulfillmentOfPromises:(NSArray<PWFilePromise*>*)promises | |
withTimeoutInterval:(NSTimeInterval)timeoutInterval | |
completionHandler:(PWCompletionHandlerWithError)completionHandler; | |
+ (NSArray<NSURL*>*)fulfilledItemsInPromises:(NSArray<PWFilePromise*>*)promises; | |
@end | |
NS_ASSUME_NONNULL_END |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PWFilePromise.m | |
// PWAppKit | |
// | |
// Created by Frank Illenberger on 09.01.18. | |
// Copyright © 2018 ProjectWizards. All rights reserved. | |
// | |
#import "PWFilePromise.h" | |
NS_ASSUME_NONNULL_BEGIN | |
static NSTimeInterval const ExistancePollingInterval = 0.2; | |
@implementation PWFilePromise | |
{ | |
NSArray<NSNumber*>* _fileSizes; | |
} | |
- (instancetype) initWithURL:(NSURL*)URL | |
isContainer:(BOOL)isContainer | |
{ | |
NSParameterAssert(URL); | |
self = [super init]; | |
_URL = URL; | |
_isContainer = isContainer; | |
return self; | |
} | |
- (BOOL)updateFulfillmentReturningIsComplete:(BOOL* _Nonnull)outIsComplete | |
error:(NSError**)outError | |
{ | |
NSParameterAssert(outIsComplete); | |
NSFileManager* manager = NSFileManager.defaultManager; | |
if(_isContainer) | |
{ | |
NSArray<NSURL*>* URLs = [manager contentsOfDirectoryAtURL:_URL | |
includingPropertiesForKeys:@[NSURLTotalFileSizeKey] | |
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants | |
error:outError]; | |
if(!URLs) | |
return NO; | |
_fulfilledItems = [URLs copy]; | |
} | |
else | |
_fulfilledItems = [manager fileExistsAtPath:_URL.path] ? @[_URL] : @[]; | |
NSArray<NSNumber*>* prevFileSizes = _fileSizes; | |
if(![self updateFileSizesReturningError:outError]) | |
return NO; | |
// We only regard the promise as being fulfilled, if the sizes have not changed for one polling durations, | |
// which indicates that the drag source has finished writing promised files. | |
*outIsComplete = _fulfilledItems.count > 0 && PWEqualObjects(prevFileSizes, _fileSizes); | |
return YES; | |
} | |
- (BOOL)updateFileSizesReturningError:(NSError**)outError | |
{ | |
NSMutableArray<NSNumber*>* sizes = [[NSMutableArray alloc] init]; | |
NSNumber* totalFileSize; | |
for(NSURL* iURL in _fulfilledItems) | |
{ | |
if(![iURL getResourceValue:&totalFileSize | |
forKey:NSURLTotalFileSizeKey | |
error:outError]) | |
return NO; | |
if(totalFileSize) | |
[sizes addObject:totalFileSize]; | |
} | |
_fileSizes = [sizes copy]; | |
return YES; | |
} | |
+ (PWDispatchQueue*)dispatchQueue | |
{ | |
static PWDispatchQueue* queue; | |
PWDispatchOnce(^{ | |
queue = [PWDispatchQueue serialDispatchQueueWithLabel:@"PWFilePromise"]; | |
}); | |
return queue; | |
} | |
+ (void)pollForFulfillmentOfPromises:(NSArray<PWFilePromise*>*)promises | |
withTimeoutInterval:(NSTimeInterval)timeoutInterval | |
completionHandler:(PWCompletionHandlerWithError)completionHandler | |
{ | |
[self pollForFulfillmentOfPromises:promises | |
timeoutDate:timeoutInterval > 0.0 ? [NSDate dateWithTimeIntervalSinceNow:timeoutInterval] : nil | |
completionHandler:completionHandler]; | |
} | |
+ (void)pollForFulfillmentOfPromises:(NSArray<PWFilePromise*>*)promises | |
timeoutDate:(nullable NSDate*)timeoutDate | |
completionHandler:(PWCompletionHandlerWithError)completionHandler | |
{ | |
NSParameterAssert(promises); | |
NSParameterAssert(timeoutDate); | |
NSParameterAssert(completionHandler); | |
[self.dispatchQueue dynamicallyDispatchBlock:^{ | |
NSMutableArray<PWFilePromise*>* incompletePromises = [[NSMutableArray alloc] init]; | |
for(PWFilePromise* iPromise in promises) | |
{ | |
NSError* error; | |
BOOL isComplete; | |
if(![iPromise updateFulfillmentReturningIsComplete:&isComplete | |
error:&error]) | |
{ | |
completionHandler([NSError ensureError:error]); | |
return; | |
} | |
if(!isComplete) | |
[incompletePromises addObject:iPromise]; | |
} | |
if(incompletePromises.count == 0 || (timeoutDate && timeoutDate.timeIntervalSinceNow < 0.0)) | |
completionHandler(/* error */ nil); | |
else | |
{ | |
[self.dispatchQueue asynchronouslyDispatchAfterDelay:ExistancePollingInterval | |
useWallTime:NO | |
block:^ | |
{ | |
[self pollForFulfillmentOfPromises:incompletePromises | |
timeoutDate:timeoutDate | |
completionHandler:completionHandler]; | |
}]; | |
} | |
}]; | |
} | |
+ (NSArray<NSURL*>*)fulfilledItemsInPromises:(NSArray<PWFilePromise*>*)promises | |
{ | |
NSMutableArray<NSURL*>* fulfilledItems = [NSMutableArray array]; | |
for(PWFilePromise* iPromise in promises) | |
[fulfilledItems addObjectsFromArray:iPromise.fulfilledItems]; | |
return fulfilledItems; | |
} | |
@end | |
NS_ASSUME_NONNULL_END |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment