Last active
June 2, 2017 13:58
-
-
Save lobianco/a3f863060e368d1d0beb76a0a55f42ef to your computer and use it in GitHub Desktop.
ALFileCopier allows you to copy a file or data to a specified path, optionally from a byte offset, with the freedom to cancel an in-progress write if desired.
This file contains 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
// | |
// ALFileCopier.h | |
// | |
// Created by Anthony Lobianco. | |
// Copyright © 2016 All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
NS_ASSUME_NONNULL_BEGIN | |
@class ALFileCopier; | |
@protocol ALFileCopierDelegate <NSFileManagerDelegate> | |
@optional | |
- (BOOL)fileCopier:(ALFileCopier *)fileCopier shouldContinueCopyingFileToPath:(NSString *)path; | |
@end | |
@interface ALFileCopier : NSFileManager | |
@property (nullable, nonatomic, weak) id<ALFileCopierDelegate> copierDelegate; | |
#pragma mark - From files | |
- (void)copyFileAtPath:(NSString *)inputPath | |
toPath:(NSString *)outputPath | |
completion:(nullable void (^) (NSURL * _Nullable))completion; | |
- (void)copyFileAtPath:(NSString *)inputPath | |
toPath:(NSString *)outputPath | |
fromByteOffset:(NSUInteger)offset | |
completion:(nullable void (^) (NSURL * _Nullable))completion; | |
#pragma mark - From data | |
- (void)copyData:(NSData *)data | |
toPath:(NSString *)outputPath | |
completion:(nullable void (^) (NSURL * _Nullable))completion; | |
- (void)copyData:(NSData *)data | |
toPath:(NSString *)outputPath | |
fromByteOffset:(NSUInteger)offset | |
completion:(nullable void (^)(NSURL * _Nullable))completion; | |
@end | |
NS_ASSUME_NONNULL_END |
This file contains 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
// | |
// ALFileCopier.m | |
// | |
// Created by Anthony Lobianco. | |
// Copyright © 2016 All rights reserved. | |
// | |
#import "ALFileCopier.h" | |
#define AL_CALL(_BLOCK, ...) (_BLOCK ? _BLOCK(__VA_ARGS__) : nil) | |
@interface ALFileCopier () | |
@property (nonatomic, readonly) dispatch_queue_t scheduler; | |
@property (nonatomic, readonly) dispatch_queue_t readScheduler; | |
@end | |
@implementation ALFileCopier | |
- (instancetype)init | |
{ | |
if ((self = [super init]) == nil) | |
{ | |
return nil; | |
} | |
_scheduler = dispatch_queue_create("com.lobianco.file-copier.scheduler", DISPATCH_QUEUE_SERIAL); | |
_readScheduler = dispatch_queue_create("com.lobianco.file-copier.read-scheduler", DISPATCH_QUEUE_SERIAL); | |
return self; | |
} | |
- (void)copyData:(NSData *)data | |
toPath:(NSString *)outputPath | |
completion:(void (^)(NSURL * _Nullable))completion | |
{ | |
[self copyData:data | |
toPath:outputPath | |
fromByteOffset:0 | |
completion:completion]; | |
} | |
- (void)copyData:(NSData *)data | |
toPath:(NSString *)outputPath | |
fromByteOffset:(NSUInteger)offset | |
completion:(void (^)(NSURL * _Nullable))completion | |
{ | |
dispatch_async(self.scheduler, ^{ | |
if (data.length == 0) | |
{ | |
NSLog(@"Data is empty."); | |
AL_CALL(completion, nil); | |
return; | |
} | |
// file sizes | |
NSUInteger inputFileSize = data.length; | |
if ([self canReadFileOfSize:inputFileSize andWriteToPath:outputPath fromByteOffset:offset] == NO) | |
{ | |
AL_CALL(completion, nil); | |
return; | |
} | |
BOOL outputFileSuccess = [self createFileAtPath:outputPath contents:nil attributes:nil]; | |
NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:outputPath]; | |
if (outputFileSuccess == NO || outputFileHandle == nil) | |
{ | |
NSLog(@"Couldn't create output file handle."); | |
AL_CALL(completion, nil); | |
return; | |
} | |
NSUInteger chunkSize = [self chunkSize]; | |
NSUInteger outputFileSize = inputFileSize - offset; | |
NSUInteger loopCount = [self loopCountForFileSize:outputFileSize]; | |
dispatch_async(self.readScheduler, ^{ | |
NSData *inputData = nil; | |
BOOL stopped = NO; | |
BOOL read = YES; | |
NSUInteger i = 0; | |
NSUInteger chunkOffset = offset; | |
while (read) | |
{ | |
if ([self shouldContinueWritingToPath:outputPath loop:i loopCount:loopCount] == NO) | |
{ | |
read = NO; | |
stopped = YES; | |
break; | |
} | |
NSInteger adjustedChunkSize = MIN(inputFileSize - chunkOffset, chunkSize); | |
@autoreleasepool { | |
inputData = [data subdataWithRange:NSMakeRange(chunkOffset, adjustedChunkSize)]; | |
if (inputData.length == 0) | |
{ | |
NSLog(@"EOF"); | |
read = NO; | |
} | |
else | |
{ | |
[outputFileHandle writeData:inputData]; | |
} | |
} | |
chunkOffset += adjustedChunkSize; | |
i++; | |
} | |
[outputFileHandle closeFile]; | |
if (stopped) | |
{ | |
if ([self removeItemAtPath:outputPath error:nil] == NO) | |
{ | |
NSLog(@"Couldn't remove partially copied file!"); | |
} | |
AL_CALL(completion, nil); | |
} | |
else | |
{ | |
AL_CALL(completion, [NSURL fileURLWithPath:outputPath]); | |
} | |
}); | |
}); | |
} | |
- (void)copyFileAtPath:(NSString *)inputPath | |
toPath:(NSString *)outputPath | |
completion:(void (^) (NSURL *))completion | |
{ | |
[self copyFileAtPath:inputPath | |
toPath:outputPath | |
fromByteOffset:0 | |
completion:completion]; | |
} | |
- (void)copyFileAtPath:(NSString *)inputPath | |
toPath:(NSString *)outputPath | |
fromByteOffset:(NSUInteger)offset | |
completion:(void (^) (NSURL *))completion | |
{ | |
dispatch_async(self.scheduler, ^{ | |
if ([self fileExistsAtPath:inputPath] == NO) | |
{ | |
NSLog(@"Input file does not exist."); | |
AL_CALL(completion, nil); | |
return; | |
} | |
// file sizes | |
long long inputFileSize = [[self attributesOfItemAtPath:inputPath error:nil][NSFileSize] longLongValue]; | |
if ([self canReadFileOfSize:inputFileSize andWriteToPath:outputPath fromByteOffset:offset] == NO) | |
{ | |
AL_CALL(completion, nil); | |
return; | |
} | |
NSFileHandle *inputFileHandle = [NSFileHandle fileHandleForReadingAtPath:inputPath]; | |
if (inputFileHandle == nil) | |
{ | |
NSLog(@"Couldn't create input file handle."); | |
AL_CALL(completion, nil); | |
return; | |
} | |
BOOL outputFileSuccess = [self createFileAtPath:outputPath contents:nil attributes:nil]; | |
NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:outputPath]; | |
if (outputFileSuccess == NO || outputFileHandle == nil) | |
{ | |
NSLog(@"Couldn't create output file handle."); | |
AL_CALL(completion, nil); | |
return; | |
} | |
NSUInteger chunkSize = [self chunkSize]; | |
// instead of checking the delegate on every single loop (which could potentially | |
// be thousands of times), let's just check it a maximum of 100 times over the | |
// course of the file read (one check for each % written). | |
// | |
long long outputFileSize = inputFileSize - offset; | |
NSUInteger loopCount = [self loopCountForFileSize:outputFileSize]; | |
// seek to the offset of the file | |
[inputFileHandle seekToFileOffset:offset]; | |
dispatch_async(self.readScheduler, ^{ | |
NSData *inputData = nil; | |
BOOL stopped = NO; | |
BOOL read = YES; | |
NSUInteger i = 0; | |
while (read) | |
{ | |
if ([self shouldContinueWritingToPath:outputPath loop:i loopCount:loopCount] == NO) | |
{ | |
read = NO; | |
stopped = YES; | |
break; | |
} | |
// using @autoreleasepool as suggested by | |
// http://www.jorambarrez.be/blog/2012/05/09/not_as_memory_efficient_as_you_thought/ | |
// and http://stackoverflow.com/a/8027618/969967 | |
// | |
@autoreleasepool { | |
inputData = [inputFileHandle readDataOfLength:chunkSize]; | |
if (inputData.length == 0) | |
{ | |
NSLog(@"EOF"); | |
read = NO; | |
} | |
else | |
{ | |
[outputFileHandle writeData:inputData]; | |
} | |
} | |
i++; | |
} | |
[inputFileHandle closeFile]; | |
[outputFileHandle closeFile]; | |
if (stopped) | |
{ | |
if ([self removeItemAtPath:outputPath error:nil] == NO) | |
{ | |
NSLog(@"Couldn't remove partially copied file!"); | |
} | |
AL_CALL(completion, nil); | |
} | |
else | |
{ | |
AL_CALL(completion, [NSURL fileURLWithPath:outputPath]); | |
} | |
}); | |
}); | |
} | |
#pragma mark - Internal methods | |
- (NSUInteger)chunkSize | |
{ | |
static NSUInteger chunkSize = 1024 * 64; // 64kb | |
return chunkSize; | |
} | |
- (NSUInteger)loopCountForFileSize:(uint64_t)size | |
{ | |
static NSUInteger numOfDelegateChecks = 100; | |
return MAX(((size / [self chunkSize]) / numOfDelegateChecks), 1); | |
} | |
- (BOOL)canReadFileOfSize:(uint64_t)size andWriteToPath:(NSString *)path fromByteOffset:(NSUInteger)offset | |
{ | |
if ([self fileExistsAtPath:path]) | |
{ | |
NSLog(@"File already exists at output path."); | |
return NO; | |
} | |
if (offset > size) | |
{ | |
NSLog(@"Offset exceeds input file size."); | |
return NO; | |
} | |
NSDictionary *attributes = [self attributesOfFileSystemForPath:[path stringByDeletingLastPathComponent] error:nil]; | |
uint64_t freeSpace = [attributes[NSFileSystemFreeSize] unsignedLongLongValue]; | |
if (size > freeSpace) | |
{ | |
NSLog(@"Not enough free space to copy file."); | |
return NO; | |
} | |
return YES; | |
} | |
- (BOOL)shouldContinueWritingToPath:(NSString *)path loop:(NSUInteger)loop loopCount:(NSUInteger)count | |
{ | |
if (loop % count == 0) | |
{ | |
if ([self.copierDelegate respondsToSelector:@selector(fileCopier:shouldContinueCopyingFileToPath:)]) | |
{ | |
if ([self.copierDelegate fileCopier:self shouldContinueCopyingFileToPath:path] == NO) | |
{ | |
NSLog(@"File copy ended prematurely."); | |
return NO; | |
} | |
} | |
} | |
return YES; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment