/* Synchronet file upload-related routines */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This program is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU General Public License				*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "sbbs.h"
#include "sauce.h"
#include "filedat.h"

/****************************************************************************/
/****************************************************************************/
bool sbbs_t::uploadfile(file_t* f)
{
	char  path[MAX_PATH + 1];
	char  str[MAX_PATH + 1] = "";
	char  ext[LEN_EXTDESC + 1] = "";
	char  tmp[MAX_PATH + 1];
	int   i;
	off_t length;
	FILE* stream;

	curdirnum = f->dir;
	if (findfile(&cfg, f->dir, f->name, NULL)) {
		errormsg(WHERE, ERR_CHK, f->name, f->dir);
		return false;
	}
	getfilepath(&cfg, f, path);
	SAFEPRINTF2(tmp, "%s%s", cfg.temp_dir, getfname(path));
	if (!fexistcase(path) && fexistcase(tmp))
		mv(tmp, path, 0);
	if (!fexistcase(path)) {
		bprintf(text[FileNotReceived], f->name);
		safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (Not received)"
		              , f->name
		              , cfg.lib[cfg.dir[f->dir]->lib]->sname, cfg.dir[f->dir]->sname);
		logline(LOG_NOTICE, "U!", str);
		return false;
	}
	f->hdr.when_written.time = (uint32_t)fdate(path);
	for (i = 0; i < cfg.total_ftests; i++)
		if (file_type_match(f->name, cfg.ftest[i]->ext)) {
			if (!chk_ar(cfg.ftest[i]->ar, &useron, &client))
				continue;
			attr(LIGHTGRAY);
			bputs(cfg.ftest[i]->workstr);

			SAFEPRINTF(str, "%ssbbsfile.nam", cfg.node_dir);
			if ((stream = fopen(str, "w")) != NULL) {
				fprintf(stream, "%s", f->name);
				fclose(stream);
			}
			SAFEPRINTF(str, "%ssbbsfile.des", cfg.node_dir);
			if ((stream = fopen(str, "w")) != NULL) {
				if (f->desc != NULL)
					fprintf(stream, "%s", f->desc);
				fclose(stream);
			}
			// Note: str (%s) is path/to/sbbsfile.des (used to be the description itself)
			int result = external(cmdstr(cfg.ftest[i]->cmd, path, str, NULL, cfg.ftest[i]->ex_mode), cfg.ftest[i]->ex_mode | EX_OFFLINE);
			term->clearline();
			if (result != 0) {
				safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (%s error code %d)"
				              , f->name
				              , cfg.lib[cfg.dir[f->dir]->lib]->sname, cfg.dir[f->dir]->sname, cfg.ftest[i]->ext
				              , result);
				logline(LOG_NOTICE, "U!", str);
				bprintf(text[FileHadErrors], f->name, cfg.ftest[i]->ext);
				if (!useron_is_sysop() || yesno(text[DeleteFileQ]))
					remove(path);
				return false;
			}
			SAFEPRINTF(str, "%ssbbsfile.nam", cfg.node_dir);
			if ((stream = fopen(str, "r")) != NULL) {
				if (fgets(str, sizeof(str), stream)) {
					truncsp(str);
					smb_new_hfield_str(f, SMB_FILENAME, str);
					getfilepath(&cfg, f, path);
				}
				fclose(stream);
			}
			SAFEPRINTF(str, "%ssbbsfile.des", cfg.node_dir);
			if ((stream = fopen(str, "r")) != NULL) {
				if (fgets(str, sizeof(str), stream)) {
					truncsp(str);
					if (*str)
						smb_new_hfield_str(f, SMB_FILEDESC, str);
				}
				fclose(stream);
			}
		}

	if ((length = flength(path)) == 0L) {
		bprintf(text[FileZeroLength], f->name);
		remove(path);
		safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (Zero length)"
		              , f->name
		              , cfg.lib[cfg.dir[f->dir]->lib]->sname, cfg.dir[f->dir]->sname);
		logline(LOG_NOTICE, "U!", str);
		return false;
	}

	bputs(text[HashingFile]);
	/* Note: Hashes file *after* running upload-testers (which could modify file) */
	bool hashed = hashfile(&cfg, f);
	bputs(text[HashedFile]);
	if (hashed) {
		for (int i = 0, k = 0; i < usrlibs; i++) {
			progress(text[Scanning], i, usrlibs);
			for (int j = 0; j < usrdirs[i]; j++, k++) {
				if (cfg.dir[usrdir[i][j]]->misc & DIR_DUPES
				    && findfile(&cfg, usrdir[i][j], /* filename: */ NULL, f)) {
					bprintf(text[FileAlreadyOnline], f->name, lib_name(usrdir[i][j]), dir_name(usrdir[i][j]));
					if (!dir_op(f->dir)) {
						remove(path);
						safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (duplicate hash)"
						              , f->name
						              , cfg.lib[cfg.dir[f->dir]->lib]->sname, cfg.dir[f->dir]->sname);
						logline(LOG_NOTICE, "U!", str);
						return false;   /* File is in database for another dir */
					}
				}
			}
		}
		progress(text[Done], usrlibs, usrlibs);
	}

	if (cfg.dir[f->dir]->misc & DIR_DIZ) {
		lprintf(LOG_DEBUG, "Extracting DIZ from: %s", path);
		if (extract_diz(&cfg, f, /* diz_fnames: */ NULL, str, sizeof(str))) {
			struct sauce_charinfo sauce;
			lprintf(LOG_DEBUG, "Parsing DIZ: %s", str);

			char*                 lines = read_diz(str, &sauce);
			format_diz(lines, ext, sizeof(ext), sauce.width, sauce.ice_color);
			free_diz(lines);
			file_sauce_hfields(f, &sauce);

			if (f->desc == NULL || f->desc[0] == 0) {
				char desc[LEN_EXTDESC + 1];
				SAFECOPY(desc, (char*)ext);
				prep_file_desc(desc, desc);
				smb_new_hfield_str(f, SMB_FILEDESC, desc);
			}
			remove(str);
		} else
			lprintf(LOG_DEBUG, "DIZ does not exist in: %s", path);
	}

	if (!(cfg.dir[f->dir]->misc & DIR_NOSTAT)) {
		logon_ulb += length;  /* Update 'this call' stats */
		logon_uls++;
		inc_upload_stats(&cfg, 1, length);
	}
	if (cfg.dir[f->dir]->misc & DIR_AONLY)  /* Forced anonymous */
		f->hdr.attr |= MSG_ANONYMOUS;
	smb_hfield_bin(f, SMB_COST, length);
	smb_hfield_str(f, SENDER, useron.alias);
	bprintf(text[FileNBytesReceived], f->name, u64toac(length, tmp));
	if (!addfile(&cfg, f, ext, /* metadata: */ NULL, &client, NULL))
		return false;

	snprintf(str, sizeof(str), "uploaded %s (%" PRId64 " bytes) to %s %s"
	         , f->name
	         , length
	         , cfg.lib[cfg.dir[f->dir]->lib]->sname
	         , cfg.dir[f->dir]->sname);
	if (cfg.dir[f->dir]->upload_sem[0])
		ftouch(cmdstr(cfg.dir[f->dir]->upload_sem, nulstr, nulstr, NULL));
	logline("U+", str);
	/**************************/
	/* Update Uploader's Info */
	/**************************/
	user_uploaded(&cfg, &useron, 1, length);
	if (cfg.dir[f->dir]->up_pct && cfg.dir[f->dir]->misc & DIR_CDTUL) { /* credit for upload */
		if (cfg.dir[f->dir]->misc & DIR_CDTMIN && cur_cps)    /* Give min instead of cdt */
			useron.min = (uint32_t)adjustuserval(&cfg, useron.number, USER_MIN
			                                     , ((ulong)(length * (cfg.dir[f->dir]->up_pct / 100.0)) / cur_cps) / 60);
		else
			useron.cdt = adjustuserval(&cfg, useron.number, USER_CDT
			                           , (int64_t)(f->cost * (cfg.dir[f->dir]->up_pct / 100.0)));
	}
	mqtt_file_upload(mqtt, &useron, f->dir, f->name, length, &client);
	user_event(EVENT_UPLOAD);

	return true;
}

