diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index 915eed0a4a8ef95e8246aae5f98ec7e606ab8854..46ba93021ae5e45647a266e5eb3d28c5962dea87 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -26,6 +26,7 @@
 #include "cp437defs.h"
 #include "ver.h"
 #include "petdefs.h"
+#include "filedat.h"
 
 #if defined(_WINSOCKAPI_)
 	extern WSADATA WSAData;
@@ -2268,6 +2269,20 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 				safe_snprintf(str, maxlen, "%u", getusrdir(current_file->dir));
 				return str;
 			}
+			if(strcmp(sp, "FILE_FTP_PATH") == 0) {
+				getfilevpath(&cfg, current_file, str, maxlen);
+				return str;
+			}
+			if(strcmp(sp, "FILE_WEB_PATH") == 0) {
+				char* p = "";
+				if(cfg.dir[current_file->dir]->vpath[0] == '\0') {
+					p = startup->web_file_vpath_prefix;
+					if(*p == '/')
+						++p;
+				}
+				safe_snprintf(str, maxlen, "%s%s", p, getfilevpath(&cfg, current_file, tmp, sizeof tmp));
+				return str;
+			}
 		}
 		if(strcmp(sp, "FILE_NAME") == 0)
 			return current_file->name;
diff --git a/src/sbbs3/filedat.c b/src/sbbs3/filedat.c
index 83051b759a9962cf2319543c545119d11cdb68c9..c345f3a0b9e313b4faafcc42e8ae54b8fc0f752e 100644
--- a/src/sbbs3/filedat.c
+++ b/src/sbbs3/filedat.c
@@ -634,13 +634,14 @@ char* getfilepath(scfg_t* cfg, file_t* f, char* path)
 	return path;
 }
 
-char* getfilevpath(scfg_t* cfg, file_t* f, char* path)
+char* getfilevpath(scfg_t* cfg, file_t* f, char* path, size_t size)
 {
+	char vpath[LEN_DIR + 1];
 	const char* name = f->name == NULL ? f->file_idx.name : f->name;
 	if(!is_valid_dirnum(cfg, f->dir))
 		return "";
-	safe_snprintf(path, MAX_PATH, "%s/%s/%s"
-		,cfg->lib[cfg->dir[f->dir]->lib]->vdir, cfg->dir[f->dir]->vdir, name);
+	safe_snprintf(path, size, "%s/%s"
+		,dir_vpath(cfg, cfg->dir[f->dir], vpath, sizeof vpath), name);
 	return path;
 }
 
diff --git a/src/sbbs3/filedat.h b/src/sbbs3/filedat.h
index 66cd1761b0f10623570380333fca8098d18e5c0c..e129ec3574abd29329000f18599a72c19769d29a 100644
--- a/src/sbbs3/filedat.h
+++ b/src/sbbs3/filedat.h
@@ -47,7 +47,7 @@ DLLEXPORT str_list_t	loadfilenames(smb_t*, const char* filespec, time_t t, enum
 DLLEXPORT void			sortfilenames(str_list_t, size_t count, enum file_sort);
 DLLEXPORT bool			updatefile(scfg_t*, file_t*);
 DLLEXPORT char*			getfilepath(scfg_t*, file_t*, char* path);
-DLLEXPORT char*			getfilevpath(scfg_t*, file_t*, char* path);
+DLLEXPORT char*			getfilevpath(scfg_t*, file_t*, char* path, size_t);
 DLLEXPORT off_t			getfilesize(scfg_t*, file_t*);
 DLLEXPORT time_t		getfiletime(scfg_t*, file_t*);
 DLLEXPORT ulong			gettimetodl(scfg_t*, file_t*, uint rate_cps);
diff --git a/src/sbbs3/js_filebase.c b/src/sbbs3/js_filebase.c
index d8d9e1b5d90ea0400af197f486f47a48848cd47b..a45bd6a14fd170ac6b5b1457b94a574d45fea4f7 100644
--- a/src/sbbs3/js_filebase.c
+++ b/src/sbbs3/js_filebase.c
@@ -181,7 +181,7 @@ set_file_properties(JSContext *cx, JSObject* obj, file_t* f, enum file_detail de
 		|| !JS_DefineProperty(cx, obj, "name", STRING_TO_JSVAL(js_str), NULL, NULL, flags))
 		return false;
 
-	if((js_str = JS_NewStringCopyZ(cx, getfilevpath(scfg, f, path))) == NULL
+	if((js_str = JS_NewStringCopyZ(cx, getfilevpath(scfg, f, path, sizeof path))) == NULL
 		|| !JS_DefineProperty(cx, obj, "vpath", STRING_TO_JSVAL(js_str), NULL, NULL, flags | JSPROP_READONLY))
 	return false;
 
diff --git a/src/sbbs3/scfg/scfgxfr2.c b/src/sbbs3/scfg/scfgxfr2.c
index f09df723cbfcfb7e93c803cc4478aae24bba771d..c3476473252349418e015a12d9b028137f2c764f 100644
--- a/src/sbbs3/scfg/scfgxfr2.c
+++ b/src/sbbs3/scfg/scfgxfr2.c
@@ -1764,8 +1764,8 @@ void dir_cfg(int libnum)
 	char* dir_transfer_path_help =
 		"`Transfer File Path:`\n"
 		"\n"
-		"This is the default storage path for files uploaded-to and available for\n"
-		"download-from this directory.\n"
+		"This is the physical storage path for files uploaded-to and available\n"
+		"for download-from this directory.\n"
 		"\n"
 		"If this setting is blank, the internal-code (lower-cased) is used as the\n"
 		"default directory name.\n"
@@ -1936,6 +1936,13 @@ void dir_cfg(int libnum)
 			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s%s","Internal Code"
 				,cfg.lib[cfg.dir[i]->lib]->code_prefix, cfg.dir[i]->code_suffix);
 			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","FidoNet Area Tag",area_tag);
+			if(cfg.dir[i]->vpath[0] != '\0')
+				SAFECOPY(str, cfg.dir[i]->vpath);
+			else {
+				init_vdir(&cfg, cfg.dir[i]);
+				SAFEPRINTF(str, "[%s]", dir_vpath(&cfg, cfg.dir[i], path, sizeof path));
+			}
+			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Virtual File Path", str);
 			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Access Requirements"
 				,cfg.dir[i]->arstr);
 			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Upload Requirements"
@@ -1964,8 +1971,7 @@ void dir_cfg(int libnum)
 				SAFECOPY(str, path);
 			else
 				SAFEPRINTF(str, "[%s]", path);
-			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Transfer File Path"
-				,str);
+			snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Transfer File Path", str);
 			if(cfg.dir[i]->maxfiles)
 				sprintf(str, "%u", cfg.dir[i]->maxfiles);
 			else
@@ -2061,83 +2067,104 @@ void dir_cfg(int libnum)
 						SAFECOPY(cfg.dir[i]->area_tag, str);
 					break;
 				case 4:
+					uifc.helpbuf=
+						"`Virtual File Path:`\n"
+						"\n"
+						"This is the path, without leading or trailing slashes, where the files\n"
+						"in this directory will appear for users of the Synchronet FTP and Web\n"
+						"servers.\n"
+						"\n"
+						"If no path is specified here, then one is automatically generated and\n"
+						"displayed here within `[`brackets`]` for your information.\n"
+						"\n"
+						"Automatically generated virtual paths use the `Virtual Sub-directories`\n"
+						"naming convention as selected for the parent File Library.\n"
+						"\n"
+						"Setting this path implies that the sysop has `also` set corresponding\n"
+						"FTP and Web Server directory aliases (i.e. in their `ftpalias.cfg` and\n"
+						"`web_alias.ini` files).\n"
+						;
+					uifc.input(WIN_L2R|WIN_SAV,0,17,"Virtual File Path"
+						,cfg.dir[i]->vpath, sizeof(cfg.dir[i]->vpath) - 1, K_EDIT | K_NOSPACE);
+					break;
+				case 5:
 					sprintf(str,"%s Access",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->arstr);
 					break;
-				case 5:
+				case 6:
 					sprintf(str,"%s Upload",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->ul_arstr);
 					break;
-				case 6:
+				case 7:
 					sprintf(str,"%s Download",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->dl_arstr);
 					break;
-				case 7:
+				case 8:
 					sprintf(str,"%s Operator",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->op_arstr);
 					break;
-				case 8:
+				case 9:
 					sprintf(str,"%s Exemption",cfg.dir[i]->sname);
 					getar(str,cfg.dir[i]->ex_arstr);
 					break;
-				case 9:
+				case 10:
 					uifc.helpbuf = dir_transfer_path_help;
 					uifc.input(WIN_L2R|WIN_SAV,0,17,"Transfer File Path"
 						,cfg.dir[i]->path,sizeof(cfg.dir[i]->path)-1,K_EDIT);
 					break;
-				case 10:
+				case 11:
 					uifc.helpbuf = max_files_help;
 					sprintf(str,"%u",cfg.dir[i]->maxfiles);
 					uifc.input(WIN_L2R|WIN_SAV,0,17,"Maximum Number of Files (0=Unlimited)"
 						,str,5,K_EDIT|K_NUMBER);
 					cfg.dir[i]->maxfiles=atoi(str);
 					break;
-				case 11:
+				case 12:
 					sprintf(str,"%u",cfg.dir[i]->maxage);
 					uifc.helpbuf = max_age_help;
 					uifc.input(WIN_MID|WIN_SAV,0,17,"Maximum Age of Files (in days)"
 						,str,5,K_EDIT|K_NUMBER);
 					cfg.dir[i]->maxage=atoi(str);
 					break;
-				case 12:
+				case 13:
 					uifc.helpbuf = up_pct_help;
 					uifc.input(WIN_MID|WIN_SAV,0,0
 						,"Percentage of Credits to Credit Uploader on Upload"
 						,ultoa(cfg.dir[i]->up_pct,tmp,10),4,K_EDIT|K_NUMBER);
 					cfg.dir[i]->up_pct=atoi(tmp);
 					break;
-				case 13:
+				case 14:
 					uifc.helpbuf = dn_pct_help;
 					uifc.input(WIN_MID|WIN_SAV,0,0
 						,"Percentage of Credits to Credit Uploader on Download"
 						,ultoa(cfg.dir[i]->dn_pct,tmp,10),4,K_EDIT|K_NUMBER);
 					cfg.dir[i]->dn_pct=atoi(tmp);
 					break;
-				case 14:
+				case 15:
 					dir_toggle_options(cfg.dir[i]);
 					break;
-			case 15:
-				while(1) {
-					n=0;
-					snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Extensions Allowed"
-						,cfg.dir[i]->exts);
-					if(!cfg.dir[i]->data_dir[0])
-						sprintf(str,"[%sdirs/]",cfg.data_dir);
-					else
-						strcpy(str,cfg.dir[i]->data_dir);
-					snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Data Directory"
-						,str);
-					snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Upload Semaphore File"
-						,cfg.dir[i]->upload_sem);
-					snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Sort Value and Direction"
-						,file_sort_desc[cfg.dir[i]->sort]);
-					snprintf(opt[n++], MAX_OPLN, "%-27.27sNow %u / Was %u","Directory Index", i, cfg.dir[i]->dirnum);
-					opt[n][0]=0;
-					uifc.helpbuf=
-						"`Directory Advanced Options:`\n"
-						"\n"
-						"This is the advanced options menu for the selected file directory.\n"
-					;
+				case 16:
+					while(1) {
+						n=0;
+						snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Extensions Allowed"
+							,cfg.dir[i]->exts);
+						if(!cfg.dir[i]->data_dir[0])
+							sprintf(str,"[%sdirs/]",cfg.data_dir);
+						else
+							strcpy(str,cfg.dir[i]->data_dir);
+						snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Data Directory"
+							,str);
+						snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Upload Semaphore File"
+							,cfg.dir[i]->upload_sem);
+						snprintf(opt[n++], MAX_OPLN, "%-27.27s%s","Sort Value and Direction"
+							,file_sort_desc[cfg.dir[i]->sort]);
+						snprintf(opt[n++], MAX_OPLN, "%-27.27sNow %u / Was %u","Directory Index", i, cfg.dir[i]->dirnum);
+						opt[n][0]=0;
+						uifc.helpbuf=
+							"`Directory Advanced Options:`\n"
+							"\n"
+							"This is the advanced options menu for the selected file directory.\n"
+							;
 						n=uifc.list(WIN_ACT|WIN_SAV|WIN_RHT|WIN_BOT,3,4,66,&adv_dflt,0
 							,"Advanced Options",opt);
 						if(n==-1)
@@ -2174,7 +2201,7 @@ void dir_cfg(int libnum)
 								break;
 						}
 					}
-				break;
+					break;
 			}
 		}
 	}
diff --git a/src/sbbs3/scfgdefs.h b/src/sbbs3/scfgdefs.h
index c4df44680a5e8bdec05c51142e58b26fc33e0b06..b220d56142be26c0478160470b9f03939ce2f3aa 100644
--- a/src/sbbs3/scfgdefs.h
+++ b/src/sbbs3/scfgdefs.h
@@ -74,6 +74,7 @@ typedef struct {							/* Transfer Directory Info */
 	char		code[LEN_EXTCODE+1];		/* Internal code (with optional lib prefix) */
 	char		code_suffix[LEN_CODE+1];	/* Eight character code suffix */
 	char		vdir[LEN_SLNAME+1];			/* Virtual Directory name */
+	char		vpath[LEN_DIR+1];			/* Optional vpath (e.g. alias) */
 	char		area_tag[FIDO_AREATAG_LEN+1];
 	char		lname[LEN_SLNAME+1],		/* Long name - used for listing */
 				sname[LEN_SSNAME+1],		/* Short name - used for prompts */
@@ -113,7 +114,7 @@ typedef struct {							/* Transfer Library Information */
 				ex_arstr[LEN_ARSTR+1], 		/* Exemption Requirements (credits) */
 				op_arstr[LEN_ARSTR+1], 		/* Operator Requirements */
 				code_prefix[LEN_CODE+1],	/* Prefix for internal code */
-				parent_path[48];			/* Parent for dir paths */
+				parent_path[LEN_DIR+1];		/* Parent for dir paths */
 	uchar		ar[LEN_ARSTR+1],
 				ul_ar[LEN_ARSTR+1],
 				dl_ar[LEN_ARSTR+1],
diff --git a/src/sbbs3/scfglib.h b/src/sbbs3/scfglib.h
index 51f02132f89c49fb1e353c645719170eb227a851..25dab3dba37fe94f2ca34a931f0bab08cc2c32dd 100644
--- a/src/sbbs3/scfglib.h
+++ b/src/sbbs3/scfglib.h
@@ -81,6 +81,7 @@ DLLEXPORT bool	is_valid_xtrnsec(scfg_t*, int);
 DLLEXPORT char *	sub_newsgroup_name(scfg_t*, sub_t*, char*, size_t);
 DLLEXPORT char *	sub_area_tag(scfg_t*, sub_t*, char*, size_t);
 DLLEXPORT char *	dir_area_tag(scfg_t*, dir_t*, char*, size_t);
+DLLEXPORT char *	dir_vpath(scfg_t*, dir_t* dir, char* path, size_t);
 
 uint nearest_sysfaddr_index(scfg_t*, faddr_t*);
 faddr_t* nearest_sysfaddr(scfg_t*, faddr_t*);
diff --git a/src/sbbs3/scfglib1.c b/src/sbbs3/scfglib1.c
index 1e2e4bf25f43e860ef91503cec3a113c439027fe..981865d0ceaca78097c16bb923d1fb5a12a60787 100644
--- a/src/sbbs3/scfglib1.c
+++ b/src/sbbs3/scfglib1.c
@@ -976,3 +976,16 @@ char* dir_area_tag(scfg_t* cfg, dir_t* dir, char* str, size_t size)
 	strupr(str);
 	return str;
 }
+
+/****************************************************************************/
+/* Returns virtual path for a file directory, without trailing slash		*/
+/****************************************************************************/
+char* dir_vpath(scfg_t* cfg, dir_t* dir, char* path, size_t size)
+{
+	if(dir->vpath[0] != '\0')
+		return dir->vpath;
+	else
+		safe_snprintf(path, size, "%s/%s"
+			,cfg->lib[dir->lib]->vdir, dir->vdir);
+	return path;
+}
diff --git a/src/sbbs3/scfglib2.c b/src/sbbs3/scfglib2.c
index 018ce12f2c0887ab0f202360234bbff3a33f7a42..ca6ac5ab88a577b8eeda46b7aadb5008d5f9e2e5 100644
--- a/src/sbbs3/scfglib2.c
+++ b/src/sbbs3/scfglib2.c
@@ -308,6 +308,7 @@ bool read_file_cfg(scfg_t* cfg, char* error, size_t maxerrlen)
 			cfg->lib[cfg->dir[i]->lib]->offline_dir=i;
 
 		init_vdir(cfg, cfg->dir[i]);
+		SAFECOPY(cfg->dir[i]->vpath, iniGetString(section, NULL, "vpath", "", value));
 
 		SAFECOPY(cfg->dir[i]->arstr, iniGetString(section, NULL, "ars", "", value));
 		SAFECOPY(cfg->dir[i]->ul_arstr, iniGetString(section, NULL, "upload_ars", "", value));
diff --git a/src/sbbs3/scfgsave.c b/src/sbbs3/scfgsave.c
index f1d6861fc9e4105c185dc3bdc68924a579d2ad64..a765101527040de6b2f27afaa85c866e71a20b9c 100644
--- a/src/sbbs3/scfgsave.c
+++ b/src/sbbs3/scfgsave.c
@@ -784,6 +784,7 @@ bool write_file_cfg(scfg_t* cfg)
 				iniSetString(&section, name, "area_tag", cfg->dir[i]->area_tag, &ini_style);
 				backslash(cfg->dir[i]->path);
 				iniSetString(&section, name, "path", cfg->dir[i]->path, &ini_style);
+				iniSetString(&section, name, "vpath", cfg->dir[i]->vpath, &ini_style);
 
 				if (cfg->dir[i]->misc&DIR_FCHK) {
 					SAFECOPY(path, cfg->dir[i]->path);