Избегайте копирования сжатых данных при использовании DeflateStream

Предположим, мы дали API-функцию f(Stream s) для помещения двоичных данных, содержащихся в потоке, в базу данных. Я хочу поместить файл в базу данных, используя f, но я хочу сжать данные заранее. Поэтому я подумал, что могу сделать следующее:

var fileStream= File.OpenRead(path);
using(var dstream = new DeflateStream(fileStream, CompressionLevel.Optimal))
   f(dstream);

Но похоже DeflateStream только пишет в поток fileStream но не читает из него при сжатии. Во всех найденных примерах CopyTo Метод потока был использован для сжатия или распаковки. Но это будет означать, что я должен сохранить копию сжатых данных в памяти, прежде чем передать ее f например, вот так:

var memoryStream = new MemoryStream();
using(var fileStream= File.OpenRead(path)) 
  using(var dstream = new DeflateStream(memoryStream, CompressionLevel.Optimal)) {
    fileStream.CopyTo(dstream);
    memoryStream.Seek(0, SeekOrigin.Begin);
    f(memoryStream);
  }    

Есть ли способ избежать использования MemoryStream?

Обновление Ради настойчивости некоторых комментаторов я добавлю полный пример:

using System;
using System.IO;
using System.IO.Compression;

public class ThisWouldBeTheDatabaseClient {
  public void f(Stream s) {
    // some implementation I don't have access to
    // The only thing I know is that it reads data from the stream in some way.
    var buffer = new byte[10];
    s.Read(buffer,0,10);
  }
}

public class Program {
  public static void Main() {
    var dummyDatabaseClient = new ThisWouldBeTheDatabaseClient();
    var dataBuffer = new byte[1000];
    var fileStream= new MemoryStream( dataBuffer ); // would be "File.OpenRead(path)" in real case
    using(var dstream = new DeflateStream(fileStream, CompressionLevel.Optimal))
        dummyDatabaseClient.f(dstream);
  }
}

Операция чтения в фиктивной реализации f выдает исключение: InvalidOperationException: чтение из потока сжатия не поддерживается. Завершая обсуждение в комментариях, я предполагаю, что желаемое поведение невозможно с DeflateStream но есть альтернативы в сторонних библиотеках.

2 ответа

Решение

Ты можешь использовать SharpCompress за это. это DeflateStream позволяет читать сжатые данные на лету, что именно то, что вы хотите.

Вот полный пример, основанный на сэре Руфо:

using System;
using System.IO;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var dataBuffer = Enumerable.Range(1, 50000).Select(e => (byte)(e % 256)).ToArray();

        using (var dataStream = new MemoryStream(dataBuffer))
        {
            // Note: this refers to SharpCompress.Compressors.Deflate.DeflateStream                
            using (var deflateStream = new DeflateStream(dataStream, CompressionMode.Compress))
            {
                ConsumeStream(deflateStream);
            }
        }
    }

    public static void ConsumeStream(Stream stream)
    {
        // Let's just prove we can reinflate to the original data...
        byte[] data;
        using (var decompressed = new MemoryStream())
        {
            using (var decompressor = new DeflateStream(stream, CompressionMode.Decompress))
            {
                decompressor.CopyTo(decompressed);
            }
            data = decompressed.ToArray();
        }
        Console.WriteLine("Reinflated size: " + data.Length);
        int errors = 0;
        for (int i = 0; i < data.Length; i++)
        {
            if (data[i] != (i + 1) % 256)
            {
                errors++;
            }
        }
        Console.WriteLine("Total errors: " + errors);
    }
}

Или используя ваш пример кода:

using System;
using System.IO;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;

public class ThisWouldBeTheDatabaseClient {
  public void f(Stream s) {
    // some implementation I don't have access to
    // The only thing I know is that it reads data from the stream in some way.
    var buffer = new byte[10];
    s.Read(buffer,0,10);
  }
}

public class Program {
  public static void Main() {
    var dummyDatabaseClient = new ThisWouldBeTheDatabaseClient();
    var dataBuffer = new byte[1000];
    var fileStream= new MemoryStream( dataBuffer ); // would be "File.OpenRead(path)" in real case
    using(var dstream = new DeflateStream(
        fileStream, CompressionMode.Compress, CompressionLevel.BestCompression))
        dummyDatabaseClient.f(dstream);
  }
}

Это теперь не вызывает исключения и будет обслуживать сжатые данные.

DeflateStream это просто оболочка и нуждается в потоке для сжатых данных. Таким образом, вы должны использовать два потока.

Есть ли способ избежать использования MemoryStream?

Да.

Вам нужен поток для хранения временных данных без использования (слишком большого количества) памяти. Вместо того, чтобы использовать MemoryStream Вы можете использовать временный файл для этого.

Для ленивых людей (таких как я в первую очередь) давайте создадим класс, который будет вести себя в основном как MemoryStream

public class TempFileStream : FileStream
{
    public TempFileStream() : base(
        path: Path.GetTempFileName(),
        mode: FileMode.Open,
        access: FileAccess.ReadWrite,
        share: FileShare.None,
        bufferSize: 4096,
        options: FileOptions.DeleteOnClose | FileOptions.Asynchronous | FileOptions.Encrypted | FileOptions.RandomAccess)
    {
    }
}

Важная часть здесь FileOptions.DeleteOnClose который удалит временный файл при удалении потока.

А потом используй

using (var compressedStream = new TempFileStream())
{
    using (var deflateStream = new DeflateStream(
        stream: compressedStream,
        compressionLevel: CompressionLevel.Optimal,
        leaveOpen: true))
    using (var fileStream = File.OpenRead(path))
    {
        fileStream.CopyTo(deflateStream);
    }

    f(compressedStream);
}
Другие вопросы по тегам