bool sbbs_t::okay_to_upload(int dirnum)
{
	char str[MAX_PATH + 1];
	char path[MAX_PATH + 1];

	if (!dirnum_is_valid(dirnum))
		return false;

	SAFECOPY(path, cfg.dir[dirnum]->path);

	if (!isdir(path)) {
		bprintf(text[DirectoryDoesNotExist], path);
		lprintf(LOG_ERR, "File directory does not exist: %s", path);
		return false;
	}

	/* get free disk space */
	int64_t space = getfreediskspace(path, 1);
	byte_estimate_to_str(space, str, sizeof(str), /* units: */ 1024, /* precision: */ 1);
	if (space < cfg.min_dspace) {
		bputs(text[LowDiskSpace]);
		lprintf(LOG_ERR, "Disk space is low: %s (%s bytes)", path, str);
		if (!dir_op(dirnum))
			return false;
	}
	bprintf(text[DiskNBytesFree], str);
	return true;
}

/****************************************************************************/
/* Uploads files                                                            */
/****************************************************************************/
bool sbbs_t::upload(int dirnum, const char* fname)
{
	char       descbeg[25] = {""}, descend[25] = {""};
	char       filename[MAX_FILENAME_LEN + 1]{};
	char       keys[256], ch, *p;
	char       str[MAX_PATH + 1];
	char       path[MAX_PATH + 1];
	char       tmp[512];
	int        i, j, k;
	file_t     f = {{}};
	str_list_t dest_user_list = NULL;

	/* Security Checks */
	if (useron.rest & FLAG('U')) {
		bputs(text[R_Upload]);
		return false;
	}
	if (dirnum == INVALID_DIR) {
		bputs(text[CantUploadHere]);
		return false;
	}
	if (!(useron.exempt & FLAG('U')) && !dir_op(dirnum)) {
		if (!chk_ar(cfg.dir[dirnum]->ul_ar, &useron, &client) || !chk_ar(cfg.lib[cfg.dir[dirnum]->lib]->ul_ar, &useron, &client)) {
			bputs(dirnum == cfg.user_dir ? text[CantUploadToUser] :
			      dirnum == cfg.sysop_dir ? text[CantUploadToSysop] : text[CantUploadHere]);
			return false;
		}
		if (cfg.dir[dirnum]->maxfiles && getfiles(&cfg, dirnum) >= cfg.dir[dirnum]->maxfiles) {
			bputs(dirnum == cfg.user_dir ? text[UserDirFull] : text[DirFull]);
			return false;
		}
	}

	if (sys_status & SS_EVENT && online == ON_REMOTE && !dir_op(dirnum))
		bprintf(text[UploadBeforeEvent], timeleft / 60);

	if (!okay_to_upload(dirnum))
		return false;

	f.dir = curdirnum = dirnum;
	if (fname == nullptr) {
		bputs(text[Filename]);
		if (getstr(filename, sizeof(filename) - 1, K_TRIM) < 1)
			return false;
		fname = filename;
	}
	if (!checkfname(fname)) {
		bprintf(text[BadFilename], fname);
		return false;
	}
	if (dirnum == cfg.sysop_dir)
		SAFEPRINTF(str, text[UploadToSysopDirQ], fname);
	else if (dirnum == cfg.user_dir)
		SAFEPRINTF(str, text[UploadToUserDirQ], fname);
	else
		SAFEPRINTF3(str, text[UploadToCurDirQ], fname, cfg.lib[cfg.dir[dirnum]->lib]->sname
		            , cfg.dir[dirnum]->sname);
	if (!yesno(str))
		return false;
	action = NODE_ULNG;
	SAFECOPY(f.file_idx.name, fname);
	getfilepath(&cfg, &f, path);
	if (fexistcase(path)) {   /* File is on disk */
		if (!dir_op(dirnum) && online != ON_LOCAL) {        /* local users or sysops */
			bprintf(text[FileAlreadyThere], fname);
			return false;
		}
		if (!yesno(text[FileOnDiskAddQ]))
			return false;
	}
	char* ext = getfext(fname);
	SAFECOPY(str, cfg.dir[dirnum]->exts);
	j = strlen(str);
	for (i = 0; i < j; i += ch + 1) { /* Check extension of upload with allowable exts */
		p = strchr(str + i, ',');
		if (p != NULL)
			*p = 0;
		ch = (char)strlen(str + i);
		if (ext != NULL && stricmp(ext + 1, str + i) == 0)
			break;
	}
	if (j && i >= j) {
		bputs(text[TheseFileExtsOnly]);
		bputs(cfg.dir[dirnum]->exts);
		term->newline();
		if (!dir_op(dirnum))
			return false;
	}
	bputs(text[SearchingForDupes]);
	bool found = findfile(&cfg, dirnum, fname, NULL);
	bputs(text[SearchedForDupes]);
	if (found) {
		bprintf(text[FileAlreadyOnline], fname, lib_name(dirnum), dir_name(dirnum));
		return false;   /* File is already in database */
	}
	for (i = k = 0; i < usrlibs; i++) {
		progress(text[SearchingForDupes], i, usrlibs);
		for (j = 0; j < usrdirs[i]; j++, k++) {
			if (usrdir[i][j] == dirnum)
				continue;   /* we already checked this dir */
			if (cfg.dir[usrdir[i][j]]->misc & DIR_DUPES
			    && findfile(&cfg, usrdir[i][j], fname, NULL)) {
				bputs(text[SearchedForDupes]);
				bprintf(text[FileAlreadyOnline], fname, lib_name(usrdir[i][j]), dir_name(usrdir[i][j]));
				if (!dir_op(dirnum))
					return false;  /* File is in database for another dir */
			}
			if (msgabort(true)) {
				bputs(text[SearchedForDupes]);
				return false;
			}
		}
	}
	bputs(text[SearchedForDupes]);
	if (cfg.dir[dirnum]->misc & DIR_RATE) {
		sync();
		bputs(text[RateThisFile]);
		ch = getkey(K_ALPHA);
		if (!IS_ALPHA(ch) || sys_status & SS_ABORT)
			return false;
		term->newline();
		SAFEPRINTF(descbeg, text[Rated], toupper(ch));
	}
	if (cfg.dir[dirnum]->misc & DIR_ULDATE) {
		now = time(NULL);
		if (descbeg[0])
			strcat(descbeg, " ");
		SAFEPRINTF(str, "%s  ", datestr(now, tmp));
		strcat(descbeg, str);
	}
	if (cfg.dir[dirnum]->misc & DIR_MULT) {
		sync();
		if (!noyes(text[MultipleDiskQ])) {
			bputs(text[HowManyDisksTotal]);
			if ((int)(i = getnum(99)) < 2)
				return false;
			bputs(text[NumberOfFile]);
			if ((int)(j = getnum(i)) < 1)
				return false;
			if (j == 1)
				upload_lastdesc[0] = 0;
			if (i > 9)
				SAFEPRINTF2(descend, text[FileOneOfTen], j, i);
			else
				SAFEPRINTF2(descend, text[FileOneOfTwo], j, i);
		} else
			upload_lastdesc[0] = 0;
	}
	else
		upload_lastdesc[0] = 0;
	if (dirnum == cfg.user_dir) {  /* User to User transfer */
		bputs(text[EnterAfterLastDestUser]);
		while (dir_op(dirnum) || strListCount(dest_user_list) < cfg.max_userxfer) {
			bputs(text[SendFileToUser]);
			if (!getstr(str, LEN_ALIAS, cfg.uq & UQ_NOUPRLWR ? K_NONE:K_UPRLWR))
				break;
			user_t user;
			if ((user.number = finduser(str)) != 0) {
				if (!dir_op(dirnum) && user.number == useron.number) {
					bputs(text[CantSendYourselfFiles]);
					continue;
				}
				char usernum[16];
				SAFEPRINTF(usernum, "%u", user.number);
				if (strListFind(dest_user_list, usernum, /* case-sensitive: */ true) >= 0) {
					bputs(text[DuplicateUser]);
					continue;
				}
				getuserdat(&cfg, &user);
				if (!user_can_download(&cfg, cfg.user_dir, &user, /* client: */ NULL, /* reason: */ NULL)) {
					bprintf(text[UserWontBeAbleToDl], user.alias);
				} else {
					bprintf(text[UserAddedToDestList], user.alias, usernum);
					strListPush(&dest_user_list, usernum);
				}
			}
			else {
				term->newline();
			}
		}
		if (strListCount(dest_user_list) < 1)
			return false;
	}

	char fdesc[LEN_FDESC + 1] = "";
	bputs(text[EnterDescNow]);
	i = LEN_FDESC - (strlen(descbeg) + strlen(descend));
	getstr(upload_lastdesc, i, K_LINE | K_EDIT | K_AUTODEL | K_TRIM);
	if (sys_status & SS_ABORT) {
		strListFree(&dest_user_list);
		return false;
	}
	if (descend[0])      /* end of desc specified, so pad desc with spaces */
		safe_snprintf(fdesc, sizeof(fdesc), "%s%-*s%s", descbeg, i, upload_lastdesc, descend);
	else                /* no end specified, so string ends at desc end */
		safe_snprintf(fdesc, sizeof(fdesc), "%s%s", descbeg, upload_lastdesc);

	char tags[64] = "";
	if ((cfg.dir[dirnum]->misc & DIR_FILETAGS) && (text[TagFileQ][0] == 0 || !noyes(text[TagFileQ]))) {
		bputs(text[TagFilePrompt]);
		getstr(tags, sizeof(tags) - 1, K_EDIT | K_LINE | K_TRIM);
	}

	if (cfg.dir[dirnum]->misc & DIR_ANON && !(cfg.dir[dirnum]->misc & DIR_AONLY)
	    && (dir_op(dirnum) || useron.exempt & FLAG('A'))) {
		if (!noyes(text[AnonymousQ]))
			f.hdr.attr |= MSG_ANONYMOUS;
	}

	bool result = false;
	smb_hfield_str(&f, SMB_FILENAME, fname);
	smb_hfield_str(&f, SMB_FILEDESC, fdesc);
	if (strListCount(dest_user_list) > 0)
		smb_hfield_str(&f, RECIPIENTLIST, strListCombine(dest_user_list, tmp, sizeof(tmp), ","));
	if (tags[0])
		smb_hfield_str(&f, SMB_TAGS, tags);
	if (fexistcase(path)) {   /* File is on disk */
		result = uploadfile(&f);
	} else {
		xfer_prot_menu(XFER_UPLOAD, &useron, keys, sizeof keys);
		SAFECAT(keys, quit_key(str));
		sync();
		if (dirnum == cfg.user_dir || !cfg.max_batup)  /* no batch user to user xfers */
			mnemonics(text[ProtocolOrQuit]);
		else {
			mnemonics(text[ProtocolBatchOrQuit]);
			SAFECAT(keys, "B");
		}
		ch = (char)getkeys(keys, 0);
		if (ch == quit_key() || (sys_status & SS_ABORT))
			result = false;
		else if (ch == 'B') {
			if (batup_total() >= cfg.max_batup)
				bputs(text[BatchUlQueueIsFull]);
			else if (batch_file_exists(&cfg, useron.number, XFER_BATCH_UPLOAD, f.name))
				bprintf(text[FileAlreadyInQueue], fname);
			else if (batch_file_add(&cfg, useron.number, XFER_BATCH_UPLOAD, &f)) {
				bprintf(text[FileAddedToUlQueue]
				        , fname, batup_total(), cfg.max_batup);
				result = true;
			}
		} else {
			i = protnum(ch, XFER_UPLOAD);
			if (i < cfg.total_prots) {
				time_t elapsed = 0;
				protocol(cfg.prot[i], XFER_UPLOAD, path, nulstr, /* cd: */ true, /* autohang: */ true, &elapsed);
				if (!(cfg.dir[dirnum]->misc & DIR_ULTIME)) /* Don't deduct upload time */
					starttime += elapsed;
				result = uploadfile(&f);
				autohangup();
			}
		}
	}
	smb_freefilemem(&f);
	strListFree(&dest_user_list);
	return result;
}

