diff --git a/FTPKit Tests/FTPKit_Tests.m b/FTPKit Tests/FTPKit_Tests.m index 661e416..88de5ab 100644 --- a/FTPKit Tests/FTPKit_Tests.m +++ b/FTPKit Tests/FTPKit_Tests.m @@ -42,6 +42,10 @@ - (void)testFtp { FTPClient * ftp = [[FTPClient alloc] initWithHost:@"localhost" port:21 username:@"unittest" password:@"unitpass"]; + // Sanity. Make sure the root path exists. This should always be true. + BOOL success = [ftp directoryExistsAtPath:@"/"]; + XCTAssertTrue(success, @""); + NSArray *contents = [ftp listContentsAtPath:@"/test" showHiddenFiles:YES]; //XCTAssertNil(contents, @"Directory should not exist"); XCTAssertEqual(0, contents.count, @""); @@ -56,7 +60,7 @@ - (void)testFtp NSURL *localUrl = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:@"ftplib.tgz"]; // Download 'ftplib.tgz' - BOOL success = [ftp downloadFile:@"/ftplib.tgz" to:localUrl.path progress:NULL]; + success = [ftp downloadFile:@"/ftplib.tgz" to:localUrl.path progress:NULL]; XCTAssertTrue(success, @""); // Upload 'ftplib.tgz' as 'copy.tgz' @@ -80,7 +84,7 @@ - (void)testFtp XCTAssertTrue(exists, @""); exists = [ftp directoryExistsAtPath:@"/badpath"]; - XCTAssertTrue(exists, @""); + XCTAssertFalse(exists, @""); bytes = [ftp fileSizeAtPath:@"/badpath.txt"]; XCTAssertEqual(-1, bytes, @""); @@ -107,6 +111,25 @@ - (void)testFtp success = [ftp createDirectoryAtPath:@"/test/test2"]; XCTAssertTrue(success, @""); + NSString *cwd = [ftp printWorkingDirectory]; + XCTAssertTrue([cwd isEqualToString:@"/"], @""); + + // Change directory to /test + success = [ftp changeDirectoryToPath:@"/test"]; + XCTAssertTrue(success, @""); + + /** + Currently the connection is not left open between calls and therefore we + will always be put back to the root directory when each command is sent. + + Uncomment this when the same connection is used between commands. + + // Make sure we are still in /test. + cwd = [ftp printWorkingDirectory]; + NSLog(@"cwd is %@", cwd); + XCTAssertTrue([cwd isEqualToString:@"/test"], @""); + */ + // List contents of 'test' contents = [ftp listContentsAtPath:@"/test" showHiddenFiles:YES]; diff --git a/FTPKit.xcodeproj/project.xcworkspace/xcshareddata/FTPKit.xccheckout b/FTPKit.xcodeproj/project.xcworkspace/xcshareddata/FTPKit.xccheckout index 7204b8a..f9cdbd2 100644 --- a/FTPKit.xcodeproj/project.xcworkspace/xcshareddata/FTPKit.xccheckout +++ b/FTPKit.xcodeproj/project.xcworkspace/xcshareddata/FTPKit.xccheckout @@ -5,7 +5,7 @@ IDESourceControlProjectFavoriteDictionaryKey IDESourceControlProjectIdentifier - 540164E0-A7B5-4DAB-B0C4-5F108479B2B0 + 318C0C2B-1164-4EF8-AF86-3CEB547A24B6 IDESourceControlProjectName FTPKit IDESourceControlProjectOriginsDictionary diff --git a/FTPKit.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate b/FTPKit.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate index 396038c..0a180f5 100644 Binary files a/FTPKit.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate and b/FTPKit.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/FTPKit/FTPClient.h b/FTPKit/FTPClient.h index dfa44c7..a99779a 100644 --- a/FTPKit/FTPClient.h +++ b/FTPKit/FTPClient.h @@ -4,6 +4,10 @@ Consider implementing more of the commands specified at: http://en.wikipedia.org/wiki/List_of_FTP_commands + Currently this creates a new connection to the FTP server for every command + issued. This means the state of the current working directory is NOT kept and. + therefore, some commands are not of use. + */ #import "FTPHandle.h" @@ -414,12 +418,70 @@ */ - (NSDate *)lastModifiedAtPath:(NSString *)remotePath; +/** + Refer to lastModifiedAtPath: + + This adds the ability to perform the operation asynchronously. + + @param remotePath Remote path to check + @param success Method called when process succeeds. 'lastModified' is the + last modified time. + @param failure Method called when process fails. + */ +- (void)lastModifiedAtPath:(NSString *)remotePath + success:(void (^)(NSDate *lastModified))success + failure:(void (^)(NSError *error))failure; + /** Check if a remote directory exists. + Please note that this internally calls [self changeDirectoryToPath:] and does + _not_ change back to the previous directory! + @param remotePath Directory to check @return YES if the directory exists. NO, otherwise */ - (BOOL)directoryExistsAtPath:(NSString *)remotePath; +/** + Refer to directoryExistsAtPath: + + This adds the ability to perform the operation asynchronously. + + @param remotePath Remote path to check + @param success Method called when process succeeds. 'exists' will be YES if the + directory exists. NO otherwise. + @param failure Method called when process fails. + */ +- (void)directoryExistsAtPath:(NSString *)remotePath + success:(void (^)(BOOL exists))success + failure:(void (^)(NSError *error))failure; + +/** + Change the working directory to remotePath. + + @note This is currently used ONLY to determine if a directory exists on the + server. The state of the cwd is not saved between commands being issued. This + is because a new connection is created for every command issued. + + Therefore, in its current state, it is used in a very limited scope. Eventually + you will be able to issue commands in the cwd. Not right now. + + @param remotePath Remote directory path to make current directory. + @return YES if the directory was successfully changed. + */ +- (BOOL)changeDirectoryToPath:(NSString *)remotePath; + +/** + Returns the current working directory. + + @note Currently this will always return the root path. This is because the + lib creates a new connection for every command issued to the server -- and + therefore the command will always being in the root path when issuing the + command. + + @return The current working directory. + */ +- (NSString *)printWorkingDirectory; + @end \ No newline at end of file diff --git a/FTPKit/FTPClient.m b/FTPKit/FTPClient.m index a8a53d7..86dbe54 100644 --- a/FTPKit/FTPClient.m +++ b/FTPKit/FTPClient.m @@ -57,6 +57,14 @@ - (NSArray *)parseListData:(NSData *)data handle:(FTPHandle *)handle showHiddent */ - (void)failedWithMessage:(NSString *)message; +/** + Convenience method that wraps failure(error) in dispatch_async(main_queue) + and ensures that the error is copied before sending back to callee -- to ensure + it doesn't get nil'ed out by the next command before the callee has a chance + to read the error. + */ +- (void)returnFailure:(void (^)(NSError *error))failure; + @end @implementation FTPClient @@ -97,8 +105,7 @@ - (long long int)fileSizeAtPath:(NSString *)path int stat = FtpSize(cPath, &bytes, FTPLIB_BINARY, conn); FtpQuit(conn); if (stat == 0) { - FKLogError(@"SIZE %@", path); - self.lastError = [NSError FTPKitErrorWithCode:451]; + FKLogError(@"File most likely does not exist %@", path); return -1; } FKLogDebug(@"%@ bytes %d", path, bytes); @@ -139,6 +146,11 @@ - (NSArray *)listContentsAtHandle:(FTPHandle *)handle showHiddenFiles:(BOOL)show self.lastError = error; return nil; } + /** + Please note: If there are no contents in the folder OR if the folder does + not exist data.bytes _will_ be 0. Therefore, you can not use this method to + determine if a directory exists! + */ [[NSFileManager defaultManager] removeItemAtPath:tmpPath error:&error]; // Log the error, but do not fail. if (error) { @@ -157,9 +169,7 @@ - (void)listContentsAtHandle:(FTPHandle *)handle showHiddenFiles:(BOOL)showHidde success(contents); }); } else if (! contents && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -202,9 +212,7 @@ - (void)downloadHandle:(FTPHandle *)handle to:(NSString *)localPath progress:(BO success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -239,9 +247,7 @@ - (void)uploadFile:(NSString *)localPath to:(NSString *)remotePath progress:(BOO success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -281,9 +287,7 @@ - (void)createDirectoryAtHandle:(FTPHandle *)handle success:(void (^)(void))succ success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -337,9 +341,7 @@ - (void)deleteHandle:(FTPHandle *)handle success:(void (^)(void))success failure success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -385,9 +387,7 @@ - (void)chmodHandle:(FTPHandle *)handle toMode:(int)mode success:(void (^)(void) success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -418,9 +418,7 @@ - (void)renamePath:(NSString *)sourcePath to:(NSString *)destPath success:(void success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -453,9 +451,7 @@ - (void)copyPath:(NSString *)sourcePath to:(NSString *)destPath success:(void (^ success(); }); } else if (! ret && failure) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure(_lastError); - }); + [self returnFailure:failure]; } }); } @@ -600,12 +596,108 @@ - (NSDate *)lastModifiedAtPath:(NSString *)remotePath return date; } +- (void)lastModifiedAtPath:(NSString *)remotePath success:(void (^)(NSDate *))success failure:(void (^)(NSError *))failure +{ + dispatch_async(_queue, ^{ + NSDate *date = [self lastModifiedAtPath:remotePath]; + if (! _lastError && success) { + dispatch_async(dispatch_get_main_queue(), ^{ + success(date); + }); + } else if (_lastError && failure) { + [self returnFailure:failure]; + } + }); +} + - (BOOL)directoryExistsAtPath:(NSString *)remotePath { - NSArray *contents = [self listContentsAtPath:remotePath showHiddenFiles:NO]; - if (contents) + /** + Test the directory by changing to the directory. If the process succeeds + then the directory exists. + + The process is to get the current working directory and change _back_ to + the previous current working directory. There is a possibility that the + second changeDirectoryToPath: may fail! This is really the price we pay + for this command as there is no other accurate way to determine this. + + Using listContentsAtPath:showHiddenFiles: will fail as it will return empty + contents even if the directory doesn't exist! So long as the command + _succeeds_ it will return an empty list. + + // Get the current working directory. We will change back to this directory + // if necessary. + NSString *cwd = [self printWorkingDirectory]; + // No need to continue. We already know the path exists by the fact that we + // are currently _in_ the directory. + if ([cwd isEqualToString:remotePath]) return YES; - return NO; + // Test directory by changing to it. + BOOL success = [self changeDirectoryToPath:remotePath]; + // Attempt to change back to the previous directory. + if (success) + [self changeDirectoryToPath:cwd]; + return success; + */ + + /** + Currently the lib creates a new connection for every command issued. + Therefore, it is unnecessary to change back to the original cwd. + */ + BOOL success = [self changeDirectoryToPath:remotePath]; + return success; +} + +- (void)directoryExistsAtPath:(NSString *)remotePath success:(void (^)(BOOL))success failure:(void (^)(NSError *))failure +{ + dispatch_async(_queue, ^{ + BOOL exists = [self directoryExistsAtPath:remotePath]; + if (! _lastError && success) { + dispatch_async(dispatch_get_main_queue(), ^{ + success(exists); + }); + } else if (_lastError && failure) { + [self returnFailure:failure]; + } + }); +} + +- (BOOL)changeDirectoryToPath:(NSString *)remotePath +{ + netbuf *conn = [self connect]; + if (conn == NULL) + return NO; + const char *cPath = [remotePath cStringUsingEncoding:NSUTF8StringEncoding]; + int stat = FtpChdir(cPath, conn); + FtpQuit(conn); + if (stat == 0) { + self.lastError = [NSError FTPKitErrorWithCode:450]; + return NO; + } + return YES; +} + +- (NSString *)printWorkingDirectory +{ + netbuf *conn = [self connect]; + if (conn == NULL) + return NO; + char cPath[kFTPKitTempBufferSize]; + int stat = FtpPwd(cPath, kFTPKitTempBufferSize, conn); + FtpQuit(conn); + if (stat == 0) { + self.lastError = [NSError FTPKitErrorWithCode:450]; + return NO; + } + return [NSString stringWithCString:cPath encoding:NSUTF8StringEncoding]; +} + +- (void)returnFailure:(void (^)(NSError *))failure +{ + NSError *error = [_lastError copy]; + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); } @end diff --git a/Libraries/include/ftplib/src/ftplib.c b/Libraries/include/ftplib/src/ftplib.c index d651e44..c2318ff 100644 --- a/Libraries/include/ftplib/src/ftplib.c +++ b/Libraries/include/ftplib/src/ftplib.c @@ -1359,7 +1359,7 @@ GLOBALDEF int FtpPwd(char *path, int max, netbuf *nControl) int l = max; char *b = path; char *s; - if (!FtpSendCmd("PWD",'2',nControl)) + if (!FtpSendCmd("PWD", '2', nControl)) return 0; s = strchr(nControl->response, '"'); if (s == NULL) diff --git a/README.md b/README.md index a645a29..e053499 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FTPKit -Version 1.0.0b RC2 +Version 1.2.0 FTPKit is an Objective-C library providing facilities implementing the client side of the File Transfer Protocol (FTP). @@ -164,6 +164,26 @@ Please note that the `progress:` parameter has not yet been implemented. // Display an error... }]; +## Check if a directory exists + + BOOL success = [ftp directoryExistsAtPath:@"/mypath"]; + if (! success) { + // Display an error... + } + + ... + + // Or, make the call asynchronous; + [ftp directoryExistsAtPath:@"/mypath" success:^(BOOL exists) { + if (exists) { + // The file exists. + } else { + // The file doesn't exist. + } + } failure:^(NSError *error) { + // Display an error... + }]; + # Setup & Integration ## Requirements diff --git a/Sample/FTPKitSample/FTPKitSample.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate b/Sample/FTPKitSample/FTPKitSample.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate index ae0b7d1..1dcf190 100644 Binary files a/Sample/FTPKitSample/FTPKitSample.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate and b/Sample/FTPKitSample/FTPKitSample.xcodeproj/project.xcworkspace/xcuserdata/eric.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sample/FTPKitSample/FTPKitSample/Tests/GeneralTest.m b/Sample/FTPKitSample/FTPKitSample/Tests/GeneralTest.m index 168e1a8..6906987 100644 --- a/Sample/FTPKitSample/FTPKitSample/Tests/GeneralTest.m +++ b/Sample/FTPKitSample/FTPKitSample/Tests/GeneralTest.m @@ -12,6 +12,16 @@ - (void)run self.ftp = [FTPClient clientWithHost:@"localhost" port:21 username:@"unittest" password:@"unitpass"]; NSURL *localUrl = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:@"ftplib.tgz"]; + [ftp directoryExistsAtPath:@"/" success:^(BOOL exists) { + if (exists) { + NSLog(@"Success: 000"); + } else { + NSLog(@"Error: Root path '/' must exist"); + } + } failure:^(NSError *error) { + NSLog(@"Error: %@", error.localizedDescription); + }]; + // Note: All of these actions will queue in the order they are called. // Note: All of these tests are 1 to 1 relationship with the tests used within // the FTPKit, except the actions are synchronized. @@ -101,7 +111,7 @@ - (void)run [ftp deleteDirectoryAtPath:@"/test" success:^(void) { NSLog(@"Error: Should have failed!"); } failure:^(NSError *error) { - NSLog(@"Success 011. Error: %@", error.localizedDescription); + NSLog(@"Success 011 -- Error: %@", error.localizedDescription); }]; [ftp deleteFileAtPath:@"/test/copy.tgz" success:^(void) { @@ -121,6 +131,22 @@ - (void)run } failure:^(NSError *error) { NSLog(@"Error: %@", error.localizedDescription); }]; + + [ftp directoryExistsAtPath:@"/badpath" success:^(BOOL exists) { + if (exists) { + NSLog(@"Error: /badpath should not exist"); + } else { + NSLog(@"Success 015"); + } + } failure:^(NSError *error) { + NSLog(@"Error: %@", error.localizedDescription); + }]; + + [ftp lastModifiedAtPath:@"/ftplib.tgz" success:^(NSDate *lastModified) { + NSLog(@"Success 016 -- Date: %@", lastModified); + } failure:^(NSError *error) { + NSLog(@"Error: %@", error.localizedDescription); + }]; } @end