//
//  File:   hzFtpClient.cpp
//
//  Legal Notice:   This file is part of the HadronZoo C++ Class Library. Copyright 2025 HadronZoo Project (http://www.hadronzoo.com)
//
//  The HadronZoo C++ Class Library is free software: You can redistribute it, and/or modify it under the terms of the GNU Lesser General Public License, as published by the Free
//  Software Foundation, either version 3 of the License, or any later version.
//
//  The HadronZoo C++ Class Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
//  A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License along with the HadronZoo C++ Class Library. If not, see http://www.gnu.org/licenses.
//
#include <fstream>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <netdb.h>
#include <utime.h>
#include "hzTextproc.h"
#include "hzFtpClient.h"
#include "hzProcess.h"
using namespace std ;
/*
**  General FTP functions
*/
hzEcode _checkpath  (hzString& ult, const hzString& current, const hzString& mod)
{
    //  Predicts the final directory path when a path is operated on by a change directory command.
    //
    //  Arguments:  1)  ult     The result of the current path operated on by the moderator
    //              2)  current The current path that must begin with the root '/' (or [A-Z]: under windows)
    //              3)  mod     The moderator.
    //
    //  Returns:    E_FORMAT    Either the current path is not a valid and absolute path. Or the moderator is not valid.
    //              E_OK        The oparation was successful.
    const char*     i ;             //  For derivation of target dir
    const char*     j ;             //  For derivation of target dir
    hzString        tgt ;           //  Directory we should end up in
    uint32_t        nPosn ;         //  Positions for manipulation of m_ServerDir
    ult.Clear() ;
    /*
    **  Check the current is an absolute path (must begin with '/')
    */
    if (!mod)
    {
        ult = current ;
        if (!ult)
            ult = "/" ;
        return E_OK ;
    }
    if (!current)
    {
        i = *mod ;
        if (*i != CHAR_FWSLASH)
        {
            ult = "invalid moderator" ;
            return E_FORMAT ;
        }
        ult = mod ;
        return E_OK ;
    }
    i = *current ;
    if (*i != CHAR_FWSLASH)
    {
        ult = "invalid current path" ;
        return E_FORMAT ;
    }
    /*
    **  Check the directory is reasonable
    */
    i = *mod ;
    if (*i == CHAR_FWSLASH)
        ult = mod ;
    else
    {
        ult = current ;
        if (mod == "..")
        {
            //  truncate our record of the remote dir
            j = *ult ;
            nPosn = CstrLast(j, CHAR_FWSLASH) ;
            if (nPosn)
                ult.Truncate(nPosn) ;
        }
        if (!memcmp(i, "../", 3))
        {
            for (;;)
            {
                if (memcmp(i, "../", 3))
                    break ;
                i += 3 ;
                j = *ult ;
                nPosn = CstrLast(j, CHAR_FWSLASH) ;
                ult.Truncate(nPosn) ;
            }
        }
        //  Add the relative path
        ult += "/" ;
        ult += i ;
    }
    return E_OK ;
}
hzEcode hzFtpClient::_setmetafile   (hzDirent& meta, char* line)
{
    //  Set file metadata (hzDirent) with a line from the server's directory listing.
    //
    //  Arguments:  1)  meta    The hzDirent to be populated
    //              2)  line    The line from the directory listing
    //
    //  Returns:    E_FORMAT    If the data is garbled
    //              E_OK        If the operation successful
    _hzfunc("hzFtpClient::_setmetafile") ;
    hzXDate     now ;           //  Current date/time
    hzXDate     date ;          //  Date of file (according to server)
    char*       i ;             //  Line iterator
    char*       pSze ;          //  Pointer to file size in line
    char*       pMon ;          //  Pointer to month in line
    char*       pDay ;          //  Pointer to day in line
    char*       pTim ;          //  Pointer to time in line
    hzString    test ;          //  Set to line
    uint32_t    file_year ;     //  Year of file
    uint32_t    file_month ;    //  Month of file
    uint32_t    file_day ;      //  Day of file
    uint32_t    file_hour ;     //  Hour of file
    uint32_t    file_min ;      //  Minute of file
    hzEcode     rc = E_OK ;     //  Return code
    now.SysDateTime() ;
    test = line ;
    i = line + 10 ;
    //  Bypass user no, username, groupname - ignore whitespace
    for (    ; *i && *i <= CHAR_SPACE ; i++) ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ; 
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    //  Get size and date - ignore whitespace
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; pSze = i ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; pMon = i ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; pDay = i ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    for (i++ ; *i && *i <= CHAR_SPACE ; i++) ; pTim = i ; for (i++ ; *i > CHAR_SPACE ; i++) ; *i = 0 ;
    meta.SetName(i + 1) ;
    meta.SetSize((uint32_t) atol(pSze)) ;
    /*
    **  Get file date
    */
    if      (!strcmp(pMon, "Jan")) file_month = 1 ;
    else if (!strcmp(pMon, "Feb")) file_month = 2 ;
    else if (!strcmp(pMon, "Mar")) file_month = 3 ;
    else if (!strcmp(pMon, "Apr")) file_month = 4 ;
    else if (!strcmp(pMon, "May")) file_month = 5 ;
    else if (!strcmp(pMon, "Jun")) file_month = 6 ;
    else if (!strcmp(pMon, "Jul")) file_month = 7 ;
    else if (!strcmp(pMon, "Aug")) file_month = 8 ;
    else if (!strcmp(pMon, "Sep")) file_month = 9 ;
    else if (!strcmp(pMon, "Oct")) file_month = 10 ;
    else if (!strcmp(pMon, "Nov")) file_month = 11 ;
    else if (!strcmp(pMon, "Dec")) file_month = 12 ;
    else
        return hzerr(E_FORMAT, "Invalid month: Line [%s]", *test) ;
    if (!IsPosint(file_day, pDay))
        return hzerr(E_FORMAT, "Invalid day: Line [%s]", *test) ;
    if (!IsPosint(file_hour, pTim))
        return hzerr(E_FORMAT, "Invalid hour: Line [%s]", *test) ;
    if (file_hour > 2000)
        { file_year = file_hour ; file_hour = 12 ; file_min = 0 ; }
    else
    {
        file_year = now.Year() ;
        if (!IsPosint(file_min, pTim + 3))
            return hzerr(E_FORMAT, "Invalid minutes: Line [%s]", *test) ;
    }
    //  Do some logic to make up for the lack of year in date
    rc = date.SetDate(file_year, file_month, file_day) ;
    if (rc != E_OK)
    {
        threadLog("Line [%s] Could not set date to %04d/%02d/%02d\n", *test, file_year, file_month, file_day) ;
        return E_FORMAT ;
    }
    rc = date.SetTime(file_hour, file_min, 0) ;
    if (rc != E_OK)
    {
        threadLog("Line [%s] Could not set time to %02d:%02d\n", *test, file_hour, file_min) ;
        return E_FORMAT ;
    }
    if (date.NoDays() > now.NoDays())
    {
        date.SetDate(now.Year() - 1, file_month, file_day) ;
        if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
            threadLog("Adjusted date that would be in the future if current year assumed - now set to %04d/%02d/%02d\n", date.Year(), file_month, file_day) ;
    }
    meta.SetCtime(date) ;
    meta.SetMtime(date) ;
    return rc ;
}
hzEcode hzFtpClient::_openpasv  (hzTcpClient& X)
{
    //  Sends a PASV command to the FTP server and opens a data channel. Return values are as follows:-
    //
    //  Arguments:  1)  X       The control channel client connection instance
    //
    //  Returns:    E_SENDFAIL  The PASV command could not be sent. The calling function should attempt a reconnect.
    //              E_RECVFAIL  The PASV response was not received. The calling function should attempt a reconnect.
    //              E_HOSTFAIL  The data channel connection failed. The calling function should attempt a reconnect.
    //              E_PROTOCOL  The PASV response was not expected. The caller should abort the session as server is unlikely to respond as expected next time.
    //              E_OK        If the operation was successful.
    //
    //  Scope:      Private to the hzFtpClient class.
    _hzfunc("hzFtpClient::_openpasv") ;
    uint32_t    nRecv ;     //  Bytes received
    uint32_t    len ;       //  Length of outgoing msg
    uint32_t    a ;         //  1st part of IP addr for data connection
    uint32_t    b ;         //  2nd part of IP addr for data connection
    uint32_t    c ;         //  3rd part of IP addr for data connection
    uint32_t    d ;         //  4th part of IP addr for data connection
    uint32_t    nA ;        //  First part of data port
    uint32_t    nB ;        //  Second part of data port
    char*       i ;         //  For processing prt nos etc
    hzEcode     rc ;        //  Return code
    /*
    **  Send PASV command
    */
    sprintf(m_c_sbuf, "PASV\r\n") ;
    len = strlen(m_c_sbuf) ;
    if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        { threadLog("PASV: Send failed\n") ; return rc ; }
    if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        { threadLog("Could not recv response to PASV command\n") ; return rc ; }
    if (m_nRescode != 227)
        { threadLog("Expected code of 227: Got %s\n", m_c_rbuf) ; return E_PROTOCOL ; }
    /*
    **  Convert recived message to a port number to connect to
    */
    i = strchr(m_c_rbuf, '(') ;
    if (!i)
        { threadLog("Expected a substring of (...) containing data conn details\n") ; return E_PROTOCOL ; }
    SplitCSV(m_Array, i + 1) ;  //, 6) ;
    a = m_Array[0] ? atoi(*m_Array[0]) : 0 ;
    b = m_Array[1] ? atoi(*m_Array[1]) : 0 ;
    c = m_Array[2] ? atoi(*m_Array[2]) : 0 ;
    d = m_Array[3] ? atoi(*m_Array[3]) : 0 ;
    nA = m_Array[4] ? atoi(*m_Array[4]) : 0 ;
    nB = m_Array[5] ? atoi(*m_Array[5]) : 0 ;
    m_nDataPort = (nA * 256) + nB ;
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("Nos are %d:%d:%d:%d, %d,%d - Will connect for data on port %d\n", a, b, c, d, nA, nB, m_nDataPort) ;
    /*
    **  Now set up data client
    */
    rc = X.ConnectStd(m_Server, m_nDataPort) ;
    if (rc != E_OK)
        { threadLog("Could not conect to FTP data port\n") ; return rc ; }
    X.SetSendTimeout(3) ;
    X.SetRecvTimeout(3) ;
    return rc ;
}
void    hzFtpClient::_logrescode    (void)
{
    //  Report FTP return codes to the logfile of the current thread
    //
    //  Arguments:  1)  fn  Function name
    //
    //  Returns:    None
    //_hzfunc("hzFtpClient::_logrescode") ;
    switch  (m_nRescode)
    {
    //  100 Codes: The requested action is being taken. Expect a reply before proceeding with a new command.
    case 110:   threadLog("110 - Restart marker reply\n") ;                             break ;
    case 120:   threadLog("120 - Service ready in (n) minutes\n") ;                     break ;
    case 125:   threadLog("125 - Data connection already open, transfer starting\n") ;  break ;
    case 150:   threadLog("150 - File status okay, about to open data connection\n") ;  break ;
    //  200 Codes: The requested action has been successfully completed.
    case 200:   threadLog("200 - Command okay\n") ;                                     break ;
    case 202:   threadLog("202 - Command not implemented\n") ;                          break ;
    case 211:   threadLog("211 - System status, or system help reply\n") ;              break ;
    case 212:   threadLog("212 - Directory status\n") ;                                 break ;
    case 213:   threadLog("213 - File status\n") ;                                      break ;
    case 214:   threadLog("214 - Help message\n") ;                                     break ;
    case 215:   threadLog("215 - NAME system type (Official sys name from list in the Assigned Numbers document)\n") ;  break ;
    case 220:   threadLog("220 - Service ready for new user\n") ;                       break ;
    case 221:   threadLog("221 - Service closing control connection (Logged out)\n") ;  break ;
    case 225:   threadLog("225 - Data connection open, no transfer in progress\n") ;    break ;
    case 226:   threadLog("226 - Closing data channel. Requested file action OK\n") ;   break ;
    case 227:   threadLog("227 - Entering Passive Mode\n") ;                            break ;
    case 230:   threadLog("230 - User logged in, proceed\n") ;                          break ;
    case 250:   threadLog("250 - Requested file action okay, completed\n") ;    break ;
    case 257:   threadLog("257 - Pathname created\n") ; break ;
    //  300 Codes: The command has been accepted, but the requested action is being held pending receipt of further information.\n") ;
    case 331:   threadLog("331 - User name okay, need password\n") ;                    break ;
    case 332:   threadLog("332 - Need account for login\n") ;                           break ;
    case 350:   threadLog("350 - Requested file action pending further info\n") ;       break ;
    //  400 Codes: The command was not accepted and the requested action did not take place. The error condition is temporary, and the action may be requested again.
    case 421:   threadLog("421 - Service not available, closing control channel\n") ;   break ;
    case 425:   threadLog("425 - Can't open data connection\n") ;                       break ;
    case 426:   threadLog("426 - Connection closed, transfer aborted\n") ;              break ;
    case 450:   threadLog("450 - File unavailable\n") ;                                 break ;
    case 451:   threadLog("451 - Local error in processing\n") ;                        break ;
    case 452:   threadLog("452 - Insufficient storage space in system\n") ;             break ;
    //  500 Codes: The command was not accepted and the requested action did not take place.
    case 500:   threadLog("500 - Syntax error, command unrecognized\n") ;               break ;
    case 501:   threadLog("501 - Syntax error in parameters or arguments\n") ;          break ;
    case 502:   threadLog("502 - Command not implemented\n") ;                          break ;
    case 503:   threadLog("503 - Bad sequence of commands\n") ;                         break ;
    case 504:   threadLog("504 - Command not implemented for that parameter\n") ;       break ;
    case 530:   threadLog("530 - User not logged in\n") ;                               break ;
    case 532:   threadLog("532 - Need account for storing files\n") ;                   break ;
    case 550:   threadLog("550 - File unavailable (not found or no access)\n") ;        break ;
    case 552:   threadLog("552 - Storage allocation exceeded\n") ;                      break ;
    case 553:   threadLog("553 - Illegal file name\n") ;                                break ;
    default:
        threadLog("No valid command received\n") ;
        break ;
    }
}
hzEcode hzFtpClient::_ftprecv   (uint32_t& nRecv, const char* callFn)
{
    //  Recieve data from the FTP control channel
    //
    //  Arguments:  1)  nRecv   Bytes received
    //              2)  callFn  Calling function
    //
    //  Returns:    E_RECVFAIL  Connection breakdown
    //              E_PROTOCOL  Invalid response (Give up)
    //              E_OK        Operation successful
    _hzfunc("hzFtpClient::_ftprecv") ;
    hzChain     msg ;           //  Recived FTP control message chain
    _ftpline    fline ;         //  Line of FTP control message
    char*       i ;             //  Line iterator
    uint32_t    nTry ;          //  Reconnection retry count
    uint32_t    code ;          //  FTP server return code
    hzEcode     rc = E_OK ;     //  Return code
    m_nRescode = 0 ;
    if (!m_Stack.Count())
    {
        //  No stored response (from an earlier multi-line response) so we have to do a recv
        for (nTry = 0 ; nTry < 10 ; nTry++)
        {
            rc = m_ConnControl.Recv(m_c_rbuf, nRecv, HZ_MAXPACKET) ;
            if (rc == E_TIMEOUT)
                continue ;
            if (rc != E_OK)
                threadLog("%s. Failed to recv from control channel (%d bytes, error=%s)\n", callFn, nRecv, Err2Txt(rc)) ;
            break ;
        }
        if (nTry == 10)
        {
            threadLog("%s. Stalled - 10 counts and out\n", callFn) ;
            return E_RECVFAIL ;
        }
        if (nTry)
            threadLog("%s. Stalled - %d counts and resumed\n", callFn, nTry) ;
        if (rc != E_OK)
            { threadLog("%s. Failed error=%s\n", callFn, Err2Txt(rc)) ; return rc ; }
        m_c_rbuf[nRecv] = 0 ;
        //  Separate the lines in the response. If there is more than one they need to be stacked. Then
        //  subsequent calls to this function can take a line from the que.
        i = m_c_rbuf ;
        if (IsDigit(i[0]) && IsDigit(i[1]) && IsDigit(i[2]))
            fline.m_code = ((i[0] - '0') * 100) + ((i[1] - '0') * 10) + (i[2] - '0') ;
        else
        {
            threadLog("%s. Invalid server response: %s\n", callFn, i) ;
            return E_PROTOCOL ;
        }
        for (;;)
        {
            if (*i == 0)
            {
                if (msg.Size())
                {
                    fline.m_msg = msg ;
                    m_Stack.Add(fline) ;
                }
                break ;
            }
            if (i[0] == CHAR_NL || (i[0] == CHAR_CR && i[1] == CHAR_NL))
            {
                if (i[0] == CHAR_CR)
                    *i++ = 0 ;
                *i++ = 0 ;
                code = ((i[0] - '0') * 100) + ((i[1] - '0') * 10) + (i[2] - '0') ;
                if (code == fline.m_code)
                    continue ;
                if (msg.Size())
                {
                    fline.m_msg = msg ;
                    m_Stack.Add(fline) ;
                    msg.Clear() ;
                    fline.m_msg.Clear() ;
                    fline.m_code = code ;
                }
                continue ;
            }
            msg.AddByte(*i) ;
            i++ ;
        }
        if (m_Stack.Count() > 1)
        {
            //  Check if these multiple lines are part of the same message (ie all have same code), if so the lines are merged.
            if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
            {
                threadLog("%s. xple control lines (%d)\n", callFn, m_Stack.Count()) ;
                for (nTry = 0 ; nTry < m_Stack.Count() ; nTry++)
                {
                    fline = m_Stack[nTry] ;
                    threadLog(" - %d %s\n", fline.m_code, *fline.m_msg) ;
                }
            }
        }
    }
    /*
    **  Process response (from server or stack)
    */
    if (!m_Stack.Count())
    {
        threadLog("%s. Empty message stack\n", callFn) ;
        return E_PROTOCOL ;
    }
    fline = m_Stack[0] ;
    m_nRescode = fline.m_code ;
    strcpy(m_c_rbuf, *fline.m_msg) ;
    if (m_Stack.Count() == 1)
        m_Stack.Clear() ;
    else
    {
        rc = m_Stack.Delete(0) ;
        if (rc != E_OK)
            threadLog("%s. Corrupt m_Stack delete\n", callFn) ;
    }
    if (rc != E_OK)
         threadLog("%s. _ftprecv error=%s\n", callFn, Err2Txt(rc)) ;
    return rc ;
}
hzEcode hzFtpClient::_reconnect (void)
{
    //  In the event of a disconnection, reconnect to server and restore the session in full. Note that this differs from the original connect call in that the
    //  possible outcomes no longer scanarios like server not found.
    //
    //  Arguments:  None
    //
    //  Returns:    E_HOSTFAIL  Reconnection unsuccessful breakdown
    //              E_PROTOCOL  Invalid response (Give up)
    //              E_OK        Operation successful
    _hzfunc("hzFtpClient::_reconnect") ;
    std::ofstream   os ;    //  Out to dirlist file
    std::ifstream   is ;    //  Read back dirlist file
    hzChain     CR ;        //  Receiver chain (control)
    hzChain     XR ;        //  Receiver chain (data)
    hzEcode     rc ;        //  Return code from publication functions
    uint32_t    nRecv ;     //  Bytes recv
    uint32_t    len ;       //  Length of client request
    threadLog("-> %s::%s\n", *m_Server, *m_ServerDir) ;
    /*
    **  Connect to server and set socket timeouts
    */
    m_ConnControl.Close() ;
    for (; m_nTries < 10 ;)
    {
        sleep(1) ;
        if ((rc = m_ConnControl.ConnectStd(m_Server, 21)) != E_OK)
        {
            if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
                threadLog("Attempt %d: Could not conect to FTP server\n", m_nTries) ;
            m_nTries++ ;
            continue ;
        }
        //  Now reconnected, receive server hello
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Did not get server hello\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        if (m_nRescode != 220)
        {
            threadLog("Expected a 220, got %s\n", m_c_rbuf) ;
            rc = E_PROTOCOL ;
            break ;
        }
        //  Send username
        sprintf(m_c_sbuf, "USER %s\r\n", *m_Username) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Failed to send USER command\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        //  Receive response
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Failed to receive respose to USER command\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        _logrescode() ;
        if (m_nRescode >= 400)
        {
            threadLog("Aborting Session (case 1)\n") ;
            rc = E_PROTOCOL ;
            break ;
        }
        //  Send password
        sprintf(m_c_sbuf, "PASS %s\r\n", *m_Password) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Failed to send PASS command (password)\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        //  Receive server response to password
        sleep(1) ;
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Failed to receive respose to password\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        _logrescode() ;
        if (m_nRescode >= 400)
        {
            threadLog("Aborting Session (case 2)\n") ;
            rc = E_PROTOCOL ;
            m_ConnControl.Close() ;
            break ;
        }
        //  Now because we are re-connecting, we just want to go to the directory we were last in. Send CWD command.
        sprintf(m_c_sbuf, "CWD %s\r\n", *m_ServerDir) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send CWD command\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        //  Receive the response
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Failed to receive CWD respose\n") ;
            m_ConnControl.Close() ;
            m_nTries++ ;
            continue ;
        }
        _logrescode() ;
        if (m_nRescode == 250)
            rc = E_OK ;
        else
        {
            rc = E_PROTOCOL ;
            m_ConnControl.Close() ;
        }
        break ;
    }
    return rc ;
}
hzEcode hzFtpClient::StartSession   (void)
{
    //  Start an FTP session (establish a connection witht the FTP server)
    //
    //  Arguments:  None
    //
    //  Returns:    E_HOSTFAIL  If the initial control channel connection could not be established. The usual remedy is to try again later.
    //              E_SENDFAIL  If either the username or password could not be sent. Broken pipe so try again later.
    //              E_RECVFAIL  If the expected reponses were not recieved. Again a broken pipe so try again later.
    //              E_PROTOCOL  If either the username or password was invalid. Fatal error as this must be sorted out before trying again.
    //              E_OK        If the operation was successful.
    _hzfunc("hzFtpClient::StartSession") ;
    std::ofstream   os ;        //  Out to dirlist file
    std::ifstream   is ;        //  Read back dirlist file
    hzChain     CR ;        //  Receiver chain (control)
    hzChain     XR ;        //  Receiver chain (data)
    hzEcode     rc ;        //  Return code from publication functions
    uint32_t    nRecv ;         //  Bytes recv
    uint32_t    len ;
    /*
    **  Connect to server and set socket timeouts
    */
    for (m_nTries = 0 ; m_nTries < 5 ; m_nTries++)
    {
        if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
            threadLog("Calling Connect for control channel at server %s\n", *m_Server) ;
        rc = m_ConnControl.ConnectStd(m_Server, 21) ;
        if (rc != E_OK)
        {
            if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
                threadLog("Attempt %d: Could not connect to FTP server control channel. (error=%s)\n", m_nTries, Err2Txt(rc)) ;
            continue ;
        }
        m_nTries = 0 ;
        break ;
    }
    if (rc != E_OK)
    {
        threadLog("Given up!\n") ;
        return rc ;
    }
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("Connected to control channel at server %s with sock %d on port 21\n", *m_Server, m_ConnControl.Sock()) ;
    rc = m_ConnControl.SetSendTimeout(10) ;
    if (rc != E_OK)
    {
        threadLog("Could not set send timeout: Quitting\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    rc = m_ConnControl.SetRecvTimeout(10) ;
    if (rc != E_OK)
    {
        threadLog("Could not set send timeout: Quitting\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("Send & Recv timeouts set\n") ;
    /*
    **  Receive server hello
    */
    rc = _ftprecv(nRecv, *_fn) ;
    if (rc != E_OK)
    {
        threadLog("Did not get server hello\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    if (m_nRescode != 220)
    {
        threadLog("Expected a 220, got %s\n", m_c_rbuf) ;
        m_ConnControl.Close() ;
        return E_PROTOCOL ;
    }
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("Got server hello ....\n") ;
    /*
    **  Send username
    */
    sprintf(m_c_sbuf, "USER %s\r\n", *m_Username) ;
    len = strlen(m_c_sbuf) ;
    rc = m_ConnControl.Send(m_c_sbuf, len) ;
    if (rc != E_OK)
    {
        threadLog("Failed to send USER command\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    rc = _ftprecv(nRecv, *_fn) ;
    if (rc != E_OK)
    {
        threadLog("Failed to receive respose to USER command\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    _logrescode() ;
    if (m_nRescode != 331)
    {
        threadLog("Bad response to USER. Expected a 331, got %d. Presume username %s invalid\n", m_nRescode, *m_Username) ;
        QuitSession() ;
        rc = E_PROTOCOL ;
        return rc ;
    }
    /*
    **  Send password
    */
    sprintf(m_c_sbuf, "PASS %s\r\n", *m_Password) ;
    len = strlen(m_c_sbuf) ;
    if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
    {
        threadLog("PASS: Send failed\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    sleep(1) ;
    if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
    {
        threadLog("Could not recv response to PASS command\n") ;
        m_ConnControl.Close() ;
        return rc ;
    }
    _logrescode() ;
    if (m_nRescode >= 400)
    {
        threadLog("Aborting Session\n") ;
        QuitSession() ;
        rc = E_PROTOCOL ;
        return rc ;
    }
    rc = GetServerDir() ;
    if (rc != E_OK)
    {
        threadLog("Could not get server directory\n") ;
        return rc ;
    }
    threadLog("Logged in to %s as %s\n", *m_Server, *m_Username) ;
    m_nTries = 0 ;
    return rc ;
}
hzEcode hzFtpClient::GetServerDir   (void)
{
    //  Obtain the current working directory on the server
    //
    //  Arguments:  None
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_PROTOCOL  If the server did not respond as expected
    //              E_OK        If the current working directory on the server was obtained
    _hzfunc("hzFtpClient::GetServerDir") ;
    char*       i ;         //  Start of pathname (in quotes)
    char*       j ;         //  End of pathname (in quotes)
    uint32_t    nRecv ;     //  Bytes received
    uint32_t    len ;       //  Outgoing msg length
    uint32_t    nTry ;      //  Connection retries
    hzEcode     rc ;        //  Standard return code
    /*
    **  Send PWD command and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the PWD command, recv response
        */
        sprintf(m_c_sbuf, "PWD\r\n") ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send PWD command\n") ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv response to PWD command\n") ;
            continue ;
        }
        break ;
    }
    if (rc == E_OK)
    {
        /*
        **  Check respons is valid
        */
        _logrescode() ;
        if (m_nRescode != 257)
        {
            threadLog("Expected a 257 return code, got %d\n", m_nRescode) ;
            rc = E_PROTOCOL ;
        }
    }
    if (rc == E_OK)
    {
        /*
        **  Set our record of the current server directory
        */
        for (i = m_c_rbuf ; *i && *i != CHAR_DQUOTE ; i++)
        if (*i)
        {
            i++ ;
            for (j = i ; *j && *j != CHAR_DQUOTE ; j++) ;
            *j = 0 ;
        }
        if (m_ServerDir != i)
            m_ServerDir = i ;
        m_nTries = 0 ;
        return E_OK ;
    }
    threadLog("Could not get server directory\n") ;
    return rc ;
}
hzEcode hzFtpClient::SetLocalDir    (const hzString& dir)
{
    //  Set the working directory on the local machine
    //
    //  Arguments:  1)  dir The taget local directory
    //
    //  Returns:    E_NOTFOUND  If the local directory does not exist
    //              E_TYPE      If the specified local directory is not a directory
    //              E_OK        If the local directory is set
    FSTAT   fs ;    //  Local file status
    if (lstat(*dir, &fs) == -1)
        return E_NOTFOUND ;
    if (!ISDIR(fs.st_mode))
        return E_TYPE ;
    m_LocalDir = dir ;
    /*
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("hzFtpClient::SetLocalDir. Set local directory to %s\n", *m_LocalDir) ;
    */
    return E_OK ;
}
hzEcode hzFtpClient::SetRemoteDir   (const hzString& dir)
{
    //  Changes the directory on the server to either an absolute or relative path. This also sets the member m_ServerDir which tracks the server directory. The
    //  protocol is simple. The client sends a CWD with the supplied argument. The server either returns a 250 if the directory was changed or it returns a 550
    //  if there is no such directory.
    //
    //  Argument:   dir     The taget remote directory
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_NOTFOUND  If the requested directory does not exist on the server or does not permit the client to access it
    //              E_OK        If the remote working directory was set
    _hzfunc("hzFtpClient::SetRemoteDir") ;
    hzString    tgt ;       //  Directory we should end up in
    uint32_t    nRecv ;     //  Length of server response
    uint32_t    len ;       //  Length of client request
    uint32_t    nTry ;      //  Connection retries
    hzEcode     rc ;        //  Return code
    /*
    **  Check the directory is reasonable
    */
    rc = _checkpath(tgt, m_ServerDir, dir) ;
    if (rc != E_OK)
    {
        threadLog("In dir %s, acting on CD %s results in %s\n", *m_ServerDir, *dir, *tgt) ;
        return rc ;
    }
    if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
        threadLog("Tgt dir is %s\n", *tgt) ;
    /*
    **  Send CWD command and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the CWD command, recv response
        */
        sprintf(m_c_sbuf, "CWD %s\r\n", *dir) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send CWD command - Reconnecting ...\n") ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv CWD response - Reconnecting ...\n") ;
            continue ;
        }
        break ;
    }
    if (rc == E_OK)
    {
        /*
        **  Check respons is valid
        */
        if (m_nRescode == 250)
            rc = E_OK ;
        else
            rc = E_NOTFOUND ;
    }
    if (rc == E_OK)
    {
        m_ServerDir = tgt ;
        if (_hzGlobal_Debug & HZ_DEBUG_CLIENT)
            threadLog("Remote dir is now %s\n", *m_ServerDir) ;
        m_nTries = 0 ;
        return E_OK ;
    }
    if (rc == E_NOTFOUND)
    {
        threadLog("Could not CD to %s (%s)\n", *tgt, m_c_rbuf) ;
        return rc ;
    }
    threadLog("Failure report (err=%s)\n", Err2Txt(rc)) ;
    return rc ;
}
hzEcode hzFtpClient::RemoteDirCreate    (const hzString& dir)
{
    //  Create a directory on the server
    //
    //  Argument:   dir     The taget remote directory
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_WRITEFAIL If the requested server directory was not created
    //              E_OK        If the remote working directory was created
    _hzfunc("hzFtpClient::RemoteDirCreate") ;
    uint32_t    nRecv ;     //  Incoming message length
    uint32_t    len ;       //  Outgoing message length
    uint32_t    nTry ;      //  Reconections
    hzEcode     rc ;        //  Return code
    /*
    **  Send MKD command and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the CWD command
        */
        sprintf(m_c_sbuf, "MKD %s\r\n", *dir) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send MKD command\n") ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv MKD response\n") ;
            continue ;
        }
        break ;
    }
    /*
    **  Check response code
    */
    _logrescode() ;
    if (m_nRescode != 257)
    {
        threadLog("Expected a 257 - Got %s\n", m_c_rbuf) ;
        return E_WRITEFAIL ;
    }
    return E_OK ;
}
hzEcode hzFtpClient::RemoteDirDelete    (const hzString& SvrDirname)
{
    //  Delete a directory on the server
    //
    //  Arguments:  1)  dir The taget remote directory
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_WRITEFAIL If the requested server directory was not deleted
    //              E_OK        If the remote working directory was deleted
    _hzfunc("hzFtpClient::RemoteDirDelete") ;
    uint32_t    nRecv ;     //  Incoming message length
    uint32_t    len ;       //  Outgoing message length
    uint32_t    nTry ;      //  Reconections
    hzEcode     rc ;        //  Return code
    /*
    **  Send RMD command and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the RMD command
        */
        sprintf(m_c_sbuf, "RMD %s\r\n", *SvrDirname) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send RMD command\n") ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv response to RMD command\n") ;
            continue ;
        } 
        break ;
    }
    /*
    **  Check response code
    */
    if (m_nRescode != 257)
    {
        threadLog("Could not delete directory\n") ;
        return E_NOTFOUND ;
    }
    return E_OK ;
}
hzEcode hzFtpClient::GetDirList (hzVect<hzDirent>& listing, const hzString& Criteria)
{
    //  Get a directory listing filtered by search criteria
    //
    //  Arguments:  1)  listing     The vector of directory entries populated by this operation
    //              2)  Criteria    The file/directory search criteria
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_PROTOCOL  If the server did not provide a listing
    //              E_OK        If the directory listing was recieved
    _hzfunc("hzFtpClient::GetDirList") ;
    hzTcpClient     X ;             //  Temporary data connection
    hzDirent        meta ;          //  Metafile
    hzChain         Listing ;       //  Temporary store for listing
    hzChain::Iter   z ;             //  For processing naked listing
    hzString        filename ;      //  Name of file
    uint32_t        nRecv ;         //  Bytes recieved this packet
    uint32_t        nTotal ;        //  Total bytes recieved
    uint32_t        len ;           //  Length of content to send
    uint32_t        nTry ;          //  Reconnections
    uint32_t        nTryData ;      //  Timeouts on the data channel
    char*           j ;             //  For loading line buffer
    char            cvLine [300] ;  //  Line buffer
    hzEcode         rc = E_OK ;     //  Return code
    /*
    **  Loop round until success
    */
    //  X.SetDebug(_hzGlobal_Debug & HZ_DEBUG_CLIENT) ;
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            X.Close() ;
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        //  First step is to open a data channel
        rc = _openpasv(X) ;
        if (rc != E_OK)
        {
            threadLog("Failed PASV .. aborting\n") ;
            continue ;
        }
        //  Send the LIST command
        if (Criteria)
            sprintf(m_c_sbuf, "LIST %s\r\n", *Criteria) ;
        else
            sprintf(m_c_sbuf, "LIST\r\n") ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send LIST command\n") ;
            continue ;
        }
        //  Get the expected 150 in the control client
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Did not recieve a response to LIST command\n") ;
            continue ;
        }
        if (m_nRescode != 150)
        {
            threadLog("Expected code of 150 but got %d. Aborting\n", m_nRescode) ;
            _logrescode() ;
            
            rc = E_PROTOCOL ;
            break ;
        }
        //  Get the listing in the data client
        nTotal = 0 ;
        Listing.Clear() ;
        for (nTryData = 0 ; nTryData < 3 ;)
        {
            rc = X.Recv(m_x_rbuf, nRecv, HZ_MAXPACKET) ;
            if (rc != E_OK)
            {
                threadLog("LIST: Read failed (total so far %d bytes)\n", nTotal) ;
                break ;
            }
            if (!nRecv)
                break ;
            nTotal += nRecv ;
            m_x_rbuf[nRecv] = 0 ;
            Listing << m_x_rbuf ;
        }
        X.Close() ;
        //  Unless the transfer is a complete success, do a reconnect.
        if (rc == E_TIMEOUT || nTryData == 3)
            continue ;
        if (rc == E_RECVFAIL)
            continue ;
        if (Listing.Size())
            threadLog("LIST: Total listing %d bytes\n", nTotal) ;
        else
            threadLog("LIST: Empty list. No files\n") ;
        //  If a list has been sent there will be a 226 sent by the server which we will need to read - if only to get rid of it before we move on.
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Did not recieve a response to LIST command\n") ;
            continue ;
        }
        if (m_nRescode != 226)
        {
            threadLog("LIST: Expected code of 226 (Xfer complete) but got %d. Aborting\n", m_nRescode) ;
            rc = E_PROTOCOL ;
        }
        //  No further communication required so drop out
        break ;
    }
    if (rc == E_OK)
    {
        //  Examine the dir listing (if there is one)
        if (!Listing.Size())
            return rc ;
        j = cvLine ;
        for (z = Listing ; !z.eof() ; z++)
        {
            if (*z == CHAR_CR)
                continue ;
            if (*z == CHAR_NL)
            {
                //  Process the line
                *j = 0 ;
                j = cvLine ;
                //  Set meta data for the file
                rc = _setmetafile(meta, cvLine) ;
                if (rc != E_OK)
                    break ;
                //  Add the meta data
                rc = listing.Add(meta) ;
                if (rc != E_OK)
                    break ;
                continue ;
            }
            *j++ = *z ;
        }
        m_nTries = 0 ;
        return rc ;
    }
    threadLog("Could not get directory listing\n\n") ;
    return rc ;
}
hzEcode hzFtpClient::FileDownload   (hzDirent& finfo)
{
    //  Downloads a file from the FTP server. This is a two-step process of firstly sending of a PASV command to open the data channel
    //  and secondly the download over the data channel. The data channel is closed after the download so this function is called for
    //  every file required.
    //
    //  Arguments:  1)  finfo   The hzDirent for the server-side file. This will have been obtained by a prior directory listing.
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_NOTFOUND  The file on the server does not exist.
    //              E_OPENFAIL  The target file on the local machine could not be opened. Presumed fatal
    //              E_WRITEFAIL The target file on the local machine could not be written too. Presumed fatal.
    //              E_OK        If the operation successful
    _hzfunc("hzFtpClient::FileDownload") ;
    struct utimbuf  svr_mtime ;     //  Used to alter date on files to match server version
    std::ofstream   os ;            //  Target file stream
    hzTcpClient     X ;             //  Data channel
    hzString        tmpFile ;       //  Download to a temporary file first, rename upon success
    hzString        tgtFile ;       //  Download to a temporary file first, rename upon success
    uint32_t        epoch ;         //  Time in seconds since 1970
    uint32_t        nRecv ;         //  Incoming message size
    uint32_t        nTotal = 0 ;    //  Total bytes downloaded
    uint32_t        len ;           //  Outgoing message length
    uint32_t        nTry ;          //  Local limit on reconnects
    hzEcode         rc ;            //  Return code
    //  Elimintate zero size files
    if (!finfo.Size())
        return E_OK ;
    //  X.SetDebug(_hzGlobal_Debug & HZ_DEBUG_CLIENT) ;
    tmpFile = m_LocalDir + "/" ;
    tmpFile += finfo.strName() ;
    tmpFile += ".tmp" ;
    tgtFile = m_LocalDir + "/" + finfo.strName() ;
    /*
    **  Loop round until success
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
start:
        nTotal = 0 ;
        if (nTry == 1)
        {
            X.Close() ;
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  First step is to open a data channel
        */
        rc = _openpasv(X) ;
        if (rc != E_OK)
        {
            threadLog("Failed PASV\n") ;
            continue ;
        }
        /*
        **  Send the RETR command and recv response
        */
        sprintf(m_c_sbuf, "RETR %s\r\n", finfo.txtName()) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send RETR command to get file %s\n", finfo.txtName()) ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not get RETR respeonse (file=%s)\n", finfo.txtName()) ;
            continue ;
        }
        /*
        **  Check response code
        */
        if (m_nRescode >= 400)
        {
            threadLog("Got bad response to RETR (%d)\n", m_nRescode) ;
            rc = E_PROTOCOL ;
            break ;
        }
        /*
        **  Then if all is well, download the file
        */
        os.open(*tmpFile) ;
        if (os.fail())
        {
            threadLog("Failed to open download temp file %s\n", *tmpFile) ;
            rc = E_OPENFAIL ;
            break ;
        }
        for (nTotal = 0 ; nTotal < finfo.Size() ;)
        {
            rc = X.Recv(m_x_rbuf, nRecv, HZ_MAXPACKET) ;
            if (rc != E_OK)
            {
                threadLog("Socket error during download of file %s\n", finfo.txtName()) ;
                X.Close() ;
                os.close() ;
                os.clear() ;
                goto start ;
            }
            if (!nRecv)
                break ;
            nTotal += nRecv ;
            os.write(m_x_rbuf, nRecv) ;
            if (os.fail())
            {
                threadLog("Failed to write data to the target file %s\n", finfo.txtName()) ;
                rc = E_WRITEFAIL ;
                break ;
            }
        }
        X.Close() ;
        os.close() ;
        if (rc == E_WRITEFAIL)
        {
            threadLog("Aborted operation to download %d of %d bytes to the target file %s - No disk space\n", nTotal, finfo.Size(), finfo.txtName()) ;
        }
        //  Initial comms phase over so exit loop
        break ;
    }
    if (rc == E_OK)
    {
        /*
        **  Rename the file to the target name, set mtime of the local file to match that of the server
        */
        rename(*tmpFile, *tgtFile) ;
        threadLog("Downloaded %d of %d bytes to the target file %s\n", nTotal, finfo.Size(), finfo.txtName()) ;
        epoch = finfo.Mtime() ;
        svr_mtime.actime = time(0) ;
        svr_mtime.modtime = epoch ;
        utime(finfo.txtName(), &svr_mtime) ;
        /*
        **  Get server progress report of download. If broken pipe reconnect but as we have the file already we don't
        **  have to repeat any steps.
        */
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not get response to RETR command (file=%s)\n", finfo.txtName()) ;
            rc = _reconnect() ;
        }
        else
        {
            if (m_nRescode != 226)
                threadLog("Expected code of 226 (Xfer complete), got %d\n", m_nRescode) ;
            if (m_nRescode >= 400)
                rc = E_RECVFAIL ;
        }
    }
    if (rc != E_OK)
        threadLog("Could not download %s\n\n", finfo.txtName()) ;
    else
    {
        threadLog("Downloaded %s\n", finfo.txtName()) ;
        m_nTries = 0 ;
    }
    return rc ;
}
hzEcode hzFtpClient::FileUpload (const hzString& SvrFilename, const hzString& LocFilename)
{
    //  Uploads a file to the FTP server. This involves both the sending of a PASV command to open the data channel and the upload over the data
    //  channel. The data channel is closed after the upload so this function is called for every file required.
    //
    //  Arguments:  1)  SvrFilename     Name file is to be called on the server
    //              2)  LocFilename     Name file has on local machine
    //
    //  Returns:    E_NOTFOUND  Named file does not exists.
    //              E_OPENFAIL  Named file could not be opened.
    //              E_PROTOCOL  Protocol error.
    //              E_OK        Operation successful, file fully uploaded.
    _hzfunc("hzFtpClient::FileDownload") ;
    hzTcpClient X ;             //  TCP client
    FSTAT       fs ;            //  File info
    ifstream    is ;            //  Target file stream
    uint32_t    nSize ;         //  Total bytes to upload (file size)
    uint32_t    nRecv ;         //  Bytes in server response
    uint32_t    nDone = 0 ;     //  Bytes uploaded
    uint32_t    len ;           //  Command length
    uint32_t    nTry ;          //  Reconnections
    hzEcode     rc ;            //  Return code
    /*
    **  Because the source file is local, first thing we do is check it exists and the size
    */
    //  X.SetDebug(_hzGlobal_Debug & HZ_DEBUG_CLIENT) ;
    if (lstat(*LocFilename, &fs) == -1)
        { threadLog("Failed to locate source file for upload %s\n", *LocFilename) ; return E_NOTFOUND ; }
    nSize = fs.st_size ;
    rc = OpenInputStrm(is, *LocFilename) ;
    if (rc != E_OK)
        return rc ;
    /*
    **  Now upload the file: Loop round until success
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        nDone = 0 ;
        if (nTry)
        {
            X.Close() ;
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        //  Next step is to open a data channel
        rc = _openpasv(X) ;
        if (rc != E_OK)
            { threadLog("Failed PASV ... trying again\n") ; continue ; }
        //  Send the STOR command and recv the response
        sprintf(m_c_sbuf, "STOR %s\r\n", *SvrFilename) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
            { threadLog("Could not send STOR command to upload file %s (attempt %d of 3)\n", *SvrFilename, nTry) ; continue ; }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv STOR response (file=%s, attempt %d of 3)\n", *SvrFilename, nTry) ;
            rc = _reconnect() ;
            continue ;
        }
        //  Check response code
        if (m_nRescode >= 400)
            { threadLog("Got bad STOR response (%d) - aborting\n", m_nRescode) ; rc = E_PROTOCOL  ; break ; }
        //  Then if all is well, upload the file
        for (nDone = 0 ; nDone < nSize && rc == E_OK ; nDone += is.gcount())
        {
            is.read(m_x_sbuf, HZ_MAXPACKET) ;
            if (!is.gcount())
                break ;
            rc = X.Send(m_x_sbuf, is.gcount()) ;
            if (rc != E_OK)
                threadLog("No socket during upload of file %s to %s\n", *LocFilename, *SvrFilename) ;
        }
        //  If not OK then retry
        if (rc != E_OK)
            continue ;
        //  Otherwise close channel and exit loop
        is.close() ;
        X.Close() ;
        break ;
    }
    //  Get server progress report of upload
    if (rc == E_OK)
    {
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
            threadLog("Could not get progress report to STOR command. Giving up (file=%s)\n", *SvrFilename) ;
        else
        {
            if (m_nRescode != 226)
                { threadLog("Expected code of 226 (Xfer complete), got %d\n", m_nRescode) ; rc = E_PROTOCOL ; }
        }
    }
    if (nDone != nSize)
    {
        threadLog("File size of %d bytes, uploaded %d\n", nSize, nDone) ;
        if (rc == E_OK)
            rc = E_SENDFAIL ;
    }
    if (rc != E_OK)
        threadLog("Could not upload %s\n\n", *LocFilename) ;
    else
    {
        threadLog("Uploaded %s\n", *LocFilename) ;
        m_nTries = 0 ;
    }
    return rc ;
}
hzEcode hzFtpClient::FileDelete (const hzString& SvrFilename)
{
    //  Delete a file on the server
    //
    //  Arguments:  1)  SvrFilename     Name file has on the server
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_WRITEFAIL If the remote file was not deleted
    //              E_OK        If the remote file was deleted
    _hzfunc("hzFtpClient::FileDelete") ;
    uint32_t    nRecv ;     //  Length of server response
    uint32_t    len ;       //  Length of client request
    uint32_t    nTry ;      //  Reconnection retry count
    hzEcode     rc ;        //  Return code
    /*
    **  Send DELE command and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the DELE command and check the response
        */
        sprintf(m_c_sbuf, "DELE %s\r\n", *SvrFilename) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send DELE command (for file %)\n", *SvrFilename) ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not recv DELE response (for file %)\n", *SvrFilename) ;
            continue ;
        }
        break ;
    }
    /*
    **  Check server response
    */
    if (m_nRescode >= 400)
    {
        threadLog("Got bad response (%d) to DELE %s\n", m_nRescode, *SvrFilename) ;
        return E_NODATA ;
    }
    return E_OK ;
}
hzEcode hzFtpClient::FileRename (const hzString& oldsvrname, const hzString& newsvrname)
{
    //  Rename a file on the server
    //
    //  Arguments:  1)  oldsvrname  Old name of file/directory on server
    //              2)  newname     New name of file/directory on server
    //
    //  Returns:    E_HOSTFAIL  If there was a communication failure and reconnect failed
    //              E_WRITEFAIL If the requested server file was not renamed
    //              E_OK        If the remote file was renamed
    _hzfunc("hzFtpClient::FileRename") ;
    uint32_t    nRecv ;     //  Length of server response
    uint32_t    len ;       //  Length of client request
    uint32_t    nTry ;      //  Reconnection retry count
    hzEcode     rc ;        //  Return code
    /*
    **  Send RNFR & RNTO commands and receive resposns, reconnect if required.
    */
    for (nTry = 0 ; nTry < 2 ; nTry++)
    {
        if (nTry == 1)
        {
            m_ConnControl.Close() ;
            rc = _reconnect() ;
            if (rc != E_OK)
                break ;
        }
        /*
        **  Send the RNFR (rename from) command and check the response
        */
        //sprintf(m_c_sbuf, "RENAME %s %s\r\n", *oldsvrname, *newsvrname) ;
        sprintf(m_c_sbuf, "RNFR %s\r\n", *oldsvrname) ;
        len = strlen(m_c_sbuf) ;
        if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
        {
            threadLog("Could not send RNFR command (file %s to %s)\n", *oldsvrname, *newsvrname) ;
            continue ;
        }
        if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
        {
            threadLog("Could not get RNFR response (file %s to %s)\n", *oldsvrname, *newsvrname) ;
            continue ;
        }
        if (m_nRescode == 350)
        {
            /*
            **  The RNFR was OK so now send the RNTO
            */
            sprintf(m_c_sbuf, "RNTO %s\r\n", *newsvrname) ;
            len = strlen(m_c_sbuf) ;
            if ((rc = m_ConnControl.Send(m_c_sbuf, len)) != E_OK)
            {
                threadLog("Could not send RNTO command (file %s to %s)\n", *oldsvrname, *newsvrname) ;
                continue ;
            }
            if ((rc = _ftprecv(nRecv, *_fn)) != E_OK)
            {
                threadLog("Could not get RNTO response (file %s to %s)\n", *oldsvrname, *newsvrname) ;
                continue ;
            }
        }
        break ;
    }
    /*
    **  Check server response
    */
    if (m_nRescode >= 400)
    {
        threadLog("Got bad response (%d) to RNFR/RNTO command (file %s to %s)\n", m_nRescode, *oldsvrname, *newsvrname) ;
        return E_NODATA ;
    }
    return E_OK ;
}
hzEcode hzFtpClient::QuitSession    (void)
{
    //  Sends a QUIT command to the FTP server to end the session
    //
    //  Arguments:  None
    //
    //  Returns:    E_SENDFAIL  If the command could not be sent
    //              E_RECVFAIL  If the server failed to acknowledge the command
    //              E_OK        If the operation was succussful
    _hzfunc("hzFtpClient::QuitSession") ;
    uint32_t    nRecv ;     //  Length of server response
    uint32_t    len ;       //  Length of client request
    hzEcode     rc ;        //  Return code
    threadLog("hzFtpClient::QuitSession. Quitting session: - ") ;
    sprintf(m_c_sbuf, "QUIT\r\n") ;
    len = strlen(m_c_sbuf) ;
    rc = m_ConnControl.Send(m_c_sbuf, len) ;
    if (rc == E_TIMEOUT)
        threadLog("Timed out whilst sending QUIT command\n") ;
    else if (rc != E_OK)
        threadLog("Could not send QUIT command\n") ;
    else
    {
        rc = m_ConnControl.Recv(m_c_sbuf, nRecv, 1024) ;
        if (rc == E_TIMEOUT)
            threadLog("Timed out whilst awaiting QUIT response\n") ;
        else if (rc != E_OK)
            threadLog("Could not recv QUIT response\n") ;
        else
            threadLog("FTP Quit Successful\n") ;
    }
    m_ConnControl.Close() ;
    return E_OK ;
}
/*
**  Section 2:  Management of mass downloads from a known FTP host
*/
hzEcode hzFtpHost::_hz_FtpDnhist::Load  (hzMapS<hzString,hzDirent>& dlist, hzString& opdir)
{
    //  If no history file create one, open it for writing and return. If there is read it in and then open it for appending.
    //
    //  Arguments:  1)  dlist   Directory listing
    //              2)  opdir   Operational directory
    //
    //  Returns:    E_ARGUMENT  If the operational directory is not supplied
    //              E_OPENFAIL  If the history file cannot be opened for reading
    //              E_OK        If the history file has been created afresh or read in
    FSTAT       fs ;        //  Local file status
    ifstream    is ;        //  Input stream
    hzDirent    de ;        //  Directory entry to be inserted
    hzString    hname ;     //  Name of history file
    hzString    fname ;     //  Name of downloaded file
    uint32_t    epoch ;     //  Recorded file epoch
    uint32_t    size ;      //  Recorded file size
    hname = opdir + "/histdn.dat" ;
    if (stat(*hname, &fs) == -1)
    {
        _file = fopen(*hname, "a") ;
        return E_OK ;
    }
    is.open(*hname) ;
    if (is.fail())
        return E_OPENFAIL ;
    for (;;)
    {
        is.getline(buf, 255) ;
        if (!is.gcount())
            break ;
        buf[10] = buf[21] = 0 ;
        IsPosint(epoch, buf) ;
        IsPosint(size, buf + 11) ;
        fname = buf + 22 ;
        de.InitNorm(0, fname, -1, size, epoch, epoch, 0777, 0, 0, 1) ;
        dlist.Insert(fname, de) ;
    }
    is.close() ;
    _file = fopen(*hname, "a") ;
    return E_OK ;
}
hzEcode hzFtpHost::_hz_FtpDnhist::Append    (uint32_t epoch, uint32_t size, const hzString& name)
{
    //  Append the download history with file data
    //
    //  Arguments:  1)  epoch   Epoch time
    //              2)  size    Size of file
    //              3)  name    Filename
    //
    //  Returns:    E_ARGUMENT  If the filename is not supplied
    //              E_WRITEFAIL If the file to be appeneded has not been opened
    //              E_OK        If the history file was appended
    if (!name)
        return E_ARGUMENT ;
    if (!_file)
        return E_WRITEFAIL ;
    fprintf(_file, "%010u %010d %s\n", epoch, size, *name) ;
    return E_OK ;
}
hzEcode hzFtpHost::Init (   const hzString& host,
                            const hzString& user,
                            const hzString& pass,
                            const hzString& remDir,
                            const hzString& locDir,
                            const hzString& criteria    )
{
    //  Initialize an FTP host or account. This means setting up all the parameters for connecting to an FTP server
    //
    //  Arguments:  1)  host        The host name
    //              2)  user        The username for the account
    //              3)  pass        The password to the account
    //              4)  remDir      The opening directory on the server (usually /)
    //              5)  locDir      The opening directory on the local machine
    //              6)  criteria    Opening search criteria for listing (if any)
    //
    //  Returns:    E_ARGUMENT  If either the hostname, username, password or local directory is not supplied
    //              E_NOCREATE  If the local directory does not exist and could not be created
    //              E_OK        If the operation was successful
    _hzfunc("hzFtpSite::Initialize") ;
    hzEcode rc = E_OK ;     //  Return code
    /*
    **  Check we have an action, that the action is associated with a publication and that it has an issue date
    */
    m_Host = host ;
    m_Username = user ;
    m_Password = pass ;
    m_Source = remDir ;
    m_Repos = locDir ;
    m_Criteria = criteria ;
    if (!host || !user || !pass || !locDir)
    {
        if (!host)      threadLog("No FTP host supplied\n") ;
        if (!user)      threadLog("No FTP username supplied\n") ;
        if (!pass)      threadLog("No FTP password supplied\n") ;
        if (!locDir)    threadLog("No FTP local directory supplied\n") ;
        return E_ARGUMENT ;
    }
    /*
    **  Formulate directory to collect files from and assert that directory
    */
    rc = AssertDir(*m_Repos, 0755) ;
    if (rc != E_OK)
        return hzwarn(rc, "Failed to assert local dir (%s)\n", *m_Repos) ;
    m_bInit = true ;
    return rc ;
}
hzEcode hzFtpHost::GetAll   (void)
{
    //  Connect to FTP Server with username and password. Go to remote pre-determined directory and obtain listing according to given criteria. For each file in the
    //  listing, check if we do not already have the (complete) file from a previous run. If not, download it to the target directory and then record this action.
    //
    //  Arguments:  None
    //
    //  Returns:    E_HOSTFAIL  If the collection is cut off and cannot be re-established within an acceptable period.
    //              E_PROTOCOL  If the action fails for reasons of protocol.
    //              E_OK        If the collection runs to completion
    _hzfunc("hzFtpSite::GetAll") ;
    hzMapS  <hzString,hzDirent> mapFilesCurr ;  //  Files already downloaded for the current issue (in name order)
    hzVect  <hzDirent>          vecFilesCurr ;  //  Files already downloaded for the current issue
    hzVect  <hzDirent>  listing ;   //  The list of files
    hzFtpClient ftp ;               //  The FTP client connection
    hzDirent    de ;                //  Directory entry
    hzDirent    de_a ;              //  Directory entry of file we have already (if applicable)
    hzString    localfile ;         //  Target file
    hzString    rento ;             //  Rename server file to ...
    uint32_t    nIndex ;            //  Directory entry iterator
    uint32_t    nAlready = 0 ;      //  When a file is skipped (it is in the listing for the criteria but we already have it), we
                                    //  increment this value.
    uint32_t    nDnloads = 0 ;      //  Number of files downloaded (this attempt).
    uint32_t    nListed = 0 ;       //  Set from the count of server files meeting the criteria. At the end we add up nAlready and
                                    //  nDnloads and only when these add up to nListed can the status be set to ACTION_COLLECTED
                                    //  otherwise status should be ACTION_STARTED
    int32_t     sys_rc ;            //  Return from system call
    char*       cmd ;               //  For system call command when needed
    hzEcode     rc = E_OK ;         //  Return from FTP functions
    /*
    **  Check we have everything we need: The publication and the olddata and newdata dirs
    */
    if (!m_bInit)
        Fatal("Permenant Failure: No FTP Parameters\n") ;
    threadLog("Collecting from server [%s,%s,(%s)] (user=%s, pass=%s) to dir %s ->", *m_Host, *m_Source, *m_Criteria, *m_Username, *m_Password, *m_Repos) ;
    threadLog("OK GO!\n") ;
    //  Get a combined listing of the historic and current data input directories
    dnh.Load(mapFilesCurr, m_Repos) ;
    rc = ReadDir(vecFilesCurr, *m_Repos) ;
    if (rc != E_OK)
    {
        threadLog("Failed to read local dir (%s)\n", *m_Repos) ;
        return rc ;
    }
    for (nIndex = 0 ; nIndex < vecFilesCurr.Count() ; nIndex++)
    {
        de = vecFilesCurr[nIndex] ;
        mapFilesCurr.Insert(de.strName(), de) ;
    }
    //  Set up FTP params
    rc = ftp.Initialize(m_Host, m_Username, m_Password) ;
    if (rc != E_OK)
    {
        threadLog("Could not INIT FTP session to server [%s] (user=%s, pass=%s). Error is %s\n", *m_Host, *m_Username, *m_Password, Err2Txt(rc)) ;
        return rc ;
    }
    //  Start FTP session
    rc = ftp.StartSession() ;
    if (rc != E_OK)
    {
        threadLog("Could not start FTP session to server [%s] (user=%s, pass=%s). Error is %s\n", *m_Host, *m_Username, *m_Password, Err2Txt(rc)) ;
        return rc ;
    }
    //  Go to the relevent remote directory
    rc = ftp.SetRemoteDir(m_Source) ;
    if (rc != E_OK)
    {
        if (rc == E_NOTFOUND)
        {
            threadLog("Permenant Failure: Expected source directory %s has not been found on the server\n", *m_Source) ;
            ftp.QuitSession() ;
            return rc ;
        }
        threadLog("Temporary error: Could not CD to %s\n", *m_Source) ;
        return rc ;
    }
    rc = ftp.SetLocalDir(m_Repos) ;
    if (rc != E_OK)
    {
        threadLog("Permenant Failure: Expected target directory %s has not been found or is otherwise in error on the server\n",
            *m_Repos) ;
        ftp.QuitSession() ;
        return rc ;
    }
    //  Get the server directory listing
    rc = ftp.GetDirList(listing, m_Criteria) ;
    if (rc != E_OK)
    {
        if (rc == E_PROTOCOL)
        {
            threadLog("Permanent error: Could not get a listing. Protocol error\n") ;
            return rc ;
        }
        threadLog("We have %d files in server listing but an error of %s - will rerun\n", listing.Count(), Err2Txt(rc)) ;
        return rc ;
    }
    //  Got listing but is there anything in it
    if (listing.Count() == 0)
    {
        //  Expected files were not there so try again later
        threadLog("Temporary error: Got directory listing but there were no files matching [%s]\n", *m_Criteria) ;
        ftp.QuitSession() ;
        return rc ;
    }
    //  Compare files and download new ones
    nListed = listing.Count() ;
    for (nIndex = 0 ; nIndex < listing.Count() ; nIndex++)
    {
        de = listing[nIndex] ;
        if (!mapFilesCurr.Exists(de.strName()))
            threadLog("Downloading file (%d of %d) %s\n", nIndex, listing.Count(), de.txtName()) ;
        else
        {
            de_a = mapFilesCurr[de.strName()] ;
            if (de_a.Size() == de.Size())
            {
                if (de_a.Mtime() >= de.Mtime())
                {
                    threadLog("Skiping file (%d of %d) %s\n", nIndex, listing.Count(), de.txtName()) ;
                    nAlready++ ;
                    continue ;
                }
            }
            threadLog("Re-fetching file (%d of %d) %s\n", nIndex, listing.Count(), de.txtName()) ;
        }
        if (!de.strName())
        {
            threadLog("Critical error: An Un-named meta file has been found in the listing. Corruption suspected\n") ;
            ftp.QuitSession() ;
            return rc ;
        }
        if (!de.Size())
            continue ;
        //  We have a new file so fetch
        localfile = m_Repos + "/" + de.strName() ;
        rc = ftp.FileDownload(de) ;
        if (rc != E_OK)
        {
            if (rc == E_OPENFAIL || rc == E_WRITEFAIL)
            {
                threadLog("Problem opening/writing file (%s) (error=%s)\n", de.txtName(), Err2Txt(rc)) ;
                return rc ;
            }
            ftp.QuitSession() ;
            return rc ;
        }
        //  The file is OK - Add it to download history
        dnh.Append(de.Mtime(), de.Size(), de.strName()) ;
        nDnloads++ ;
        //  Deal with case where file is zipped
        if (strstr(de.txtName(), ".zip"))
        {
            cmd = new char[300] ;
            sprintf(cmd, "/usr/bin/unzip -q -j -n -d %s %s", *m_Repos, *localfile) ;
            sys_rc = system(cmd) ;
            if (sys_rc)
            {
                threadLog("System call [%s] returns %d\n", cmd, sys_rc) ;
                delete cmd ;
                return rc ;
            }
            threadLog("System call [%s] returns OK\n", cmd) ;
            unlink(*localfile) ;
            delete cmd ;
        }
    }
    //  Quit FTP session, return to original dir and finish.
    ftp.QuitSession() ;
    if ((nDnloads + nAlready) == nListed)
    {
        rc = ReadDir(vecFilesCurr, *m_Repos) ;
        if (rc != E_OK)
        {
            threadLog("Failed to read target dir (%s) after download - delaying for 15 mins\n", *m_Repos) ;
            return rc ;
        }
        else
        {
            hzXDate     now ;       //  System date and time
            uint32_t    eHi ;       //  Latest file found
            uint32_t    eIs ;       //  System date and time as epoch
            now.SysDateTime() ;
            eIs = now.AsEpoch() ;
            eHi = 0 ;
            for (nIndex = 0 ; nIndex < vecFilesCurr.Count() ; nIndex++)
            {
                de = vecFilesCurr[nIndex] ;
                if (eHi < de.Mtime())
                    eHi = de.Mtime() ;
            }
            if (eHi > (eIs - 600))
                threadLog("Oldest file less than 10 mins old - delaying just in case\n") ;
        }
    }
    else
    {
        threadLog("Total of %d listed on server. We have %d already and %d were downloaded. %d files remain outstanding\n",
            nListed, nAlready, nDnloads, (nListed - nAlready - nDnloads)) ;
    }
    return rc ;
}