Skip to content

Instantly share code, notes, and snippets.

@depth42
Created October 14, 2019 11:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save depth42/334a3fd901442b2f7dd793dacc777add to your computer and use it in GitHub Desktop.
Save depth42/334a3fd901442b2f7dd793dacc777add to your computer and use it in GitHub Desktop.
//
// 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
//
// 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