Skip to content
Snippets Groups Projects
bat_xfer.cpp 24.3 KiB
Newer Older
/* Synchronet batch file transfer functions */

/****************************************************************************
 * @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"

/****************************************************************************/
/* This is the batch menu section                                           */
/****************************************************************************/
void sbbs_t::batchmenu()
{
    char	str[129],tmp2[250],ch;
	int		sort = -1;
    int64_t	totalcdt,totalsize;
	str_list_t ini;
	str_list_t filenames;
	if(cfg.batxfer_mod[0] && !batchmenu_inside) {
		batchmenu_inside = true;
		exec_bin(cfg.batxfer_mod, &main_csi);
		batchmenu_inside = false;
		return;
	}

	if(batdn_total() < 1 && batup_total() < 1 && cfg.upload_dir==INVALID_DIR) {
		bputs(text[NoFilesInBatchQueue]);
	if(useron.misc&(RIP) && !(useron.misc&EXPERT))
	while(online && (cfg.upload_dir!=INVALID_DIR || batdn_total() || batup_total())) {
		if(!(useron.misc&(EXPERT|RIP))) {
			sys_status&=~SS_ABORT;
			if(lncntr) {
				CRLF;
				if(lncntr)          /* CRLF or SYNC can cause pause */
		bputs(text[BatchMenuPrompt]);
		SAFEPRINTF(keys,"CDLRU?\r%c", quit_key());
		if(ch==quit_key() || ch=='\r') {	/* Quit */
		switch(ch) {
			case '?':
				if(useron.misc&(EXPERT|RIP))
				if(batup_total() < 1) {
					bputs(text[UploadQueueIsEmpty]);
				} else {
					if(text[ClearUploadQueueQ][0]==0 || !noyes(text[ClearUploadQueueQ])) {
						if(clearbatul())
							bputs(text[UploadQueueCleared]);
				if(batdn_total() <1 ) {
					bputs(text[DownloadQueueIsEmpty]);
				} else {
					if(text[ClearDownloadQueueQ][0]==0 || !noyes(text[ClearDownloadQueueQ])) {
						if(clearbatdl())
							bputs(text[DownloadQueueCleared]); 
				break;
			case 'D':
				start_batch_download();
				break;
			case 'L':
				ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
				filenames = iniGetSectionList(ini, NULL);
				if(strListCount(filenames)) {
					sort = !noyes(text[SortAlphaQ]);
					if(sort)
						strListSortAlphaCase(filenames);
					bputs(text[UploadQueueLstHdr]);
					for(size_t i = 0; filenames[i] != NULL && !msgabort(); ++i) {
						const char* filename = filenames[i];
						char value[INI_MAX_VALUE_LEN];
						bprintf(text[UploadQueueLstFmt]
							,i+1, filename
							,iniGetString(ini, filename, "desc", text[NoDescription], value));
					}
				} else
					bputs(text[UploadQueueIsEmpty]);

				iniFreeStringList(filenames);
				iniFreeStringList(ini);

				totalsize = 0;
				totalcdt = 0;
				ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
				filenames = iniGetSectionList(ini, NULL);
				if(strListCount(filenames)) {
					if(sort == -1)
						sort = !noyes(text[SortAlphaQ]);
					if(sort)
						strListSortAlphaCase(filenames);
					bputs(text[DownloadQueueLstHdr]);
					for(size_t i = 0; filenames[i] && !msgabort(); ++i) {
						const char* filename = filenames[i];
						file_t f = {{}};
						if(!batch_file_load(&cfg, ini, filename, &f))
							continue;
						getfilesize(&cfg, &f);
						getfiletime(&cfg, &f);
						bprintf(text[DownloadQueueLstFmt],i+1
							,byte_estimate_to_str(f.cost, tmp, sizeof(tmp), 1, 1)
							,byte_estimate_to_str(f.size, str, sizeof(str), 1, 1)
								? sectostr((uint)(f.size/(uint64_t)cur_cps), tmp2)
								: "??:??:??"
							,datestr(f.time)
						);
						totalsize += f.size;
						totalcdt += f.cost;
					if(!msgabort())
						bprintf(text[DownloadQueueTotals]
							,byte_estimate_to_str(totalcdt, tmp, sizeof(tmp), 1, 1)
							,byte_estimate_to_str(totalsize, str, sizeof(tmp), 1, 1)
							,cur_cps ? sectostr((uint)(totalsize/(uint64_t)cur_cps), tmp2) : "??:??:??"); 
				} else
					bputs(text[DownloadQueueIsEmpty]);
				iniFreeStringList(filenames);
				iniFreeStringList(ini);
				if(sort == TRUE) {
					batch_list_sort(&cfg, useron.number, XFER_BATCH_UPLOAD);
					batch_list_sort(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
				}
				if((n = batup_total()) > 0) {
					bprintf(text[RemoveWhichFromUlQueue], n);
					if(getstr(str, MAX_FILENAME_LEN, K_NONE) > 0)
						if(!batch_file_remove(&cfg, useron.number, XFER_BATCH_UPLOAD, str) && (n = atoi(str)) > 0)
							batch_file_remove_n(&cfg, useron.number, XFER_BATCH_UPLOAD, n - 1);
				if((n = batdn_total()) > 0) {
					bprintf(text[RemoveWhichFromDlQueue], n);
					if(getstr(str, MAX_FILENAME_LEN, K_NONE) > 0)
						if(!batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, str) && (n = atoi(str)) > 0)
							batch_file_remove_n(&cfg, useron.number, XFER_BATCH_DOWNLOAD, n - 1);
				break;
		   case 'U':
				if(useron.rest&FLAG('U')) {
					bputs(text[R_Upload]);
bool sbbs_t::batch_upload()
{
	char str[129];
	char tmp[512];
	char ch;
	int i;

	if(batup_total() < 1 && !okay_to_upload(cfg.upload_dir)) {
		bputs(text[UploadQueueIsEmpty]);
		return false;
	}
	xfer_prot_menu(XFER_BATCH_UPLOAD);
	if(!create_batchup_lst())
		return false;
	sync();
	mnemonics(text[ProtocolOrQuit]);
	SAFEPRINTF(str,"%c",quit_key());
	for(i=0;i<cfg.total_prots;i++)
		if(cfg.prot[i]->batulcmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client)) {
			sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
			SAFECAT(str,tmp);
		}
	ch=(char)getkeys(str,0);
	if(ch==quit_key() || !online)
		return false;
	for(i=0;i<cfg.total_prots;i++)
		if(cfg.prot[i]->batulcmd[0] && cfg.prot[i]->mnemonic==ch
			&& chk_ar(cfg.prot[i]->ar,&useron,&client))
			break;
	if(i >= cfg.total_prots)
		return false;
	SAFEPRINTF(str,"%sBATCHUP.LST",cfg.node_dir);
	action=NODE_ULNG;
	sync();
	delfiles(cfg.temp_dir,ALLFILES);
	time_t elapsed = 0;
	protocol(cfg.prot[i],XFER_BATCH_UPLOAD,str,nulstr, /* cd: */true, /* autohang: */true, &elapsed);
	if(batup_total() < 1 && is_valid_dirnum(cfg.upload_dir) && !(cfg.dir[cfg.upload_dir]->misc&DIR_ULTIME))
		starttime+=elapsed;
	bool result = process_batch_upload_queue();
	delfiles(cfg.temp_dir,ALLFILES);
	autohangup();
	return result;
}

/****************************************************************************/
/* Download files from batch queue                                          */
/****************************************************************************/
bool sbbs_t::start_batch_download()
	char	ch;
	char	tmp[32];
	char	str[MAX_PATH+1];
	char 	path[MAX_PATH+1];

	if(useron.rest&FLAG('D')) {     /* Download restriction */
		bputs(text[R_Download]);

	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);

	size_t file_count = iniGetSectionCount(ini, NULL);
	if(file_count < 1) {
		bputs(text[DownloadQueueIsEmpty]);
		iniFreeStringList(ini);
	}
	str_list_t filenames = iniGetSectionList(ini, NULL);

	if(file_count == 1) {	// Only one file in the queue? Perform a non-batch (e.g. XMODEM) download
		const char* filename = filenames[0];
		bool result = false;
		if(!batch_file_load(&cfg, ini, filename, &f)) {
			bprintf(text[FileDoesNotExist], filename);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
		} else {
			uint reason = R_Download;
			if(!can_user_download(&cfg, f.dir, &useron, &client, &reason)) {
				bputs(text[reason]);
				batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
			}
			else if(f.cost > user_available_credits(&useron))
				bprintf(text[YouOnlyHaveNCredits]
					,u64toac(user_available_credits(&useron), tmp));
			else if(!(cfg.dir[f.dir]->misc&DIR_TFREE) && gettimetodl(&cfg, &f, cur_cps) > timeleft
				&& !dir_op(f.dir) && !(useron.exempt&FLAG('T')))
				bputs(text[NotEnoughTimeToDl]);
			else {
				putnode_downloading(getfilesize(&cfg, &f));
				result = sendfile(&f, useron.prot, /* autohang: */true);
			}
			if(result == true)
				batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, f.name);
		}
		iniFreeStringList(ini);
		iniFreeStringList(filenames);
		smb_freefilemem(&f);
		return result;
	}

	for(size_t i=0; filenames[i] != NULL; ++i) {
		const char* filename = filenames[i];
		progress(text[Scanning], i, file_count);
		if(!batch_file_load(&cfg, ini, filename, &f)) {
			bprintf(text[FileDoesNotExist], filename);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
			continue;
		uint reason = R_Download;
		if(!can_user_download(&cfg, f.dir, &useron, &client, &reason)) {
			bputs(text[reason]);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
		} else
			totalcdt += f.cost;
		smb_freefilemem(&f);
	bputs(text[Scanned]);
	if(totalcdt > user_available_credits(&useron)) {
		bprintf(text[YouOnlyHaveNCredits]
			,u64toac(user_available_credits(&useron),tmp));
		iniFreeStringList(ini);
		iniFreeStringList(filenames);
	int64_t totalsize = 0;
	int64_t totaltime = 0;
	for(size_t i=0; filenames[i] != NULL; ++i) {
		file_t f = {{}};
		if(!batch_file_get(&cfg, ini, filenames[i], &f))
			continue;
		totalsize += getfilesize(&cfg, &f);
		if(!(cfg.dir[f.dir]->misc&DIR_TFREE))
			totaltime += gettimetodl(&cfg, &f, cur_cps);
		SAFECAT(list, getfilepath(&cfg, &f, path));
		SAFECAT(list, " ");
		smb_freefilemem(&f);
	iniFreeStringList(ini);
	iniFreeStringList(filenames);

	if(!(useron.exempt&FLAG('T')) && !SYSOP && totaltime > (int64_t)timeleft) {
		bputs(text[NotEnoughTimeToDl]);
	i = protnum(useron.prot);
	if (i >= cfg.total_prots) {
		xfer_prot_menu(XFER_BATCH_DOWNLOAD);
		sync();
		mnemonics(text[ProtocolOrQuit]);
		SAFEPRINTF(str,"%c",quit_key());
		for(i=0;i<cfg.total_prots;i++)
			if(cfg.prot[i]->batdlcmd[0] && chk_ar(cfg.prot[i]->ar,&useron,&client)) {
				sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
				SAFECAT(str,tmp); 
			}
		ch=(char)getkeys(str,0);
		if(ch==quit_key() || sys_status&SS_ABORT)
			return(false);
		for(i=0;i<cfg.total_prots;i++)
			if(cfg.prot[i]->batdlcmd[0] && cfg.prot[i]->mnemonic==ch
				&& chk_ar(cfg.prot[i]->ar,&useron,&client))
				break;
	}
	if(i>=cfg.total_prots || !create_batchdn_lst((cfg.prot[i]->misc&PROT_NATIVE) ? true:false)) {
#if 0 // NFB-TODO: Download events
	list=NULL;
	for(i=0;i<batdn_total;i++) {
		curdirnum=batdn_dir[i]; 		/* for ARS */
		unpadfname(batdn_name[i],fname);
		if(cfg.dir[batdn_dir[i]]->seqdev) {
			lncntr=0;
			SAFEPRINTF2(path,"%s%s",cfg.temp_dir,fname);
				seqwait(cfg.dir[batdn_dir[i]]->seqdev);
				bprintf(text[RetrievingFile],fname);
					,cfg.dir[batdn_dir[i]]->path
				mv(str,path,1); /* copy the file to temp dir */
				if(getnodedat(cfg.node_num,&thisnode,true)==0) {
					thisnode.aux=40; /* clear the seq dev # */
					putnodedat(cfg.node_num,&thisnode);
				}
				,batdn_alt[i]>0 && batdn_alt[i]<=cfg.altpaths
				? cfg.altpath[batdn_alt[i]-1]
				: cfg.dir[batdn_dir[i]]->path
				,fname);
		if(list==NULL)
			list_len=0;
		else
			list_len=strlen(list)+1;	/* add one for ' ' */
		char* np;
		if((np=(char*)realloc(list,list_len+strlen(path)+1	/* add one for '\0'*/))==NULL) {
			free(list);
			errormsg(WHERE,ERR_ALLOC,"list",list_len+strlen(path));
		if(!list_len)
			strcpy(list,path);
		else {
			strcat(list," ");
			strcat(list,path);
		}
		for(j=0;j<cfg.total_dlevents;j++) {
			if(stricmp(cfg.dlevent[j]->ext,batdn_name[i]+9))
				continue;
rswindell's avatar
rswindell committed
			if(!chk_ar(cfg.dlevent[j]->ar,&useron,&client))
				continue;
			bputs(cfg.dlevent[j]->workstr);
			external(cmdstr(cfg.dlevent[j]->cmd,path,nulstr,NULL),EX_OUTL);
	SAFEPRINTF(str,"%sBATCHDN.LST",cfg.node_dir);
	putnode_downloading(totalsize);
	error=protocol(cfg.prot[xfrprot],XFER_BATCH_DOWNLOAD,str,list,/* cid: */false, /* autohang: */true, &elapsed);
	if(cfg.prot[xfrprot]->misc&PROT_DSZLOG || !error)
		batch_download(xfrprot);
		notdownloaded(totalsize, elapsed);
	else
		downloadedbytes(totalsize, elapsed);

/****************************************************************************/
/* Creates the file BATCHDN.LST in the node directory. Returns true if      */
/* everything goes okay, false if not.                                      */
/****************************************************************************/
bool sbbs_t::create_batchdn_lst(bool native)
	SAFEPRINTF(path, "%sBATCHDN.LST", cfg.node_dir);
	FILE* fp = fopen(path, "wb");
	if(fp == NULL) {
		errormsg(WHERE, ERR_OPEN, path);
		return false;
	bool result = false;
	uint64_t totalcdt = 0;
	const char* list_desc = "Batch Download File List";
	bprintf(text[CreatingFileList], list_desc);
	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
	for(size_t i = 0; filenames[i] != NULL; ++i) {
		const char* filename = filenames[i];
		file_t f = {};
		f.dir = batch_file_dir(&cfg, ini, filename);
		uint reason = R_Download;
		if(!can_user_download(&cfg, f.dir, &useron, &client, &reason)) {
			bputs(text[reason]);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
			continue;
		}
		if(!loadfile(&cfg, f.dir, filename, &f, file_detail_index)) {
			errormsg(WHERE, "loading file", filename, i);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
			continue;
		}
		getfilepath(&cfg, &f, path);
		if(!fexistcase(path)) {
			bprintf(text[FileDoesNotExist], path);
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
		}
		else if(totalcdt + f.cost > user_available_credits(&useron)) {
			char tmp[128];
			bprintf(text[YouOnlyHaveNCredits]
				,u64toac(user_available_credits(&useron), tmp));
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
		}
		else {
#ifdef _WIN32
			if(!native) {
				char tmp[MAX_PATH + 1];
				GetShortPathName(path, tmp, sizeof(tmp));
				SAFECOPY(path, tmp);
			}
#endif
			fprintf(fp, "%s\r\n", path);
			totalcdt += f.cost;
			result = true;
	fclose(fp);
	iniFreeStringList(ini);
	iniFreeStringList(filenames);
	bprintf(text[CreatedFileList], list_desc);
}

/****************************************************************************/
/* Creates the file BATCHUP.LST in the node directory. Returns true if      */
/* everything goes okay, false if not.                                      */
/* This list is not used by any protocols to date.                          */
/****************************************************************************/
bool sbbs_t::create_batchup_lst()
{
	SAFEPRINTF(path,"%sBATCHUP.LST",cfg.node_dir);
	FILE* fp = fopen(path, "wb");
	if(fp == NULL) {
		errormsg(WHERE, ERR_OPEN, path);
		return false;
	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
	for(size_t i = 0; filenames[i] != NULL; ++i) {
		const char* filename = filenames[i];
		file_t f = {{}};
		if(!batch_file_get(&cfg, ini, filename, &f))
			continue;
		fprintf(fp, "%s%s\r\n", cfg.dir[f.dir]->path, filename);
	fclose(fp);
	iniFreeStringList(ini);
	iniFreeStringList(filenames);
	return true;
}

/****************************************************************************/
/* Processes files that were supposed to be received in the batch queue     */
/****************************************************************************/
bool sbbs_t::process_batch_upload_queue()
	char src[MAX_PATH + 1];
	char dest[MAX_PATH + 1];

	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_UPLOAD);
	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
	for(size_t i = 0; filenames[i] != NULL; ++i) {
		const char* filename = filenames[i];
		int dir = batch_file_dir(&cfg, ini, filename);
		curdirnum = dir; /* for ARS */
		lncntr = 0; /* defeat pause */

		SAFEPRINTF2(src, "%s%s", cfg.temp_dir, filename);
		SAFEPRINTF2(dest, "%s%s", cfg.dir[dir]->path, filename);
		if(fexistcase(src) && fexistcase(dest)) { /* file's in two places */
			bprintf(text[FileAlreadyOnline], filename, lib_name(dir), dir_name(dir));
			remove(src);    /* this is the one received */
			continue;
			mv(src, dest, /* copy: */false);

		file_t f = {{}};
		if(!batch_file_get(&cfg, ini, filename, &f))
			continue;
			batch_file_remove(&cfg, useron.number, XFER_BATCH_DOWNLOAD, filename);
	iniFreeStringList(filenames);
	iniFreeStringList(ini);

	if(cfg.upload_dir == INVALID_DIR) // no blind upload dir specified
		return uploaded > 0 && batup_total() == 0;

	DIR* dir = opendir(cfg.temp_dir);
	DIRENT* dirent;
	while(dir!=NULL && (dirent=readdir(dir))!=NULL) {
		SAFEPRINTF2(src, "%s%s", cfg.temp_dir,dirent->d_name);
		if(isdir(src))
rswindell's avatar
rswindell committed
			continue;
		if(!checkfname(dirent->d_name)) {
			bprintf(text[BadFilename], dirent->d_name);
			continue;
		}
		SAFEPRINTF2(dest, "%s%s", cfg.dir[cfg.upload_dir]->path, dirent->d_name);
		if(fexistcase(dest)) {
			bprintf(text[FileAlreadyOnline], dirent->d_name
				,lib_name(cfg.upload_dir)
				,dir_name(cfg.upload_dir));
rswindell's avatar
rswindell committed
			continue;
		}
		if(mv(src, dest, /* copy: */false))
			progress(text[SearchingForDupes], x, usrlibs);
			for(y=0;y<usrdirs[x];y++)
				if(cfg.dir[usrdir[x][y]]->misc&DIR_DUPES
					&& findfile(&cfg,usrdir[x][y], dirent->d_name, NULL))
		bputs(text[SearchedForDupes]);
rswindell's avatar
rswindell committed
		if(x<usrlibs) {
			bprintf(text[FileAlreadyOnline], dirent->d_name
				,lib_name(usrdir[x][y]), dir_name(usrdir[x][y]));
			file_t f = {{}};
			f.dir = cfg.upload_dir;
			smb_hfield_str(&f, SMB_FILENAME, dirent->d_name);
	if(dir!=NULL)
		closedir(dir);

	return uploaded > 0 && batup_total() == 0;
}

/****************************************************************************/
/* Processes files that were supposed to be sent from the batch queue       */
/* xfrprot is -1 if downloading files from within QWK (no DSZLOG)           */
/****************************************************************************/
void sbbs_t::batch_download(int xfrprot)
{
	FILE* fp = batch_list_open(&cfg, useron.number, XFER_BATCH_DOWNLOAD, /* for_modify: */true);
	if(fp == NULL)
		return;
	str_list_t ini = iniReadFile(fp);
	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
	for(size_t i = 0; filenames[i] != NULL; ++i) {
		char* filename = filenames[i];
		lncntr=0;                               /* defeat pause */
		if(xfrprot==-1 || checkprotresult(cfg.prot[xfrprot], 0, filename)) {
			file_t f = {{}};
			if(!batch_file_load(&cfg, ini, filename, &f)) {
				errormsg(WHERE, "loading file", filename, i);
				continue;
			}
			iniRemoveSection(&ini, filename);
			if(cfg.dir[f.dir]->misc&DIR_TFREE && cur_cps)
				starttime+=f.size/(ulong)cur_cps;
			downloadedfile(&f);
			smb_freefilemem(&f);
	iniWriteFile(fp, ini);
	iniCloseFile(fp);
	iniFreeStringList(ini);
	iniFreeStringList(filenames);
}

/****************************************************************************/
/* Adds a list of files to the batch download queue 						*/
/****************************************************************************/
void sbbs_t::batch_add_list(char *list)
{
    char	str[1024];
	char	path[MAX_PATH + 1];
    FILE *	stream;
	file_t	f;

	if((stream=fnopen(&file,list,O_RDONLY))!=NULL) {
		bputs(text[SearchingAllLibs]);
		while(!feof(stream)) {
			checkline();
			if(!online)
				break;
			if(!fgets(str, sizeof(str) - 1,stream))
			for(i=j=k=0;i<usrlibs;i++) {
				for(j=0;j<usrdirs[i];j++,k++) {
					outchar('.');
					if(k && !(k%5))
						bputs("\b\b\b\b\b     \b\b\b\b\b");
					if(loadfile(&cfg, usrdir[i][j], str, &f, file_detail_normal)) {
						if(fexist(getfilepath(&cfg, &f, path)))
							addtobatdl(&f);
						else
							bprintf(text[FileIsNotOnline],f.name);
						smb_freefilemem(&f);
						break;
					}
				if(j<usrdirs[i])
		fclose(stream);
		remove(list);

/**************************************************************************/
/* Add file 'f' to batch download queue. Return 1 if successful, 0 if not */
/**************************************************************************/
bool sbbs_t::addtobatdl(file_t* f)
{
    char	str[256],tmp2[256];
	char 	tmp[512];
    uint	i;
	uint64_t	totalcost, totalsize;
	uint64_t	totaltime;
	if(!can_user_download(&cfg, f->dir, &useron, &client, &reason)) {
		bprintf(text[CantAddToQueue],f->name);
	if(getfilesize(&cfg, f) < 1) {
		bprintf(text[CantAddToQueue], f->name);
		bprintf(text[FileIsNotOnline], f->name);
		return false;

	str_list_t ini = batch_list_read(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
	if(iniSectionExists(ini, f->name)) {
		bprintf(text[FileAlreadyInQueue], f->name);
		iniFreeStringList(ini);
		return false;

	bool result = false;
	str_list_t filenames = iniGetSectionList(ini, /* prefix: */NULL);
	if(strListCount(filenames) >= cfg.max_batdn) {
		bprintf(text[CantAddToQueue] ,f->name);
		bputs(text[BatchDlQueueIsFull]);
	} else {
		totalcost = 0;
		totaltime = 0;
		totalsize = 0;
		for(i=0; filenames[i] != NULL; ++i) {
			const char* filename = filenames[i];
			file_t bf = {{}};
			if(!batch_file_load(&cfg, ini, filename, &bf))
				continue;
			totalcost += bf.cost;
			totalsize += getfilesize(&cfg, &bf);
			if(!(cfg.dir[bf.dir]->misc&DIR_TFREE) && cur_cps)
				totaltime += bf.size/(ulong)cur_cps;
			smb_freefilemem(&bf);
		}
		if(cfg.dir[f->dir]->misc&DIR_FREE)
			f->cost=0L;
		if(!is_download_free(&cfg,f->dir,&useron,&client))
			totalcost += f->cost;
		if(totalcost > user_available_credits(&useron)) {
			bprintf(text[CantAddToQueue],f->name);
			bprintf(text[YouOnlyHaveNCredits],u64toac(user_available_credits(&useron),tmp));
		} else {
			totalsize += f->size;
			if(!(cfg.dir[f->dir]->misc&DIR_TFREE) && cur_cps)
				totaltime += f->size/(ulong)cur_cps;
			if(!(useron.exempt&FLAG('T')) && totaltime > timeleft) {
				bprintf(text[CantAddToQueue],f->name);
				bputs(text[NotEnoughTimeToDl]);
			} else {
				if(batch_file_add(&cfg, useron.number, XFER_BATCH_DOWNLOAD, f)) {
					bprintf(text[FileAddedToBatDlQueue]
						,f->name, strListCount(filenames) + 1, cfg.max_batdn
						,byte_estimate_to_str(totalcost, tmp, sizeof(tmp), 1, 1)
						,byte_estimate_to_str(totalsize, tmp2, sizeof(tmp2), 1, 1)
						,sectostr((uint)(totalsize/MAX((uint64_t)cur_cps, 1)),str));
					result = true;
				}
			}
		}
	}
	iniFreeStringList(ini);
	iniFreeStringList(filenames);
	return result;
}

bool sbbs_t::clearbatdl(void)
{
	return batch_list_clear(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
}

bool sbbs_t::clearbatul(void)
{
	return batch_list_clear(&cfg, useron.number, XFER_BATCH_UPLOAD);
}

size_t sbbs_t::batdn_total(void)
{
	return batch_file_count(&cfg, useron.number, XFER_BATCH_DOWNLOAD);
}

size_t sbbs_t::batup_total(void)
{
	return batch_file_count(&cfg, useron.number, XFER_BATCH_UPLOAD);