Pages

Monday 8 April 2019

Umbraco Courier - Azure Storage

When you use Umbraco Courier to transfer items from development to production or vice-versa, you may run into a small problem when you store your media in AzureStorage, as is highly recommended.

The problem

When transferring media items with Courier, the data and the blob (= the actual image) are transferred to the target environment, but at the target, Umbraco is saving the blob to the default location on disk, not checking its FileSystemProviders, telling it to transfer the blob to AzureStorage.

The solution

I made a small modification to Hugo's solution on the Umbraco forum, where he raises the issue, then solves it himself.

You'll need to provide 3 EventHandlers.

The first one will run on the sending environment, telling the target to queue a postprocessor for the image.

public class MediaEventTrigger : ApplicationEventHandler
{
  protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
  {
    ExtractionManagerEvents.ExtractedItemResources += ExtractedItemResources; ;
  }

  private void ExtractedItemResources(object sender, ItemEventArgs e)
  {
    var provider = sender as RevisionExtraction;

    //make sure the media event only fires for media items, not other resources
    if (e.Item.ItemId.ProviderId == ItemProviderIds.mediapropertyDataItemProviderGuid)
    {
      var media = e.Item.Dependencies.Single(i => i.ItemId.ProviderId == ItemProviderIds.mediaItemProviderGuid);
      provider.Context.ExecuteEvent("QueueCourierMediaEvent", e.ItemId, new SerializableDictionary<string, string>());
    }
  }
}

The second one will be running at the receiving end. It will queue the third, instructing it to move the blob to Azure Storage.

public class MediaEventListener : ItemEventProvider
{
  public override string Alias => "QueueCourierMediaEvent";

  public override void Execute(ItemIdentifier itemId, SerializableDictionary<string, string> parameters)
  {
    ExecutionContext.QueueEvent("UpdateMediaPath", itemId, parameters, EventManagerSystemQueues.DeploymentComplete);
  }
}

And now for the actual moving of the blob.

public class MediaEventQueueListener : ItemEventProvider
{
  public override string Alias => "UpdateMediaPath";

  public override void Execute(ItemIdentifier itemId, SerializableDictionary<string, string> parameters)
  {
    var fs = FileSystemProviderManager.Current.GetFileSystemProvider<MediaFileSystem>();
    var ms = ApplicationContext.Current.Services.MediaService;

    var media = ms.GetById(Guid.Parse(itemId.Id));
    var oldPath = GetMediaPath(media);
    if (!string.IsNullOrEmpty(oldPath))
    {
      var absolutePath = HttpContext.Current.Server.MapPath(oldPath); //path to file on filesystem
      using (var fileStream = System.IO.File.OpenRead(absolutePath))
      {
        fs.AddFile(oldPath, fileStream); //will be picked up by FileSystemProvider and saved to AzureStorage
      }
     Directory.Delete(Path.GetDirectoryName(absolutePath), true);
     }
  }

  private string GetMediaPath(IMedia media)
  {
    //Image may use ImageCropper (or not), so test presence of Json
    var umbracoFile = media.GetValue<string>(Constants.Conventions.Media.File);
    if (!string.IsNullOrWhiteSpace(umbracoFile))
    {
      if (umbracoFile.StartsWith("{"))
      {
        //the umbracofile property contains the src for the image, which at the beginning is the location of the file on the filesystem.
        var umbracoFileJson = JsonConvert.DeserializeObject<ImageCropDataSet>(umbracoFile);
        return $"~{umbracoFileJson.Src}";
      }
      else
      {
        //Contains the path directly
        return umbracoFile;
      }
    }

    return string.Empty;
  }
}

Shouldn't we rename?

No. Or, not really.

Let's say we are transferring media/1028/flower.jpg from Dev to Prod. The path will remain media/1028/flower.jpg, because that just makes life easy.

You could argue that we need to rename each item's folder with a Guid or something, so it becomes unique, but then you would have to write another handler, parsing and updating all your content, as you'd need to update all media-paths in grid-content referencing your media.

If you always migrate in one direction, this becomes a non-issue, as their will be (almost) no name-collisions on the media folder.

Name-collisions?

Suppose someone manually added tree.jpg to Prod directly.
It is possible that - after transfer from Dev - we now have media/1028/flower.jpg and media/1028/tree.jpg in the same folder on Azure storage. I can live with that.

The only scenario where this becomes a problem is, if you uploaded a different flower.jpg and the blob is overwritten when you transfer the one from Dev.

In my case, this is extremely unlikely, but it's good to at least be aware of this possibility.

No comments:

Post a Comment