How to create an external blob provider

From Sense/Net Wiki
Jump to: navigation, search
  •  
  •  
  •  
  •  
  • 100%
  • 6.5.5
  • Enterprise
  • Community
  • Planned

Overview

This article provides an example implementation of the IBlobProvider interface. This particular implementation (LocalDiskChunkBlobProvider) saves binary data to files of predefined size inside a folder, each file containing one chunk. Please note that an optimal chunk size has not been tested for and the optimal size may very well depend on the particular system and its resources. The underlying IBlobProvider interface and in-depth description as to the purpose of the methods are explained in detail in the article Blob provider. As a context is saved for every binary stream in the Files table, it is very well possible to set an individual chunk size for every stream. So, one could come up with varying chunk sizes for different size ranges if deemed necessary. Consequently, it is also possible to adjust the chunk size any time after deployment.

This particular implementation is not an enterprise level solution and comes as-is with no warranty whatsoever.

The 10-byte chunk size is merely for testing purposes, it is highly recommended to have it changed to at least several magnitudes higher (e.g., 64000 for ~64kB or 1000000 for ~1MB).


Example

using SenseNet.ContentRepository.Storage.Data.SqlClient.Blob;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using SenseNet.Diagnostics;
 
namespace SenseNet.ContentRepository.Tests.Data
{
    class LocalDiskChunkBlobProvider : IBlobProvider
    {
        private static StringBuilder _trace = new StringBuilder();
        public static string Trace { get { return _trace.ToString(); } }
 
 
        private static int _chunkByteSize = 10; //ten bytes by default
        public static int ChunkByteSize { get;set; }
 
        internal class LocalDiskChunkBlobProviderData
        {
            public Guid Id { get; set; }
            public string Trace { get; set; }
            public int ChunkSize { get; set; }
 
        }
 
        private static string _rootDirectory;
        public LocalDiskChunkBlobProvider()
        {
            _rootDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data\\" + GetType().Name);
            if (Directory.Exists(_rootDirectory))
            {
                foreach (var path in Directory.GetFiles(_rootDirectory))
                    System.IO.File.Delete(path);
            }
            else
            {
                Directory.CreateDirectory(_rootDirectory);
            }
        }
 
        public static void _ClearTrace()
        {
            _trace.Clear();
        }
 
        /*========================================================================== provider implementation */
 
        public object ParseData(string providerData)
        {
            return BlobStorageContext.DeserializeBlobProviderData<LocalDiskChunkBlobProviderData>(providerData);
        }
 
        public void Allocate(BlobStorageContext context)
        {
            //Create a guaranteed unique identifier that will
            //be used as the name of the new folder to store the chunks
            var id = Guid.NewGuid();
 
            //Pass the created id to the method that creates the actual
            //folder for the chunks
            CreateFolder(id);
 
            //Initialize the BlobProviderData with the unique id and the
            //set chunk size
            context.BlobProviderData = new LocalDiskChunkBlobProviderData { Id = id, ChunkSize = _chunkByteSize };
 
            //Optionally, make a note of this operation in the log
            SnTrace.Test.Write("LocalDiskChunkBlobProvider.Allocate: " + id);
        }
 
        public void Delete(BlobStorageContext context)
        {
            //By casting the BlobProviderData with the right type, 
            //we can acquire the id that is needed for the delete operation
            var id = ((LocalDiskChunkBlobProviderData)context.BlobProviderData).Id;
 
            //Call the delete method and pass the relevant id to remove all
            //unnecessary consituents of the previously stored content
            DeleteFolder(id);
 
            //Optionally, make a note of this operation in the log
            SnTrace.Test.Write("LocalDiskChunkBlobProvider.Delete: " + id);
        }
 
        public Stream GetStreamForRead(BlobStorageContext context)
        {
            //By casting the BlobProviderData, arriving with the context,
            //with the right type, we can create the required input
            //parameter for the following method call
            var providerData = (LocalDiskChunkBlobProviderData)context.BlobProviderData;
 
            //Retrieve and return the read-only stream assembled from
            //all chunks. Necessary parameters can be extracted from the context.
            return new FileSystemChunkReaderStream(providerData, context.Length, GetDirectoryPath(providerData.Id));
        }
 
        public Stream GetStreamForWrite(BlobStorageContext context)
        {
            //By casting the BlobProviderData, arriving with the context,
            //with the right type, we can create the required input
            //parameter for the following method call
            var providerData = (LocalDiskChunkBlobProviderData)context.BlobProviderData;
 
            //Retrieve and return the writable stream assembled from all
            //chunks. Necessary parameters can be extracted from the context.
            return new FileSystemChunkWriterStream(providerData, context.Length, GetDirectoryPath(providerData.Id));
        }
 
        public Stream CloneStream(BlobStorageContext context, Stream stream)
        {
            //Optionally, make a note of this operation in the log
            SnTrace.Test.Write("LocalDiskChunkBlobProvider.CloneStream: {0}", context.FileId);
 
            //Examine the incoming stream against the required type.
            //If it is not a match then throw an exception
            if (!(stream is FileSystemChunkReaderStream))
                throw new InvalidOperationException("Stream must be a FileSystemChunkReaderStream in the local disk provider.");
 
            //By taking advantage of already existing logic,
            //call GetStreamForRead with the incoming context
            return GetStreamForRead(context);
        }
 
        public void Write(BlobStorageContext context, long offset, byte[] buffer)
        {
            //By casting the BlobProviderData, arriving with the
            //context, with the right type, we can create the
            //providerdata and use its fields and properties
            //to retrieve necessary data
            var providerData = (LocalDiskChunkBlobProviderData)context.BlobProviderData;
 
            //Set the appropriate id so chunks will be written
            //in the right folder
            var originalFileId = providerData.Id;
 
            //Set appropriate chunk size as, inside a particular
            //folder, chunks must have the same length
            var originalChunkSize = providerData.ChunkSize;
 
            //Determine the length of the incoming binary
            var currentBlobSize = context.Length;
 
            //Check if the incoming offset (the point of start of write)
            //and the length of the binary data to be written are
            //consistent with settings (e.g., one may not start writing
            //content mid-chunk)
            AssertValidChunks(currentBlobSize, originalChunkSize, offset, buffer.Length);
 
            //The following piece of code takes apart the incoming 
            //byte array according to the set chunk size and writes
            //every chunk into a separate file
            var length = buffer.Length;
            var sourceOffset = 0;
            while (length > 0)
            {
                var chunkIndex = Convert.ToInt32( (offset / originalChunkSize) );
                var currentChunkLength = Math.Min(originalChunkSize, length);
                var bytes = new byte[currentChunkLength];
                Array.ConstrainedCopy(buffer, sourceOffset, bytes, 0, currentChunkLength);
 
                WriteChunk(((LocalDiskChunkBlobProviderData)context.BlobProviderData).Id, chunkIndex, bytes);
 
                length -= bytes.Length;
                offset += originalChunkSize;
                sourceOffset += originalChunkSize;
            }
 
            //Optionally, make a note of this operation in the log
            SnTrace.Test.Write("LocalDiskChunkBlobProvider.Write");
        }
 
        /*================================================================================== utility methods */
 
        private static string GetDirectoryPath(Guid id)
        {
            //Concatenate the root folder name with the incoming
            //id in order to return a complete path
            return Path.Combine(_rootDirectory, id.ToString());
        }
 
        private static string GetFilePath(Guid id, int chunkIndex)
        {
            //Concatenate the complete path with the incoming
            //chunkIndex in order to return the full path of the chunk file
            return Path.Combine(GetDirectoryPath(id), chunkIndex.ToString());
        }
 
        public static void WriteChunk(Guid id, int chunkIndex, byte[] bytes)
        {
            //Use existing logic to write chunk
            using (var stream = new FileStream(GetFilePath(id, chunkIndex), FileMode.OpenOrCreate))
                stream.Write(bytes, 0, bytes.Length);
        }
 
        private void CreateFolder(Guid id)
        {
            //Create target folder for the new content using the
            //incoming id (that is based on a guaranteed unique
            //identifier, or GUID)
            Directory.CreateDirectory(GetDirectoryPath(id));
        }
 
        private void AssertValidChunks(long currentBlobSize, int chunkSize, long offset, int size)
        {
            //Make sure there is no remainder when dividing
            //the offset (start of write) with the set chunk
            //size. If there is, the operation is illegal,
            //as write may not occur mid-chunk
            if (offset % chunkSize > 0)
                throw new Exception("Invalid offset");
        }
 
