Skip to content

Instantly share code, notes, and snippets.

@TheDreamsWind
Created September 25, 2022 19:47
Show Gist options
  • Save TheDreamsWind/1fc5645f70bb04777f0e1db56b7ea639 to your computer and use it in GitHub Desktop.
Save TheDreamsWind/1fc5645f70bb04777f0e1db56b7ea639 to your computer and use it in GitHub Desktop.
[SO-a/73847407/5690248] `TDWFTPUploader` a helper class to upload files to ftp servers in iOS/macOS
//
// TDWFTPUploader.h
// iOSPlayground
//
// Created by Aleksandr Medvedev on 25.09.2022.
//
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
typedef void(^TDWFTPUploaderCallback)(NSError *_Nullable);
@interface TDWFTPUploader : NSObject
@property (strong, readonly, nonatomic) NSURL *fileURL;
@property (strong, readonly, nonatomic) NSURL *uploadURL;
- (instancetype)initWithFileURL:(NSURL *)fileURL
uploadURL:(NSURL *)uploadURL
userLogin:(nullable NSString *)login
userPassword:(nullable NSString *)password;
- (void)resumeWithCallback:(nullable TDWFTPUploaderCallback)callback;
@end
NS_ASSUME_NONNULL_END
//
// TDWFTPUploader.m
// iOSPlayground
//
// Created by Aleksandr Medvedev on 25.09.2022.
//
#import "TDWFTPUploader.h"
typedef NS_ENUM(int8_t, TDWFTPUploaderState) {
TDWFTPUploaderStateSuspended,
TDWFTPUploaderStateRunning,
TDWFTPUploaderStateCancelled,
TDWFTPUploaderStateFinished,
};
@interface TDWFTPUploader () <NSStreamDelegate>
@property (strong, readonly, nonatomic) NSInputStream *inputStream;
@property (strong, readonly, nonatomic) NSOutputStream *outputStream;
@property (assign, nonatomic) TDWFTPUploaderState state;
@property (strong, readonly, nonatomic) dispatch_queue_t stateAccessQueue;
@property (assign, readonly, nonatomic) uint8_t *dataBuffer;
@property (assign, nonatomic) size_t dataBufferOffset;
@property (assign, nonatomic) size_t dataBufferLimit;
@property (strong, nonatomic) NSError *error;
@property (copy, nonatomic) TDWFTPUploaderCallback callback;
@end
static const size_t kDataBufferSize = 1 << 15;
@implementation TDWFTPUploader
@synthesize state = _state;
#pragma mark Lifecycle
- (instancetype)initWithFileURL:(NSURL *)fileURL
uploadURL:(NSURL *)uploadURL
userLogin:(NSString *)login
userPassword:(NSString *)password {
if (self = [super init]) {
NSInputStream *readStream = [NSInputStream inputStreamWithURL:fileURL];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
CFWriteStreamRef writeStream = CFWriteStreamCreateWithFTPURL(kCFAllocatorDefault, (__bridge CFURLRef)uploadURL);
if (!readStream || !writeStream) {
return nil;
}
_outputStream = (__bridge_transfer NSOutputStream *)writeStream;
if (login) {
[_outputStream setProperty:login forKey:(__bridge NSString *)kCFStreamPropertyFTPUserName];
}
if (password) {
[_outputStream setProperty:password forKey:(__bridge NSString *)kCFStreamPropertyFTPPassword];
}
#pragma GCC diagnostic pop
_fileURL = fileURL;
_uploadURL = uploadURL;
_inputStream = readStream;
_outputStream.delegate = self;
_state = TDWFTPUploaderStateSuspended;
_stateAccessQueue = dispatch_queue_create("the.dreams.wind.queues.access.TDWFTPUploader.state", DISPATCH_QUEUE_CONCURRENT);
_dataBuffer = malloc(kDataBufferSize);
_callback = nil;
}
return self;
}
- (void)resumeWithCallback:(nullable TDWFTPUploaderCallback)callback {
if (self.state != TDWFTPUploaderStateSuspended) {
return;
}
self.callback = callback;
self.state = TDWFTPUploaderStateRunning;
[self performSelectorInBackground:@selector(p_uploadDataInBackground) withObject:nil];
}
- (void)dealloc {
if (_outputStream) {
[_outputStream close];
}
if (_inputStream) {
[_inputStream close];
}
free(_dataBuffer);
}
#pragma mark Properties
- (TDWFTPUploaderState)state {
__block TDWFTPUploaderState tempState;
dispatch_sync(_stateAccessQueue, ^{
tempState = _state;
});
return tempState;
}
- (void)setState:(TDWFTPUploaderState)state {
typeof(self) __weak weakSelf = self;
dispatch_barrier_async(_stateAccessQueue, ^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
strongSelf->_state = state;
});
}
#pragma mark NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
if (self.state != TDWFTPUploaderStateRunning) {
[aStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
return;
}
switch (eventCode) {
case NSStreamEventOpenCompleted:
[_inputStream open];
return;
case NSStreamEventHasSpaceAvailable:
if (_dataBufferOffset == _dataBufferLimit) {
NSInteger bytesRead = [_inputStream read:_dataBuffer maxLength:kDataBufferSize];
switch (bytesRead) {
case -1:
[self p_cancelWithError:_inputStream.streamError];
return;
case 0:
[aStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
self.state = TDWFTPUploaderStateFinished;
return;
default:
_dataBufferOffset = 0;
_dataBufferLimit = bytesRead;
}
}
if (_dataBufferOffset != _dataBufferLimit) {
NSInteger bytesWritten = [_outputStream write:&_dataBuffer[_dataBufferOffset]
maxLength:_dataBufferLimit - _dataBufferOffset];
if (bytesWritten == -1) {
[self p_cancelWithError:_outputStream.streamError];
return;
} else {
self.dataBufferOffset += bytesWritten;
}
}
return;
case NSStreamEventErrorOccurred:
[self p_cancelWithError:_outputStream.streamError];
return;
default:
break;
}
}
#pragma mark Private
- (void)p_cancelWithError:(NSError *)error {
self.state = TDWFTPUploaderStateCancelled;
self.error = error;
[_outputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
}
- (void)p_uploadDataInBackground {
@autoreleasepool {
NSRunLoop *loop = NSRunLoop.currentRunLoop;
[_outputStream scheduleInRunLoop:loop forMode:NSDefaultRunLoopMode];
[_outputStream open];
while (
self.state == TDWFTPUploaderStateRunning &&
[loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
) {}
if (_callback) {
_callback(_error);
}
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment