// Copyright 1997-2002 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import <OWF/OWFTPSession.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniNetworking/OmniNetworking.h>
#import <OmniBase/system.h>

#import <OWF/OWAddress.h>
#import <OWF/OWAuthorizationCredential.h>
#import <OWF/OWAuthorizationRequest.h>
#import <OWF/OWContentCache.h>
#import <OWF/OWDataStream.h>
#import <OWF/OWFileInfo.h>
#import <OWF/OWNetLocation.h>
#import <OWF/OWObjectStream.h>
#import <OWF/OWSourceProcessor.h>
#import <OWF/OWURL.h>

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/FTP.subproj/OWFTPSession.m,v 1.25 2002/03/09 01:53:52 kc Exp $")

@interface OWFTPSession (Private)

+ (void)contentCacheFlushedNotification:(NSNotification *)notification;

- (NSData *)storeData;

- (void)setAddress:(OWAddress *)anAddress;
- (void)setPipeline:(OWPipeline *)aPipeline;
- (void)setProcessor:(OWProcessor *)aProcessor;
- (void)cacheSession;

- (void)setCurrentTransferType:(NSString *)aTransferType;
- (NSString *)systemType;

- (BOOL)readResponse;
- (BOOL)sendCommand:(NSString *)command;
- (BOOL)sendCommandFormat:(NSString *)aFormat, ...;

- (void)connect;
- (void)disconnect;

- (ONTCPSocket *)passiveDataSocket;
- (ONTCPSocket *)activeDataSocket;
- (ONSocketStream *)dataSocketStream;

- (void)changeAbsoluteDirectory:(NSString *)path;
- (void)getFile:(NSString *)path;
- (void)getDirectory:(NSString *)path;
- (void)storeData:(NSData *)storeData atPath:(NSString *)path;
- (void)removeFileAtPath:(NSString *)path;
- (void)displayErrorReply:(NSString *)aReply message:(NSString *)aMessage;

- (NSString *)systemTypeForSystemReply:(NSString *)systemReply;

@end

@implementation OWFTPSession

enum {OPEN_SESSIONS, NO_SESSIONS};

static BOOL OWFTPSessionDebug = NO;

static NSMutableDictionary *openSessions;
static NSLock *openSessionsLock;
static NSTimeInterval timeout;
static NSString *asciiTransferType = @"a";
static NSString *imageTransferType = @"i";
static NSString *defaultPassword = nil;

+ (void)initialize;
{
    OBINITIALIZE;

    openSessions = [[NSMutableDictionary alloc] init];
    openSessionsLock = [[NSLock alloc] init];
    timeout = 120.0; // Overridden in +readDefaults
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentCacheFlushedNotification:) name:OWContentCacheFlushedCacheNotification object:nil];
}

+ (void)didLoad;
{
    [[OFController sharedController] addObserver:self];
}

+ (void)controllerDidInitialize:(OFController *)controller;
{
    [self readDefaults];
}

+ (void)readDefaults;
{
    NSUserDefaults *userDefaults;

    userDefaults = [NSUserDefaults standardUserDefaults];
    defaultPassword = [[userDefaults stringForKey:@"OWFTPAnonymousPassword"] retain];
    if (defaultPassword == nil || [defaultPassword isEqualToString:@""])
	defaultPassword = [[NSString alloc] initWithFormat:@"%@@%@", [[NSProcessInfo processInfo] processName], [ONHost domainName]];
    timeout = [userDefaults floatForKey:@"OWFTPSessionTimeout"];
}

+ (OWFTPSession *)ftpSessionForNetLocation:(NSString *)aNetLocation;
{
    OWFTPSession *session;
    NSString *cacheKey;

    cacheKey = aNetLocation;
    [openSessionsLock lock];
    NS_DURING {
	session = [openSessions objectForKey:cacheKey];
	if (session) {
	    [session retain];
	    [openSessions removeObjectForKey:cacheKey];
	}
    } NS_HANDLER {
        session = nil;
	NSLog(@"%@", [localException reason]);
    } NS_ENDHANDLER;
    [openSessionsLock unlock];
    if (!session)
        session = [[self alloc] initWithNetLocation:aNetLocation];

    return [session autorelease];
}

+ (OWFTPSession *)ftpSessionForAddress:(OWAddress *)anAddress;
{
    OWFTPSession *session;

    session = [self ftpSessionForNetLocation:[[anAddress url] netLocation]];
    [session setAddress:anAddress];
    return session;
}

+ (int)defaultPort;
{
    return 21;
}

- initWithNetLocation:(NSString *)aNetLocation;
{
    if (![super init])
	return nil;

    sessionCacheKey = [aNetLocation retain];
    failedCredentials = [[NSMutableArray alloc] init];
    return self;
}

- (void)dealloc;
{
    [self disconnect];
    [sessionCacheKey release];
    [controlSocketStream release];
    [lastReply release];
    [lastMessage release];
    [currentPath release];
    [currentTransferType release];
    [systemType release];
    [ftpAddress release];
    [failedCredentials release];
    [super dealloc];
}

- (void)fetchForProcessor:(OWProcessor *)aProcessor inPipeline:(OWPipeline *)aPipeline;
{
    NSException *raisedException = nil;
    NSData *storeData = nil;
    NSString *ftpPath, *ftpMethod;
    enum { Fetching, Storing, Deleting } action;
    
    ftpMethod = [ftpAddress methodString];
    if ([ftpMethod isEqualToString:@"GET"]) {
        action = Fetching;
    } else if ([ftpMethod isEqualToString:@"PUT"]) {
        action = Storing;
        storeData = [self storeData];
    } else if ([ftpMethod isEqualToString:@"DELETE"]) {
        /* DELETE is a valid HTTP/1.1 method */
        action = Deleting;
    } else {
        [NSException raise:@"UnsupportedMethod" format:NSLocalizedStringFromTableInBundle(@"FTP does not support the \"%@\" method", @"OWF", [OWFTPSession bundle], ftpsession error), ftpMethod];
    }
    
    ftpPath = [[ftpAddress url] fetchPath];
    
    // special case for ftp: the fetchPath shouldn't start with / if the path begins with ~. Needs to be done here instead of in OWURL fetchPath because this rule doesn't apply to other protocols (like http).
    if ([ftpPath length] > 1 && [ftpPath characterAtIndex:1] == '~')
        ftpPath = [ftpPath substringFromIndex:1];
    
    NS_DURING {
	[self setPipeline:aPipeline];
	[self setProcessor:aProcessor];
	[self connect];
        switch (action) {
            case Fetching:
                if ([ftpPath hasSuffix:@"/"])
                    [self getDirectory:ftpPath];
                else
                    [self getFile:ftpPath];
                break;
            case Storing:
                [self storeData:[self storeData] atPath:ftpPath];
                break;
            case Deleting:
                [self removeFileAtPath:ftpPath];
                break;
        }
    } NS_HANDLER {
	raisedException = localException;
    } NS_ENDHANDLER;
    [self cacheSession];
    if (raisedException)
        [raisedException raise];
}

- (void)abortOperation;
{
    abortOperation = YES;
    [abortSocket abortSocket];
    abortSocket = nil;
    [[controlSocketStream socket] abortSocket];
}

@end

@implementation OWFTPSession (Private)

+ (void)contentCacheFlushedNotification:(NSNotification *)notification;
{
    // When the content cache is flushed, flush all the cached HTTP sessions
    [openSessionsLock lock];
    NS_DURING {
        [openSessions removeAllObjects];
    } NS_HANDLER {
        NSLog(@"+[%@ %@]: caught exception %@", NSStringFromClass(self), NSStringFromSelector(_cmd), localException);
    } NS_ENDHANDLER;
    [openSessionsLock unlock];
}

- (NSData *)storeData
{
    NSData *contentData, *contentStringData;
    NSString *contentString;

    contentString = [[ftpAddress methodDictionary] objectForKey:OWAddressContentStringMethodKey];
    contentData = [[ftpAddress methodDictionary] objectForKey:OWAddressContentDataMethodKey];

    if (contentString) {
        contentStringData = [contentString dataUsingEncoding:NSISOLatin1StringEncoding];
        if (!contentStringData)
            [NSException raise:@"UnencodableString" format:NSLocalizedStringFromTableInBundle(@"FTP data contains characters which cannot be converted to ISO Latin-1", @"OWF", [OWFTPSession bundle], ftpsession error)];

        /* -dataByAppendingData: handles the contentData==nil case */
        return [contentStringData dataByAppendingData:contentData];
    } else {
        return contentData;
    }
}

- (void)setAddress:(OWAddress *)anAddress;
{
    if (ftpAddress == anAddress)
	return;
    [ftpAddress release];
    ftpAddress = [anAddress retain];
}

- (void)setPipeline:(OWPipeline *)aPipeline;
{
    nonretainedPipeline = aPipeline;
}

- (void)setProcessor:(OWProcessor *)aProcessor;
{
    nonretainedProcessor = aProcessor;
}

- (void)cacheSession;
{
    [self setPipeline:nil];
    [self setProcessor:nil];
    [self setAddress:nil];
    [openSessionsLock lock];
    NS_DURING {
	[openSessions setObject:self forKey:sessionCacheKey];
    } NS_HANDLER {
	NSLog(@"%@", [localException reason]);
    } NS_ENDHANDLER;
    [openSessionsLock unlock];
}

//

- (void)setCurrentTransferType:(NSString *)aTransferType;
{
    if ([currentTransferType isEqualToString:aTransferType])
	return;

    [nonretainedProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Setting transfer type to %@", @"OWF", [OWFTPSession bundle], ftpsession status), aTransferType];
    if (![self sendCommandFormat:@"TYPE %@", aTransferType])
	[NSException raise:@"SetTransferTypeFailed" format:NSLocalizedStringFromTableInBundle(@"Failed to change transfer type: %@", @"OWF", [OWFTPSession bundle], ftpsession error - TYPE command failed), lastReply];
    [currentTransferType release];
    currentTransferType = [aTransferType retain];
}

- (NSString *)systemType;
{
    if (systemType)
	return systemType;
    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Checking system type", @"OWF", [OWFTPSession bundle], ftpsession status - will send SYST command)];
    if ([self sendCommand:@"SYST"])
	systemType = [[self systemTypeForSystemReply:[lastReply substringFromIndex:4]] retain];
    else
	systemType = [@"unknown" retain];
    [nonretainedProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"System is %@", @"OWF", [OWFTPSession bundle], ftpsession status - result of SYST command), systemType];
    return systemType;
}


//

- (BOOL)readResponse;
{
    NSString *reply;
    NSMutableString *message = nil;

    if (abortOperation)
	[NSException raise:@"FetchAborted" format:NSLocalizedStringFromTableInBundle(@"Fetch stopped", @"OWF", [OWFTPSession bundle], ftpsession error)];

    reply = [controlSocketStream readLine];
    if (OWFTPSessionDebug)
	NSLog(@"FTP Rx...%@", reply);

    if (reply == nil || [reply length] < 4)
	[NSException raise:@"ResponseInvalid" format:NSLocalizedStringFromTableInBundle(@"Invalid response from FTP server", @"OWF", [OWFTPSession bundle], ftpsession error)];

    if ([reply characterAtIndex:3] == '-') {
	NSString *endPrefix;
	NSString *messageLine;

	message = [reply mutableCopy];
	[message appendString:@"\n"];
	endPrefix = [NSString stringWithFormat:@"%@ ", [reply substringToIndex:3]];
	do {
	    messageLine = [controlSocketStream readLine];
	    if (OWFTPSessionDebug)
		NSLog(@"FTP Rx...%@", messageLine);
            if (messageLine == nil)
                [NSException raise:@"ResponseInvalid" format:NSLocalizedStringFromTableInBundle(@"Invalid response from FTP server", @"OWF", [OWFTPSession bundle], ftpsession error)];
	    [message appendString:messageLine];
	    [message appendString:@"\n"];
	} while (![messageLine hasPrefix:endPrefix]);
	reply = messageLine;
    }
    [lastReply release];
    lastReply = [reply retain];
    lastReplyIntValue = [reply intValue];
    [lastMessage release];
    lastMessage = [(message ? (NSString *)message : reply) retain];
    return lastReplyIntValue < 400;
}