        private void DeleteFolder(Guid id)
        {
            //Retrieve the full directory path using the incoming id
            var myPath = GetDirectoryPath(id);
 
            //Only do the operation if the folder actually exists
            if (System.IO.Directory.Exists(myPath))
            {
                System.IO.DirectoryInfo dirinfo = new DirectoryInfo(myPath);
 
                //List and then remove all files under the folder to be
                //deleted before attempting to remove the folder itself
                foreach (FileInfo file in dirinfo.GetFiles())
                {
                    file.Delete();
                }
 
                //Finally, remove the folder
                foreach (DirectoryInfo dir in dirinfo.GetDirectories())
                {
                    dir.Delete(true);
                }
            }
        }
 
 
    }
}

Reader stream example

This class is a simple example related to the file system blob provider above.

using System;
using System.IO;
 
namespace SenseNet.ContentRepository.Tests.Data
{
    internal class FileSystemChunkReaderStream : Stream
    {
        private readonly string _directoryPath;
 
        private readonly int _chunkSize;
        private int _currentChunkIndex;
 
        private int _loadedChunkIndex = -1;
        private byte[] _loadedBytes;
 
        public FileSystemChunkReaderStream(LocalDiskChunkBlobProvider.LocalDiskChunkBlobProviderData providerData, long fullSize, string directoryPath)
        {
            _directoryPath = directoryPath;
            Length = fullSize;
            _chunkSize = providerData.ChunkSize;
        }
 
        public override bool CanRead => true;
        public override bool CanSeek => true;
        public override bool CanWrite => false;
        public override long Length { get; }
 
        private long __position;
        public override long Position
        {
            get { return __position; }
            set
            {
                _currentChunkIndex = (value / _chunkSize).ToInt();
                __position = value;
            }
        }
 
        public override void Flush()
        {
            throw new NotSupportedException();
        }
 
        public override int Read(byte[] buffer, int offset, int count)
        {
            count = Math.Min((Length - Position), count).ToInt();
 
            var totalCount = 0;
            while (count > 0)
            {
                var chunkOffset = (long)_chunkSize * _currentChunkIndex;
 
                if (_currentChunkIndex != _loadedChunkIndex)
                {
                    _loadedBytes = LoadChunk(_currentChunkIndex);
                    if (_loadedBytes == null)
                        throw new ApplicationException($"Chunk not found. ChunkIndex:{_currentChunkIndex}");
 
                    _loadedChunkIndex = _currentChunkIndex;
                }
                var copiedCount = CopyBytes(_loadedBytes, (Position - chunkOffset).ToInt(), buffer, offset, count);
 
                Position += copiedCount;
                offset += copiedCount;
                count -= copiedCount;
                totalCount += copiedCount;
                if (Position >= Length)
                    break;
            }
            return totalCount;
        }
 
        private byte[] LoadChunk(int chunkIndex)
        {
            var path = Path.Combine(_directoryPath, chunkIndex.ToString());
 
            byte[] bytes;
 
            using (var stream = new FileStream(path, FileMode.Open))
            {
                var streamLength = stream.Length.ToInt();
                bytes = new byte[streamLength];
                stream.Read(bytes, 0, streamLength);
            }
            return bytes;
        }
 
        private static int CopyBytes(byte[] source, int sourceOffset, byte[] target, int targetOffset, int expectedCount)
        {
            var availableSourceCount = source.Length - sourceOffset;
            var availableTargetCount = target.Length - targetOffset;
            var availableCount = Math.Min(Math.Min(availableSourceCount, availableTargetCount), expectedCount);
 
            Array.ConstrainedCopy(source, sourceOffset, target, targetOffset, availableCount);
 
            return availableCount;
        }
 
        public override long Seek(long offset, SeekOrigin origin)
        {
            var position = Position;
            switch (origin)
            {
                case SeekOrigin.Begin:
                    position = offset;
                    break;
                case SeekOrigin.Current:
                    position += offset;
                    break;
                case SeekOrigin.End:
                    position = Length - offset;
                    break;
            }
            if (position < 0 || position > Length - 1)
                throw new ApplicationException(String.Format("Invalid offset. Expected max:{0}, requested:{1}", Length, offset));
            Position = position;
            return position;
        }
 
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }
 
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }
    }
}

