// module backupEH.cpp
//
// This is a simple model for backup over Internet to a directory based on a WEB server.
// Uses the command line utility FTP under Windows - not tested under UNIX/Linux!!!
// The program accepts a directory and a date as parameters (see the usage() procedure).
// It traverses the given directory and checks each file (except some exceptions) for a modification date
// that is equal or nwer to tge given date.
// If the candidate file is newer it prepare a FTP command file and starts FTP transferring the
// file to the WEB server specified in the FTP command file.
// A pattern file for the ftp commands "ftpcmd.pat" must exist. This pattern file must be adapted to 
// the respective user/password/server name etc.
// Runs in a command window (black window)
//
// Copyright Dr.E.Huckert 08-2016

// Compile with Digtal Mars C++:  dmc -mn -DDMC -DWIN32 -Ic:\huckert\dm\stlport backupEH.cpp
// Should also run with other C++ compilers like g++ or Microsoft - but I didn't test that!
// No strange external libraries are used
      
// Versions:
//  V1.2 added protocol file
//  V1.3 reformatted, file names containing spaces surrounded by quotes
//  V1.5 option -test introduced (tests a backup run: protocol of all files)
//          output of the total  summary of bytes that will be backuped

#include <windows.h>
#ifdef DMC
#include <iostream.h>   // fuer Digital Mars
#else
#include <iostream>     // fuer Microsoft etc.
#endif
#include <string>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>

using namespace std;    // evtl. fuer Microsoft weg

int logging      = 0;  // switch on with the "-v" option
int testMode     = 0;
int noFilesSaved = 0;
int noCandidates = 0;
unsigned long noBytes = 0L;

// --------------------------------------------------------------------
// Get the size of a file whose name is given (based on C API)
// Note: returns 0 on error !!!
unsigned long getFileSize(const char *filename) 
{
  struct stat st; 
  int cRet;
  //
  cRet = ::stat(filename, &st);
  if (::logging)
    cout << "getFileSize(): result stat()=" << cRet << endl;
  if (cRet == 0)
      return (unsigned long)(st.st_size);
  return 0L; 
}

// ---------------------------------------------------------------
// convert a date format to an epoch value
// strict format YYYY-MM-DD here assumed
// convert this to a vanilla Epoch time
time_t timestamp2long(const char *tstamp)
{
  struct tm zeit;
  memset((char *)&zeit, 0, sizeof(struct tm));
  //printf("tstamp=%s\n", tstamp);
  zeit.tm_year = atoi(&tstamp[0]) - 1900;
  //printf("tm_year=%d\n", zeit.tm_year);
  zeit.tm_mon  = atoi(&tstamp[5]) - 1;
  zeit.tm_mday = atoi(&tstamp[8]);
  //zeit.tm_hour = atoi(&tstamp[11]);
  //zeit.tm_min  = atoi(&tstamp[14]);
  //zeit.tm_sec  = atoi(&tstamp[17]);
  zeit.tm_hour = 0;
  zeit.tm_min  = 0;
  zeit.tm_sec  = 0;
  return ((time_t)mktime(&zeit));
}  // end timestamp2long()
      
// ---------------------------------------------------------
//  returns current date in format DD-MM-YYYY
void get_current_date(char *dest)
{
  time_t   ltime;
  struct tm *newtime;
  short  offset;
  time(&ltime);
  newtime = localtime(&ltime);
  offset  = 1900;
  ::sprintf(dest,"%02d-%02d-%04d",
          (int)newtime->tm_mday,
          (int)newtime->tm_mon+1,
          (int)newtime->tm_year + offset);
}   // end get_current_date()
      
// ---------------------------------------------------------------------
// delivers the Epoch time in secs
time_t getEpochTime(void)
{
  time_t   ltime;
  ltime = time(0);
  return ltime;
}   // end UTIL::getEpochTime()

// -----------------------------------------------------------------
void cleanBuffer(char *buf)
{
int n;
 for (n=0; buf[n] != 0; n++)
 {
   if (buf[n] == '\n') buf[n] = 0;
   if (buf[n] == '\r') buf[n] = 0;
 }
}   // end clearBuffer()

// ------------------------------------------------------------------
// prepare (=modify) the existing ftp command file ("ftpcmd.pat") by
// inserting the given file name thus producing file "ftpcmd.txt"
// Returns 0 if OK, < 0 upon error
int prepareFTPfile(const char *fileName)
{
  int ret = -1;
  int nb, hasSpace;
  FILE *fd1 = NULL;
  FILE *fd2 = NULL;
  char readBuf[256];
  //
  // Note: ftpcmd.pat must exist in the directory from where backEH.exe
  // is started. It is not taken from the directory given as  an argument nor
  // taken from elsewhere in the path!!!!!
  fd1 = ::fopen("ftpcmd.pat", "r");
  if (fd1 == NULL)
  {
    ret = -1;
    goto zurueck;
  }
  fd2 = ::fopen("ftpcmd.txt", "w");
  if (fd2 == NULL)
  {
    ret = -2;
    goto zurueck;
  }
  //
  while (1)
  {
    if (::fgets(readBuf, sizeof(readBuf) - 1, fd1) == NULL)
      break;
    if (::strlen(readBuf) > (sizeof(readBuf) - 32))
      continue;
    if (::strncmp(readBuf, "put ", 4) == 0)
    {
      cleanBuffer(readBuf);
      hasSpace = 0;
      if (::strchr(readBuf, ' ') != NULL) hasSpace = 1; // V1.3
      if (hasSpace)
        ::strcat(readBuf, "\"");
      ::strcat(readBuf, fileName);
      if (hasSpace)
        ::strcat(readBuf, "\"");  // V1.3
      ::strcat(readBuf, "\n");
      ret = 0;
    }
    nb = ::fputs(readBuf, fd2);
    if (nb < 0)
    {
      ret = -3;
      goto zurueck;
    }
  }   // end while (1)
  //
  ret = 0;
  //
  zurueck:
  if (fd1 != NULL)
    ::fclose(fd1);
  if (fd2 != NULL)
    ::fclose(fd2);
  if (::logging)
    cout << "result prepareFTPfile() = " << ret << endl;
  return ret;
}   // end prepareFTPfile()

// ------------------------------------------------------------------
// Traverse a directory and the embbeded directories
// Calls itself recursively!
// Executes the external FTP command after having prepared a controle file for FTP
void traverseDirectory(const char *directory,
           const char *datum)     // format YYYY-MM-DD
{
  WIN32_FIND_DATA file;
  HANDLE hsearch;
  string dirFn = "";
  string fileAbsPath = "";
  struct stat statbuf;
  int cRet = 0;
  int doIt = 0;
        static FILE *fdp = NULL;
  //
  if (::logging)
    cout << "traverseDirectory(): directory=" <<
      directory << endl;
  dirFn = directory;
  dirFn = dirFn + "\\*";
  hsearch = ::FindFirstFile(dirFn.c_str(), &file);
  if (hsearch == INVALID_HANDLE_VALUE)
  {
    cout << "ERROR: invalid FindFirstFile argument=" << dirFn << endl;
    return;
  }
  //
  do
  {
    if (::strcmp(file.cFileName, "..") == 0)
      // don't look into the upward link
      continue;
    if (::strcmp(file.cFileName, ".") == 0)
      // ignore the actual directory
      continue;
    // this is a directory - we do nothing special
    fileAbsPath = directory;
    fileAbsPath = fileAbsPath + "\\";
    fileAbsPath = fileAbsPath + file.cFileName;
    if (file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    {
       // this is a directory: recursive call
      if (::logging)
      cout << "--- directory=" << fileAbsPath << endl;
      traverseDirectory(fileAbsPath.c_str(), datum);
    }
    else
    {
      // this is a normal file
      if (::logging)
        cout << "file name=" << fileAbsPath << endl;
      // get the modification date of the file 
      int cRet = stat(fileAbsPath.c_str(), &statbuf);
      if (cRet != 0)
      {
        cout << "ERROR: cannot get the modification date for file=" <<
        fileAbsPath << endl;
        return;
      }
      time_t fileDate = statbuf.st_mtime;  
      if (::logging)
        cout << "act.time=" << getEpochTime() <<
          "  file time=" << fileDate << endl;
      //
      // compare the file date with the given date
      time_t paramDate = timestamp2long(datum);
      if (fileDate >= paramDate)
      {
        if (::logging)
          cout << "file will be saved..." << endl;
        //
        // Exclude some file types and some files
        // This should be modified for the specific local needs
        // Could also be replaced by a mechanism that accepts an external configuration file
        doIt =1;
        //if (fileAbsPath.find(".exe") != string::npos) doIt = 0;
        if (fileAbsPath.find(".map") != string::npos) doIt = 0;
        if (fileAbsPath.find(".obj") != string::npos) doIt = 0;
        if (fileAbsPath.find(".bak") != string::npos) doIt = 0;
        if (fileAbsPath.find("..")   != string::npos) doIt = 0;
        if ((fileAbsPath.find(".")   != string::npos) &&
             (fileAbsPath.length() == 1))            doIt = 0;
        //if (fileAbsPath.find(".EXE") != string::npos) doIt = 0;
        if (fileAbsPath.find(".MAP") != string::npos) doIt = 0;
        if (fileAbsPath.find(".OBJ") != string::npos) doIt = 0;
        if (fileAbsPath.find(".BAK") != string::npos) doIt = 0;
        if (fileAbsPath.find(".int") != string::npos) doIt = 0;
        if (fileAbsPath.find("ftpcmd.pat") != string::npos) doIt = 0;
        if (fileAbsPath.find("ftpcmd.txt") != string::npos) doIt = 0;
        //
        if (doIt)
        {
          if (::testMode)
          {
            cout << "backup candidate: " << fileAbsPath << endl;
            noCandidates++;
            // update the number of bytes to be saved
            ::noBytes += ::getFileSize(fileAbsPath.c_str());
          }
          else
          {
            // prepare the ftp command file ftpcmd.txt
            cRet = prepareFTPfile(fileAbsPath.c_str());
            if (cRet < 0)
            {
              if (cRet == -1)
                 cout << "ERROR: cannot open the control file ftpcmd.pat" << endl;
              else
              cout << "ERROR: cannot prepare the control file ftpcmd.txt" << endl;
                return;
            }
            // execute the FTP command
            cRet = ::system("ftp -s:ftpcmd.txt www.huckert.com");
            if (cRet < 0)
            {
              cout << "ERROR: cannot execute the ftp command" << endl;
              return;
            } 
            // update the number of bytes to be saved
            ::noBytes += ::getFileSize(fileAbsPath.c_str());
            //
            cout << "success for file=" << fileAbsPath << endl;
            noFilesSaved++;
            // write a protocol file
            if (fdp == NULL)
              fdp = ::fopen("backupEH.protocol", "w");
            if (fdp != NULL)
            {
              char buf[256];
              if (noFilesSaved == 1)
              {
                ::fprintf(fdp, "directory: %s\nfrom date: %s\n", 
                          directory, datum);
              }
              get_current_date(buf);
              ::fprintf(fdp, "file OK: %s  %s\n", buf, fileAbsPath.c_str());
            }   // end if (fdp != NULL)
          }   //  if else
        }   // end if (doIt)
        else
        {
          if (::logging)
            cout << "file excluded: " << fileAbsPath << endl;
        }
      }   // end if (fileDate >= paramDate)
    }   // end else
  } while (::FindNextFile(hsearch, &file));
  //
  if (fdp != NULL)
  {
    ::fclose(fdp);
    fdp = NULL;
  }
  ::FindClose(hsearch); 
}   // end traverseDirectory()

// -----------------------------------------------------
void usage()
{
  cout << "usage: backupEH_directory date  [-test] [-v]" << endl;
  cout << "date in format YYYY-MM-DD" << endl;
  cout << "Copyright Dr.E.Huckert 08-2016 V1.5" << endl;
}   // end usage()

// ----------------------------------------------------
int main(int argc, char *argv[])
{
  int n;
  if (argc < 3)
  {
    usage();
    ::exit(0);
  }
  for (int n=0; n < argc; n++)
  {
    if (::strcmp(argv[n], "-v") == 0)
      ::logging = 1;
    if (::strcmp(argv[n], "-test") == 0)
      ::testMode = 1;
  }
  // basic syntax check for the  date argument
  if (::strlen(argv[2]) != 10)
  {
    cout << "ERROR date format" << endl;
    usage();
    exit(1);
  }
  for (int n=0; argv[2][n] != 0; n++)
  {
    if (::strchr("0123456789-!", argv[2][n]) == NULL)
    {
      cout << "ERROR date format - invalid chars" << endl;
      exit(2);
    }
  }
  // This function traverses the given directory, compares the time stamp on the file
  // with the given date and takes the appropriate action (starts the FTP command)
  traverseDirectory(argv[1], argv[2]);
  //
  cout << "-------------------------------------" << endl;
  if (::testMode)
    cout << "no backup candidates=" << noCandidates << endl;
  else
    cout << "no files saved=" << noFilesSaved << endl;
  cout << "total no. of bytes=" << noBytes  <<
       " " << (noBytes / 1024L) << " KB" <<
       " " << (double)(noBytes / 1000000.0) << " MB" << endl; 
  //
  return 0;
}   // end  main()
