Skip to content

Commit ee55504

Browse files
committed
Configurable media storage path for web farms
1 parent dfe093a commit ee55504

7 files changed

Lines changed: 126 additions & 37 deletions

File tree

changelog.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
### Breaking change
66
* Removed Web API plugin from open source core
77

8+
### New Features
9+
* Output Cache with "donut hole caching" (commercial plugin)
10+
* REDIS providers for Business Cache, Output Cache and Session State (commercial plugin)
11+
* Microsoft AZURE provider for media storage (commercial plugin)
12+
* Message Bus for inter-process messaging between servers (commercial plugin)
13+
* Configurable media storage path for web farms
14+
* Added option to skip shipping method selection in checkout process when only one shipping method is active
15+
* Added options to capture salutation and title in addresses and customer info
16+
* Added projection to control the export of individually visible associated products
17+
* #1002 Web API: Add support for addresses and customer roles navigation property of customer entity
18+
* #966 Implement new tax calculation logic for shipping and payment fees (Calculate with rate of highest cart amount)
19+
* #922 New option whether to include the weight of free shipping products in shipping by weight calculation
20+
* #724 Allow discounts to be applied to manufacturers
21+
822
### Improvements
923
* Added order message token for accepting third party email handover
1024
* ECB currency exchange rate provider now cross calculates rates based on euro rates
@@ -14,15 +28,6 @@
1428
* #1008 Export: Add support for description projection to all product exporting providers
1529
* #1015 Implement Entity Picker in discount requirements
1630

17-
### New Features
18-
* Added option to skip shipping method selection in checkout process when only one shipping method is active
19-
* Added options to capture salutation and title in addresses and customer info
20-
* Added projection to control the export of individually visible associated products
21-
* #1002 Web API: Add support for addresses and customer roles navigation property of customer entity
22-
* #966 Implement new tax calculation logic for shipping and payment fees
23-
* #922 New option whether to include the weight of free shipping products in shipping by weight calculation
24-
* #724 Allow discounts to be applied to manufacturers
25-
2631
### Bugfixes
2732
* Currency wasn't displayed at shipping estimation
2833
* SKU, EAN, MPN of last attribute combination was exported for all combinations

src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,18 @@ public static string Mask(this string value, int length)
265265
}
266266

267267
[DebuggerStepThrough]
268-
public static bool IsWebUrl(this string value)
268+
public static bool IsWebUrl(this string value, bool schemeIsOptional = false)
269269
{
270-
return !String.IsNullOrEmpty(value) && RegularExpressions.IsWebUrl.IsMatch(value.Trim());
270+
if (String.IsNullOrEmpty(value))
271+
return false;
272+
273+
if (schemeIsOptional)
274+
{
275+
Uri uri;
276+
return Uri.TryCreate(value, UriKind.Absolute, out uri);
277+
}
278+
279+
return RegularExpressions.IsWebUrl.IsMatch(value.Trim());
271280
}
272281

273282
[DebuggerStepThrough]

src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,95 @@
33
using System.IO;
44
using System.Linq;
55
using System.Threading.Tasks;
6+
using System.Web;
67
using System.Web.Hosting;
78
using SmartStore.Utilities;
89

910
namespace SmartStore.Core.IO
1011
{
1112
public class LocalFileSystem : IFileSystem
1213
{
13-
private readonly string _basePath; // /base
14-
private readonly string _virtualPath; // ~/base
15-
private readonly string _publicPath; // /Shop/base
16-
private readonly string _storagePath; // C:\SMNET\base
14+
private string _root;
15+
private string _publicPath; // /Shop/base
16+
private string _storagePath; // C:\SMNET\base
1717

1818
public LocalFileSystem()
19-
: this(string.Empty)
19+
: this(string.Empty, string.Empty)
2020
{
2121
}
2222

2323
protected internal LocalFileSystem(string basePath)
24+
: this(basePath, string.Empty)
2425
{
25-
// for testing purposes
26-
_basePath = basePath.EmptyNull().EnsureStartsWith("/").TrimEnd('/');
26+
}
27+
28+
protected internal LocalFileSystem(string basePath, string publicPath)
29+
{
30+
basePath = basePath.EmptyNull();
31+
32+
var pathIsAbsolute = FileSystemHelper.IsFullPath(basePath);
2733

28-
_virtualPath = "~" + _basePath;
34+
NormalizeStoragePath(ref basePath, pathIsAbsolute);
2935

30-
_storagePath = CommonHelper.MapPath(_virtualPath, false);
36+
_publicPath = NormalizePublicPath(publicPath, basePath, pathIsAbsolute);
3137

32-
var appPath = "";
38+
_root = basePath;
39+
}
40+
41+
private void NormalizeStoragePath(ref string basePath, bool basePathIsAbsolute)
42+
{
43+
if (basePathIsAbsolute)
44+
{
45+
// Path is fully qualified (UNC or absolute with drive letter)
46+
47+
// In this case this is our root path...
48+
basePath = basePath.TrimEnd(Path.DirectorySeparatorChar);
49+
50+
// ...AND our physical storage path
51+
_storagePath = basePath;
52+
}
53+
else
54+
{
55+
// Path is relative to the app root
56+
basePath = basePath.TrimEnd('/').EnsureStartsWith("/");
57+
_storagePath = CommonHelper.MapPath("~" + basePath, false);
58+
}
59+
}
60+
61+
private string NormalizePublicPath(string publicPath, string basePath, bool basePathIsAbsolute)
62+
{
63+
publicPath = publicPath.EmptyNull();
64+
65+
if (basePathIsAbsolute)
66+
{
67+
if (publicPath.IsEmpty() || (!publicPath.StartsWith("~/") && !publicPath.IsWebUrl(true)))
68+
{
69+
throw new ArgumentException("When the base path is a fully qualified path, the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath));
70+
}
71+
}
72+
73+
var appVirtualPath = HostingEnvironment.IsHosted
74+
? HostingEnvironment.ApplicationVirtualPath.TrimEnd('/')
75+
: string.Empty;
76+
77+
if (publicPath.StartsWith("~/"))
78+
{
79+
// prepend application virtual path
80+
return appVirtualPath + publicPath.Substring(1);
81+
}
3382

34-
if (HostingEnvironment.IsHosted)
83+
if (publicPath.IsEmpty())
3584
{
36-
appPath = HostingEnvironment.ApplicationVirtualPath;
85+
// > /MyAppRoot/Media
86+
return appVirtualPath + basePath;
3787
}
3888

39-
_publicPath = appPath.TrimEnd('/') + _basePath;
89+
return publicPath;
4090
}
4191

4292
public string Root
4393
{
44-
get { return _basePath; }
94+
get { return _root; }
4595
}
4696

4797
/// <summary>
@@ -62,7 +112,7 @@ protected virtual string MapStorage(string path)
62112
/// <returns>The relative path combined with the public path in an URL friendly format ('/' character for directory separator).</returns>
63113
protected virtual string MapPublic(string path)
64114
{
65-
return string.IsNullOrEmpty(path) ? _publicPath : Path.Combine(_publicPath, path).Replace(Path.DirectorySeparatorChar, '/').Replace(" ", "%20");
115+
return string.IsNullOrEmpty(path) ? _publicPath : HttpUtility.UrlDecode(Path.Combine(_publicPath, path).Replace(Path.DirectorySeparatorChar, '/'));
66116
}
67117

68118
static string Fix(string path)
@@ -83,14 +133,14 @@ public virtual string GetStoragePath(string url)
83133
{
84134
if (url.HasValue())
85135
{
86-
if (url.StartsWith(_virtualPath))
136+
if (url.StartsWith("~/"))
87137
{
88-
return url.Substring(_virtualPath.Length).Replace('/', Path.DirectorySeparatorChar).Replace("%20", " ");
138+
url = VirtualPathUtility.ToAbsolute(url);
89139
}
90140

91141
if (url.StartsWith(_publicPath))
92142
{
93-
return url.Substring(_publicPath.Length).Replace('/', Path.DirectorySeparatorChar).Replace("%20", " ");
143+
return HttpUtility.UrlDecode(url.Substring(_publicPath.Length).Replace('/', Path.DirectorySeparatorChar));
94144
}
95145
}
96146

src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111

1212
namespace SmartStore.Utilities
1313
{
14-
1514
public static partial class CommonHelper
16-
{
17-
15+
{
1816
/// <summary>
1917
/// Generate random digit code
2018
/// </summary>
@@ -54,7 +52,7 @@ public static int GenerateRandomInteger(int min = 0, int max = 2147483647)
5452
/// </remarks>
5553
public static string MapPath(string path, bool findAppRoot = true)
5654
{
57-
Guard.ArgumentNotNull(() => path);
55+
Guard.NotNull(path, nameof(path));
5856

5957
if (HostingEnvironment.IsHosted)
6058
{
@@ -209,6 +207,5 @@ private static bool TryAction<T>(Func<T> func, out T output)
209207
return false;
210208
}
211209
}
212-
213210
}
214211
}

src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,5 +290,18 @@ public static void ClearFile(string path)
290290
}
291291
catch (Exception) { }
292292
}
293+
294+
/// <summary>
295+
/// Checks whether the given path is a fully qualified absolute path (either UNC or rooted with drive letter)
296+
/// </summary>
297+
/// <param name="path">Path to check</param>
298+
/// <returns><c>true</c> if path is fully qualified</returns>
299+
public static bool IsFullPath(string path)
300+
{
301+
return !String.IsNullOrWhiteSpace(path)
302+
&& path.IndexOfAny(System.IO.Path.GetInvalidPathChars().ToArray()) == -1
303+
&& Path.IsPathRooted(path)
304+
&& !Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal);
305+
}
293306
}
294307
}

src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public interface IMediaFileSystem : IFileSystem
1515
public class MediaFileSystem : LocalFileSystem, IMediaFileSystem
1616
{
1717
public MediaFileSystem()
18-
: base(CommonHelper.GetAppSetting<string>("sm:MediaBasePath", "Media"))
18+
: base(CommonHelper.GetAppSetting<string>("sm:MediaStoragePath", "/Media"), CommonHelper.GetAppSetting<string>("sm:MediaPublicPath"))
1919
{
2020
this.TryCreateFolder("Thumbs");
2121
this.TryCreateFolder("Uploaded");

src/Presentation/SmartStore.Web/Web.config

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,23 @@
8080
<add key="sm:EnableLegacyRoutesMapping" value="true" />
8181
<add key="sm:ClearPluginsShadowDirectoryOnStartup" value="false" />
8282
<add key="sm:TempDirectory" value="~/App_Data/_temp" />
83-
<add key="sm:MediaBasePath" value="Media" />
84-
<add key="sm:MediaPublicUrl" value="~/Media" />
83+
<!--
84+
Storage path for media files and assets like pictures, thumbs, uploads, email attachments etc.
85+
Must be either an app local relative path or a fully qualified physical path to a shared location. E.g.:
86+
- "Media" or "/Media" points to the subfolder named "Media" in your application root
87+
- "F:\SharedMedia" points to a (mapped network) drive
88+
- "\\Server1\SharedMedia" points to a network drive.
89+
In web farms, you should specify the same UNC or mapped network drive on each server.
90+
-->
91+
<add key="sm:MediaStoragePath" value="/Media" />
92+
<!--
93+
Public base path to the media storage used to generate URLs for output HTML.
94+
Leave empty if "sm:MediaStoragePath" is an app local relative path, OR:
95+
- specify a virtual path, like "~/Media", or
96+
- specify a fully qualified URL, like "http://static.myshop.com/media"
97+
Please consider that this path or URL must point internally to the storage folder specified in "sm:MediaStoragePath"
98+
-->
99+
<add key="sm:MediaPublicPath" value="" />
85100
<add key="sm:BizImportMediaDirectory" value="Uploaded/static" />
86101
<add key="sm:PluginsIgnoredDuringInstallation" value="SmartStore.DemoShopControlling, SmartStore.WebApi, SmartStore.Glimpse" />
87102
<!-- Task Scheduler sweep interval in minutes (1 recommended) -->

0 commit comments

Comments
 (0)