Writer stream example

This class is a simple example related to the file system blob provider above.

using System;
using System.IO;
 
namespace SenseNet.ContentRepository.Tests.Data
{
    internal class FileSystemChunkWriterStream : Stream
    {
        private readonly Guid _id;
        private readonly int _chunkSize;
 
        private int _currentChunkIndex;
        private int _currentChunkPosition;
        private byte[] _buffer;
        private bool _flushIsNecessary;
 
        public FileSystemChunkWriterStream(LocalDiskChunkBlobProvider.LocalDiskChunkBlobProviderData providerData, long fullSize)
        {
            Length = fullSize;
            _chunkSize = providerData.ChunkSize;
            _id = providerData.Id;
        }
 
        public override bool CanRead => false;
        public override bool CanSeek => false;
        public override bool CanWrite => true;
 
        public override long Length { get; }
 
        private long __position;
        public override long Position
        {
            get { return __position; }
            set
            {
                // set Position value only through the private SetPosition method
                throw new NotSupportedException();
            }
        }
        private void SetPosition(long position)
        {
            _currentChunkIndex = (position / _chunkSize).ToInt();
            _currentChunkPosition = (position % _chunkSize).ToInt();
            __position = position;
        }
 
        public override void Flush()
        {
            // nothing to write to the db
            if (!_flushIsNecessary)
                return;
 
            var bytesToWrite = _currentChunkPosition;
            var chunkIndex = _currentChunkIndex;
 
            // If the current chunk position is 0, that means we are at the beginning of the
            // next chunk, so we have to write all bytes (chunk size) from the buffer using
            // the previous chunk index.
            if (_currentChunkPosition == 0)
            {
                bytesToWrite = _chunkSize;
                chunkIndex = _currentChunkIndex - 1;
            }
 
            byte[] bytes;
            if (bytesToWrite == _buffer.Length)
            {
                bytes = _buffer;
            }
            else
            {
                bytes = new byte[bytesToWrite];
                Array.ConstrainedCopy(_buffer, 0, bytes, 0, bytesToWrite);
            }
 
            LocalDiskChunkBlobProvider.WriteChunk(_id, chunkIndex, bytes);
 
            _flushIsNecessary = false;
        }
 
        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }
 
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }
 
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }
 
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException(nameof(buffer));
            if (offset < 0 || count < 0 || buffer.Length < offset + count)
                throw new ArgumentException(string.Format("Invalid write parameters: buffer length {0}, offset {1}, count {2}.",
                    buffer.Length, offset, count));
 
            // nothing to write
            if (count == 0)
                return;
 
            if (Position >= Length)
                throw new InvalidOperationException("Stream length exceeded.");
 
            // Initialize buffer here and not in the constructor 
            // to allocate memory only when it is needed.
            if (_buffer == null)
                _buffer = new byte[_chunkSize];
 
            var bytesToWrite = count;
            while (bytesToWrite > 0)
            {
                // if the inner buffer is already full, write it to the db
                if (_currentChunkPosition >= _chunkSize || _currentChunkPosition == 0 && _flushIsNecessary)
                {
                    Flush();
 
                    if (_currentChunkPosition >= _chunkSize)
                    {
                        // reset inner buffer position and move to the next chunk index
                        _currentChunkPosition = 0;
                        _currentChunkIndex++;
                    }
                }
 
                // we can only write so much bytes in one round as many slots are left in the inner buffer
                var maxBytesToWrite = Math.Min(bytesToWrite, _chunkSize - _currentChunkPosition);
 
                Array.ConstrainedCopy(buffer, offset, _buffer, _currentChunkPosition, maxBytesToWrite);
 
                bytesToWrite -= maxBytesToWrite;
                offset += maxBytesToWrite;
                _currentChunkPosition += maxBytesToWrite;
                _flushIsNecessary = true;
            }
 
            SetPosition(Position + count);
        }
 
        protected override void Dispose(bool disposing)
        {
            try
            {
                this.Flush();
            }
            finally
            {
                base.Dispose(disposing);
            }
        }
    }
}

Related links

References

There are no external references for this article.