/****************************************************************************/
/* Checks directory for 'dir' and prompts user to enter description for     */
/* the files that aren't in the database.                                   */
/* Returns 1 if the user aborted, 0 if not.                                 */
/****************************************************************************/
bool sbbs_t::bulkupload(int dirnum)
{
	char    str[MAX_PATH + 1];
	char    path[MAX_PATH + 1];
	char    desc[LEN_FDESC + 1];
	smb_t   smb;
	file_t  f;
	DIR*    dir;
	DIRENT* dirent;

	memset(&f, 0, sizeof(f));
	f.dir = dirnum;
	bprintf(text[BulkUpload], cfg.lib[cfg.dir[dirnum]->lib]->sname, cfg.dir[dirnum]->sname);
	SAFECOPY(path, cfg.dir[dirnum]->path);

	int result = smb_open_dir(&cfg, &smb, dirnum);
	if (result != SMB_SUCCESS) {
		errormsg(WHERE, ERR_OPEN, smb.file, result, smb.last_error);
		return false;
	}
	action = NODE_ULNG;
	sync();
	str_list_t list = loadfilenames(&smb, ALLFILES, /* time_t */ 0, FILE_SORT_NATURAL, NULL);
	smb_close(&smb);
	dir = opendir(path);
	while (dir != NULL && (dirent = readdir(dir)) != NULL && !msgabort()) {
		char fname[SMB_FILEIDX_NAMELEN + 1];
		SAFEPRINTF2(str, "%s%s", path, dirent->d_name);
		if (isdir(str))
			continue;
#ifdef _WIN32
		/* Skip hidden/system files on Win32 */
		if (getfattr(str) & (_A_HIDDEN | _A_SYSTEM))
			continue;
#endif
		smb_fileidxname(dirent->d_name, fname, sizeof(fname));
		if (strListFind(list, fname, /* case-sensitive: */ FALSE) < 0) {
			smb_freemsgmem(&f);
			smb_hfield_str(&f, SMB_FILENAME, dirent->d_name);
			char tmp[64];
			bprintf(text[BulkUploadDescPrompt], format_filename(f.name, fname, 12, /* pad: */ FALSE), byte_estimate_to_str(flength(str), tmp, sizeof tmp, 1, 1));
			if (strcmp(f.name, fname) != 0)
				SAFECOPY(desc, f.name);
			else
				desc[0] = 0;
			getstr(desc, LEN_FDESC, K_LINE | K_EDIT | K_AUTODEL);
			if (sys_status & SS_ABORT)
				break;
			if (strcmp(desc, "-") == 0) /* don't add this file */
				continue;
			smb_hfield_str(&f, SMB_FILEDESC, desc);
			uploadfile(&f);
		}
	}
	if (dir != NULL)
		closedir(dir);
	strListFree(&list);
	smb_freemsgmem(&f);
	if (sys_status & SS_ABORT)
		return true;
	return false;
}

bool sbbs_t::recvfile(char *fname, char prot, bool autohang)
{
	char keys[128];
	char str[128];
	char ch;
	int  i;
	bool result = false;

	if (prot)
		ch = toupper(prot);
	else {
		xfer_prot_menu(XFER_UPLOAD, &useron, keys, sizeof keys);
		SAFECAT(keys, quit_key(str));
		mnemonics(text[ProtocolOrQuit]);
		ch = (char)getkeys(keys, 0);

		if (ch == quit_key() || sys_status & SS_ABORT)
			return false;
	}
	i = protnum(ch, XFER_UPLOAD);
	if (i < cfg.total_prots) {
		if (protocol(cfg.prot[i], XFER_UPLOAD, fname, fname, true, autohang) == 0)
			result = true;
		autohangup();
	}

	return result;
}