/* Get.c */

#include "Sys.h"

#ifdef HAVE_UTIME_H
#	include <utime.h>
#else
struct	utimbuf {time_t actime; time_t modtime;};
#endif 

#include "Util.h"
#include "RCmd.h"
#include "Xfer.h"
#include "Cmds.h"
#include "Glob.h"
#include "Get.h"
#include "DateSize.h"
#include "List.h"
#include "Getopt.h"

int gMayUTime = kDoUTime;	/* User variable. */

extern longstring gPager;
extern longstring gRemoteCWD;
extern longstring gLocalCWD;
extern int gTransferType;
extern int gStdout;
extern char *gOptArg;
extern int gOptInd;

long BinaryGetProc(char *buf, size_t bufsize, XferSpecPtr xp)
{
	long len;

	len = (long) read(xp->inStream, buf, bufsize);
	return (len);
}	/* BinaryGetProc */




long BinaryPutProc(char *buf, size_t bufsize, XferSpecPtr xp)
{
	long len;

	len = (long) write(xp->outStream, buf, bufsize);
	if (len != (long)bufsize)
		len = -1L;
	return (len);
}	/* BinaryPutProc */




long AsciiGetRemoteProc(char *buf, size_t bufsize, XferSpecPtr xp)
{
	long len;

	len = (long) read(xp->inStream, buf, bufsize);
	return (len);
}	/* AsciiGetRemoteProc */




long AsciiPutLocalProc(char *buf, size_t bufsize, XferSpecPtr xp)
{
	long len, len2;
	char *i, *o;
	long ct;

	/* In ASCII mode, all end-of-lines are denoted by CR/LF.  For
	 * UNIX, we don't want that, we want just LFs, so skip all the
	 * CR's.
	 */
	for (len = bufsize, i = o = buf, ct = 0; ct < len; ct++, ++i) {
		if (*i != '\r')
			*o++ = *i;
	}
	len = o - buf;

	len2 = (long) write(xp->outStream, buf, len);
	if (len2 != len)
		len2 = -1L;
	return (len2);
}	/* AsciiPutLocalProc */




int BinaryGet(XferSpecPtr xp)
{
	int result;

	/* This is supposed to be done previously, if you wanted accurate
	 * file sizes from GetDateAndSize.  We don't do a SETBINARY here.
	 * Instead, we set it to gTransferType, which we know is not
	 * ascii.  Most often this will mean binary mode, but perhaps we're
	 * dealing with a tenex machine.
	 */
	SetType(gTransferType);

	xp->getBlock = BinaryGetProc;
	xp->putBlock = BinaryPutProc;
	/* xp->inStream = gDataSocket;  RDataCmd fills this in when it gets it. */

	/* Send the request and do the transfer. */
	result = RDataCmd(xp, "RETR %s", xp->remoteFileName);

	return (result);
}	/* BinaryGet */





int AsciiGet(XferSpecPtr xp)
{
	int result;

	/* This is supposed to be done previously, if you wanted accurate
	 * file sizes from GetDateAndSize.
	 */
	SETASCII;
	
	/* Setup the parameter block to give to RDataCmd. */
	xp->getBlock = AsciiGetRemoteProc;
	xp->putBlock = AsciiPutLocalProc;
	/* xp->inStream = gDataSocket;  RDataCmd fills this in when it gets it. */

	/* Send the request and do the transfer. */
	result = RDataCmd(xp, "RETR %s", xp->remoteFileName);

	return (result);
}	/* AsciiGet */



/* From the pathname given in remoteName, get a local filename for
 * the current local directory.  Then open the actual file for writing.
 */
static
void GetLocalName(GetOptionsPtr gopt, string localName)
{
	char *cp;

	if ((cp = gopt->lName) == NULL) {
		/* We're supposed to pick it. */
		cp = strrchr(gopt->rName, '/');
		if (cp == NULL)
			cp = gopt->rName;
		else
			cp++;
		
	}
	gopt->lName = Strncpy(localName, cp, sizeof(string));
}	/* GetLocalName */




