CFFTPSample.c

/*
    File:  CFFTPSample.c
 
    Abstract:  This file shows how to use the CFFTPStream API to download and
    upload files using FTP, and also shows how to parse FTP directory listings.
 
    Version:  1.2
 
    (c) Copyright 2006 Apple Computer, Inc. All rights reserved.
 
    IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
    ("Apple") in consideration of your agreement to the following terms, and
    your use, installation, modification or redistribution of this Apple
    software constitutes acceptance of these terms.  If you do not agree with
    these terms, please do not use, install, modify or redistribute this Apple
    software.
 
    In consideration of your agreement to abide by the following terms, and
    subject to these terms, Apple grants you a personal, non-exclusive license,
    under Apple's copyrights in this original Apple software (the "Apple
    Software"), to use, reproduce, modify and redistribute the Apple Software,
    with or without modifications, in source and/or binary forms; provided that
    if you redistribute the Apple Software in its entirety and without
    modifications, you must retain this notice and the following text and
    disclaimers in all such redistributions of the Apple Software. Neither the
    name, trademarks, service marks or logos of Apple Computer, Inc. may be used
    to endorse or promote products derived from the Apple Software without
    specific prior written permission from Apple.  Except as expressly stated in
    this notice, no other rights or licenses, express or implied, are granted by
    Apple herein, including but not limited to any patent rights that may be
    infringed by your derivative works or by other works in which the Apple
    Software may be incorporated.
 
    The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
    WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
    WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
    PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
    COMBINATION WITH YOUR PRODUCTS.
 
    IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION
    AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER
    THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
    OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
              
    Change History (most recent first):
        1.2      May 2, 2006
        1.1    July 25, 2005
        1.0   August 6, 2004
*/
 
#include <CoreServices/CoreServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>
 
#include <sys/dirent.h>
#include <sys/stat.h>
#include <unistd.h>         // getopt
#include <string.h>         // strmode
#include <stdlib.h>
#include <inttypes.h>
 
 
#pragma mark ***** Common Code and Data Structures
 
 
/* When using file streams, the 32KB buffer is probably not enough.
A good way to establish a buffer size is to increase it over time.
If every read consumes the entire buffer, start increasing the buffer
size, and at some point you would then cap it. 32KB is fine for network
sockets, although using the technique described above is still a good idea.
This sample avoids the technique because of the added complexity it
would introduce. */
#define kMyBufferSize  32768
 
 
/* MyStreamInfo holds the state of a particular operation (download, upload, or 
directory listing.  Some fields are only valid for some operations, as explained 
by their comments. */
typedef struct MyStreamInfo {
 
    CFWriteStreamRef  writeStream;              // download (destination file stream) and upload (FTP stream) only
    CFReadStreamRef   readStream;               // download (FTP stream), upload (source file stream), directory list (FTP stream)
    CFDictionaryRef   proxyDict;                // necessary to workaround <rdar://problem/3745574>, per discussion below
    SInt64            fileSize;                 // download only, 0 indicates unknown
    UInt32            totalBytesWritten;        // download and upload only
    UInt32            leftOverByteCount;        // upload and directory list only, number of valid bytes at start of buffer
    UInt8             buffer[kMyBufferSize];    // buffer to hold left over bytes
 
} MyStreamInfo;
 
 
static const CFOptionFlags kNetworkEvents = 
      kCFStreamEventOpenCompleted
    | kCFStreamEventHasBytesAvailable
    | kCFStreamEventEndEncountered
    | kCFStreamEventCanAcceptBytes
    | kCFStreamEventErrorOccurred;
    
 
/* MyStreamInfoCreate creates a MyStreamInfo 'object' with the specified read and write stream. */
static void
MyStreamInfoCreate(MyStreamInfo ** info, CFReadStreamRef readStream, CFWriteStreamRef writeStream)
{
    MyStreamInfo * streamInfo;
 
    assert(info != NULL);
    assert(readStream != NULL);
    // writeStream may be NULL (this is the case for the directory list operation)
    
    streamInfo = malloc(sizeof(MyStreamInfo));
    assert(streamInfo != NULL);
    
    streamInfo->readStream        = readStream;
    streamInfo->writeStream       = writeStream;
    streamInfo->proxyDict         = NULL;           // see discussion of <rdar://problem/3745574> below
    streamInfo->fileSize          = 0;
    streamInfo->totalBytesWritten = 0;
    streamInfo->leftOverByteCount = 0;
 
    *info = streamInfo;
}
 
 
/* MyStreamInfoDestroy destroys a MyStreamInfo 'object', cleaning up any resources that it owns. */                                       
static void
MyStreamInfoDestroy(MyStreamInfo * info)
{
    assert(info != NULL);
    
    if (info->readStream) {
        CFReadStreamUnscheduleFromRunLoop(info->readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        (void) CFReadStreamSetClient(info->readStream, kCFStreamEventNone, NULL, NULL);
        
        /* CFReadStreamClose terminates the stream. */
        CFReadStreamClose(info->readStream);
        CFRelease(info->readStream);
    }
 
    if (info->writeStream) {
        CFWriteStreamUnscheduleFromRunLoop(info->writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        (void) CFWriteStreamSetClient(info->writeStream, kCFStreamEventNone, NULL, NULL);
        
        /* CFWriteStreamClose terminates the stream. */
        CFWriteStreamClose(info->writeStream);
        CFRelease(info->writeStream);
    }
 
    if (info->proxyDict) {
        CFRelease(info->proxyDict);             // see discussion of <rdar://problem/3745574> below
    }
    
    free(info);
}
 
 
/* MyCFStreamSetUsernamePassword applies the specified user name and password to the stream. */
static void
MyCFStreamSetUsernamePassword(CFTypeRef stream, CFStringRef username, CFStringRef password)
{
    Boolean success;
    assert(stream != NULL);
    assert( (username != NULL) || (password == NULL) );
    
    if (username && CFStringGetLength(username) > 0) {
 
        if (CFGetTypeID(stream) == CFReadStreamGetTypeID()) {
            success = CFReadStreamSetProperty((CFReadStreamRef)stream, kCFStreamPropertyFTPUserName, username);
            assert(success);
            if (password) {
                success = CFReadStreamSetProperty((CFReadStreamRef)stream, kCFStreamPropertyFTPPassword, password);
                assert(success);
            }
        } else if (CFGetTypeID(stream) == CFWriteStreamGetTypeID()) {
            success = CFWriteStreamSetProperty((CFWriteStreamRef)stream, kCFStreamPropertyFTPUserName, username);
            assert(success);
            if (password) {
                success = CFWriteStreamSetProperty((CFWriteStreamRef)stream, kCFStreamPropertyFTPPassword, password);
                assert(success);
            }
        } else {
            assert(false);
        }
    }
}
 
 
/* MyCFStreamSetFTPProxy applies the current proxy settings to the specified stream.  This returns a 
reference to the proxy dictionary that we used because of <rdar://problem/3745574>, discussed below. */
static void
MyCFStreamSetFTPProxy(CFTypeRef stream, CFDictionaryRef * proxyDictPtr)
{
    CFDictionaryRef  proxyDict;
    CFNumberRef      passiveMode;
    CFBooleanRef     isPassive;
    Boolean          success;
    
    assert(stream != NULL);
    assert(proxyDictPtr != NULL);
    
    /* SCDynamicStoreCopyProxies gets the current Internet proxy settings.  Then we call
    CFReadStreamSetProperty, with property name kCFStreamPropertyFTPProxy, to apply the
    settings to the FTP read stream. */
    proxyDict = SCDynamicStoreCopyProxies(NULL);
    assert(proxyDict != NULL);    
    
    /* Get the FTP passive mode setting from the proxy dictionary.  Because of a bug <rdar://problem/3625438>
    setting the kCFStreamPropertyFTPProxy property using the SCDynamicStore proxy dictionary does not
    currently set the FTP passive mode setting on the stream, so we need to do it ourselves. 
    Also, <rdar://problem/4526438> indicates that out in the real world some people are setting 
    kSCPropNetProxiesFTPPassive to a Boolean, as opposed to a number.  That's just incorrect, 
    but I've hardened the code against it. */
    passiveMode = CFDictionaryGetValue(proxyDict, kSCPropNetProxiesFTPPassive);
    if ( (passiveMode != NULL) && (CFGetTypeID(passiveMode) == CFNumberGetTypeID()) ) {
        int         value;
        
        success = CFNumberGetValue(passiveMode, kCFNumberIntType, &value);
        assert(success);
        
        if (value) isPassive = kCFBooleanTrue;
        else isPassive = kCFBooleanFalse;
    } else {
        assert(false);
        isPassive = kCFBooleanTrue;         // if prefs malformed, we just assume true
    }
        
    if (CFGetTypeID(stream) == CFReadStreamGetTypeID()) {
        success = CFReadStreamSetProperty((CFReadStreamRef)stream, kCFStreamPropertyFTPProxy, proxyDict);
        assert(success);
        success = CFReadStreamSetProperty((CFReadStreamRef)stream, kCFStreamPropertyFTPUsePassiveMode, isPassive);
        assert(success);
    } else if (CFGetTypeID(stream) == CFWriteStreamGetTypeID()) {
        success = CFWriteStreamSetProperty((CFWriteStreamRef)stream, kCFStreamPropertyFTPProxy, proxyDict);
        assert(success);
        success = CFWriteStreamSetProperty((CFWriteStreamRef)stream, kCFStreamPropertyFTPUsePassiveMode, isPassive);
        assert(success);
    } else {
        fprintf(stderr, "This is not a CFStream\n");
    }
 
    /* Prior to Mac OS X 10.4, CFFTPStream has a bug <rdar://problem/3745574> that causes it to reference the 
    proxy dictionary that you applied /after/ it has released its last reference to that dictionary.  This causes 
    a crash.  We work around this bug by holding on to our own reference to the proxy dictionary until we're 
    done with the stream.  Thus, our reference prevents the dictionary from being disposed, and thus CFFTPStream 
    can access it safely.  So, rather than release our reference to the proxy dictionary, we pass it back to 
    our caller and require it to release it. */
    
    // CFRelease(proxyDict);  After bug #3745574 is fixed, we'll be able to release the proxyDict here.
    
    *proxyDictPtr = proxyDict;
}
 
 
#pragma mark ***** Download Command
 
 
/* MyDownloadCallBack is the stream callback for the CFFTPStream during a download operation. 
Its main purpose is to read bytes off the FTP stream for the file being downloaded and write 
them to the file stream of the destination file. */
static void
MyDownloadCallBack(CFReadStreamRef readStream, CFStreamEventType type, void * clientCallBackInfo)
{
    MyStreamInfo      *info = (MyStreamInfo *)clientCallBackInfo;
    CFIndex           bytesRead = 0, bytesWritten = 0;
    CFStreamError     error;
    CFNumberRef       cfSize;
    SInt64            size;
    float             progress;
    
    assert(readStream != NULL);
    assert(info       != NULL);
    assert(info->readStream == readStream);
 
    switch (type) {
 
        case kCFStreamEventOpenCompleted:
            /* Retrieve the file size from the CFReadStream. */
            cfSize = CFReadStreamCopyProperty(info->readStream, kCFStreamPropertyFTPResourceSize);
            
            fprintf(stderr, "Open complete\n");
            
            if (cfSize) {
                if (CFNumberGetValue(cfSize, kCFNumberSInt64Type, &size)) {
                    fprintf(stderr, "File size is %" PRId64 "\n", size);
                    info->fileSize = size;
                }
                CFRelease(cfSize);
            } else {
                fprintf(stderr, "File size is unknown\n");
                assert(info->fileSize == 0);            // It was set up this way by MyStreamInfoCreate.
            }
            break;
        case kCFStreamEventHasBytesAvailable:
 
            /* CFReadStreamRead will return the number of bytes read, or -1 if an error occurs
            preventing any bytes from being read, or 0 if the stream's end was encountered. */
            bytesRead = CFReadStreamRead(info->readStream, info->buffer, kMyBufferSize);
            if (bytesRead > 0) {
                /* Just in case we call CFWriteStreamWrite and it returns without writing all
                the data, we loop until all the data is written successfully.  Since we're writing
                to the "local" file system, it's unlikely that CFWriteStreamWrite will return before
                writing all the data, but it would be bad if we simply exited the callback because
                we'd be losing some of the data that we downloaded. */
                bytesWritten = 0;
                while (bytesWritten < bytesRead) {
                    CFIndex result;
 
                    result = CFWriteStreamWrite(info->writeStream, info->buffer + bytesWritten, bytesRead - bytesWritten);
                    if (result <= 0) {
                        fprintf(stderr, "CFWriteStreamWrite returned %ld\n", result);
                        goto exit;
                    }
                    bytesWritten += result;
                }
                info->totalBytesWritten += bytesWritten;
            } else {
                /* If bytesRead < 0, we've hit an error.  If bytesRead == 0, we've hit the end of the file.  
                In either case, we do nothing, and rely on CF to call us with kCFStreamEventErrorOccurred 
                or kCFStreamEventEndEncountered in order for us to do our clean up. */
            }
            
            if (info->fileSize > 0) {
                progress = 100*((float)info->totalBytesWritten/(float)info->fileSize);
                fprintf(stderr, "\r%.0f%%", progress);
            }
            break;
        case kCFStreamEventErrorOccurred:
            error = CFReadStreamGetError(info->readStream);
            fprintf(stderr, "CFReadStreamGetError returned (%d, %ld)\n", error.domain, error.error);
            goto exit;
        case kCFStreamEventEndEncountered:
            fprintf(stderr, "\nDownload complete\n");
            goto exit;
        default:
            fprintf(stderr, "Received unexpected CFStream event (%d)", type);
            break;
    }
    return;
    
exit:    
    MyStreamInfoDestroy(info);
    CFRunLoopStop(CFRunLoopGetCurrent());
    return;
}
 
 
/* MySimpleDownload implements the download command.  It sets up a MyStreamInfo 'object' 
with the read stream being an FTP stream of the file to download and the write stream being 
a file stream of the destination file.  It then returns, and the real work happens 
asynchronously in the runloop.  The function returns true if the stream setup succeeded, 
and false if it failed. */
static Boolean
MySimpleDownload(CFStringRef urlString, CFURLRef destinationFolder, CFStringRef username, CFStringRef password)
{
    CFReadStreamRef        readStream;
    CFWriteStreamRef       writeStream;
    CFStreamClientContext  context = { 0, NULL, NULL, NULL, NULL };
    CFURLRef               downloadPath, downloadURL;
    CFStringRef            fileName;
    Boolean                dirPath, success = true;
    MyStreamInfo           *streamInfo;
 
    assert(urlString != NULL);
    assert(destinationFolder != NULL);
    assert( (username != NULL) || (password == NULL) );
 
    /* Returns true if the CFURL path represents a directory. */
    dirPath = CFURLHasDirectoryPath(destinationFolder);
    if (!dirPath) {
        fprintf(stderr, "Download destination must be a directory.\n");
        return false;
    }
    
    /* Create a CFURL from the urlString. */
    downloadURL = CFURLCreateWithString(kCFAllocatorDefault, urlString, NULL);
    assert(downloadURL != NULL);
 
    /* Copy the end of the file path and use it as the file name. */
    fileName = CFURLCopyLastPathComponent(downloadURL);
    assert(fileName != NULL);
 
    /* Create the downloadPath by taking the destination folder and appending the file name. */
    downloadPath = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, destinationFolder, fileName, false);
    assert(downloadPath != NULL);
    CFRelease(fileName);
 
    /* Create a CFWriteStream for the file being downloaded. */
    writeStream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, downloadPath);
    assert(writeStream != NULL);
    CFRelease(downloadPath);
    
    /* CFReadStreamCreateWithFTPURL creates an FTP read stream for downloading from an FTP URL. */
    readStream = CFReadStreamCreateWithFTPURL(kCFAllocatorDefault, downloadURL);
    assert(readStream != NULL);
    CFRelease(downloadURL);
    
    /* Initialize our MyStreamInfo structure, which we use to store some information about the stream. */
    MyStreamInfoCreate(&streamInfo, readStream, writeStream);
    context.info = (void *)streamInfo;
 
    /* CFWriteStreamOpen will return success/failure.  Opening a stream causes it to reserve all the
    system resources it requires.  If the stream can open non-blocking, this will always return TRUE;
    listen to the run loop source to find out when the open completes and whether it was successful. */
    success = CFWriteStreamOpen(writeStream);
    if (success) {
    
        /* CFReadStreamSetClient registers a callback to hear about interesting events that occur on a stream. */
        success = CFReadStreamSetClient(readStream, kNetworkEvents, MyDownloadCallBack, &context);
        if (success) {
 
            /* Schedule a run loop on which the client can be notified about stream events.  The client
            callback will be triggered via the run loop.  It's the caller's responsibility to ensure that
            the run loop is running. */
            CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
            
            MyCFStreamSetUsernamePassword(readStream, username, password);
            MyCFStreamSetFTPProxy(readStream, &streamInfo->proxyDict);
            
            /* Setting the kCFStreamPropertyFTPFetchResourceInfo property will instruct the FTP stream
            to fetch the file size before downloading the file.  Note that fetching the file size adds
            some time to the length of the download.  Fetching the file size allows you to potentially
            provide a progress dialog during the download operation. You will retrieve the actual file
            size after your CFReadStream Callback gets called with a kCFStreamEventOpenCompleted event. */
            CFReadStreamSetProperty(readStream, kCFStreamPropertyFTPFetchResourceInfo, kCFBooleanTrue);
            
            /* CFReadStreamOpen will return success/failure.  Opening a stream causes it to reserve all the
            system resources it requires.  If the stream can open non-blocking, this will always return TRUE;
            listen to the run loop source to find out when the open completes and whether it was successful. */
            success = CFReadStreamOpen(readStream);
            if (success == false) {
                fprintf(stderr, "CFReadStreamOpen failed\n");
                MyStreamInfoDestroy(streamInfo);
            }
        } else {
            fprintf(stderr, "CFReadStreamSetClient failed\n");
            MyStreamInfoDestroy(streamInfo);
        }
    } else {
        fprintf(stderr, "CFWriteStreamOpen failed\n");
        MyStreamInfoDestroy(streamInfo);
    }
 
    return success;
}
 
 
#pragma mark ***** Upload Command
 
 
/* MyUploadCallBack is the stream callback for the CFFTPStream during an upload operation. 
Its main purpose is to wait for space to become available in the FTP stream (the write stream), 
and then read bytes from the file stream (the read stream) and write them to the FTP stream. */
static void
MyUploadCallBack(CFWriteStreamRef writeStream, CFStreamEventType type, void * clientCallBackInfo)
{
    MyStreamInfo     *info = (MyStreamInfo *)clientCallBackInfo;
    CFIndex          bytesRead;
    CFIndex          bytesAvailable;
    CFIndex          bytesWritten;
    CFStreamError    error;
    
    assert(writeStream != NULL);
    assert(info        != NULL);
    assert(info->writeStream == writeStream);
 
    switch (type) {
 
        case kCFStreamEventOpenCompleted:
            fprintf(stderr, "Open complete\n");
            break;
        case kCFStreamEventCanAcceptBytes:
 
            /* The first thing we do is check to see if there's some leftover data that we read
            in a previous callback, which we were unable to upload for whatever reason. */
            if (info->leftOverByteCount > 0) {
                bytesRead = 0;
                bytesAvailable = info->leftOverByteCount;
            } else {
                /* If not, we try to read some more data from the file.  CFReadStreamRead will 
                return the number of bytes read, or -1 if an error occurs preventing 
                any bytes from being read, or 0 if the stream's end was encountered. */
                bytesRead = CFReadStreamRead(info->readStream, info->buffer, kMyBufferSize);
                if (bytesRead < 0) {
                    fprintf(stderr, "CFReadStreamRead returned %ld\n", bytesRead);
                    goto exit;
                }
                bytesAvailable = bytesRead;
            }
            bytesWritten = 0;
            
            if (bytesAvailable == 0) {
                /* We've hit the end of the file being uploaded.  Shut everything down. 
                Previous versions of this sample would terminate the upload stream 
                by writing zero bytes to the stream.  After discussions with CF engineering, 
                we've decided that it's better to terminate the upload stream by just 
                closing the stream. */
                fprintf(stderr, "\nEnd up uploaded file; closing down\n");
                goto exit;
            } else {
 
                /* CFWriteStreamWrite returns the number of bytes successfully written, -1 if an error has
                occurred, or 0 if the stream has been filled to capacity (for fixed-length streams).
                If the stream is not full, this call will block until at least one byte is written. 
                However, as we're in the kCFStreamEventCanAcceptBytes callback, we know that at least 
                one byte can be written, so we won't block. */
 
                bytesWritten = CFWriteStreamWrite(info->writeStream, info->buffer, bytesAvailable);
                if (bytesWritten > 0) {
 
                    info->totalBytesWritten += bytesWritten;
                    
                    /* If we couldn't upload all the data that we read, we temporarily store the data in our MyStreamInfo
                    context until our CFWriteStream callback is called again with a kCFStreamEventCanAcceptBytes event. 
                    Copying the data down inside the buffer is not the most efficient approach, but it makes the code 
                    significantly easier. */
                    if (bytesWritten < bytesAvailable) {
                        info->leftOverByteCount = bytesAvailable - bytesWritten;
                        memmove(info->buffer, info->buffer + bytesWritten, info->leftOverByteCount);
                    } else {
                        info->leftOverByteCount = 0;
                    }
                } else if (bytesWritten < 0) {
                    fprintf(stderr, "CFWriteStreamWrite returned %ld\n", bytesWritten);
                    /* If CFWriteStreamWrite failed, the write stream is dead.  We will clean up 
                    when we get kCFStreamEventErrorOccurred. */
                }
            }
            
            /* Print a status update if we made any forward progress. */
            if ( (bytesRead > 0) || (bytesWritten > 0) ) {
                fprintf(stderr, "\rRead %7ld bytes; Wrote %8ld bytes", bytesRead, info->totalBytesWritten);
            }
            break;
        case kCFStreamEventErrorOccurred:
            error = CFWriteStreamGetError(info->writeStream);
            fprintf(stderr, "CFReadStreamGetError returned (%d, %ld)\n", error.domain, error.error);
            goto exit;
        case kCFStreamEventEndEncountered:
            fprintf(stderr, "\nUpload complete\n");
            goto exit;
        default:
            fprintf(stderr, "Received unexpected CFStream event (%d)", type);
            break;
    }
    return;
    
exit:
    MyStreamInfoDestroy(info);
    CFRunLoopStop(CFRunLoopGetCurrent());
    return;
}
 
 
 
/* MySimpleUpload implements the upload command.  It sets up a MyStreamInfo 'object' 
with the read stream being a file stream of the file to upload and the write stream being 
an FTP stream of the destination file.  It then returns, and the real work happens 
asynchronously in the runloop.  The function returns true if the stream setup succeeded, 
and false if it failed. */
static Boolean
MySimpleUpload(CFStringRef uploadDirectory, CFURLRef fileURL, CFStringRef username, CFStringRef password)
{
    CFWriteStreamRef       writeStream;
    CFReadStreamRef        readStream;
    CFStreamClientContext  context = { 0, NULL, NULL, NULL, NULL };
    CFURLRef               uploadURL, destinationURL;
    CFStringRef            fileName;
    Boolean                success = true;
    MyStreamInfo           *streamInfo;
 
    assert(uploadDirectory != NULL);
    assert(fileURL != NULL);
    assert( (username != NULL) || (password == NULL) );
    
    /* Create a CFURL from the upload directory string */
    destinationURL = CFURLCreateWithString(kCFAllocatorDefault, uploadDirectory, NULL);
    assert(destinationURL != NULL);
 
    /* Copy the end of the file path and use it as the file name. */
    fileName = CFURLCopyLastPathComponent(fileURL);
    assert(fileName != NULL);
 
    /* Create the destination URL by taking the upload directory and appending the file name. */
    uploadURL = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, destinationURL, fileName, false);
    assert(uploadURL != NULL);
    CFRelease(destinationURL);
    CFRelease(fileName);
    
    /* Create a CFReadStream from the local file being uploaded. */
    readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, fileURL);
    assert(readStream != NULL);
    
    /* Create an FTP write stream for uploading operation to a FTP URL. If the URL specifies a
    directory, the open will be followed by a close event/state and the directory will have been
    created. Intermediary directory structure is not created. */
    writeStream = CFWriteStreamCreateWithFTPURL(kCFAllocatorDefault, uploadURL);
    assert(writeStream != NULL);
    CFRelease(uploadURL);
    
    /* Initialize our MyStreamInfo structure, which we use to store some information about the stream. */
    MyStreamInfoCreate(&streamInfo, readStream, writeStream);
    context.info = (void *)streamInfo;
 
    /* CFReadStreamOpen will return success/failure.  Opening a stream causes it to reserve all the
    system resources it requires.  If the stream can open non-blocking, this will always return TRUE;
    listen to the run loop source to find out when the open completes and whether it was successful. */
    success = CFReadStreamOpen(readStream);
    if (success) {
        
        /* CFWriteStreamSetClient registers a callback to hear about interesting events that occur on a stream. */
        success = CFWriteStreamSetClient(writeStream, kNetworkEvents, MyUploadCallBack, &context);
        if (success) {
 
            /* Schedule a run loop on which the client can be notified about stream events.  The client
            callback will be triggered via the run loop.  It's the caller's responsibility to ensure that
            the run loop is running. */
            CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
            
            MyCFStreamSetUsernamePassword(writeStream, username, password);
            MyCFStreamSetFTPProxy(writeStream, &streamInfo->proxyDict);
            
            /* CFWriteStreamOpen will return success/failure.  Opening a stream causes it to reserve all the
            system resources it requires.  If the stream can open non-blocking, this will always return TRUE;
            listen to the run loop source to find out when the open completes and whether it was successful. */
            success = CFWriteStreamOpen(writeStream);
            if (success == false) {
                fprintf(stderr, "CFWriteStreamOpen failed\n");
                MyStreamInfoDestroy(streamInfo);
            }
        } else {
            fprintf(stderr, "CFWriteStreamSetClient failed\n");
            MyStreamInfoDestroy(streamInfo);
        }
    } else {
        fprintf(stderr, "CFReadStreamOpen failed\n");
        MyStreamInfoDestroy(streamInfo);
    }
 
    return success;
}
 
 
#pragma mark ***** List Command
 
 
/* MyPrintDirectoryListing prints a FTP directory entry, represented by a CFDictionary 
as returned by CFFTPCreateParsedResourceListing, as a single line of text, much like 
you'd get from "ls -l". */
static void
MyPrintDirectoryListing(CFDictionaryRef dictionary)
{
    CFDateRef             cfModDate;
    CFNumberRef           cfType, cfMode, cfSize;
    CFStringRef           cfOwner, cfName, cfLink, cfGroup;
    char                  owner[256], group[256], name[256];
    char                  permString[12], link[1024];
    SInt64                size;
    SInt32                mode, type;
 
    assert(dictionary != NULL);
 
    /* You should not assume that the directory entry dictionary will contain all the possible keys.
    Most of the time it will, however, depending on the FTP server, some of the keys may be missing. */
        
    cfType = CFDictionaryGetValue(dictionary, kCFFTPResourceType);
    if (cfType) {
        assert(CFGetTypeID(cfType) == CFNumberGetTypeID());
        CFNumberGetValue(cfType, kCFNumberSInt32Type, &type);
        
        cfMode = CFDictionaryGetValue(dictionary, kCFFTPResourceMode);
        if (cfMode) {
            assert(CFGetTypeID(cfMode) == CFNumberGetTypeID());
            CFNumberGetValue(cfMode, kCFNumberSInt32Type, &mode);
            
            /* Converts inode status information into a symbolic string */
            strmode(mode + DTTOIF(type), permString);
            
            fprintf(stderr, "%s ", permString);
        }
    }
    
    cfOwner = CFDictionaryGetValue(dictionary, kCFFTPResourceOwner);
    if (cfOwner) {
        assert(CFGetTypeID(cfOwner) == CFStringGetTypeID());
        CFStringGetCString(cfOwner, owner, sizeof(owner), kCFStringEncodingASCII);
        fprintf(stderr, "%9s", owner);
    }
    
    cfGroup = CFDictionaryGetValue(dictionary, kCFFTPResourceGroup);
    if (cfGroup) {
        assert(CFGetTypeID(cfGroup) == CFStringGetTypeID());
        CFStringGetCString(cfGroup, group, sizeof(group), kCFStringEncodingASCII);
        fprintf(stderr, "%9s", group);
    }
    
    cfSize = CFDictionaryGetValue(dictionary, kCFFTPResourceSize);
    if (cfSize) {
        assert(CFGetTypeID(cfSize) == CFNumberGetTypeID());
        CFNumberGetValue(cfSize, kCFNumberSInt64Type, &size);
        fprintf(stderr, "%9lld ", size);
    }
    
    cfModDate = CFDictionaryGetValue(dictionary, kCFFTPResourceModDate);
    if (cfModDate) {
        CFLocaleRef           locale;
        CFDateFormatterRef    formatDate;
        CFDateFormatterRef    formatTime;
        CFStringRef           cfDate;
        CFStringRef           cfTime;
        char                  date[256];
        char                  time[256];
 
        assert(CFGetTypeID(cfModDate) == CFDateGetTypeID());
 
        locale = CFLocaleCopyCurrent();
        assert(locale != NULL);
        
        formatDate = CFDateFormatterCreate(kCFAllocatorDefault, locale, kCFDateFormatterShortStyle, kCFDateFormatterNoStyle   );
        assert(formatDate != NULL);
 
        formatTime = CFDateFormatterCreate(kCFAllocatorDefault, locale, kCFDateFormatterNoStyle,    kCFDateFormatterShortStyle);
        assert(formatTime != NULL);
 
        cfDate = CFDateFormatterCreateStringWithDate(kCFAllocatorDefault, formatDate, cfModDate);
        assert(cfDate != NULL);
 
        cfTime = CFDateFormatterCreateStringWithDate(kCFAllocatorDefault, formatTime, cfModDate);
        assert(cfTime != NULL);
 
        CFStringGetCString(cfDate, date, sizeof(date), kCFStringEncodingUTF8);
        CFStringGetCString(cfTime, time, sizeof(time), kCFStringEncodingUTF8);
        fprintf(stderr, "%10s %5s ", date, time);
 
        CFRelease(cfTime);
        CFRelease(cfDate);
        CFRelease(formatTime);
        CFRelease(formatDate);
        CFRelease(locale);
    }
 
    /* Note that this sample assumes UTF-8 since that's what the Mac OS X
    FTP server returns, however, some servers may use a different encoding. */
    cfName = CFDictionaryGetValue(dictionary, kCFFTPResourceName);
    if (cfName) {
        assert(CFGetTypeID(cfName) == CFStringGetTypeID());
        CFStringGetCString(cfName, name, sizeof(name), kCFStringEncodingUTF8);
        fprintf(stderr, "%s", name);
 
        cfLink = CFDictionaryGetValue(dictionary, kCFFTPResourceLink);
        if (cfLink) {
            assert(CFGetTypeID(cfLink) == CFStringGetTypeID());
            CFStringGetCString(cfLink, link, sizeof(link), kCFStringEncodingUTF8);
            if (strlen(link) > 0) fprintf(stderr, " -> %s", link);
        }
    }
 
    fprintf(stderr, "\n");
}
 
 
/* MyDirectoryListingCallBack is the stream callback for the CFFTPStream during a directory 
list operation. Its main purpose is to read bytes off the FTP stream, which is returning bytes 
of the directory listing, parse them, and 'pretty' print the resulting directory entries. */
static void
MyDirectoryListingCallBack(CFReadStreamRef readStream, CFStreamEventType type, void * clientCallBackInfo)
{
    MyStreamInfo     *info = (MyStreamInfo *)clientCallBackInfo;
    CFIndex          bytesRead;
    CFStreamError    error;
    CFDictionaryRef  parsedDict;
    
    assert(readStream != NULL);
    assert(info       != NULL);
    assert(info->readStream == readStream);
 
    switch (type) {
 
        case kCFStreamEventOpenCompleted:
            fprintf(stderr, "Open complete\n");
            break;
        case kCFStreamEventHasBytesAvailable:
        
            /* When we get here, there are bytes to be read from the stream.  There are two cases:
            either info->leftOverByteCount is zero, in which case we complete processed the last 
            buffer full of data (or we're at the beginning of the listing), or 
            info->leftOverByteCount is non-zero, in which case there are that many bytes at the 
            start of info->buffer that were left over from the last time that we were called. 
            By definition, any left over bytes were insufficient to form a complete directory 
            entry.
            
            In both cases, we just read the next chunk of data from the directory listing stream 
            and append it to our buffer.  We then process the buffer to see if it now contains 
            any complete directory entries. */
 
            /* CFReadStreamRead will return the number of bytes read, or -1 if an error occurs
            preventing any bytes from being read, or 0 if the stream's end was encountered. */
            bytesRead = CFReadStreamRead(info->readStream, info->buffer + info->leftOverByteCount, kMyBufferSize - info->leftOverByteCount);
            if (bytesRead > 0) {
                const UInt8 *   nextByte;
                CFIndex         bytesRemaining;
                CFIndex         bytesConsumedThisTime;
 
                /* Parse directory entries from the buffer until we either run out of bytes 
                or we stop making forward progress (indicating that the buffer does not have 
                enough bytes of valid data to make a complete directory entry). */
 
                nextByte       = info->buffer;
                bytesRemaining = bytesRead + info->leftOverByteCount;
                do
                {                    
 
                    /* CFFTPCreateParsedResourceListing parses a line of file or folder listing
                    of Unix format, and stores the extracted result in a CFDictionary. */
                    bytesConsumedThisTime = CFFTPCreateParsedResourceListing(NULL, nextByte, bytesRemaining, &parsedDict);
                    if (bytesConsumedThisTime > 0) {
 
                        /* It is possible for CFFTPCreateParsedResourceListing to return a positive number 
                        but not create a parse dictionary.  For example, if the end of the listing text 
                        contains stuff that can't be parsed, CFFTPCreateParsedResourceListing returns 
                        a positive number (to tell the calle that it's consumed the data), but doesn't 
                        create a parse dictionary (because it couldn't make sens of the data).
                        So, it's important that we only try to print parseDict if it's not NULL. */
                        
                        if (parsedDict != NULL) {
                            MyPrintDirectoryListing(parsedDict);
                            CFRelease(parsedDict);
                        }
 
                        nextByte       += bytesConsumedThisTime;
                        bytesRemaining -= bytesConsumedThisTime;
 
                    } else if (bytesConsumedThisTime == 0) {
                        /* This should never happen because we supply a pretty large buffer. 
                        Still, we handle it by leaving the loop, which leaves the remaining 
                        bytes in the buffer. */
                    } else if (bytesConsumedThisTime == -1) {
                        fprintf(stderr, "CFFTPCreateParsedResourceListing parse failure\n");
                        goto exit;
                    }
 
                } while ( (bytesRemaining > 0) && (bytesConsumedThisTime > 0) );
                
                /* If any bytes were left over, leave them in the buffer for next time. */
                if (bytesRemaining > 0) {
                    memmove(info->buffer, nextByte, bytesRemaining);                    
                }
                info->leftOverByteCount = bytesRemaining;
            } else {
                /* If bytesRead < 0, we've hit an error.  If bytesRead == 0, we've hit the end of the 
                directory listing.  In either case, we do nothing, and rely on CF to call us with 
                kCFStreamEventErrorOccurred or kCFStreamEventEndEncountered in order for us to do our 
                clean up. */
            }
            break;
        case kCFStreamEventErrorOccurred:
            error = CFReadStreamGetError(info->readStream);
            fprintf(stderr, "CFReadStreamGetError returned (%d, %ld)\n", error.domain, error.error);
            goto exit;
        case kCFStreamEventEndEncountered:
            fprintf(stderr, "Listing complete\n");
            goto exit;
        default:
            fprintf(stderr, "Received unexpected CFStream event (%d)", type);
            break;
    }
    return;
 
exit:
    MyStreamInfoDestroy(info);
    CFRunLoopStop(CFRunLoopGetCurrent());
    return;
}
 
 
/* MySimpleDirectoryListing implements the directory list command.  It sets up a MyStreamInfo 
'object' with the read stream being an FTP stream of the directory to list and with no 
write stream.  It then returns, and the real work happens asynchronously in the runloop.  
The function returns true if the stream setup succeeded, and false if it failed. */
static Boolean
MySimpleDirectoryListing(CFStringRef urlString, CFStringRef username, CFStringRef password)
{
    CFReadStreamRef        readStream;
    CFStreamClientContext  context = { 0, NULL, NULL, NULL, NULL };
    CFURLRef               downloadURL;
    Boolean                success = true;
    MyStreamInfo           *streamInfo;
 
    assert(urlString != NULL);
 
    downloadURL = CFURLCreateWithString(kCFAllocatorDefault, urlString, NULL);
    assert(downloadURL != NULL);
 
    /* Create an FTP read stream for downloading operation from an FTP URL. */
    readStream = CFReadStreamCreateWithFTPURL(kCFAllocatorDefault, downloadURL);
    assert(readStream != NULL);
    CFRelease(downloadURL);
        
    /* Initialize our MyStreamInfo structure, which we use to store some information about the stream. */
    MyStreamInfoCreate(&streamInfo, readStream, NULL);
    context.info = (void *)streamInfo;
 
    /* CFReadStreamSetClient registers a callback to hear about interesting events that occur on a stream. */
    success = CFReadStreamSetClient(readStream, kNetworkEvents, MyDirectoryListingCallBack, &context);
    if (success) {
 
        /* Schedule a run loop on which the client can be notified about stream events.  The client
        callback will be triggered via the run loop.  It's the caller's responsibility to ensure that
        the run loop is running. */
        CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        
        MyCFStreamSetUsernamePassword(readStream, username, password);
        MyCFStreamSetFTPProxy(readStream, &streamInfo->proxyDict);
    
        /* CFReadStreamOpen will return success/failure.  Opening a stream causes it to reserve all the
        system resources it requires.  If the stream can open non-blocking, this will always return TRUE;
        listen to the run loop source to find out when the open completes and whether it was successful. */
        success = CFReadStreamOpen(readStream);
        if (success == false) {
            fprintf(stderr, "CFReadStreamOpen failed\n");
            MyStreamInfoDestroy(streamInfo);
        }
    } else {
        fprintf(stderr, "CFReadStreamSetClient failed\n");
        MyStreamInfoDestroy(streamInfo);
    }
 
    return success;
}
 
 
#pragma mark ***** Main
 
 
static void
MyPrintUsage(const char *executablePath)
{
    const char *programName = strrchr(executablePath, '/') ? strrchr(executablePath, '/') + 1 : executablePath;
    
    fprintf(stderr, "usage: %s -d ftp://ftp.example.com/file.txt [username [password]]\n",   programName);
    fprintf(stderr, "       %s -u ftp://ftp.example.com/ /file.txt [username [password]]\n", programName);
    fprintf(stderr, "       %s -l ftp://ftp.example.com/ [username [password]]\n",           programName);
}
 
 
                                            
int
main (int argc, const char * argv[])
{
    CFStringRef  urlString;
    CFStringRef  username = NULL, password = NULL;
    CFURLRef     destinationURL, fileURL;
    char         *cwd;
    int          operation;
    Boolean      status;
    
    /* Must pass in at least three arguments to the command-line tool. */
    if (argc < 3) { MyPrintUsage(argv[0]); return EXIT_FAILURE; }
 
    /* Retrieve the first argument and return an error if it's not 'd', 'u', or 'l'. */
    operation = getopt(argc, (char * const *)argv, "dul");
    if (operation == -1) { MyPrintUsage(argv[0]); return EXIT_FAILURE; }
    
    /* Use the second argument as the URL and create a CFString from it. */
    urlString = CFStringCreateWithCString(NULL, argv[2], kCFStringEncodingUTF8);
    assert(urlString != NULL);
    
    switch (operation) {
        case 'd':  // Download.
            cwd = getcwd(NULL, 0);     // Get a pointer to the current working directory path.
            assert(cwd != NULL);
            
            /* Create a CFURL from the current working directory CFString. */
            destinationURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *) cwd, strlen(cwd), true);
            assert(destinationURL != NULL);
            free(cwd);
            
            /* If there's a third command-line argument, use it as the username for the download. */
            if (argv[3]) {
                username = CFStringCreateWithCString(NULL, argv[3], kCFStringEncodingUTF8);
                assert(username != NULL);
            
                /* If there's a fourth command-line argument, use it as the password for the download. */
                if (argv[4]) {
                    password = CFStringCreateWithCString(NULL, argv[4], kCFStringEncodingUTF8);
                    assert(password != NULL);
                }
            }
            
            /* Start downloading the specified URL to the current working directory. */
            status = MySimpleDownload(urlString, destinationURL, username, password);
            if (!status) fprintf(stderr, "MySimpleDownload failed\n");
 
            CFRelease(destinationURL);
            break;
        case 'u':  // Upload.
        
            /* Use the third argument as the path of the file to upload. */
            if (argv[3]) {
                fileURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *) argv[3], strlen(argv[3]), false);
                assert(fileURL != NULL);
            
                /* If there's a fourth command-line argument, use it as the username for the upload. */
                if (argv[4]) {
                    username = CFStringCreateWithCString(NULL, argv[4], kCFStringEncodingUTF8);
                    assert(username != NULL);
                
                    /* If there's a fifth command-line argument, use it as the password for the upload. */
                    if (argv[5]) {
                        password = CFStringCreateWithCString(NULL, argv[5], kCFStringEncodingUTF8);
                        assert(password != NULL);
                    }
                }
            
                /* Start uploading a file to the specified URL destination. */
                status = MySimpleUpload(urlString, fileURL, username, password);
                if (!status) fprintf(stderr, "MySimpleUpload failed\n");
                
                CFRelease(fileURL);
            
            } else {
                status = false;
                fprintf(stderr, "You must specify a file to upload.\n");
            }
            
            break;
        case 'l': // Directory Listing.
        
            /* If there's a third command-line argument, use it as the username for the directory listing. */
            if (argv[3]) {
                username = CFStringCreateWithCString(NULL, argv[3], kCFStringEncodingUTF8);
                assert(username != NULL);
            
                /* If there's a fourth command-line argument, use it as the password for the directory listing. */
                if (argv[4]) {
                    password = CFStringCreateWithCString(NULL, argv[4], kCFStringEncodingUTF8);
                    assert(password != NULL);
                }
            }
            
            /* Retrieve the directory listing for the specified URL. */
            status = MySimpleDirectoryListing(urlString, username, password);
            if (!status) fprintf(stderr, "MySimpleDirectoryListing failed\n");
            
            break;
        default:
            assert(false);
            status = false;         // just to quieten a warning
            break;
    }
    CFRelease(urlString);
    if (username) CFRelease(username);
    if (password) CFRelease(password);
    
    /* Start the run loop to receive asynchronous callbacks. */
    if (status) CFRunLoopRun();
 
    return EXIT_SUCCESS;
}