Skip to content

Instantly share code, notes, and snippets.

@lobianco
Last active June 2, 2017 13:58
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 lobianco/a3f863060e368d1d0beb76a0a55f42ef to your computer and use it in GitHub Desktop.
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.
//
// 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
//
// 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