Skip to content

Commit 7b15b71

Browse files
committed
Adds our-self-host
1 parent 1f0ce8b commit 7b15b71

File tree

7 files changed

+303
-0
lines changed

7 files changed

+303
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Our.Umbraco.TagHelpers.Services;
3+
using Umbraco.Cms.Core.Composing;
4+
using Umbraco.Cms.Core.DependencyInjection;
5+
6+
namespace Our.Umbraco.TagHelpers.Composing
7+
{
8+
public class SelfHostServiceComposer : IComposer
9+
{
10+
public void Compose(IUmbracoBuilder builder)
11+
{
12+
builder.Services.AddSingleton<ISelfHostService, SelfHostService>();
13+
}
14+
}
15+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using System;
3+
using System.IO;
4+
using Umbraco.Cms.Core;
5+
6+
namespace Our.Umbraco.TagHelpers.Extensions
7+
{
8+
[Obsolete("This should be removed, when the package gets upgraded past Umbraco 10")]
9+
internal static class WebHostEnvironmentExtensions
10+
{
11+
12+
/// <summary>
13+
/// Maps a virtual path to a physical path to the application's web root
14+
/// </summary>
15+
/// <remarks>
16+
/// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the
17+
/// content root are the same, however
18+
/// in netcore the web root is /wwwroot therefore this will Map to a physical path within wwwroot.
19+
/// </remarks>
20+
21+
[Obsolete("This should be removed, when the package gets upgraded past Umbraco 10")]
22+
public static string MapPathWebRoot(this IWebHostEnvironment webHostEnvironment, string path)
23+
{
24+
var root = webHostEnvironment.WebRootPath;
25+
26+
// Create if missing
27+
if (string.IsNullOrWhiteSpace(root))
28+
{
29+
root = webHostEnvironment.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot");
30+
}
31+
32+
var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
33+
34+
// TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX
35+
// IOHelper would check if the path passed in started with the root, and not prepend the root again if it did,
36+
// however if you are requesting a path be mapped, it should always assume the path is relative to the root, not
37+
// absolute in the file system. This error will help us find and fix improper uses, and should be removed once
38+
// all those uses have been found and fixed
39+
if (newPath.StartsWith(root))
40+
{
41+
throw new ArgumentException(
42+
"The path appears to already be fully qualified. Please remove the call to MapPathWebRoot");
43+
}
44+
45+
return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash));
46+
}
47+
}
48+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Our.Umbraco.TagHelpers.Models
2+
{
3+
public class SelfHostedFile
4+
{
5+
public string? ExternalUrl { get; internal set; }
6+
public string? FileName { get; internal set; }
7+
public string? FolderPath { get; internal set; }
8+
public string? Url { get; internal set; }
9+
10+
public override string? ToString()
11+
{
12+
return Url;
13+
}
14+
}
15+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Microsoft.AspNetCore.Razor.TagHelpers;
2+
using Our.Umbraco.TagHelpers.Services;
3+
using System.Threading.Tasks;
4+
using Umbraco.Extensions;
5+
6+
namespace Our.Umbraco.TagHelpers
7+
{
8+
/// <summary>
9+
/// Downloads the specified file (in href or src) to wwwroot and changes the link to local.
10+
/// </summary>
11+
[HtmlTargetElement("*", Attributes = "our-self-host")]
12+
public class SelfHostTagHelper : TagHelper
13+
{
14+
private readonly ISelfHostService _selfHostService;
15+
16+
public SelfHostTagHelper(ISelfHostService selfHostService)
17+
{
18+
_selfHostService = selfHostService;
19+
}
20+
21+
[HtmlAttributeName("folder")]
22+
public string? FolderName { get; set; }
23+
[HtmlAttributeName("src")]
24+
public string? SrcAttribute { get; set; }
25+
[HtmlAttributeName("href")]
26+
public string? HrefAttribute { get; set; }
27+
[HtmlAttributeName("ext")]
28+
public string? Extension { get; set; }
29+
public string Url => SrcAttribute.IfNullOrWhiteSpace(HrefAttribute);
30+
31+
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
32+
{
33+
var url = (Url.StartsWith("//") ? $"https:{Url}" : Url);
34+
var selfHostedFile = await _selfHostService.SelfHostFile(url, FolderName, Extension);
35+
36+
if (SrcAttribute.IsNullOrWhiteSpace() == false)
37+
{
38+
output.Attributes.SetAttribute("data-original-src", SrcAttribute);
39+
output.Attributes.SetAttribute("src", selfHostedFile.Url);
40+
}
41+
else if (HrefAttribute.IsNullOrWhiteSpace() == false)
42+
{
43+
output.Attributes.SetAttribute("data-original-href", HrefAttribute);
44+
output.Attributes.SetAttribute("href", selfHostedFile.Url);
45+
}
46+
47+
output.Attributes.Remove(new TagHelperAttribute("umb-self-host"));
48+
output.Attributes.Remove(new TagHelperAttribute("folder"));
49+
output.Attributes.Remove(new TagHelperAttribute("ext"));
50+
}
51+
52+
53+
}
54+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Our.Umbraco.TagHelpers.Models;
2+
using System.Threading.Tasks;
3+
4+
namespace Our.Umbraco.TagHelpers.Services
5+
{
6+
public interface ISelfHostService
7+
{
8+
Task<SelfHostedFile> SelfHostFile(string url, string? subfolder = null, string? fileExtension = null);
9+
}
10+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.Configuration;
3+
using Our.Umbraco.TagHelpers.Extensions;
4+
using Our.Umbraco.TagHelpers.Models;
5+
using System;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Threading.Tasks;
10+
using Umbraco.Cms.Core.Cache;
11+
using Umbraco.Cms.Core.Logging;
12+
using Umbraco.Extensions;
13+
14+
namespace Our.Umbraco.TagHelpers.Services
15+
{
16+
public class SelfHostService : ISelfHostService
17+
{
18+
private readonly IProfilingLogger _logger;
19+
private readonly IAppPolicyCache _runtimeCache;
20+
private readonly IWebHostEnvironment _hostingEnvironment;
21+
private readonly IConfiguration _config;
22+
23+
public SelfHostService(
24+
IProfilingLogger logger,
25+
IAppPolicyCache appPolicyCache,
26+
IWebHostEnvironment hostingEnvironment,
27+
IConfiguration config
28+
)
29+
{
30+
_logger = logger;
31+
_runtimeCache = appPolicyCache;
32+
_hostingEnvironment = hostingEnvironment;
33+
_config = config;
34+
}
35+
36+
public async Task<SelfHostedFile> SelfHostFile(string url, string? subfolder = null, string? fileExtension = null)
37+
{
38+
return await _runtimeCache.GetCacheItem($"Our.Umbraco.TagHelpers.Services.SelfHostService.SelfHostedFile({url}, {subfolder}, {fileExtension})", async () =>
39+
{
40+
using (_logger.TraceDuration<ISelfHostService>($"Start generating SelfHostedFile: {url}", $"Finished generating SelfHostedFile: {url}"))
41+
{
42+
var uri = new Uri(url, UriKind.Absolute);
43+
44+
var selfHostedFile = new SelfHostedFile()
45+
{
46+
ExternalUrl = url,
47+
FileName = uri.Segments.Last() + fileExtension.IfNotNull(ext => ext.EnsureStartsWith(".")),
48+
FolderPath = GetFolderPath(uri, subfolder)
49+
};
50+
51+
selfHostedFile.Url = await GetSelfHostedUrl(selfHostedFile);
52+
return selfHostedFile;
53+
}
54+
});
55+
}
56+
57+
private string GetFolderPath(Uri uri, string? subfolder = null)
58+
{
59+
var folderPath = _config["Our.Umbraco.TagHelpers.SelfHost.RootFolder"].IfNullOrWhiteSpace("~/assets"); ;
60+
61+
if (subfolder.IsNullOrWhiteSpace() == false) folderPath += subfolder.EnsureStartsWith("/");
62+
63+
folderPath += GetRemoteFolderPath(uri);
64+
65+
return folderPath;
66+
}
67+
68+
private string GetRemoteFolderPath(Uri uri)
69+
{
70+
var segments = uri?.Segments;
71+
72+
// if there is more than 2 segments (first segment is the root, last segment is the file)
73+
// we can extract the folderpath
74+
if (segments?.Length > 2)
75+
{
76+
segments = segments.Skip(1).SkipLast(1).ToArray();
77+
78+
// remove trailing slash from segments
79+
segments = segments.Select(x => x.Replace("/", "")).ToArray();
80+
81+
// join segments with slash
82+
return string.Join("/", segments).EnsureStartsWith("/");
83+
}
84+
85+
return string.Empty;
86+
}
87+
private async Task<string> GetSelfHostedUrl(SelfHostedFile file)
88+
{
89+
var filePath = $"{file.FolderPath}/{file.FileName}";
90+
var localPath = _hostingEnvironment.MapPathWebRoot(file.FolderPath);
91+
var localFilePath = _hostingEnvironment.MapPathWebRoot(filePath);
92+
93+
if (!File.Exists(localFilePath))
94+
{
95+
using (_logger.TraceDuration<ISelfHostService>($"Start downloading SelfHostedFile: {file.ExternalUrl} to {localFilePath}", $"Finished downloading SelfHostedFile: {file.ExternalUrl} to {localFilePath}"))
96+
{
97+
var content = await GetUrlContent(file.ExternalUrl);
98+
if (content != null)
99+
{
100+
if (!Directory.Exists(localPath)) Directory.CreateDirectory(localPath);
101+
await File.WriteAllBytesAsync(localFilePath, content);
102+
return filePath;
103+
}
104+
else
105+
{
106+
return file.ExternalUrl;
107+
}
108+
}
109+
}
110+
111+
return filePath;
112+
}
113+
114+
private static async Task<byte[]?> GetUrlContent(string url)
115+
{
116+
using (var client = new HttpClient())
117+
{
118+
using (var result = await client.GetAsync(url))
119+
{
120+
if (result is not null && result.IsSuccessStatusCode)
121+
{
122+
return await result.Content.ReadAsByteArrayAsync();
123+
}
124+
else
125+
{
126+
return null;
127+
}
128+
}
129+
}
130+
}
131+
}
132+
}

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,35 @@ This example will turn off the automatic clearing of the tag helper cache if 'an
431431
</our-cache>
432432
```
433433

434+
## `our-self-host`
435+
This is a tag helper attribute that can be applied to any element using a `src` or `href` attribute in the razor template or partial. It will automatically download and self hosting of third party assets, like javascript, css or images.
436+
437+
### Simple Example
438+
```cshtml
439+
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" our-self-host>
440+
```
441+
442+
This will download the linked file to your local filesystem, and swap out the src attribute with a reference to the now locally hosted file.
443+
444+
### Folder location for downloaded files
445+
By default the files will be saved in `~/assets/`, and keep the folder path of the url. The root folder can be configured in appsettings.json, by adding a value at `Our.Umbraco.TagHelpers.SelfHost.RootFolder` specifying the desired rootfolder. The default value is `~/assets/`.
446+
447+
You can further divide the files into folders, by adding a `folder` attribute to the script tag, eg:
448+
```cshtml
449+
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" our-self-host folder="libraries">
450+
```
451+
This will save the file in `~/assets/libraries/[email protected]/dist/cdn.min.js`.
452+
453+
### Handling extensionless urls to files
454+
In case the url is extensionless, like `https://unpkg.com/alpinejs`, you can add an `ext` attribute, for specifying the extension of the file, eg:
455+
```cshtml
456+
<script src="https://unpkg.com/alpinejs" our-self-host ext="js">
457+
```
458+
This will save the file as `~/assets/alpinejs.js`, enabling eg. MIME types for .js files.
459+
460+
### Caching
461+
The file is saved once, and never updated unless you manually remove the file. The lookup for the local file is cached in the Runtime Cache.
462+
434463
## Video 📺
435464
[![How to create ASP.NET TagHelpers for Umbraco](https://user-images.githubusercontent.com/1389894/138666925-15475216-239f-439d-b989-c67995e5df71.png)](https://www.youtube.com/watch?v=3fkDs0NwIE8)
436465

0 commit comments

Comments
 (0)