- (BOOL)sendCommand:(NSString *)command;
{
    if (abortOperation)
	[NSException raise:@"FetchAborted" format:NSLocalizedStringFromTableInBundle(@"Fetch stopped", @"OWF", [OWFTPSession bundle], ftpsession error)];

    if (OWFTPSessionDebug) {
	if ([command hasPrefix:@"PASS "])
	    NSLog(@"FTP Tx...PASS ********");
	else
	    NSLog(@"FTP Tx...%@", command);
    }

    [controlSocketStream writeString:[command stringByAppendingString:@"\r\n"]];
    return [self readResponse];
}

- (BOOL)sendCommandFormat:(NSString *)aFormat, ...;
{
    NSString *commandString;
    BOOL success;
    va_list argList;

    va_start(argList, aFormat);
    commandString = [[NSString alloc] initWithFormat:aFormat arguments:argList];
    va_end(argList);
    success = [self sendCommand:commandString];
    [commandString release];
    return success;
}

//

- (void)connect;
{
    ONTCPSocket *tcpSocket;
    NSString *netLocation;
    OWNetLocation *parsedNetLocation;
    NSString *username, *password, *port;

    abortOperation = NO;
    if (controlSocketStream) {
	BOOL connectionStillValid;

	NS_DURING {
	    connectionStillValid = [self sendCommand:@"NOOP"];
	} NS_HANDLER {
	    connectionStillValid = NO;
	} NS_ENDHANDLER;
	if (connectionStillValid)
	    return;
	[controlSocketStream release];
	[currentPath release];
	[currentTransferType release];
	[systemType release];
	[lastReply release];
    }
    controlSocketStream = nil;
    currentPath = nil;
    currentTransferType = nil;
    systemType = nil;
    lastReply = nil;

    netLocation = [[ftpAddress url] netLocation];
    [nonretainedProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Contacting %@", @"OWF", [OWFTPSession bundle], ftp session status), netLocation];
    tcpSocket = [ONTCPSocket tcpSocket];
    [tcpSocket setReadBufferSize:32*1024];
    controlSocketStream = [[ONSocketStream alloc] initWithSocket:tcpSocket];
    parsedNetLocation = [[ftpAddress url] parsedNetLocation];
    port = [parsedNetLocation port];
    [tcpSocket connectToHost:[ONHost hostForHostname:[parsedNetLocation hostname]] port:port ? [port intValue] : [isa defaultPort]];
    [nonretainedProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Contacted %@", @"OWF", [OWFTPSession bundle], ftp session status), netLocation];
    [self readResponse];

    username = [parsedNetLocation username];
    password = nil;
    if (!username) {
	username = @"anonymous";
    	if (!(password = [parsedNetLocation password]))
            password = defaultPassword;
    }
    
    if (![self sendCommandFormat:@"USER %@", username]) {
	NSString *errorReply;
	
	[self displayErrorReply:lastReply message:lastMessage];
	errorReply = [[lastReply retain] autorelease];
	[self disconnect];
	[NSException raise:@"LoginFailed" format:NSLocalizedStringFromTableInBundle(@"Login to %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - USER command rejected), netLocation, errorReply];
    }
    
    if (lastReplyIntValue == 331) { // Need password
        OWAuthorizationCredential *tryThis = nil;
        if (username != nil && password == nil) {
            OWAuthorizationRequest *getPassword;
            int credentialIndex;
            NSArray *moreCredentials;
            
            getPassword = [[[OWAuthorizationRequest authorizationRequestClass] alloc] initForType:OWAuth_FTP netLocation:parsedNetLocation defaultPort:[isa defaultPort] pipeline:[nonretainedProcessor pipeline] challenge:nil promptForMoreThan:failedCredentials];
            moreCredentials = [getPassword credentials];
            
            if (!moreCredentials) {
                NSString *errorString = [[[getPassword errorString] retain] autorelease];
                [getPassword release];
                [self displayErrorReply:nil message:errorString];
                [self disconnect];
                [NSException raise:@"LoginFailed" format:NSLocalizedStringFromTableInBundle(@"Login to %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - PASS required but no password available to us), netLocation, errorString];
            }
            
            // set tryThis to the first credential returned that's not in failedCredentials.
            credentialIndex = 0;
            while ([failedCredentials indexOfObjectIdenticalTo:(tryThis = [moreCredentials objectAtIndex:credentialIndex])] != NSNotFound)
                credentialIndex ++;
            // we don't need to check against [moreCredentials count], above, because OWAuthorizationRequest will never return an array containing only objects that are also in failedCredentials.
            
            [[tryThis retain] autorelease];
            [getPassword release];
        }
        
        if (tryThis)
            password = [tryThis valueForKey:@"password"];
            
	if (![self sendCommandFormat:@"PASS %@", password]) {
	    NSString *errorReply;
	    
            if (tryThis) {
                [failedCredentials addObject:tryThis];
            }
	    [self displayErrorReply:lastReply message:lastMessage];
	    errorReply = [[lastReply retain] autorelease];
	    // UNDONE: ask for a new password
	    [self disconnect];
            [NSException raise:@"LoginFailed" format:NSLocalizedStringFromTableInBundle(@"Login to %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - PASS command failed), netLocation, errorReply];
	}
    }
    
    if (failedCredentials) {
        // We logged in successfully. Clear out the list of failed passwords.
        [failedCredentials removeAllObjects];
    }
    
    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Logged in", @"OWF", [OWFTPSession bundle], ftpsession status)];
    currentTransferType = asciiTransferType;
}

- (void)disconnect;
{
    if (!controlSocketStream)
	return;
    NS_DURING {
	[self sendCommand:@"QUIT"];
    } NS_HANDLER {
        // Well, we're closing the connection anyway...
    } NS_ENDHANDLER;
    [controlSocketStream release];
    controlSocketStream = nil;
}

//

- (ONTCPSocket *)passiveDataSocket;
{
    NSCharacterSet *digits;
    ONTCPSocket *dataSocket;
    NSScanner *scanner;
    unsigned int returnCode;
    unsigned int byte, byteCount;
    unsigned int ip = 0, port = 0;
    struct in_addr ipAddress;

    if (![self sendCommand:@"PASV"])
	return nil;

    digits = [NSCharacterSet decimalDigitCharacterSet];
    scanner = [NSScanner scannerWithString:lastReply];
    [scanner scanInt:&returnCode];
    if (returnCode != 227)
	return nil;

    byteCount = 4;
    while (byteCount--) {
	[scanner scanUpToCharactersFromSet:digits intoString:NULL];
	[scanner scanInt:&byte];
	ip <<= 8;
	ip |= byte;
    }

    byteCount = 2;
    while (byteCount--) {
	[scanner scanUpToCharactersFromSet:digits intoString:NULL];
	[scanner scanInt:&byte];
	port <<= 8;
	port |= byte;
    }

    ipAddress.s_addr = htonl(ip);
    dataSocket = [ONTCPSocket tcpSocket];
    [dataSocket setReadBufferSize:32 * 1024];
    NS_DURING {
	abortSocket = dataSocket;
        [dataSocket connectToAddress:
            [ONHostAddress hostAddressWithInternetAddress:&ipAddress]
                                port:port];
	abortSocket = nil;
    } NS_HANDLER {
	abortSocket = nil;
	if (abortOperation)
	    [localException raise];
	dataSocket = nil;
    } NS_ENDHANDLER;
    return dataSocket;
}

- (ONTCPSocket *)activeDataSocket;
{
    ONTCPSocket *dataSocket;
    unsigned long ip;
    unsigned short port;

    dataSocket = [ONTCPSocket tcpSocket];
    [dataSocket setReadBufferSize:32*1024];
    [dataSocket startListeningOnAnyLocalPort];
    ip = htonl([(ONTCPSocket *)[controlSocketStream socket]
		localAddressHostNumber]);
    port = htons([dataSocket localAddressPort]);
    if (![self sendCommandFormat:@"PORT %d,%d,%d,%d,%d,%d",
	  (int)*((unsigned char *)(&ip) + 0),
	  (int)*((unsigned char *)(&ip) + 1),
	  (int)*((unsigned char *)(&ip) + 2),
	  (int)*((unsigned char *)(&ip) + 3),
	  (int)*((unsigned char *)(&port) + 0),
	  (int)*((unsigned char *)(&port) + 1)])
	return nil;
    return dataSocket;
}

- (ONSocketStream *)dataSocketStream;
{
    ONTCPSocket *dataSocket;

    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Opening data stream", @"OWF", [OWFTPSession bundle], ftpsession status)];
    if ((dataSocket = [self passiveDataSocket]) || (dataSocket = [self activeDataSocket]))
	return [[[ONSocketStream alloc] initWithSocket:dataSocket] autorelease];
    return nil;
}

//

- (void)changeAbsoluteDirectory:(NSString *)path;
{
    if ([path length] == 0)
	path = @"/";
    if (currentPath == path || [currentPath isEqualToString:path])
	return;
    [nonretainedProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Changing directory to %@", @"OWF", [OWFTPSession bundle], ftpsession status), path];
    if (![self sendCommandFormat:@"CWD %@", [NSString decodeURLString:path]]) {
	// UNDONE: Try breaking the path into individual components, and CD'ing through each level
	[NSException raise:@"CannotChangeDirectory" format:NSLocalizedStringFromTableInBundle(@"Changing directory to %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - CWD failed), path, lastReply];
    }
    [currentPath release];
    currentPath = [path retain];
}

- (void)getFile:(NSString *)path;
{
    NSString *file, *decodedFileName;
    ONSocketStream *inputSocketStream;
    OWDataStream *outputDataStream;
    unsigned int contentLength;
    NSAutoreleasePool *autoreleasePool = nil;

    file = [OWURL lastPathComponentForPath:path];
    [self changeAbsoluteDirectory:[OWURL stringByDeletingLastPathComponentFromPath:path]];
    [self setCurrentTransferType:imageTransferType];
    inputSocketStream = [self dataSocketStream];
    abortSocket = [inputSocketStream socket];
    decodedFileName = [NSString decodeURLString:file];
    if (![self sendCommandFormat:@"RETR %@", decodedFileName]) {
        NSString *oldLastReply = [[lastReply retain] autorelease];
        int lastReplyMajor, lastReplyMiddle;
        
	abortSocket = nil;
        
        lastReplyMajor = lastReplyIntValue / 100;
        lastReplyMiddle = ( lastReplyIntValue / 10 ) - ( lastReplyMajor * 10 );
	if ((lastReplyMajor == 5 && lastReplyMiddle != 0 && lastReplyMiddle != 2 && lastReplyMiddle != 3) ||
                [lastReply containsString:@"not a plain file"] ||
                [lastReply containsString:@"not a regular file"] ||
                [lastReply containsString:@"Not a regular file"]) {
            /* If we can't retrieve a file try chdir'ing into it; if that succeeds, try to retrieve a directory listing */
            if ([self sendCommandFormat:@"CWD %@", decodedFileName]) {
                NSString *newPath = [path stringByAppendingString:@"/"];
                [currentPath release];
                currentPath = [newPath retain];
                [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Not a plain file; retrying as directory", @"OWF", [OWFTPSession bundle], ftpsession status)];
                [nonretainedPipeline addSourceContent:[ftpAddress addressForRelativeString:newPath]];
                [nonretainedPipeline cacheContent];
                [nonretainedPipeline startProcessingContent];
                return;
            }
        }
        [NSException raise:@"RetrieveFailed"  format:NSLocalizedStringFromTableInBundle(@"Retrieve of %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error), file, oldLastReply];
    }

    outputDataStream = [[OWDataStream alloc] init];
    [outputDataStream setContentTypeAndEncodingFromFilename:path isLocalFile:NO];
    [nonretainedPipeline addSourceContent:outputDataStream];
    [nonretainedPipeline cacheContent];
    [nonretainedPipeline startProcessingContent];

    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Reading data", @"OWF", [OWFTPSession bundle], ftpsession status)];
    contentLength = 0;

    if ([lastReply hasSuffix:@" bytes)."]) {
	NSRange sizeHintRange;

	sizeHintRange = [lastReply rangeOfString:@" (" options:NSLiteralSearch | NSBackwardsSearch];
	if (sizeHintRange.length > 0)
	    contentLength = [[lastReply substringFromIndex:NSMaxRange(sizeHintRange)]
	       intValue];
    }
    if (contentLength == 0) {
	OWFileInfo *fileInfo;

        fileInfo = [[nonretainedPipeline contentCacheForLastAddress] contentOfType:[OWFileInfo contentType]];
        if ([fileInfo size] != nil)
            contentLength = [[fileInfo size] intValue];
    }

    NS_DURING {
	NSData *data;
	unsigned int dataBytesRead, bytesInThisPool;

	autoreleasePool = [[NSAutoreleasePool alloc] init];    
	bytesInThisPool = 0;
	dataBytesRead = 0;
	[nonretainedProcessor processedBytes:dataBytesRead ofBytes:contentLength];

	while ((data = [inputSocketStream readData])) {
	    unsigned int dataLength;

	    dataLength = [data length];
	    dataBytesRead += dataLength;
	    bytesInThisPool += dataLength;
	    [nonretainedProcessor processedBytes:dataBytesRead ofBytes:contentLength];
	    [outputDataStream writeData:data];
	    if (bytesInThisPool > 64 * 1024) {
		[autoreleasePool release];
		autoreleasePool = [[NSAutoreleasePool alloc] init];
		bytesInThisPool = 0;
	    }
	}
    } NS_HANDLER {
	abortSocket = nil;
	[outputDataStream dataAbort];
        [outputDataStream release];
	[localException retain];
	[autoreleasePool release];
	[[localException autorelease] raise];
    } NS_ENDHANDLER;

    [outputDataStream dataEnd];
    [outputDataStream release];
    abortSocket = nil;
    [autoreleasePool release];
    if (![self readResponse]) // "226 Transfer complete"
	[NSException raise:@"RetrieveStopped" format:NSLocalizedStringFromTableInBundle(@"Retrieve of %@ stopped: %@", @"OWF", [OWFTPSession bundle], ftpsession error), file, lastReply];
}    
	
- (void)getDirectory:(NSString *)path;
{
    ONSocketStream *inputSocketStream;
    OWDataStream *outputDataStream;
    NSData *data;
    unsigned int dataBytesRead;
    NSString *currentSystemType;

    [self changeAbsoluteDirectory:path];
    [self setCurrentTransferType:asciiTransferType];
    currentSystemType = [self systemType];
    inputSocketStream = [self dataSocketStream];
    if (![self sendCommandFormat:@"LIST"]) {
	[NSException raise:@"ListAborted" format:NSLocalizedStringFromTableInBundle(@"List stopped: %@", @"OWF", [OWFTPSession bundle], ftpsession error - LIST command failed), lastReply];
    }

    outputDataStream = [[OWDataStream alloc] init];
    [outputDataStream setContentTypeString:[NSString stringWithFormat:@"OWFTPDirectory/%@", currentSystemType]];
    [nonretainedPipeline addSourceContent:outputDataStream];
    [nonretainedPipeline cacheContent];
    [nonretainedPipeline startProcessingContent];

    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Reading directory", @"OWF", [OWFTPSession bundle], ftpsession status)];
    dataBytesRead = 0;
    NS_DURING {
        [nonretainedProcessor processedBytes:dataBytesRead];
        while ((data = [inputSocketStream readData])) {
            dataBytesRead += [data length];
            [nonretainedProcessor processedBytes:dataBytesRead];
            [outputDataStream writeData:data];
            if (abortOperation)
                [NSException raise:@"FetchAborted" format:NSLocalizedStringFromTableInBundle(@"Fetch stopped", @"OWF", [OWFTPSession bundle], ftpsession error)];
        }
    } NS_HANDLER {
	abortSocket = nil;
	[outputDataStream dataAbort];
        [outputDataStream release];
	[localException raise];
    } NS_ENDHANDLER;
    
    [outputDataStream dataEnd];
    [outputDataStream release];
    if (![self readResponse]) // "226 Transfer complete"
	[NSException raise:@"ListAborted" format:NSLocalizedStringFromTableInBundle(@"List stopped: %@", @"OWF", [OWFTPSession bundle], ftpsession error), lastReply];
}

#define BLOCK_SIZE (4096)

- (void)storeData:(NSData *)storeData atPath:(NSString *)path;
{
    NSString *file;
    ONSocketStream *outputSocketStream;
    unsigned int contentLength;
    NSAutoreleasePool *autoreleasePool;

    file = [OWURL lastPathComponentForPath:path];
    [self changeAbsoluteDirectory:[OWURL stringByDeletingLastPathComponentFromPath:path]];
    [self setCurrentTransferType:imageTransferType];
    OMNI_POOL_START {
        outputSocketStream = [[self dataSocketStream] retain];
    } OMNI_POOL_END;
    abortSocket = [outputSocketStream socket];
    if (![self sendCommandFormat:@"STOR %@", [NSString decodeURLString:file]]) {
        abortSocket = nil;
        [NSException raise:@"StoreFailed" format:NSLocalizedStringFromTableInBundle(@"Store of %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - STOR command failed), file, lastReply];
    }

    [nonretainedProcessor setStatusString:NSLocalizedStringFromTableInBundle(@"Storing data", @"OWF", [OWFTPSession bundle], ftpsession status)];
    contentLength = [storeData length];

    NS_DURING {
        NSData *data;
        unsigned int dataBytesWritten, bytesInThisPool;

        autoreleasePool = [[NSAutoreleasePool alloc] init];    
        bytesInThisPool = 0;
        dataBytesWritten = 0;
        [nonretainedProcessor processedBytes:dataBytesWritten ofBytes:contentLength];

        while (dataBytesWritten < contentLength) {
            NSRange subdataRange;

            subdataRange.location = dataBytesWritten;
            subdataRange.length = BLOCK_SIZE;
            if (NSMaxRange(subdataRange) > contentLength)
                subdataRange.length = contentLength - dataBytesWritten;
            data = [storeData subdataWithRange:subdataRange];
            dataBytesWritten += subdataRange.length;
            bytesInThisPool += subdataRange.length;
            [nonretainedProcessor processedBytes:dataBytesWritten ofBytes:contentLength];
            [outputSocketStream writeData:data];
            if (bytesInThisPool > 64 * 1024) {
                [autoreleasePool release];
                autoreleasePool = [[NSAutoreleasePool alloc] init];
                bytesInThisPool = 0;
            }
        }
    } NS_HANDLER {
        abortSocket = nil;
        [localException retain];
        [autoreleasePool release];
        [[localException autorelease] raise];
    } NS_ENDHANDLER;

    abortSocket = nil;
    [autoreleasePool release];
    [outputSocketStream release];
    if (![self readResponse]) // "226 Transfer complete"
        [NSException raise:@"StoreAborted" format:NSLocalizedStringFromTableInBundle(@"Store of %@ stopped: %@", @"OWF", [OWFTPSession bundle], ftpsession error), file, lastReply];
}    

- (void)removeFileAtPath:(NSString *)path;
{
    NSString *file;

    file = [OWURL lastPathComponentForPath:path];
    [self changeAbsoluteDirectory:[OWURL stringByDeletingLastPathComponentFromPath:path]];
    if (![self sendCommandFormat:@"DELE %@", [NSString decodeURLString:file]])
        [NSException raise:@"RemoveFailed" format:NSLocalizedStringFromTableInBundle(@"Delete of %@ failed: %@", @"OWF", [OWFTPSession bundle], ftpsession error - DELE command failed), file, lastReply];
}

- (void)displayErrorReply:(NSString *)aReply message:(NSString *)aMessage;
{
    OWObjectStream *outputObjectStream;

    [nonretainedPipeline contentError];
    outputObjectStream = [[OWObjectStream alloc] init];
    [outputObjectStream setContentTypeString:@"ObjectStream/ErrorContent"];
    [outputObjectStream writeObject:[aReply substringFromIndex:4]];
    [outputObjectStream writeObject:aMessage];
    [outputObjectStream dataEnd];
    [nonretainedPipeline addSourceContent:outputObjectStream];
    [outputObjectStream release];
    [nonretainedPipeline startProcessingContent];
}

//

- (NSString *)systemTypeForSystemReply:(NSString *)systemReply;
{
    NSRange whitespaceRange;

    if ([systemReply hasPrefix:@"UNIX Type: L8MAC-OSMachTen"])
	return @"MacOS-MachTen";
    if ([systemReply hasPrefix:@"MACOS Peter's Server"])
	return @"MacOS-PeterLewis";
    if ([systemReply containsString:@"MAC-OS TCP/ConnectII"])
	return @"MacOS-TCPConnectII";

    // Return first word
    whitespaceRange = [systemReply rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]];
    if (whitespaceRange.length > 0)
	return [systemReply substringToIndex:whitespaceRange.location];

    return systemReply;
}

@end
