cloudscribe/cloudscribe.MetaWeblog

Where to handle posting image through api/metablog

Closed this issue · 5 comments

I am using a SimpleContent blog with support for metaweblog.
If I use OLW to create new text-only posts it's all good, but if I try to create a post with an image then I suspect it is somehow configured by default to look for a local path.

  • If the path does not exist under wwwroot I get a Exception thrown: 'System.IO.DirectoryNotFoundException' in System.Private.CoreLib.ni.dll
  • If the path is a URL I get a Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.ni.dll
  • If the path is a relative path to a folder that exists under wwwroot it uploads an image with a Guid name under the folder in wwwroot.

I've seen the server gets a POST request to http://localhost:5000/api/metaweblog from the OLW client and the IPostCommands,Create is only executed if the LocalMediaVirtualPath specified folder exists in wwwroot.

By default this IPostCommands,Create will add the image as a local image (i.e: <a href=\"/img/65e8f237-56a1-46c1-a7f1-87779a70455b.png\"><img src=\"/img/b2038112-1d85-4a4f-adbd-ba01fecc225e.png\"></a>)

I could add my code in a custom IPostCommands implementation to detect any <a> with a source that includes the LocalMediaVirtualPath and add there the code to upload the image to a blob store, for example, and replace the a src="http://blob/guid.png". But this doesn't seem a good solution because the image must exist physically in LocalMediaVirtualPath before being uploaded.

So I would need to have my blob storage upload implementation somewhere before the IPostCommands,Create is executed.

My question is: What approach to follow or where to add this custom implementation?

Thank you.

the MetawebLog library in this repo requires one to bring their own IMetaweblogService

SimpleContent provides its implementation of MetaweblogService which internally calls on BlogService

For handling posted files BlogService depends on IMediaProcessor which has a default implementation in FileSystemMediaProcessor

so part of the needed extension points do already exist but in reviewing it there is another method on BlogService: ResolveMediaUrl that is currently tightly coupled to file system storage. I will refactor that today to make it part of IMediaProcessor so that all of what you need can be done by implementing a custom version of that interface. Then I will publish updated nugets and write a document in the SimpleContent wiki explaining what is needed to implement custom image storage. I will post a link here when that is all done.

perhaps after that if you make an implementation of IMediaProcessor for blob storage you could consider sharing that back with the SimpleContent project so that others could use it as well?

as promised I have published updated nugets for SimpleContent.Models and SimpleContent.Web and I have created a document here to explain about how images are handled and available extension point

https://github.com/joeaudette/cloudscribe.SimpleContent/wiki/Image-Processing-and-Storage-extension-points

Thanks for that! The documentation was very helpful.

It works well with blob Storage when using OLW to create posts with images.

This is my implementation of IMediaProcessor in case it's useful for somebody although it's all very straightforward.

public class BlogMediaService : IMediaProcessor
{
    private readonly IMediaRepository _mediaRepository;

    public BlogMediaService(IMediaRepository mediaRepository)
    {
        _mediaRepository = mediaRepository;
    }

    public Task ConvertBase64EmbeddedImagesToFilesWithUrls(string mediaVirtualPath, IPage page)
    {
        throw new NotImplementedException();
    }

    public Task ConvertBase64EmbeddedImagesToFilesWithUrls(string mediaVirtualPath, IPost post)
    {
        throw new NotImplementedException();
    }

    public Task<string> ResolveMediaUrl(string mediaVirtualPath, string fileName)
    {
        var baseUri = _mediaRepository.GetBlobStorageBaseUri();
        return Task.FromResult($"{baseUri}/{Constants.BlogValues.BLOG_BLOB_IMAGE_CONTAINER_NAME}/{fileName}");
    }

    public async Task SaveMedia(string mediaVirtualPath, string fileName, byte[] bytes)
    {
        using(var stream = new MemoryStream(bytes))
        {
            var uri = await _mediaRepository.PutBlobAsync(Constants.BlogValues.BLOG_BLOB_IMAGE_CONTAINER_NAME, fileName, stream);
        }
    }
}

and it's relying on my media repository, which is a repository implementation that access Azure storage. Something like this:

    public class MediaRepository : IMediaRepository
    {
        private readonly IberodevBlobContext _context;

        public MediaRepository(IberodevBlobContext context)
        {
            _context = context;
        }

        public Uri GetBlobStorageBaseUri()
        {
            return _context.BlobClient.StorageUri.PrimaryUri;
        }

        public async Task<Uri> PutBlobAsync(string containerName, string blobName, Stream inputStream, string contentType = null)
        {
            CloudBlobContainer blobContainer = _context.BlobClient.GetContainerReference(containerName);
            CloudBlockBlob blob = blobContainer.GetBlockBlobReference(blobName);
            await blob.UploadFromStreamAsync(inputStream);
            return blob.Uri;
        }
    }

public class IberodevBlobContext : IDisposable
{
    public IberodevBlobContext(IOptions<AzureBlobStorageOptions> blobStorageSettingsOptions)
    {
        if (blobStorageSettingsOptions == null || blobStorageSettingsOptions.Value == null)
        {
            throw new SettingsNotFoundException(
                $"Azure Blob Storage settings section not found in {Constants.AppSettings.APP_SETTINGS_FILE_FULLNAME}");
        }
        var blobStorageOptions = blobStorageSettingsOptions.Value;
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(blobStorageOptions.ConnectionString);
        BlobClient = storageAccount.CreateCloudBlobClient();
    }

    public CloudBlobClient BlobClient { get; set; }

    public void Dispose()
    {
        //CloudStorageAccount and CloudBlobClient hold no state
        BlobClient = null;
    }
}

Thanks for sharing!
Now that I explained it and documented at and saw your implementation with a few not implemented methods, it got me thinking I need to refactor this interface and the implementation. Sorry for the breaking change but since you are the first one I know of overriding the implementation I thought it best to go ahead and fix it. I have published new nugets for cloudscribe.SimpleContent.Models where the interface is defined and cloudscribe.SimpleContent.Web where the FileSystemMediaProcessor lives.

I changed the interface so now there is only one method to convertbase64 it takes a string and returns a string, removing the duplication of code for Post vs Page.

Also the methods on FileSystemMediaProcessor are now virtual so you could inherit from that implementation and just override the methods you want. Your current implementation would throw the not implemented exception if content were edited from the web page. This could be avoided by inheriting and using the existing method for base64, or your method could just return the original string without any processing instead of throwing an error.

I've updated the wiki page as well.

That's excellent, thank you. I have just updated my code to accommodate your changes, now the ConvertBase64EmbeddedImagesToFilesWithUrls can be inherited.

public class BlogMediaService : FileSystemMediaProcessor, IMediaProcessor
{
    private readonly IMediaRepository _mediaRepository;

    public BlogMediaService(
        IMediaRepository mediaRepository,
        ILogger<FileSystemMediaProcessor> logger, 
        IHostingEnvironment env) : base(logger, env)
    {
        _mediaRepository = mediaRepository;
    }

    public override Task<string> ResolveMediaUrl(string mediaVirtualPath, string fileName)
    {
        var baseUri = _mediaRepository.GetBlobStorageBaseUri();
        return Task.FromResult($"{baseUri}/{Constants.BlogValues.BLOG_BLOB_IMAGE_CONTAINER_NAME}/{fileName}");
    }

    public override async Task SaveMedia(string mediaVirtualPath, string fileName, byte[] bytes)
    {
        using(var stream = new MemoryStream(bytes))
        {
            var uri = await _mediaRepository.PutBlobAsync(Constants.BlogValues.BLOG_BLOB_IMAGE_CONTAINER_NAME, fileName, stream);
        }
    }
}