Jak zapisać strumień do pliku w C #?

713

Mam StreamReaderobiekt, który zainicjowałem ze strumieniem, teraz chcę zapisać ten strumień na dysku (strumień może być .giflub .jpglub .pdf).

Istniejący kod:

StreamReader sr = new StreamReader(myOtherObject.InputStream);
  1. Muszę zapisać to na dysku (mam nazwę pliku).
  2. W przyszłości mogę chcieć zapisać to na SQL Server.

Mam również typ kodowania, który będzie potrzebny, jeśli będę go przechowywać na serwerze SQL, prawda?

Loadman
źródło
1
Co to jest myOtherObject?
anhtv13,
2
Nadal nie akceptujesz odpowiedzi na to pytanie?
Brett Rigby,
@BrettRigby Odpowiedź Jona Skeeta, jest prawie automatycznie akceptowana: D
Ricardo Dias Morais

Odpowiedzi:

912

Jak podkreśla Tilendor w odpowiedzi Jona Skeeta, strumienie mają CopyTometodę od .NET 4.

var fileStream = File.Create("C:\\Path\\To\\File");
myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
myOtherObject.InputStream.CopyTo(fileStream);
fileStream.Close();

Lub ze usingskładnią:

using (var fileStream = File.Create("C:\\Path\\To\\File"))
{
    myOtherObject.InputStream.Seek(0, SeekOrigin.Begin);
    myOtherObject.InputStream.CopyTo(fileStream);
}
Antoine Leclair
źródło
66
Pamiętaj, że musisz zadzwonić, myOtherObject.InputStream.Seek(0, SeekOrigin.Begin)jeśli jeszcze nie jesteś na początku lub nie skopiujesz całego strumienia.
Steve Rukuts
3
Jeśli ten strumień wejściowy jest pobierany z połączenia HTTP, wówczas buforuje i pobiera, a następnie zapisuje wszystkie bajty ze źródła ?????
dbw
2
Utworzyłem przeglądarkę plików PDF, w której korzystam ze strumienia, po powiązaniu strumienia i zapisaniu pliku pdf przy użyciu tego samego strumienia, bez użycia „Seek (0, SeekOrigin.Begin)” nie będę mógł zapisać poprawnego dokumentu. więc +1 za wzmiankę o „Seek (0, SeekOrigin.Begin)”
user2463514
myOtherObject.InputStream.CopyTo (fileStream); ten wiersz zawiera błąd: odmowa dostępu.
sulhadin
2
myOtherObject ??
Harry
531

ty wolno używać StreamReaderdo plików binarnych (takich jak gif lub jpg). StreamReaderdotyczy danych tekstowych . Prawie na pewno stracisz dane, jeśli użyjesz ich do dowolnych danych binarnych. (Jeśli użyjesz Encoding.GetEncoding (28591) prawdopodobnie będziesz w porządku, ale o co chodzi?)

Dlaczego w ogóle potrzebujesz StreamReader? Dlaczego nie po prostu zachować dane binarne jako dane binarne i zapisać je z powrotem na dysk (lub SQL) jako dane binarne?

EDIT: Jak to wydaje się być coś ludzie chcą zobaczyć ... jeśli nie po prostu chcesz skopiować jednego strumienia do drugiego (np do pliku) używać coś takiego:

/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

Aby użyć go do zrzucenia strumienia do pliku, na przykład:

using (Stream file = File.Create(filename))
{
    CopyStream(input, file);
}

Zauważ, że Stream.CopyTozostał wprowadzony w .NET 4, służąc w zasadzie do tego samego celu.

