38

I know how to encode / decode a simple string to / from base64.

But how would I do that if the data is already been written to a FileStream object. Let's say I have only access to the FileStream object not to the previously stored original data in it. How would I encode a FileStream to base64 before I flush the FileStream to a file.

Ofc I could just open my file and encode / decode it after I have written the FileStream to the file, but I would like to do this all in one single step without doing two file operations one after another. The file could be larger and it would also take double time to load, encode and save it again after it was just saved a short time before.

Maybe someone of you knows a better solution? Can I convert the FileStream to a string, encode the string and then convert the string back to a FileStream for example or what would I do and how would such a code look like?

feedwall
  • 1,213
  • 6
  • 25
  • 48
  • 1
    I'm not sure I totally understand your question, but it's possible to use built-in classes to provide a stream which will transform binary data to or from base 64 data. You could then interpose such a stream between your writes and a file output stream (such as is commonly done with compressing streams and encrypting streams). An example is here: http://netpl.blogspot.co.uk/2011/05/builtin-base64-streaming.html – Matthew Watson Oct 02 '13 at 09:49
  • Possible duplicate of [How to convert an Stream into a byte\[\] in C#?](http://stackoverflow.com/questions/1080442/how-to-convert-an-stream-into-a-byte-in-c) – Liam Dec 10 '15 at 11:06
  • 1
    Possible duplicate of [Is there a Base64Stream for .NET? where?](http://stackoverflow.com/questions/2525533/is-there-a-base64stream-for-net-where) – xmedeko Mar 02 '17 at 16:01
  • Isn`t [this](https://stackoverflow.com/a/25919641/1042934) the answer? – Ognyan Dimitrov Aug 25 '17 at 11:01
  • 1
    Do not forget: stream.Seek(0, SeekOrigin.Begin); at the beginning of method... ;-) – David Oct 03 '18 at 12:53

6 Answers6

35

An easy one as an extension method

public static class Extensions
{
    public static Stream ConvertToBase64(this Stream stream)
    {
        byte[] bytes;
        using (var memoryStream = new MemoryStream())
        {
            stream.CopyTo(memoryStream);
            bytes = memoryStream.ToArray();
        }

        string base64 = Convert.ToBase64String(bytes);
        return new MemoryStream(Encoding.UTF8.GetBytes(base64));
    }
}
chris31389
  • 5,798
  • 2
  • 44
  • 55
  • 3
    This results in the `stream` being buffered entirely into memory (with multiple copies too, as you don't set an initial `capacity`). This is not a practical solution for files larger than a few megabytes - and will certainly break for files larger than 2GB (as `MemoryStream` uses a single `Byte[]` internally). People also report `MemoryStream` breaking for sizes over 256MB: https://stackoverflow.com/questions/15595061/outofmemoryexception-while-populating-memorystream-256mb-allocation-on-16gb-sys – Dai Sep 06 '19 at 10:32
  • @Dai I think that if you are trying to base64 encode a stream that is large, maybe there is a better alternative than base64 encoding? I'm doing it to supply a file within a JSON web request, if there is a large file (MBs+), then it doesn't make sense for me to do that. – chris31389 Sep 17 '19 at 10:36
26

When dealing with large streams, like a file sized over 4GB - you don't want to load the file into memory (as a Byte[]) because not only is it very slow, but also may cause a crash as even in 64-bit processes a Byte[] cannot exceed 2GB (or 4GB with gcAllowVeryLargeObjects).

Fortunately there's a neat helper in .NET called ToBase64Transform which processes a stream in chunks. For some reason Microsoft put it in System.Security.Cryptography and it implements ICryptoTransform (for use with CryptoStream), but disregard that ("a rose by any other name...") just because you aren't performing any cryprographic tasks.

You use it with CryptoStream like so:

using System.Security.Cryptography;
using System.IO;

//

using( FileStream   inputFile    = new FileStream( @"C:\VeryLargeFile.bin", FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 1024 * 1024, useAsync: true ) ) // When using `useAsync: true` you get better performance with buffers much larger than the default 4096 bytes.
using( CryptoStream base64Stream = new CryptoStream( inputFile, new ToBase64Transform(), CryptoStreamMode.Read ) )
using( FileStream   outputFile   = new FileStream( @"C:\VeryLargeBase64File.txt", FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1024 * 1024, useAsync: true ) )
{
    await base64Stream.CopyToAsync( outputFile ).ConfigureAwait(false);
}
Dai
  • 110,988
  • 21
  • 188
  • 277
  • 3
    Note that the leaveOpen property is invalid in netstandard2.0, but is accepted in 472 – statler Sep 15 '19 at 23:10
  • This is almost perfect, except the resultant stream doesn't support Seek, which I need. I swear every day I work with C# System lib I find something I need to re-implement properly. – Yarek T Apr 14 '21 at 15:24
  • @YarekT If you're using streams then you should never need to seek (the only exception being `FileStream`). If you find yourself needing to seek in a non-disk stream then your system is likely designed wrong. – Dai Apr 14 '21 at 15:51
  • @Dai Interesting. I'm using `FluentFtp`'s `UploadAsync` method which takes a stream. I assumed it wouldn't seek, but it does. I'm guessing they designed it for regular file streams. – Yarek T Apr 15 '21 at 08:19
  • 1
    @YarekT According to this issue, it's because FluentFTP needs to know the length of a stream beforehand (which is reasonable), however it does that by requiring streams be seekable (which is wrong - but a consequence of the design of `System.IO.Stream` which doesn't let it expose a `Length` unless `CanSeek == true`, grrr): https://github.com/robinrodricks/FluentFTP/issues/668 – Dai Apr 15 '21 at 08:22
16

You may try something like that:

    public Stream ConvertToBase64(Stream stream)
    {
        Byte[] inArray = new Byte[(int)stream.Length];
        Char[] outArray = new Char[(int)(stream.Length * 1.34)];
        stream.Read(inArray, 0, (int)stream.Length);
        Convert.ToBase64CharArray(inArray, 0, inArray.Length, outArray, 0);
        return new MemoryStream(Encoding.UTF8.GetBytes(outArray));
    }
Jacob
  • 697
  • 1
  • 6
  • 18
  • 8
    Where does 1.34 come from? – DanDan May 28 '14 at 16:14
  • 5
    A byte holds 8 bits. A base64 doesn't use bytes but chars. Not any chars, but specific chars that can be converted to 6 bits. So the in-array is smaller than the our-array by a factor 6/8. 8 divided by 6 is 1,33333 so if you take 1.34 the out array will always be just big enough. – user1884155 Jul 10 '14 at 11:53
  • 2
    You need to get the new size from `Convert.ToBase64CharArray` and then do `Array.Resize(ref base64Chars, newSize);`. Otherwise, you have extra bytes in the final output. – toddmo Aug 10 '15 at 17:05
  • 7
    The 1.34 is wrong! The extra 0.0333 gives you some space which is too small for small lengths and unnecessary big for big arrays. Instead of flooring (cast to int) you should do a ceiling `(int)Math.Ceiling(stream.Length * 8.0 / 6.0)` so you get the exact length. – Karsten Gutjahr Sep 11 '15 at 10:02
  • Note that if `stream` happens to be a `MemoryStream`, you can just use [`stream.ToArray()`](https://docs.microsoft.com/en-us/dotnet/api/system.io.memorystream.toarray?view=netframework-4.7.2) and avoid the 8/6 calculations. – jrh Oct 26 '18 at 19:03
12

You can also encode bytes to Base64. How to get this from a stream see here: How to convert an Stream into a byte[] in C#?

Or I think it should be also possible to use the .ToString() method and encode this.

Community
  • 1
  • 1
alpham8
  • 1,220
  • 2
  • 12
  • 30
  • Since the input is a stream, a much more useful answer is the one below that transforms the stream to another (B64 encoded) stream. – simon Jan 21 '21 at 14:55
7

A simple Stream extension method would do the job:

public static class StreamExtensions
{
    public static string ConvertToBase64(this Stream stream)
    {
        var bytes = new Byte[(int)stream.Length];

        stream.Seek(0, SeekOrigin.Begin);
        stream.Read(bytes, 0, (int)stream.Length);

        return Convert.ToBase64String(bytes);
    }
}

The methods for Read (and also Write) and optimized for the respective class (whether is file stream, memory stream, etc.) and will do the work for you. For simple task like this, there is no need of readers, and etc.

The only drawback is that the stream is copied into byte array, but that is how the conversion to base64 via Convert.ToBase64String works unfortunately.

Vasil Popov
  • 1,049
  • 13
  • 20
5

Since the file will be larger, you don't have very much choice in how to do this. You cannot process the file in place since that will destroy the information you need to use. You have two options that I can see:

  1. Read in the entire file, base64 encode, re-write the encoded data.
  2. Read the file in smaller pieces, encoding as you go along. Encode to a temporary file in the same directory. When you are finished, delete the original file, and rename the temporary file.

Of course, the whole point of streams is to avoid this sort of scenario. Instead of creating the content and stuffing it into a file stream, stuff it into a memory stream. Then encode that and only then save to disk.

David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389