Skip to main content

Publish Binaries to Mapped Structure Groups

Today's TBB of the Week comes from the high demand in the field to publish binary assets to different mapped Structure Groups. By default SDL Tridion offers two ways of publishing binaries:
  1. All binaries publish to a folder defined in your Publication properties;
  2. All binaries rendered by a given template publish to a folder corresponding to a given Structure Group;
In my view, both cases are terrible, over-simplified and not representing a real use-case. Nobody in the field wants all binaries in one folder and nobody separates binary locations by template. Instead, everybody wants a mapping mechanism that takes a binary and publishes it to a given folder, defined by a Structure Group, and this mapping is done using some kind of metadata.

More often than not, the metadata is the TCM Folder location of the Multimedia Component. I have seen this implemented numerous times. So the solution to publish binaries to a given location implies finding a mapping from a TCM Folder to a Structure Group.

Normally, a Multimedia Component resides in a Folder, so that is given. However, the corresponding Structure Group does not exist. So, first, the Structure Group structure has to be created (this will be the subject of a future post). Typically we write an event system that creates the Structure Group structure upon the Multimedia Component save. The structure of these Structure Groups follows 1-to-1 the structure of TCM Folders where the MMC resides in.

Second, we publish the binary to the mapped Structure Group. Let's assume the Structure Group has been created and what we need is to find it. This is the goal of today's TBB, which was originally written by my colleague Eric Huiza, so credits go to him. Hope you understand the code :)

Description

Name
Publish Binary TBB
Type
·    Template in .NET Assembly
Description
Used to:
·    Publishes a Binary to a given Structure Group;
Notes:
This TBB publishes the current Component.
It expects the current Component in the Package is a Multimedia Component.
It publishes the Component to a Structure Group that is mapped to the Multimedia Component’s Folder location.
The Structure Group structure must exist before hand (created by event system) – this TBB will not create any SGs.
Parameters
TBB uses the following items in the Package:
·    AssetRootFolder – TcmUri of the Folder acting as root for the binary assets;
·    AssetRootStructureGroup – TcmUri of the Structure Group acting as root for binary assets;
Applicable to
Component Templates

This TBB goes well with Read Configuration Items TBB - used to configure the AssetRootFolder and AssetRootStructureGroup parameters.

The Code

In its purest form, this TBB does nothing more than calling a utility method:

[TcmTemplateTitle("Publish Binary TBB")]
public class PublishBinary : ITemplate {

    public void Transform(Engine engine, Package package) {
        Item componentItem = package.GetByName(Package.ComponentName);
        Component component = engine.GetObject(componentItem) as Component;

        if (component.BinaryContent != null) {
            GenericUtils utils = new GenericUtils();
            utils.PublishBinary(engine, package, component);
        }
    }
}

The real implementation is in the utility method PublishBinary:

public Binary PublishBinary(Engine engine, Package package, Component multimediaComponent) {
    TcmUri origUri = multimediaComponent.Id;
    TcmUri localUri = engine.LocalizeUri(origUri);
    if (!origUri.Equals(localUri)) {
        multimediaComponent = engine.GetObject(localUri) as Component;
    }

    BinaryContent binaryContent = multimediaComponent.BinaryContent;
    if (binaryContent == null) {
        return null;
    }

    Session session = engine.GetSession();
    TcmUri rootFolderUri = new TcmUri(package.GetValue("AssetRootFolder"));
    TcmUri rootStructureGroupUri = new TcmUri(package.GetValue("AssetRootStructureGroup"));

    Folder rootFolder = new Folder(rootFolderUri, session);
    StructureGroup rootStructureGroup = new StructureGroup(rootStructureGroupUri, session);

    Stack<string> structureGroupsLookUp = new Stack<string>();
    PopulateStructureGroupsForLookUp((Folder)multimediaComponent.OrganizationalItem, rootFolder, structureGroupsLookUp);

    OrganizationalItemItemsFilter structureGroupsFilter = new OrganizationalItemItemsFilter(session);
    structureGroupsFilter.Recursive = false;
    structureGroupsFilter.ItemTypes = new Tridion.ContentManager.ItemType[] { ItemType.StructureGroup };

    StructureGroup publishStructureGroup = GetStructureGroupForPublication(rootStructureGroup, structureGroupsLookUp, structureGroupsFilter);
    Binary binary = engine.PublishingContext.RenderedItem.AddBinary(multimediaComponent, publishStructureGroup,
        Regex.Replace(binaryContent.Filename, "\\W", string.Empty));

    return binary;
}

The logic first populates (finds) the mapped Structure Groups corresponding to each element in the binary's Folder path. The implementation is here:

public void PopulateStructureGroupsForLookUp(Folder currentFolder, Folder rootFolder, Stack<string> structureGroupsLookUp) {
    if (currentFolder != null && currentFolder.Id != rootFolder.Id) {
        structureGroupsLookUp.Push(currentFolder.Title);
        PopulateStructureGroupsForLookUp((Folder)currentFolder.OrganizationalItem, rootFolder, structureGroupsLookUp);
    }
}

Note: the code above does not check for invalid characters in the Folder Title. Such characters might prevent the identification of the right Structure Group. For example, characters not allowed in a folder/file name by the operating system.

Second, the mapped Structure Group is used to publish to:

public StructureGroup GetStructureGroupForPublication(StructureGroup rootStructureGroup, Stack<string> structureGroupsLookUp, OrganizationalItemItemsFilter filter) {
    while (structureGroupsLookUp.Count > 0) {
        string structureGroupName = structureGroupsLookUp.Pop();

        StructureGroup childStructureGroup = rootStructureGroup.GetItems(filter).
            Cast<StructureGroup>().
            Where(w => w.Title.Equals(structureGroupName)).
            FirstOrDefault();

        if (childStructureGroup == null) {
            break;
        } else {
            rootStructureGroup = childStructureGroup;
        }
    }

    return rootStructureGroup;
}