Jon Skeet
źródło
6
Wydaje się, że to tak częsty przypadek, że jestem zaskoczony, że nie ma go w .NET. Widzę ludzi tworzących tablice bajtów wielkości całego pliku, co może powodować problemy dla dużych plików.
Tilendor
81
@Tilendor: Jest obecny jako metoda rozszerzenia w .NET 4. (CopyTo)
Jon Skeet
33
Nie sądzę, że jest to metoda rozszerzenia, ale jest nowa w klasie Stream.
Kugel
9
@Kugel: Masz rację, przepraszam. Miałem to jako metodę rozszerzenia w bibliotece narzędziowej, ale teraz, gdy jest w samym Streamie, moja metoda rozszerzenia nie zostaje wywołana.
Jon Skeet
4
@Florian: Jest to dość arbitralne - wystarczająco mała wartość, aby uniknąć zajmowania zbyt dużej ilości pamięci, i wystarczająco duża, aby przenieść rozsądną porcję na raz. Byłoby dobrze mieć 16 K, może 32 K - po prostu uważałbym, aby nie skończyć na stosie dużych obiektów.
Jon Skeet
77
public void CopyStream(Stream stream, string destPath)
{
  using (var fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
  {
    stream.CopyTo(fileStream);
  }
}
Darren Corbett
źródło
28
Prawdopodobnie nie powinieneś umieszczać streamobiektu w using(){}nawiasie. Twoja metoda nie utworzyła strumienia, więc nie należy go wyrzucać.
LarsTech
2
Zamiast tego musisz FileStreamzamiast tego użyć, w przeciwnym razie będzie on otwarty, dopóki nie zostanie wyrzucony śmieci.
Pavel Chikulaev
Przekonałem się, że twoje podejście było znacznie bliżej rozwiązania mojego problemu w WinForm z moją klasą bramy klasy AWS S3! Dziękuję Ci!
Luiz Eduardo
2
To działało dobrze, ale mam wyjście 0 KB. Zamiast tego musiałem zrobić to dla prawidłowego wyjścia: File.WriteAllBytes(destinationFilePath, input.ToArray());. W moim przypadku, inputjest MemoryStreamprzyjście z obrębu ZipArchive.
SNag
23
private void SaveFileStream(String path, Stream stream)
{
    var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
    stream.CopyTo(fileStream);
    fileStream.Dispose();
}
jhonjairoroa87
źródło
1
To działało dobrze, ale mam wyjście 0 KB. Zamiast tego musiałem zrobić to dla prawidłowego wyjścia: File.WriteAllBytes(destinationFilePath, input.ToArray());. W moim przypadku, inputjest MemoryStreamprzyjście z obrębu ZipArchive.
SNag
2
To pomogło mi zrozumieć, co robiłem źle. Nie zapomnij jednak przejść do początku strumienia: stream.Seek(0, SeekOrigin.Begin);
Nathan Bills,
9

Nie otrzymuję wszystkich odpowiedzi CopyTo , a być może systemy korzystające z aplikacji mogły nie zostać zaktualizowane do wersji .NET 4.0+. Wiem, że niektórzy chcieliby zmusić ludzi do aktualizacji, ale kompatybilność też jest dobra.

Inna sprawa, że ​​nie używam strumienia do kopiowania z innego strumienia. Dlaczego nie tylko:

byte[] bytes = myOtherObject.InputStream.ToArray();

Gdy masz bajty, możesz łatwo zapisać je w pliku:

public static void WriteFile(string fileName, byte[] bytes)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write))
    {
        fs.Write(bytes, 0, (int)bytes.Length);
        //fs.Close();
    }
}

Ten kod działa tak, jak przetestowałem go z .jpgplikiem, choć przyznaję, że użyłem go tylko z małymi plikami (mniej niż 1 MB). Jeden strumień, bez kopiowania między strumieniami, niepotrzebne kodowanie, wystarczy napisać bajty! Nie musisz nadmiernie komplikować rzeczy, StreamReaderjeśli masz już strumień, na który można bytesbezpośrednio przekonwertować.ToArray() !

Jedyne potencjalne wady, które widzę podczas robienia tego w ten sposób, to to, że masz duży plik, mając go jako strumień, a użycie .CopyTo()lub odpowiednik pozwala FileStreamna przesyłanie strumieniowe zamiast korzystania z tablicy bajtów i odczytywania bajtów jeden po drugim. W rezultacie może być wolniej. Ale nie powinien się dusić, ponieważ .Write()metoda FileStreamuchwytów zapisujących bajty, i robi to tylko jeden bajt na raz, więc nie zapycha pamięci, z wyjątkiem tego, że będziesz musiał mieć wystarczającą ilość pamięci, aby utrzymać strumień jako byte[]przedmiot . W mojej sytuacji, w której korzystałem z tego, OracleBlobmusiałem iść do byte[], był wystarczająco mały, a poza tym i tak nie było dla mnie żadnego streamingu, więc po prostu wysłałem moje bajty do mojej funkcji powyżej.

