umbraco / Umbraco-CMS

Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.
https://umbraco.com
MIT License
4.5k stars 2.69k forks source link

GetFullPath doesn't work as expected #11539

Closed bjarnef closed 2 years ago

bjarnef commented 3 years ago

Which exact Umbraco version are you using? For example: 8.13.1 - don't just write v8

9.0.1

Bug summary

I have a custom Tag Helper where I inject MediaFileManager. From this I can also access IFileSystem via the FileSystem property.

Here is an example, where File is a relative path returned from mediaItem.Url():

namespace MyProject.Core.TagHelpers
{
    using System;
    using System.IO;

    using Microsoft.AspNetCore.Html;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Microsoft.Extensions.Logging;
    using Umbraco.Cms.Core.Hosting;
    using Umbraco.Cms.Core.IO;

    [HtmlTargetElement("svg-icon", TagStructure = TagStructure.NormalOrSelfClosing)]
    public class SvgIconTagHelper : TagHelper
    {
        private readonly IIOHelper _ioHelper;
        private readonly IHostingEnvironment _hostingEnvironment;
        private readonly MediaFileManager _mediaFileManager;
        private readonly IFileSystem _fileSystem;
        private readonly ILogger<SvgIconTagHelper> _logger;

        public SvgIconTagHelper(
            IIOHelper ioHelper,
            IHostingEnvironment hostingEnvironment,
            MediaFileManager mediaFileManager,
            IFileSystem _fileSystem,
            ILogger<SvgIconTagHelper> logger)
        {
            //_webHostEnvironment = webHostEnvironment;
            _ioHelper = ioHelper;
            _hostingEnvironment = hostingEnvironment;
            _mediaFileManager = mediaFileManager;
            _logger = logger;
        }

        /// <summary>
        /// Path to SVG file
        /// </summary>
    public string File { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (string.IsNullOrWhiteSpace(File))
            {
                throw new ArgumentNullException(nameof(File));
            }

            output.TagName = "span";

            var classAttribute = output.Attributes["class"];
            var classes = (classAttribute?.Value as HtmlString)?.Value ?? string.Empty;
            classes += " icon";
            classes = classes.Trim();
            output.Attributes.SetAttribute("class", classes);

            output.TagMode = TagMode.StartTagAndEndTag;

            try
            {
                //string filePath = _ioHelper.GetRelativePath(File);
                //var path = _mediaFileManager.FileSystem.GetFullPath(filePath);

                var fullPath = _mediaFileManager.FileSystem.GetFullPath(File);

                _logger.LogInformation("Full path {Path}", fullPath);

                if (_mediaFileManager.FileSystem.FileExists(fullPath))
                {
                    var stream = _mediaFileManager.FileSystem.OpenFile(fullPath);
                    stream.Seek(0, SeekOrigin.Begin);

                    using (StreamReader sr = new StreamReader(stream))
                    {
                        var svg = sr.ReadToEnd();
                        if (!string.IsNullOrEmpty(svg))
                        {
                            output.Content.SetHtmlContent(svg);
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed reading SVG icon");
            }
        }
    }

}

Locally this works an GetFullPath() returns C:\VSProjects\my-project\UmbracoCloud\src\UmbracoProject\wwwroot\media\rybamegd\document.svg and render the SVG Icon.

However on Umbraco Cloud is log this path from GetFullPath(): Full path "media/~/media/rybamegd/document.svg"

I would have expected it to return an absolute path to the media file. Not sure if it's a core issue or maybe the implementation of GetFullPath() here? https://github.com/umbraco/Umbraco.StorageProviders/blob/main/src/Umbraco.StorageProviders.AzureBlob/IO/AzureBlobFileSystem.cs#L209-L216

The tag helper is used as the following:

<svg-icon file="@icon.Url()"></svg-icon>

Specifics

No response

Steps to reproduce

Inject MediaFileManager to a custom Tag Helper or just in view file for testing.

Use the following to get the full path to the file.

var path = mediaItem.Url();
var fullPath = _mediaFileManager.FileSystem.GetFullPath(path);

Expected result / actual result

No response

bjarnef commented 3 years ago

@ronaldbarendse do you know this should work on Umbraco Cloud, which by default use blob storage? would it use the implementation of GetFullPath() here? https://github.com/umbraco/Umbraco.StorageProviders/blob/main/src/Umbraco.StorageProviders.AzureBlob/IO/AzureBlobFileSystem.cs#L209-L216

ronaldbarendse commented 3 years ago

@bjarnef The GetFullPath() method is indeed a bit weird, as Azure Blob Storage (used on Umbraco Cloud) can't return a full path to the file, only an absolute URL. Because FileExists() and OpenFile() only require the relative path, I would recommend removing the call or change it to GetRelativePath() instead.

bjarnef commented 3 years ago

@ronaldbarendse I changed to the following instead:

string path = _ioHelper.GetRelativePath(File);

_logger.LogInformation("Full path {Path}", path);

if (_mediaFileManager.FileSystem.FileExists(path))
{
    var stream = _mediaFileManager.FileSystem.OpenFile(path);
    stream.Seek(0, SeekOrigin.Begin);

    using (StreamReader sr = new StreamReader(stream))
    {
        var svg = sr.ReadToEnd();
        if (!string.IsNullOrEmpty(svg))
        {
            output.Content.SetHtmlContent(svg);
        }
    }
}

but it doesn't seem to render SVG icon locally or on Umbraco Cloud.

On Umbraco Cloud the path is logged as ~/media/rybamegd/document.svg which looks similar to the path format here: https://our.umbraco.com/documentation/Reference/V9-Config/filesystemProviders#get-the-contents-of-a-file-as-a-stream

bjarnef commented 3 years ago

Because FileExists() and OpenFile() only require the relative path

It would be great it the methods had a comment about this as it currently not is easy to know which format the path should be.

bjarnef commented 3 years ago

Okay, so if I trim ~ from path it works:

using Umbraco.Extensions;

...

string path = _ioHelper.GetRelativePath(File);

path = path.TrimStart("~")

_logger.LogInformation("Full path {Path}", path);

if (_mediaFileManager.FileSystem.FileExists(path))
{
    var stream = _mediaFileManager.FileSystem.OpenFile(path);
    stream.Seek(0, SeekOrigin.Begin);

    using (StreamReader sr = new StreamReader(stream))
    {
        var svg = sr.ReadToEnd();
        if (!string.IsNullOrEmpty(svg))
        {
            output.Content.SetHtmlContent(svg);
        }
    }
}
bjarnef commented 3 years ago

Instead of including Umbraco.Extensions namespace, it is also possible to use this instead:

string path = _ioHelper.GetRelativePath(File);

path = path.TrimStart(Umbraco.Cms.Core.Constants.CharArrays.Tilde);
ronaldbarendse commented 3 years ago

Mixing virtual paths (starting with ~/) and physical file paths doesn't make much sense in ASP.NET Core, as the web root is abstracted as an IFileProvider you can access from IWebHostEnvironment.WebRootFileProvider. You can also use IHostEnvironment.ContentRootFileProvider to get application content files that aren't publicly served...

Umbraco's IFileSystem is a separate thing, as it also includes write support, so I would suggest rewriting your tag helper to either use this or the IFileProvider... I'm experimenting on trying to combine these two concepts in PR https://github.com/umbraco/Umbraco.StorageProviders/pull/11.

bjarnef commented 3 years ago

@ronaldbarendse in this case I could just use value of File property but _ioHelper.GetRelativePath(File) would return relative URL in case I pass in absolute URL.

IFileSystem is actual what I already use via _mediaFileManager.FileSystem. I haven't checked IFileProvider though, but a quick glance at source in GitHub repository it seems the only reference is here: https://github.com/umbraco/Umbraco-CMS/blob/v9/contrib/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs#L19

namespace MyProject.Core.TagHelpers
{
    using System;
    using System.IO;
    using Microsoft.AspNetCore.Html;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Microsoft.Extensions.Logging;
    using Umbraco.Cms.Core.Hosting;
    using Umbraco.Cms.Core.IO;
    using Constants = Umbraco.Cms.Core.Constants;

    [HtmlTargetElement("svg-icon", TagStructure = TagStructure.NormalOrSelfClosing)]
    public class SvgIconTagHelper : TagHelper
    {
        private readonly IIOHelper _ioHelper;
        private readonly IHostingEnvironment _hostingEnvironment;
        private readonly MediaFileManager _mediaFileManager;
        private readonly ILogger<SvgIconTagHelper> _logger;

        public SvgIconTagHelper(
            IIOHelper ioHelper,
            IHostingEnvironment hostingEnvironment,
            MediaFileManager mediaFileManager,
            ILogger<SvgIconTagHelper> logger)
        {
            _ioHelper = ioHelper;
            _hostingEnvironment = hostingEnvironment;
            _mediaFileManager = mediaFileManager;
            _logger = logger;
        }

        /// <summary>
    /// Path to SVG file
    /// </summary>
    public string File { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (string.IsNullOrWhiteSpace(File))
            {
                throw new ArgumentNullException(nameof(File));
            }

            output.TagName = "span";

            var classAttribute = output.Attributes["class"];
            var classes = (classAttribute?.Value as HtmlString)?.Value ?? string.Empty;
            classes += " icon";
            classes = classes.Trim();
            output.Attributes.SetAttribute("class", classes);

            output.TagMode = TagMode.StartTagAndEndTag;

            try
            {
                string path = _ioHelper.GetRelativePath(File);

                path = path.TrimStart(Constants.CharArrays.Tilde);

                if (_mediaFileManager.FileSystem.FileExists(path))
                {
                    var stream = _mediaFileManager.FileSystem.OpenFile(path);
                    stream.Seek(0, SeekOrigin.Begin);

                    using (StreamReader sr = new StreamReader(stream))
                    {
                        var svg = sr.ReadToEnd();
                        if (!string.IsNullOrEmpty(svg))
                        {
                            output.Content.SetHtmlContent(svg);
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed reading SVG icon");
            }
        }
    }

}
umbrabot commented 2 years ago

Hiya @bjarnef,

Just wanted to let you know that we noticed that this issue got a bit stale and might not be relevant any more.

We will close this issue for now but we're happy to open it up again if you think it's still relevant (for example: it's a feature request that's not yet implemented, or it's a bug that's not yet been fixed).

To open it this issue up again, you can write @umbrabot still relevant in a new comment as the first line. It would be super helpful for us if on the next line you could let us know why you think it's still relevant.

For example:

@umbrabot still relevant This bug can still be reproduced in version x.y.z

This will reopen the issue in the next few hours.

Thanks, from your friendly Umbraco GitHub bot :robot: :slightly_smiling_face: