Skip to content

Augment Get-IshPublicationOutputData with protocol OpenApiWithOpenIdConnect implementation #245

Description

@ddemeyer

Problem

Get-IshPublicationOutputData downloads publication output blobs over SOAP using a byte-level chunk loop:

  1. PublicationOutput25.GetDataObjectInfoByIshLngRef(lngRef) — fetches file size, extension, and edition token.
  2. A for loop from offset = 0 to ishDataObject.Size in steps of IshSession.ChunkSize, each iteration calling PublicationOutput25.GetNextDataObjectChunkByIshLngRef(...).

Each chunk is a separate SOAP round-trip. For large publication outputs (ZIP archives, HTML Help deliverables, etc.) this produces tens to hundreds of sequential HTTP calls, making download slow — especially over high-latency connections.

The compatibility table confirms API25.PublicationOutput.GetNextDataObjectChunkByIshLngRef is replaced by API30.GetPublicationContentByLanguageCardId, which is supported from Tridion Docs 15.1 onward.

Solution

The OpenAPI 3.0 server exposes a single-shot streaming endpoint:

GET /v3/Publications/ByLanguageCardId/{languageCardId}/Content
→ 200 application/octet-stream

Generated NSwag client method:

IshSession.OpenApiISH30Client.GetPublicationContentByLanguageCardIdAsync(long languageCardId)
// returns Task<FileResponse> where FileResponse.Stream is the raw binary body

This replaces the entire chunk loop with one HTTP request whose response body is streamed directly to disk via Stream.CopyTo(FileStream). When the REST path is not applicable (wrong protocol or server too old), the cmdlet silently falls back to the existing SOAP chunk loop — no error, no warning.

Scope

  • In scope: GetIshPublicationOutputData.cs — add capability-gated OpenApiWithOpenIdConnect fast path.
  • Out of scope: GetIshPublicationOutput.cs (metadata retrieval) — no REST equivalent for RetrieveMetadata / RetrieveMetadataByIshLngRefs is available in the current OpenAPI spec.

Implementation Plan

Replace the flat SOAP block in ProcessRecord() with a capability-gated if/else

The switch (IshSession.Protocol) pattern used elsewhere for hard protocol dispatch does not apply here — this is a silent fallback, so an if with a compound condition is clearer:

if (IshSession.Protocol == Enumerations.Protocol.OpenApiWithOpenIdConnect &&
    (IshSession.ServerIshVersion.MajorVersion > 15 ||
     (IshSession.ServerIshVersion.MajorVersion == 15 && IshSession.ServerIshVersion.MinorVersion >= 1)))
{
    // Fast path: single streaming HTTP GET — no chunking.
    // File extension is still sourced from GetDataObjectInfoByIshLngRef to guarantee
    // an identical filename regardless of protocol. Content-Disposition is not used
    // because the cmdlet is a public API and output must be consistent across protocols.
    string xmlIshDataObject = IshSession.PublicationOutput25.GetDataObjectInfoByIshLngRef(lngRef);
    XmlDocument xmlIshDataObjectDocument = new XmlDocument();
    xmlIshDataObjectDocument.LoadXml(xmlIshDataObject);
    IshDataObject ishDataObject = new IshDataObject(
        (XmlElement)xmlIshDataObjectDocument.SelectSingleNode("ishdataobjects/ishdataobject"));
    string tempFilePath = FileNameHelper.GetDefaultPublicationOutputFileName(
        tempLocation, ishObject, ishDataObject.FileExtension);
    WriteDebug($"Writing lngRef[{lngRef}] via OpenAPI stream to [{tempFilePath}] {++current}/{ishObjects.Length}");
    using (var fileResponse = IshSession.OpenApiISH30Client
               .GetPublicationContentByLanguageCardIdAsync(lngRef)
               .GetAwaiter().GetResult())
    using (FileStream fs = File.Create(tempFilePath))
    {
        // IshSession.ChunkSize is reused as the CopyTo buffer size: it keeps memory usage
        // bounded for files up to 1 GB+ while avoiding unnecessary syscall overhead.
        fileResponse.Stream.CopyTo(fs, IshSession.ChunkSize);
    }
    fileInfo.Add(new FileInfo(tempFilePath));
}
else
{
    // Fallback: SOAP chunk loop — used for all SOAP protocols, explicit SOAP protocol
    // choice on OpenApiWithOpenIdConnect sessions, and servers older than 15.1.
    string xmlIshDataObject = IshSession.PublicationOutput25.GetDataObjectInfoByIshLngRef(lngRef);
    XmlDocument xmlIshDataObjectDocument = new XmlDocument();
    xmlIshDataObjectDocument.LoadXml(xmlIshDataObject);
    IshDataObject ishDataObject = new IshDataObject(
        (XmlElement)xmlIshDataObjectDocument.SelectSingleNode("ishdataobjects/ishdataobject"));
    string tempFilePath = FileNameHelper.GetDefaultPublicationOutputFileName(
        tempLocation, ishObject, ishDataObject.FileExtension);
    WriteDebug($"Writing lngRef[{lngRef}] to [{tempFilePath}] {++current}/{ishObjects.Length}");
    using (FileStream fs = File.Create(tempFilePath))
    {
        for (int offset = 0; offset < ishDataObject.Size; offset += IshSession.ChunkSize)
        {
            int size = IshSession.ChunkSize;
            long offsetCount = offset;
            var response = IshSession.PublicationOutput25.GetNextDataObjectChunkByIshLngRef(
                new PublicationOutput25ServiceReference.GetNextDataObjectChunkByIshLngRefRequest(
                    lngRef, ishDataObject.Ed, offsetCount, size));
            offsetCount = response.offSet;
            size = response.size;
            fs.Write(response.bytes, 0, size);
        }
    }
    fileInfo.Add(new FileInfo(tempFilePath));
}

Build validation

dotnet restore Source/ISHRemote/ISHRemote.sln
dotnet build --no-restore --no-incremental --configuration release Source/ISHRemote/ISHRemote.sln
Invoke-ScriptAnalyzer -Path Source/ISHRemote/Trisoft.ISHRemote/Scripts -Recurse

Files to Change

File Change
Cmdlets/PublicationOutput/GetIshPublicationOutputData.cs Replace flat SOAP block with capability-gated if/else in ProcessRecord()
Doc/ReleaseNotes*.md Add enhancement bullet

Compatibility table reference

Method 14.0.x 15.0.x 15.1.x 15.2.x 15.3.x Replaced by
API25.PublicationOutput.GetNextDataObjectChunkByIshLngRef S S S S S API30.GetPublicationContentByLanguageCardId
API30.GetPublicationContentByLanguageCardId - - S S S (this is the replacement)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions