// Copyright 1997-1999 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 "OWFTPSession.h"

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

#import "OWAddress.h"
#import "OWContentCache.h"
#import "OWDataStream.h"
#import "OWFileInfo.h"
#import "OWNetLocation.h"
#import "OWObjectStream.h"
#import "OWSourceProcessor.h"
#import "OWURL.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/FTP.subproj/OWFTPSession.m,v 1.3 1999/07/03 00:29:53 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;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
	return;
    initialized = YES;

    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;
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidInit:) name:OFControllerDidInitNotification object:nil];
}

+ (void)controllerDidInit:(NSNotification *)notification;
{
    [self readDefaults];
}

+ (void)readDefaults;
{
    OFUserDefaults *userDefaults;

    userDefaults = [OFUserDefaults sharedUserDefaults];
    defaultPassword = [[userDefaults stringForKey:@"OWFTPAnonymousPassword"] retain];
    if (!defaultPassword || [defaultPassword length] == 0)
	defaultPassword = [[NSString alloc] initWithFormat:@"%@@%@", NSUserName(), [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];
    return self;
}

- (void)dealloc;
{
    [self disconnect];
    [sessionCacheKey release];
    [controlSocketStream release];
    [lastReply release];
    [lastMessage release];
    [currentPath release];
    [currentTransferType release];
    [systemType release];
    [ftpAddress 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:@"FTP does not support the \"%@\" method", ftpMethod];
    }
    
    ftpPath = [[ftpAddress url] fetchPath];
    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:@"Content-String"];
    contentData = [[ftpAddress methodDictionary] objectForKey:@"Content-Data"];

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

        /* -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:@"Setting transfer type to %@", aTransferType];
    if (![self sendCommandFormat:@"TYPE %@", aTransferType])
	[NSException raise:@"SetTransferTypeFailed"
	 format:@"Failed to change transfer type: %@", lastReply];
    [currentTransferType release];
    currentTransferType = [aTransferType retain];
}

- (NSString *)systemType;
{
    if (systemType)
	return systemType;
    [nonretainedProcessor setStatusString:@"Checking system type"];
    if ([self sendCommand:@"SYST"])
	systemType = [[self systemTypeForSystemReply:[lastReply substringFromIndex:4]] retain];
    else
	systemType = [@"unknown" retain];
    [nonretainedProcessor setStatusFormat:@"System is %@", systemType];
    return systemType;
}


//

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

    if (abortOperation)
	[NSException raise:@"Fetch stopped" format:@"Fetch stopped"];

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

    if (!reply || [reply length] < 4)
	[NSException raise:@"Bad response" format:@"Invalid response from FTP server"];

    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);
	    [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:@"Fetch stopped" format:@"Fetch stopped"];

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

    [controlSocketStream writeString:command];
    [controlSocketStream writeString:@"\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:@"Connecting to %@", 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]];
    [self readResponse];
    [nonretainedProcessor setStatusFormat:@"Connected to %@", netLocation];

    if (!(username = [parsedNetLocation 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:@"Login failed" format:@"Login to %@ failed: %@", netLocation, errorReply];
    }
    if (lastReplyIntValue == 331) { // Need password
	while (![self sendCommandFormat:@"PASS %@", password]) {
	    NSString *errorReply;
	    
	    [self displayErrorReply:lastReply message:lastMessage];
	    errorReply = [[lastReply retain] autorelease];
	    // UNDONE: ask for a new password
	    [self disconnect];
            [NSException raise:@"Login failed" format:@"Login to %@ failed: %@", netLocation, errorReply];
	}
    }
    [nonretainedProcessor setStatusString:@"Logged in"];
    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:@"Opening data stream"];
    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:@"Changing directory to %@", path];
    if (![self sendCommandFormat:@"CWD %@", [OWURL decodeURLString:path]]) {
	// UNDONE: Try breaking the path into individual components, and CD'ing through each level
	[NSException raise:@"CannotChangeDirectory" format:@"Changing directory to %@ failed: %@", path, lastReply];
    }
    [currentPath release];
    currentPath = [path retain];
}

- (void)getFile:(NSString *)path;
{
    NSString *file;
    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];
    if (![self sendCommandFormat:@"RETR %@", [OWURL decodeURLString:file]]) {
	abortSocket = nil;
	if ([lastReply containsString:@"not a plain file"] || [lastReply containsString:@"not a regular file"] || [lastReply containsString:@"Not a regular file"] || ([systemType isEqualToString:@"Windows_NT"] && lastReplyIntValue == 550)) {
	    [nonretainedProcessor setStatusString:@"Not a plain file; retrying as directory"];
	    [nonretainedPipeline addSourceContent:[OWAddress addressForString:[[ftpAddress addressString] stringByAppendingString:@"/"]]];
	    [nonretainedPipeline cacheContent];
	    [nonretainedPipeline startProcessingContent];
	    return;
	} else {
	    [NSException raise:@"Retrieve failed"  format:@"Retrieve of %@ failed: %@", file, lastReply];
	}
    }

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

    [nonretainedProcessor setStatusString:@"Reading data"];
    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]];
	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:@"Retrieve stopped" format:@"Retrieve of %@ stopped: %@", 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:@"List aborted: %@", lastReply];
    }

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

    [nonretainedProcessor setStatusString:@"Reading directory"];
    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:@"Fetch aborted: %@"];
        }
    } 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:@"List aborted: %@", 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 %@", [OWURL decodeURLString:file]]) {
        abortSocket = nil;
        [NSException raise:@"StoreFailed" format:@"Store of %@ failed: %@", file, lastReply];
    }

    [nonretainedProcessor setStatusString:@"Storing data"];
    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:@"Store of %@ aborted: %@", file, lastReply];
}    

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

    file = [OWURL lastPathComponentForPath:path];
    [self changeAbsoluteDirectory:[OWURL stringByDeletingLastPathComponentFromPath:path]];
    if (![self sendCommandFormat:@"DELE %@", [OWURL decodeURLString:file]])
        [NSException raise:@"RemoveFailed" format:@"Delete of %@ 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