void SetLocalFileTimes(int doUTime, time_t remoteModTime, char *lname)
{
	struct utimbuf ut;

	/* Restore the modifcation date of the new file to
	 * what it was on the remote host, if possible.
	 */
	if ((doUTime == kDoUTime) && (remoteModTime != kModTimeUnknown)) {
		time(&ut.actime);
		ut.modtime = remoteModTime;
		(void) utime(lname, &ut);
	}
}	/* SetLocalFileTimes */




int TruncReOpenReceiveFile(XferSpecPtr xp)
{
	int fd;
	
	close(xp->outStream);
	fd = open(xp->localFileName, O_WRONLY | O_TRUNC | O_CREAT,
				S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	if (fd < 0) {
		/* Should never get here, since we were able to open this
		 * very file for appending earlier.
		 */
		Error(kDoPerror, "Can't re-open local file %s.\n", xp->localFileName);
		
		/* Try to give it something to write to anyway. */
		fd = open("/dev/null", O_WRONLY);
		xp->outStream = fd;
		
		if (fd < 0) {
			/* Don't core, please. */
			Exit(kExitPanic);
		}
		return (-1);
	}
	xp->outStream = fd;
	return (0);
}	/* TruncReOpenReceiveFile */




int DoGet(GetOptionsPtr gopt)
{
	int fd;
	int result;
	string local;
	long fileSize;
	time_t modifTime;
	int doReports;
	struct stat st;
	size_t restartPt;
	char *mode;
	time_t now;
	XferSpecPtr xp;

	if (gTransferType == 'A') {
		/* Have to set the type here, because GetDateAndSize() may
		 * use the SIZE command, and the result of that depends
		 * on the current transfer type setting.
		 */
		SETASCII;
	} else {
		SetType(gTransferType);
	}
	
	/* See if we can get some info about the file first. */
	fileSize = GetDateAndSize(gopt->rName, &modifTime);
	restartPt = SZ(0);
	doReports = 0;

	if (gopt->outputMode == kDumpToStdout) {
		fd = gStdout;
		STRNCPY(local, kLocalFileIsStdout);
		/* Don't have progress reports going if we're piping or
		 * dumping to the screen.
		 */
	} else {
		GetLocalName(gopt, local);
		mode = "w";
		if (stat(local, &st) == 0) {
			/* File exists on the local host.  We must decide whether
			 * we really want to fetch this file, since we might have
			 * it here already.  But when in doubt, we will go ahead
			 * and fetch the file.
			 */
			if (!gopt->overwrite) {
				if (modifTime != kModTimeUnknown) {
					/* We know the date of the remote file. */
					DebugMsg("Local file %s has size %lu and is dated %s",
						local,
						(unsigned long) st.st_size,
						ctime(&st.st_mtime)
					);
					if (modifTime < st.st_mtime) {
						/* Remote file is older than existing local file. */
						PrintF("Already have %s.\n", gopt->rName);
						return (0);
					} else if (modifTime == st.st_mtime) {
						/* Remote file is same age. */
						if (fileSize != kSizeUnknown) {
							/* If the local file is smaller, then we
							 * should attempt to restart the transfer
							 * from where we left off, since we the remote
							 * file has the same date.
							 */
							if (st.st_size < fileSize) {
								restartPt = SZ(st.st_size);
								mode = "a";
							} else if (st.st_size == fileSize) {
								PrintF("Already have %s.\n", gopt->rName);
								return (0);
							} else {
								DebugMsg("Overwriting %s; local file has same date,\n",
									gopt->lName);
								DebugMsg("but local file is larger, so fetching remote version anyway.\n");
							}
						} else {
							DebugMsg("Overwriting %s; local file has same date,\n",
									gopt->lName);
							DebugMsg("but can't determine remote size, so fetching remote version anyway.\n");
						}
					} else {
						/* Remote file is more recent.  Fetch the
						 * whole file.
						 */
						DebugMsg("Overwriting %s; remote was newer.\n",
							gopt->lName);
					}
				} else {
					/* We don't know the date of the file.
					 * We won't be able to safely assume anything about
					 * the remote file.  It is legal to have a more
					 * recent remote file (which we don't know), with a
					 * smaller (or greater, or equal even) size.  We
					 * will just have to fetch it no matter what.
					 */
					DebugMsg("Overwriting %s; couldn't determine remote file date.\n",
						gopt->lName);
				}
			} else {
				DebugMsg("Explicitly overwriting %s.\n", gopt->lName);
			}
		} else {
			/* We don't have a local file with the same name as the remote,
			 * but we may also want to avoid doing the transfer of this
			 * file.  For example, this is where we check the remote
			 * file's date if we were told to only get files which are
			 * less than X days old.
			 */
			if (gopt->newer > 0) {
				time(&now);
				if ((unsigned long) (now - (gopt->newer * 86400)) > modifTime) {
					DebugMsg("Skipping %s, older than %d days.\n",
						gopt->rName, gopt->newer);
					return (0);
				}
			}
		}
		if (*mode == 'w')
			fd = open(local, O_WRONLY | O_TRUNC | O_CREAT,
				S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		else
			fd = open(local, O_WRONLY | O_APPEND | O_CREAT,
				S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		if (fd < 0) {
			Error(kDoPerror, "Can't open local file %s.\n", local);
			return (-1);
		}
		doReports = gopt->doReports;
	}

	xp = InitXferSpec();
	xp->netMode = kNetReading;
	xp->outStream = fd;
	
	/* This group is needed for the progress reporting and logging stuff.
	 * Otherwise, it isn't that important.
	 */
	xp->doReports = doReports;
	xp->localFileName = local;
	xp->remoteFileName = gopt->rName;
	xp->expectedSize = fileSize;
	xp->startPoint = restartPt;
	xp->doUTime = gopt->doUTime;
	xp->remoteModTime = modifTime;
	
	if (gTransferType == 'A') {
		result = AsciiGet(xp);
	} else {		
		result = BinaryGet(xp);
	}

	if (fd != gStdout) {
		(void) close(fd);
		if ((result < 0) && (xp->bytesTransferred < 1L)) {
			/* An error occurred, and we didn't transfer anything,
			 * so remove empty file we just made.
			 */
			(void) UNLINK(local);
		} else {
			/* Restore the modifcation date of the new file to
			 * what it was on the remote host, if possible.
			 */
			SetLocalFileTimes(gopt->doUTime, modifTime, local);
		}
	}

	DoneWithXferSpec(xp);
	return (result);
}	/* DoGet */




void InitGetOutputMode(GetOptionsPtr gopt, int outputMode)
{
	gopt->outputMode = outputMode;
	if (outputMode == kSaveToDisk) {
		gopt->doUTime = gMayUTime;
		gopt->doReports = 1;
	} else {
		gopt->doUTime = kDontUTime;	
		gopt->doReports = 0;
	}
}	/* InitGetOutputMode */




void InitGetOptions(GetOptionsPtr gopt)
{
	PTRZERO(gopt, sizeof(GetOptions));
}	/* InitGetOptions */




int SetGetOption(GetOptionsPtr gopt, int opt, char *optArg)
{
	int i;

	switch (opt) {		
		case 'f':	/* Force overwrite (no reget, no newer) */
			gopt->overwrite = 1;
			break;
		case 'G':	/* No glob */
			gopt->noGlob = 1;
			break;
		case 'R':	/* Recursive */
			gopt->recursive = 1;
			break;
		case 'n':	/* Get files if not X days or newer.  */
			i = atoi(optArg);
			if (i <= 0) {
				EPrintF("Option to -n must be greater than zero.\n");
				return (kUsageErr);
			}
			gopt->newer = i;
			break;
		case 'z':	/* Get one file x, and save as y. */
			gopt->saveAs = 1;
			break;
		default:
			return (kUsageErr);
	}
	return (kNoErr);
}	/* SetGetOption */




int GetGetOptions(int argc, char **argv, GetOptionsPtr gopt)
{
	int opt;
	int i;

	/* When this is called, we are always writing to disk.
	 * In other words, we have no colon-mode to worry about.
	 */
	InitGetOptions(gopt);
	InitGetOutputMode(gopt, kSaveToDisk);
	
	/* Tell Getopt() that we want to start over with a new command. */
	GetoptReset();
	while ((opt = Getopt(argc, argv, "fGRn:z")) >= 0) {
		if (SetGetOption(gopt, opt, gOptArg) == kUsageErr)
			return (kUsageErr);
	}
	return (kNoErr);
}	/* GetGetOptions */




#ifdef HAVE_SYMLINK
static
int GetSymLinkInfo(char *dst, size_t siz, char *rLink)
{
	LineList fileList;
	char *cp;
	int result;

	result = -1;
	*dst = '\0';
	InitLineList(&fileList);
	ListToMemory(&fileList, "LIST", kListDirNamesOnlyMode, rLink);
	if (fileList.first != NULL) {
		cp = fileList.first->line;
		*cp++ = '\0';
		for (cp += strlen(cp) - 1; ; cp--) {
			if (*cp == '\0')
				goto done;
			if ((cp[0] == '>') && (cp[-1] == '-'))
				break;
		}
		(void) Strncpy(dst, cp + 2, siz);
		result = 0;
	}
done:
	DisposeLineListContents(&fileList);
	return (result);
}	/* GetSymLinkInfo */
#endif	/* HAVE_SYMLINK */



int GetDir(GetOptionsPtr gopt, char *dName, char *rRoot, char *lRoot)
{
	LineList dirFiles;
	LinePtr dirFile;
	char *rd;	/* Remote directory path. */
	char *ld;	/* Local directory path. */
	char *rf;	/* Complete remote pathname for an item. */
	char *lf;	/* Complete local pathname for an item. */ 
	char *sl;	/* What a symlink points to. */
	char *iName;
	int fType;

	rd = NULL;
	ld = NULL;
	rf = NULL;
	lf = NULL;
	
	if ((rd = StrDup(rRoot)) == NULL)
		goto fail;
	if ((rd = PtrCatSlash(rd, dName)) == NULL)
		goto fail;

	if ((ld = StrDup(lRoot)) == NULL)
		goto fail;
	if ((ld = PtrCatSlash(ld, dName)) == NULL)
		goto fail;
	
	/* Create this directory on the local host first. */
	if (MkDirs(ld)) {
		EPrintF("Could not create directory '%s.'\n", ld);
		goto fail;
	}
	
	/* Get the names of all files and subdirs. */
	InitLineList(&dirFiles);
	GetFileList(&dirFiles, rd);

	/* Get all the files first. */
	for (dirFile = dirFiles.first; dirFile != NULL; dirFile = dirFile->next) {
		fType = (int) dirFile->line[0];
		if ((fType == '-') || (fType == 'l')) {
			iName = dirFile->line + 1;
			if ((rf = StrDup(rd)) == NULL)
				goto fail;
			if ((rf = PtrCatSlash(rf, iName)) == NULL)
				goto fail;
			if ((lf = StrDup(ld)) == NULL)
				goto fail;
			if ((lf = PtrCatSlash(lf, iName)) == NULL)
				goto fail;
			if (fType == '-') {
				gopt->rName = rf;
				gopt->lName = lf;
				DoGet(gopt);
			} else {
#ifdef HAVE_SYMLINK
				sl = (char *) malloc(SZ(512));
				if (sl != NULL) {
					if (GetSymLinkInfo(sl, SZ(512), rf) == 0)
						(void) symlink(sl, lf);
					free(sl);
				}
#endif	/* HAVE_SYMLINK */
			}
			free(rf);
			free(lf);
			rf = NULL;
			lf = NULL;
		}
	}
	
	/* Now get subdirectories. */
	for (dirFile = dirFiles.first; dirFile != NULL; dirFile = dirFile->next) {
		fType = (int) dirFile->line[0];
		if (fType == 'd') {
			iName = dirFile->line + 1;
			if (GetDir(gopt, iName, rd, ld) < 0)
				break;
		}
	}

	free(ld);
	free(rd);
	DisposeLineListContents(&dirFiles);
	return (0);
	
fail:
	if (rd != NULL)
		free(rd);
	if (ld != NULL)
		free(ld);
	if (rf != NULL)
		free(rf);
	if (lf != NULL)
		free(lf);
	return (-1);
}	/* GetDir */




int RemoteFileType(char *fName)
{
	LineList fileList;
	char *cp;
	int result;
	int i;

	result = 0;
	InitLineList(&fileList);
	ListToMemory(&fileList, "LIST", kListDirNamesOnlyMode, fName);
	if (fileList.first != NULL) {
		cp = fileList.first->line;
		/* Do a quick check and see if it looks like a unix ls line. */
		for (i=1; i<=3; i++)
			if ((cp[i] != 'r') && (cp[i] != 'w') && (cp[i] != 'x') && (cp[i] != '-'))
				goto done;
		result = (int) cp[0];
	}
done:
	DisposeLineListContents(&fileList);
	return (result);
}	/* RemoteFileType */





int DoGetWithGlobbingAndRecursion(GetOptionsPtr gopt)
{
	int err;
	LineList globFiles;
	LinePtr globFile;
	char *cp;
	int fType;
	int result;
	longstring rcwd;
		
	err = 0;
	InitLineList(&globFiles);
	RemoteGlob(&globFiles, gopt->rName, kListNoFlags);
	
	for (globFile = globFiles.first; globFile != NULL;
		globFile = globFile->next)
	{
		if (gopt->recursive) {
			fType = RemoteFileType(globFile->line);
			if (fType == 'd') {
				if ((cp = strrchr(globFile->line, '/')) != NULL) {
					/* If the user said something like
					 * "get -R /pub/a/b/c/d" we want to just write the
					 * contents of the 'd' as a subdirectory of the local
					 * directory, and not create ./pub, ./pub/a, etc.
					 */
					STRNCPY(rcwd, gRemoteCWD);
					*cp++ = '\0';
					if (DoChdir(globFile->line) == 0) {
						GetDir(gopt, cp, gRemoteCWD, gLocalCWD);
					}
					/* Restore the directory we were in before. */
					(void) DoChdir(rcwd);
				} else {
					/* Otherwise, the user gave a simple path, so it was
					 * something like "get -R pub"
					 */
					GetDir(gopt, globFile->line, gRemoteCWD, gLocalCWD);
				}
			} else if (fType == 'l') {
				EPrintF("Ignoring symbolic link '%s'\n",
					globFile->line);
			} else if (fType == '-') {
				goto regFile;
			}
		} else {
regFile:
			gopt->rName = globFile->line;
			gopt->lName = NULL;	/* Make it later. */
			result = DoGet(gopt);
			if (result < 0)
				err = -1;
		}
	}
	DisposeLineListContents(&globFiles);
	
	return (err);
}	/* DoGetWithGlobbingAndRecursion */




/* Fetch one or more remote files. */
int GetCmd(int argc, char **argv)
{
	int i, result, errs;
	GetOptions gopt;
	
	
	if (GetGetOptions(argc, argv, &gopt) == kUsageErr)
		return (kUsageErr);
		
	argv += gOptInd;
	argc -= gOptInd;
	errs = 0;

	if (gopt.noGlob || gopt.saveAs) {
		for (i=0; i<argc; i++) {
			gopt.rName = argv[i];
			if (gopt.saveAs) {
				if (++i < argc)
					gopt.lName = argv[i];	/* Use this name. */
				else
					return (kUsageErr);
			} else {
				gopt.lName = NULL;	/* Make it later. */
			}
			result = DoGet(&gopt);
			if (result < 0)
				--errs;
		}
	} else {
		for (i=0; i<argc; i++) {
			gopt.rName = argv[i];
			errs += DoGetWithGlobbingAndRecursion(&gopt);
		}
	}
	
	return (errs);
}	/* GetCmd */




/* We need to make something we can give to popen.  This is simple
 * if it is a plain file, but if they wanted to page a compressed
 * file we have to prepend the correct filter before the pager name.
 */
static
int MakePageCmdLine(char *cmd, size_t siz, char *remote_file)
{
	int useZCat;
	int useGZCat;
	int binaryPage;
	int len;
	
	useZCat = 0;
	useGZCat = 0;
	binaryPage = 0;
	
	len = (int) strlen(remote_file);

	if (len > 2) {
 		    if (remote_file[len - 2] == '.') {
			/* Check for .Z files. */
			if (remote_file[len-1] == 'Z')
				useZCat = 1;

			/* Check for .z (gzip) files. */
			if (remote_file[len - 1] == 'z')
				useGZCat = 1;
		}
	}

	if (len > 3) {
		/* Check for ".gz" (gzip) files. */
		if (STREQ(remote_file + len - 3, ".gz"))
			useGZCat = 1;
	}

	/* Run compressed remote files through zcat, then the pager.
	 * If GZCAT was defined, we also try paging gzipped files.
	 */	
	if (useGZCat) {
#ifdef GZCAT
		(void) Strncpy(cmd, GZCAT, siz);
		(void) Strncat(cmd, " | ", siz);
		(void) Strncat(cmd, gPager, siz);
#else
		PrintF("NcFTP wasn't configured to page gzipped files.\n");
#endif
	} else if (useZCat) {
#ifdef ZCAT
		(void) Strncpy(cmd, ZCAT, siz);
		(void) Strncat(cmd, " | ", siz);
		(void) Strncat(cmd, gPager, siz);
#else
#	ifdef GZCAT
		/* gzcat can do .Z's also. */
		(void) Strncpy(cmd, GZCAT, siz);
		(void) Strncat(cmd, " | ", siz);
		(void) Strncat(cmd, gPager, siz);
#	else
		PrintF("NcFTP wasn't configured to page compressed files.\n");
#	endif
#endif
	} else {
		(void) Strncpy(cmd, gPager, siz);
	}

	binaryPage = (useZCat || useGZCat);
	return (binaryPage);
}	/* MakePageCmdLine */




/* View a remote file through your pager. */
int DoPage(char *remoteName)
{
	FILE *fp;
	int result;
	longstring pageCmd;
	int binaryPage;
	XferSpecPtr xp;

	if (gPager[0] == '\0') {
		EPrintF("You haven't specified a program to use as a pager.\n");
		EPrintF("You can set this from the preferences screen (prefs command).\n");
		return -1;
	}

	binaryPage = MakePageCmdLine(pageCmd, sizeof(pageCmd), remoteName);
	DebugMsg("%s page: %s\n",
		binaryPage ? "Binary" : "Ascii",
		pageCmd
	);

	SaveScreen();
	fp = POpen(pageCmd, "w");
	if (fp == NULL) {
		Error(kDoPerror, "Could not run %s.\n", pageCmd);
		return -1;
	}

	xp = InitXferSpec();
	xp->netMode = kNetReading;
	xp->outStream = fileno(fp);
	
	/* This group is needed for the progress reporting and logging stuff.
	 * Otherwise, it isn't that important.
	 */
	xp->doReports = kNoReports;
	xp->localFileName = kLocalFileIsStdout;
	xp->remoteFileName = remoteName;

	if (!binaryPage) {
		/* Try to use text mode for paging, so newlines get converted. */
		result = AsciiGet(xp);
	} else {
		/* Must use binary, or else zcat will complain about corrupted
		 * input files, since we'd be converting carriage-returns.
		 */
		result = BinaryGet(xp);
	}
	DoneWithXferSpec(xp);
	(void) PClose(fp);
	RestoreScreen(1);
	return (result);
}	/* DoPage */




/* View one or more remote files through your pager. */
int PageCmd(int argc, char **argv)
{
	int i, result, errs;
	LineList globFiles;
	LinePtr globFile;

	for (i=1, errs=0; i<argc; i++) {
		InitLineList(&globFiles);
		RemoteGlob(&globFiles, argv[i], kListNoFlags);
		for (globFile = globFiles.first; globFile != NULL;
			globFile = globFile->next)
		{
			result = DoPage(globFile->line);
			if (result < 0)
				--errs;
		}
		DisposeLineListContents(&globFiles);
	}

	return (errs);
}	/* PageCmd */