Inną opcją, wykorzystującą strumień, byłoby użycie go z CopyStreamfunkcją Jona Skeeta, która była w innym poście - służy to tylko FileStreamdo pobrania strumienia wejściowego i utworzenia z niego bezpośrednio pliku. Nie działa File.Create, tak jak on (co początkowo wydawało mi się problematyczne, ale później stwierdził, że to prawdopodobnie tylko błąd VS ...).

/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ( (len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }    
}

public static void WriteFile(string fileName, Stream inputStream)
{
    string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    if (!path.EndsWith(@"\")) path += @"\";

    if (File.Exists(Path.Combine(path, fileName)))
        File.Delete(Path.Combine(path, fileName));

    using (FileStream fs = new FileStream(Path.Combine(path, fileName), FileMode.CreateNew, FileAccess.Write)
    {
        CopyStream(inputStream, fs);
    }

    inputStream.Close();
    inputStream.Flush();
}
vapcguy
źródło
1
Nie trzeba dzwonić z Closepowoduusing()
Alex78191
@ Alex78191 Jeśli mówisz inputStream.Close(), spójrz ponownie - inputStreamjest wysyłany jako zmienna. usingJest w path+filenamestrumieniu wyjściowym. Jeśli mówiłeś o tym fs.Close()w środku using, przepraszam, miałeś rację, a ja to usunąłem.
vapcguy
8
//If you don't have .Net 4.0  :)

public void SaveStreamToFile(Stream stream, string filename)
{  
   using(Stream destination = File.Create(filename))
      Write(stream, destination);
}

//Typically I implement this Write method as a Stream extension method. 
//The framework handles buffering.

public void Write(Stream from, Stream to)
{
   for(int a = from.ReadByte(); a != -1; a = from.ReadByte())
      to.WriteByte( (byte) a );
}

/*
Note, StreamReader is an IEnumerable<Char> while Stream is an IEnumbable<byte>.
The distinction is significant such as in multiple byte character encodings 
like Unicode used in .Net where Char is one or more bytes (byte[n]). Also, the
resulting translation from IEnumerable<byte> to IEnumerable<Char> can loose bytes
or insert them (for example, "\n" vs. "\r\n") depending on the StreamReader instance
CurrentEncoding.
*/
Jerzy
źródło
16
Kopiowanie strumienia bajt po bajcie (przy użyciu ReadByte / WriteByte) będzie znacznie wolniejsze niż kopiowanie bufor po buforze (przy użyciu Read (byte [], int, int) / Write (byte [], int, int)).
Kevin,
6

Dlaczego nie użyć obiektu FileStream?

public void SaveStreamToFile(string fileFullPath, Stream stream)
{
    if (stream.Length == 0) return;

    // Create a FileStream object to write a stream to a file
    using (FileStream fileStream = System.IO.File.Create(fileFullPath, (int)stream.Length))
    {
        // Fill the bytes[] array with the stream data
        byte[] bytesInStream = new byte[stream.Length];
        stream.Read(bytesInStream, 0, (int)bytesInStream.Length);

        // Use FileStream object to write to the specified file
        fileStream.Write(bytesInStream, 0, bytesInStream.Length);
     }
}
Adrian
źródło
46
co jeśli strumień wejściowy ma długość 1 GB - ten kod spróbuje przydzielić bufor 1 GB :)
Buthrakaur
1
To nie działa z ResponseStream, ponieważ ma nieznaną długość.
Tomas Kubes
Chociaż prawdą jest, że musisz mieć dostępną pamięć byte[], myślę, że rzadkie byłoby przesyłanie strumieniowe pliku 1 GB + BLOB do pliku ... chyba że masz witrynę, która przechowuje torrenty DVD ... Plus , większość komputerów ma obecnie przynajmniej 2 GB pamięci RAM… Zastrzeżenie jest ważne, ale myślę, że jest to przypadek, w którym prawdopodobnie jest „wystarczająco dobry” do większości zadań.
vapcguy
Serwery WWW w ogóle nie tolerują takich przypadków, chyba że na stronie jest aktywny tylko jeden użytkownik naraz.
NateTheGreatt
6

Inną opcją jest doprowadzenie strumienia do byte[]i użycie File.WriteAllBytes. To powinno zrobić:

using (var stream = new MemoryStream())
{
    input.CopyTo(stream);
    File.WriteAllBytes(file, stream.ToArray());
}

Zawijanie go w metodzie rozszerzenia daje lepszą nazwę:

public void WriteTo(this Stream input, string file)
{
    //your fav write method:

    using (var stream = File.Create(file))
    {
        input.CopyTo(stream);
    }

    //or

    using (var stream = new MemoryStream())
    {
        input.CopyTo(stream);
        File.WriteAllBytes(file, stream.ToArray());
    }

    //whatever that fits.
}
nawfal
źródło
3
Jeśli dane wejściowe są zbyt duże, otrzymasz wyjątek braku pamięci. Opcja kopiowania treści ze strumienia wejściowego do strumienia plików jest znacznie lepsza
Ykok
4
public void testdownload(stream input)
{
    byte[] buffer = new byte[16345];
    using (FileStream fs = new FileStream(this.FullLocalFilePath,
                        FileMode.Create, FileAccess.Write, FileShare.None))
    {
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
             fs.Write(buffer, 0, read);
        }
    }
}
Angelo
źródło
Dostarczanie buforowanego strumienia wejściowego bezpośrednio do FileStream- fajnie!
vapcguy
3

Oto przykład, który wykorzystuje odpowiednie zastosowania i implementację idisposable:

static void WriteToFile(string sourceFile, string destinationfile, bool append = true, int bufferSize = 4096)
{
    using (var sourceFileStream = new FileStream(sourceFile, FileMode.OpenOrCreate))
    {
        using (var destinationFileStream = new FileStream(destinationfile, FileMode.OpenOrCreate))
        {
            while (sourceFileStream.Position < sourceFileStream.Length)
            {
                destinationFileStream.WriteByte((byte)sourceFileStream.ReadByte());
            }
        }
    }
}

... i jest też to

    public static void WriteToFile(FileStream stream, string destinationFile, int bufferSize = 4096, FileMode mode = FileMode.OpenOrCreate, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.ReadWrite)
    {
        using (var destinationFileStream = new FileStream(destinationFile, mode, access, share))
        {
            while (stream.Position < stream.Length) 
            {
                destinationFileStream.WriteByte((byte)stream.ReadByte());
            }
        }
    }

Kluczem jest zrozumienie prawidłowego użycia use (które powinno zostać zaimplementowane przy tworzeniu instancji obiektu, który implementuje idisposable, jak pokazano powyżej) oraz dobre wyobrażenie o tym, jak właściwości działają dla strumieni. Pozycja jest dosłownie indeksem w strumieniu (który zaczyna się od 0), po którym następuje odczyt każdego bajtu przy użyciu metody readbyte. W tym przypadku zasadniczo używam go zamiast zmiennej pętli for i po prostu pozwalam mu przejść przez całą długość aż do długości, która jest LITERALNIE końcem całego strumienia (w bajtach). Ignoruj ​​w bajtach, ponieważ jest praktycznie taki sam, a będziesz mieć coś prostego i eleganckiego, takiego jak ten, który wszystko rozwiąże.

Należy również pamiętać, że metoda ReadByte po prostu rzutuje bajt na liczbę całkowitą w procesie i można go po prostu przekonwertować.

Dodam kolejną implementację, którą niedawno napisałem, aby utworzyć rodzaj dynamicznego bufora, aby zapewnić sekwencyjne zapisywanie danych, aby zapobiec ogromnemu przeciążeniu

private void StreamBuffer(Stream stream, int buffer)
{
    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        var memoryBuffer = memoryStream.GetBuffer();

        for (int i = 0; i < memoryBuffer.Length;)
        {
            var networkBuffer = new byte[buffer];
            for (int j = 0; j < networkBuffer.Length && i < memoryBuffer.Length; j++)
            {
                networkBuffer[j] = memoryBuffer[i];
                i++;
            }
            //Assuming destination file
            destinationFileStream.Write(networkBuffer, 0, networkBuffer.Length);
        }
    }
}

Wyjaśnienie jest dość proste: wiemy, że musimy pamiętać o całym zestawie danych, które chcemy zapisać, a także, że chcemy zapisywać tylko określone ilości, więc chcemy, aby pierwsza pętla z ostatnim parametrem była pusta (tak jak podczas gdy ). Następnie inicjalizujemy bufor tablicy bajtów, który jest ustawiony na rozmiar tego, co zostało przekazane, a za pomocą drugiej pętli porównujemy j z rozmiarem bufora i rozmiarem oryginalnego, a jeśli jest większy niż rozmiar oryginału tablica bajtów, zakończ bieg.

Andrew Ramshaw
źródło