Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blob support for non image files #53

Open
thecaptncode opened this issue Oct 17, 2021 · 5 comments
Open

Blob support for non image files #53

thecaptncode opened this issue Oct 17, 2021 · 5 comments

Comments

@thecaptncode
Copy link

Is there a way to use an IBlobProvider to return a non-image file such as a PDF to the browser? I have set one up to read varbinary(max) from our SQL server and it works well displaying and resizing images but non-image documents do not appear to be working. It would be great if I could expand our IBlobProvider to handle those as well.

Thanks!
Greg

@lilith
Copy link
Member

lilith commented Oct 17, 2021 via email

@thecaptncode
Copy link
Author

We are storing uploaded attachments to digital documents in SQL Server and have been using ImageResizer to serve them up as well as serve and resize catalog images. This approach with disk caching allows us to improve performance over file servers, is very scalable and we can record statistics on retrievals.

I have an IBlobProverd now that can retrieve the stored images data. It is working well with images but I have not succeeded in getting non-image files like a PDF from the database.

@thecaptncode
Copy link
Author

@lilith - Is there any way to have the ImageFlow middleware use an IBlobProvider for non image file types?

@ezdavis1993
Copy link

@thecaptncode Could you share your code for retrieving stored images from SQL Server? I'm trying to do this and can't figure it out.

@thecaptncode
Copy link
Author

@ezdavis1993 Sure. Here is what I have. I'm sure there is room for improvement.

I wrote an IFileProvider wrapper for it so I can use it with UseStaticFiles as well to solve this issue. It seems to work but it definitely needs improvements like caching.

Hope it helps,
Greg

using Imazen.Common.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Threading.Tasks;

namespace ImageServer
{
    /// <summary>
    /// Register in ConfigureServices with: services.AddImageflowSqlBlobService(...);
    /// </summary>
    public class SqlBlobProvider : IBlobProvider
    {
        private readonly SqlBlobServiceOptions _options;
        private readonly ILogger<SqlBlobProvider> _logger;
        private static readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager = new();


        public SqlBlobProvider(SqlBlobServiceOptions options, ILogger<SqlBlobProvider> logger)
        {
            _options = options;
            _logger = logger;
            _logger.Log(LogLevel.Information, "Blob service starting");
        }

        public IEnumerable<string> GetPrefixes()
        {
            return _options.ProcessedPrefix;
        }

