< Summary

Information
Class: Elsa.Http.Services.ZipManager
Assembly: Elsa.Http
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Http/Services/ZipManager.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 79
Coverable lines: 79
Total lines: 193
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 22
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
CreateAsync()0%2040%
LoadAsync()0%2040%
CreateZipArchiveAsync()0%7280%
CreateCachedZipBlobAsync()100%210%
CreateBlob(...)0%620%
GetDownloadableMetadata(...)0%2040%
GetTempFilePath()100%210%
Cleanup(...)100%210%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Http/Services/ZipManager.cs

#LineLine coverage
 1using System.IO.Compression;
 2using Elsa.Common;
 3using Elsa.Http.Options;
 4using FluentStorage.Blobs;
 5using Microsoft.Extensions.Logging;
 6using Microsoft.Extensions.Options;
 7
 8namespace Elsa.Http.Services;
 9
 10/// <summary>
 11/// Provides a helper service for zipping downloadable content.
 12/// </summary>
 13internal class ZipManager
 14{
 15    private readonly ISystemClock _clock;
 16    private readonly IFileCacheStorageProvider _fileCacheStorageProvider;
 17    private readonly IOptions<HttpFileCacheOptions> _fileCacheOptions;
 18    private readonly ILogger<ZipManager> _logger;
 19
 20    /// <summary>
 21    /// Initializes a new instance of the <see cref="ZipManager"/> class.
 22    /// </summary>
 023    public ZipManager(ISystemClock clock, IFileCacheStorageProvider fileCacheStorageProvider, IOptions<HttpFileCacheOpti
 24    {
 025        _clock = clock;
 026        _fileCacheStorageProvider = fileCacheStorageProvider;
 027        _fileCacheOptions = fileCacheOptions;
 028        _logger = logger;
 029    }
 30
 31    public async Task<(Blob, Stream, Action)> CreateAsync(
 32        ICollection<Func<ValueTask<Downloadable>>> downloadables,
 33        bool cache,
 34        string? downloadCorrelationId,
 35        string? downloadAsFilename = default,
 36        string? contentType = default,
 37        CancellationToken cancellationToken = default)
 38    {
 39        // Create a temporary file.
 040        var tempFilePath = GetTempFilePath();
 41
 42        // Create a zip archive from the downloadables.
 043        await CreateZipArchiveAsync(tempFilePath, downloadables, cancellationToken);
 44
 45        // Create a blob with metadata for resuming the download.
 046        var zipBlob = CreateBlob(tempFilePath, downloadAsFilename, contentType);
 47
 48        // If resumable downloads are enabled, cache the file.
 049        if (cache && !string.IsNullOrWhiteSpace(downloadCorrelationId))
 050            await CreateCachedZipBlobAsync(tempFilePath, downloadCorrelationId, downloadAsFilename, contentType, cancell
 51
 052        var zipStream = File.OpenRead(tempFilePath);
 053        return (zipBlob, zipStream, () => Cleanup(tempFilePath));
 054    }
 55
 56    /// <summary>
 57    /// Loads a cached zip blob for the specified download correlation ID.
 58    /// </summary>
 59    /// <param name="downloadCorrelationId">The download correlation ID.</param>
 60    /// <param name="cancellationToken">An optional cancellation token.</param>
 61    /// <returns>A tuple containing the blob and the stream.</returns>
 62    public async Task<(Blob, Stream)?> LoadAsync(string downloadCorrelationId, CancellationToken cancellationToken = def
 63    {
 064        var fileCacheStorage = _fileCacheStorageProvider.GetStorage();
 065        var fileCacheFilename = $"{downloadCorrelationId}.tmp";
 066        var blob = await fileCacheStorage.GetBlobAsync(fileCacheFilename, cancellationToken);
 67
 068        if (blob == null)
 069            return null;
 70
 71        // Check if the blob has expired.
 072        var expiresAt = DateTimeOffset.Parse(blob.Metadata["ExpiresAt"]);
 73
 074        if (_clock.UtcNow > expiresAt)
 75        {
 76            // File expired. Try to delete it.
 77            try
 78            {
 079                await fileCacheStorage.DeleteAsync(blob.FullPath, cancellationToken);
 080            }
 081            catch (Exception e)
 82            {
 083                _logger.LogWarning(e, "Failed to delete expired file {FullPath}", blob.FullPath);
 084            }
 85
 086            return null;
 87        }
 88
 089        var stream = await fileCacheStorage.OpenReadAsync(blob.FullPath, cancellationToken);
 090        return (blob, stream);
 091    }
 92
 93    /// <summary>
 94    /// Creates a zip archive from the specified <see cref="Downloadable"/> instances.
 95    /// </summary>
 96    private async Task CreateZipArchiveAsync(string filePath, IEnumerable<Func<ValueTask<Downloadable>>> downloadables, 
 97    {
 098        var currentFileIndex = 0;
 99
 100        // Write the zip archive to the temporary file.
 0101        await using var tempFileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read, buf
 102
 0103        using var zipArchive = new ZipArchive(tempFileStream, ZipArchiveMode.Create, true);
 0104        foreach (var downloadableFunc in downloadables)
 105        {
 0106            var downloadable = await downloadableFunc();
 0107            var entryName = !string.IsNullOrWhiteSpace(downloadable.Filename) ? downloadable.Filename : $"file-{currentF
 0108            var entry = zipArchive.CreateEntry(entryName);
 0109            var fileStream = downloadable.Stream;
 0110            await using var entryStream = entry.Open();
 0111            await fileStream.CopyToAsync(entryStream, cancellationToken);
 0112            await entryStream.FlushAsync(cancellationToken);
 0113            entryStream.Close();
 0114            currentFileIndex++;
 0115        }
 0116    }
 117
 118    /// <summary>
 119    /// Creates a cached zip blob for the specified file.
 120    /// </summary>
 121    /// <param name="localPath">The full path of the file to upload.</param>
 122    /// <param name="downloadCorrelationId">The download correlation ID.</param>
 123    /// <param name="downloadAsFilename">The filename to use when downloading the file.</param>
 124    /// <param name="contentType">The content type of the file.</param>
 125    /// <param name="cancellationToken">An optional cancellation token.</param>
 126    private async Task CreateCachedZipBlobAsync(string localPath, string downloadCorrelationId, string? downloadAsFilena
 127    {
 0128        var fileCacheStorage = _fileCacheStorageProvider.GetStorage();
 0129        var fileCacheFilename = $"{downloadCorrelationId}.tmp";
 0130        var expiresAt = _clock.UtcNow.Add(_fileCacheOptions.Value.TimeToLive);
 0131        var cachedBlob = CreateBlob(fileCacheFilename, downloadAsFilename, contentType, expiresAt);
 0132        await fileCacheStorage.WriteFileAsync(fileCacheFilename, localPath, cancellationToken);
 0133        await fileCacheStorage.SetBlobAsync(cachedBlob, cancellationToken: cancellationToken);
 0134    }
 135
 136    /// <summary>
 137    /// Creates a blob for the specified file.
 138    /// </summary>
 139    /// <param name="fullPath">The full path of the file.</param>
 140    /// <param name="downloadAsFilename">The filename to use when downloading the file.</param>
 141    /// <param name="contentType">The content type of the file.</param>
 142    /// <param name="expiresAt">The date and time at which the file expires.</param>
 143    /// <returns>The blob.</returns>
 144    private Blob CreateBlob(string fullPath, string? downloadAsFilename, string? contentType, DateTimeOffset? expiresAt 
 145    {
 0146        (downloadAsFilename, contentType) = GetDownloadableMetadata(downloadAsFilename, contentType);
 147
 0148        var now = _clock.UtcNow;
 149
 0150        var blob = new Blob(fullPath)
 0151        {
 0152            Metadata =
 0153            {
 0154                ["ContentType"] = contentType,
 0155                ["Filename"] = downloadAsFilename
 0156            },
 0157            CreatedTime = now,
 0158            LastModificationTime = now
 0159        };
 160
 0161        if(expiresAt.HasValue)
 0162            blob.Metadata["ExpiresAt"] = expiresAt.Value.ToString("O");
 163
 0164        return blob;
 165    }
 166
 167    private (string downloadAsFilename, string contentType) GetDownloadableMetadata(string? contentType, string? downloa
 168    {
 0169        contentType = !string.IsNullOrWhiteSpace(contentType) ? contentType : System.Net.Mime.MediaTypeNames.Application
 0170        downloadAsFilename = !string.IsNullOrWhiteSpace(downloadAsFilename) ? downloadAsFilename : "download.zip";
 171
 0172        return (downloadAsFilename, contentType);
 173    }
 174
 175    private string GetTempFilePath()
 176    {
 0177        var tempFileName = Path.GetRandomFileName();
 0178        var tempFilePath = Path.Combine(_fileCacheOptions.Value.LocalCacheDirectory, tempFileName);
 0179        return tempFilePath;
 180    }
 181
 182    private void Cleanup(string filePath)
 183    {
 184        try
 185        {
 0186            File.Delete(filePath);
 0187        }
 0188        catch (Exception e)
 189        {
 0190            _logger.LogWarning(e, "Failed to delete temporary file {TempFilePath}", filePath);
 0191        }
 0192    }
 193}