A strange side-effect of this code (may be seen as a feature, actually) is that in the case when the mapped Structure Group does not exist (for some reason), it will in fact use the closest existing Structure Group relative to the missing desired SG.


Comments

Anonymous said…
thank you for sharing ...
Is it possible to publish binary to a folder structure rather structure group.
Add binary methods accepts parameter of structure group only..
I need your suggestion on publishing to folder structure.
Please suggest.
Anonymous said…
Addition to my question... need to move all .gifs to GIFfolder and rest other type to MISCfolder
Mihai Cădariu said…
Using the provided Tridion API, you can only publish to a Structure Group. That in turn maps to a folder on the file system of the Presentation Server.

However, if you are looking for the ability to publish to a Tridion Folder, that's not possible.

There are ways to integrate/extend the logic, but it would be a very specific custom solution.
Mihai Cădariu said…
For publishing your GIFs to a certain folder, you will need to map a particular Structure Group for all your GIFs.

For example, you can do that programmatically by looking at the Multimedia Component filename extension and map it to a certain Structure Group. Have a look at my other post as well for an example mapping logic http://yatb.mitza.net/2013/01/event-system-to-create-mapped-structure.html
Anonymous said…
I have created structure group in the parent publication. When I publish a binary from the relevant child publication I am not able to publish the binary to the in herited structure group in the child.
Please suggest is do able or any solution for this..
Mihai Cădariu said…
There is no limitation with publishing to an inherited SG.

Make sure you are in fact publishing from the correct Publication and that you are referring the SG within that particular Publication.
Unknown said…
Hi Mihai

Thanks for the article. I have a question, is it possible to override the publication path while calling the AddBinary API. In my case it always uses the path mentioned in the publication metadata.

Thanks
Anonymous said…
Hi Mihai,

Thank you for the information.

I came accross a scenario and I wanted to share with you. Please suggest if you have answers

My TBB creates a structure group and move the binaries into it if the SG already exist for the second time it just moves the binaries. Incase if I delete the structure and re try the publish my TBB again create a structure group with the same expected name and try to move the binaries and this time it thows an error "Attempting to deploy a binary xx to a different location while still in use by deployeed items that are not being redeployed in the transaction. ".
Mihai Cădariu said…
That deployer error only happens if the Directory of the SGs have somehow changed. Check the path of the Binary in the CD DB or the path on file system and compare it to your path given by the 'newly created SGs'. Somewhere there must be a difference. Make sure that the SG path matches identically the Binary path.
Mihai Cădariu said…
Hi Vinoth -- late reply, I know :)

When publishing to an SG using AddBinary, the binary will placed under the root path and root URL specified in the Publication properties. There is no way to change that from the API (unless you go in modifying it in some Deployer/Storage extension).

The behaviour makes sense -- you are actually publishing to the directory defined by the SG. That includes the Publication path from Publication properties (and Publication URL if you are looking at the URLs).

Popular posts from this blog

Toolkit - Dynamic Content Queries

This post if part of a series about the  File System Toolkit  - a custom content delivery API for SDL Tridion. This post presents the Dynamic Content Query capability. The requirements for the Toolkit API are that it should be able to provide CustomMeta queries, pagination, and sorting -- all on the file system, without the use third party tools (database, search engines, indexers, etc). Therefore I had to implement a simple database engine and indexer -- which is described in more detail in post Writing My Own Database Engine . The querying logic does not make use of cache. This means the query logic is executed every time. When models are requested, the models are however retrieved using the ModelFactory and those are cached. Query Class This is the main class for dynamic content queries. It is the entry point into the execution logic of a query. The class takes as parameter a Criterion (presented below) which triggers the execution of query in all sub-criteria of a Criterio

A DD4T.net Implementation - Custom Binary Publisher

The default way to publish binaries in DD4T is implemented in class DD4T.Templates.Base.Utils.BinaryPublisher and uses method RenderedItem.AddBinary(Component) . This produces binaries that have their TCM URI as suffix in their filename. In my recent project, we had a requirement that binary file names should be clean (without the TCM URI suffix). Therefore, it was time to modify the way DD4T was publishing binaries. The method in charge with publishing binaries is called PublishItem and is defined in class BinaryPublisher . I therefore extended the BinaryPublisher and overrode method PublishItem. public class CustomBinaryPublisher : BinaryPublisher { private Template currentTemplate; private TcmUri structureGroupUri; In its simplest form, method PublishItem just takes the item and passes it to the AddBinary. In order to accomplish the requirement, we must specify a filename while publishing. This is the file name part of the binary path of Component.BinaryConten

Scaling Policies

This post is part of a bigger topic Autoscaling Publishers in AWS . In a previous post we talked about the Auto Scaling Groups , but we didn't go into details on the Scaling Policies. This is the purpose of this blog post. As defined earlier, the Scaling Policies define the rules according to which the group size is increased or decreased. These rules are based on instance metrics (e.g. CPU), CloudWatch custom metrics, or even CloudWatch alarms and their states and values. We defined a Scaling Policy with Steps, called 'increase_group_size', which is triggered first by the CloudWatch Alarm 'Publish_Alarm' defined earlier. Also depending on the size of the monitored CloudWatch custom metric 'Waiting for Publish', the Scaling Policy with Steps can add a difference number of instances to the group. The scaling policy sets the number of instances in group to 1 if there are between 1000 and 2000 items Waiting for Publish in the queue. It also sets the