        public bool SupportsPath(string virtualPath)
        {
            _logger.Log(LogLevel.Information, $"Blob service received image with path: {virtualPath}");
            foreach (string prefix in _options.StaticPrefix)
            {
                if (virtualPath.StartsWith(prefix,
                    _options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
                {
                    return false;
                }
            }

            foreach (string prefix in _options.ProcessedPrefix)
            {
                if (virtualPath.StartsWith(prefix,
                    _options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
                {
                    return true;
                }
            }

            return false;
        }

        public async Task<IBlobData> Fetch(string virtualPath)
        {
            if (!SupportsPath(virtualPath))
            {
                _logger.Log(LogLevel.Information, $"Blob service doesn't support: {virtualPath}");
                return null;
            }

            _logger.Log(LogLevel.Information, $"Blob service fetchng: {virtualPath}");
            (string key, string file) = _options.ContainerKeyFilterFunction(virtualPath, _options);

            if (key != null)
            {
                try
                {
                    using (SqlConnection Conn = new SqlConnection(_options.ConnectionString, _options.Credential))
                    {
                        await Conn.OpenAsync();
                        using SqlCommand command = new("SELECT DocDat, DocObj FROM DOCUMENTS WHERE DocKey = @key and DocFnm = @file", Conn);
                        command.Parameters.Add("@key", SqlDbType.Char, 32).Value = key;
                        command.Parameters.Add("@file", SqlDbType.VarChar, 50).Value = file;

                        // The reader needs to be executed with the SequentialAccess behavior to enable network streaming
                        // Otherwise ReadAsync will buffer the entire BLOB into memory which can cause scalability issues or even OutOfMemoryExceptions
                        using SqlDataReader reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
                        if (await reader.ReadAsync())
                        {
                            if (!await reader.IsDBNullAsync(0))
                                return new SqlBlob(reader.GetDateTime(0), reader.GetStream(1));
                        }
                        await Conn.CloseAsync();
                    }
                }
                catch (Exception ex)
                {
                    string msg = $"Error occured fetching SQL blob \"{virtualPath}\".";
                    _logger.Log(LogLevel.Error, ex, msg);
                    throw new BlobMissingException(msg, ex);
                }
            }

            _logger.Log(LogLevel.Information, $"Blob service did not find: {key} / {file}");

            if (string.IsNullOrEmpty(_options.NotFoundImagePath))
                throw new BlobMissingException($"SQL blob \"{virtualPath}\" not found.");

            try
            {
                using FileStream noimage = File.Open(_options.NotFoundImagePath, FileMode.Open);
                return new SqlBlob(new DateTime(1601, 1, 1), noimage);
            }
            catch (Exception ex)
            {
                string msg = $"Error occured fetching 'not found' image \"{virtualPath}\".";
                _logger.Log(LogLevel.Error, ex, msg);
                throw new BlobMissingException(msg, ex);
            }
        }

        internal class SqlBlob : IBlobData
        {
            private readonly DateTime? DocDate = null;
            private readonly Stream DocStream = new RecyclableMemoryStream(_recyclableMemoryStreamManager);
            private bool _disposed = false;

            #region Constructor / Dispose / Finalizer

            /// <summary>
            /// Sql Blob results
            /// </summary>
            /// <param name="ModificationDate">Modification data of the document</param>
            /// <param name="Doc">Document stream</param>
            internal SqlBlob(DateTime ModificationDate, Stream Doc)
            {
                DocDate = ModificationDate;
                Doc.CopyTo(DocStream);
                Doc.Close();
                Doc.Dispose();
                DocStream.Position = 0;
            }

            /// <summary>
            /// Class dispose handler
            /// </summary>
            /// <param name="disposing">Dispose was explicitly called</param>
            protected void Dispose(bool disposing)
            {
                if (_disposed)
                {
                    return;
                }

                if (disposing)
                {
                    DocStream?.Dispose();
                }

                _disposed = true;
            }

            /// <summary>
            /// Dispose of SQL Blob class
            /// </summary>
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            /// <summary>
            /// SQL Blob class finalizer
            /// </summary>
            ~SqlBlob() => Dispose(false);

            #endregion Constructor / Dispose / Finalizer

            /// <summary>
            /// Does document exist?
            /// </summary>
            public bool? Exists => true;

            /// <summary>
            /// Last modification date of document
            /// </summary>
            public DateTime? LastModifiedDateUtc => DocDate;

            /// <summary>
            /// Open document
            /// </summary>
            /// <returns>Document contents stream</returns>
            public Stream OpenRead()
            {
                return DocStream;
            }
        }
    }

    public static class SqlBlobServiceExtensions
    {
        public static IServiceCollection AddImageflowSqlBlobService(this IServiceCollection services,
            SqlBlobServiceOptions options)
        {
            services.AddSingleton<IBlobProvider>((container) =>
            {
                ILogger<SqlBlobProvider> logger = container.GetRequiredService<ILogger<SqlBlobProvider>>();
                return new SqlBlobProvider(options, logger);
            });

            return services;
        }
    }

    public class SqlBlobServiceOptions
    {
        public string ConnectionString { get; init; }

        public SqlCredential Credential { get; init; }

        public string NotFoundImagePath { get; init; }

        public bool IgnorePrefixCase { get; init; }

        public List<string> ProcessedPrefix { get; init; }

        public List<string> StaticPrefix { get; init; }

        /// <summary>
        /// Can block container/key pairs by returning null
        /// </summary>
        public Func<string, SqlBlobServiceOptions, (string key, string file)> ContainerKeyFilterFunction = (virtualPath, options) =>
        {
            string path = null;
            foreach (string prefix in options.ProcessedPrefix)
            {
                if (virtualPath.StartsWith(prefix,
                    options.IgnorePrefixCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
                {
                    path = virtualPath[prefix.Length..].TrimStart('/');
                }
            }

            if (path == null)
                return (null, null);

            int indexOfSlash = path.IndexOf('/');
            if (indexOfSlash < 1) return (null, null);

            string key = path[..indexOfSlash];
            string file = path[(indexOfSlash + 1)..];

            return (key, file);
        };
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants