11

How can I overwrite contents of a ZipArchiveEntry? Following code using StreamWriter with StringBuilder fails if the new file contents are shorter than the original ones, for example:

using System.IO.Compression;
//...
using (var archive = ZipFile.Open("Test.zip", ZipArchiveMode.Update))
{
   StringBuilder document;
   var entry = archive.GetEntry("foo.txt");//entry contents "foobar123"
   using (StreamReader reader = new StreamReader(entry.Open()))
   {
      document = new StringBuilder(reader.ReadToEnd());
   }

   document.Replace("foobar", "baz"); //builder contents "baz123"

   using (StreamWriter writer = new StreamWriter(entry.Open()))
   {
      writer.Write(document); //entry contents "baz123123", expected "baz123"
   }
}

Produces file containing new and old contents mixed up "baz123123" instead of expected "baz123". Is there perhaps a way how to discard the old contents of ZipArchiveEntry before writing the new ones?
note: I do not want to extract the file, I would like to change contents of the archive.

wondra
  • 3,271
  • 3
  • 29
  • 48
  • @mjwills just ctrl+c and ctrl+v the code and press run. (Need "Test.zip" archive with foo.txt file though, cant really attach it to the question but making one should be fast enough) – wondra Oct 18 '17 at 12:31
  • @wondra get contents, remove entry, modify contents, add entry with modified contents. – Nkosi Oct 18 '17 at 12:35
  • @Nkosi so there is no way to *modify* even when opened with `ZipArchiveMode.Update`? Sounds kind of silly API design. Consider posting it as an answer. – wondra Oct 18 '17 at 12:40
  • @wondra updating the archive means you are either adding, moving or removing an entry from the archive. – Nkosi Oct 18 '17 at 12:51

3 Answers3

20

An alternative is to SetLength(document.Length) of the entry.Open() stream.

using(var stream = entry.Open())
{
   stream.SetLength(document.Length);
   using (StreamWriter writer = new StreamWriter(stream))
   {
      writer.Write(document); //entry contents "baz123"
   }
}
wondra
  • 3,271
  • 3
  • 29
  • 48
  • 6
    Excellent answer! `SetLength(0)` also worked for me in a scenario where the final length is not known upfront. – Paul B. Oct 19 '18 at 14:12
  • This seems to work, but I'm a bit scared about the length of multibyte (unicode) contents. It looks like setting the raw length of the stream would open the way for other errors when string encoding comes into play. Did you have any such problems? – Ishmaeel Mar 01 '19 at 08:12
  • 1
    Ah. I just noticed Paul B's comment. That solution also works, and honestly, I'm much more comfortable with it. Now I know that the stream is expanding to accommodate the actual data. – Ishmaeel Mar 01 '19 at 08:19
12

The below code maintains your basic code structure, but explicitly deletes and recreates the file to ensure that 'leftover' data does not remain.

using (var archive = ZipFile.Open("Test.zip", ZipArchiveMode.Update))
{
    StringBuilder document;
    var entry = archive.GetEntry("foo.txt");//entry contents "foobar123"
    using (StreamReader reader = new StreamReader(entry.Open()))
    {
       document = new StringBuilder(reader.ReadToEnd());
    }

    entry.Delete();
    entry = archive.CreateEntry("foo.txt");
    document.Replace("foobar", "baz"); //builder contents "baz123"

    using (StreamWriter writer = new StreamWriter(entry.Open()))
    {
       writer.Write(document);
    }
}
mjwills
  • 23,389
  • 6
  • 40
  • 63
8

Updating the archive means you are either adding, moving or removing an entry from the archive.

Consider doing the following steps.

  • Get the entry content

  • Remove the entry from the archive (take note of name/location)

  • Modify content as desired.

  • Add modified content back to the archive.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 2
    Just found out setting `entry.Open()` stream's `SetLength(document.Length)` also works. An overload with `bool append` would be welcomed none the less if the API is to be consistent with `Stream`s. – wondra Oct 18 '17 at 12:56
  • 1
    @wondra you should consider adding that information as a self answer to help others in the future who may have the same problem. – Nkosi Oct 18 '17 at 12:57
  • @wondra I think you should unaccept my answer and accept your self answer as I believe you answer is the more logical option. I will delete my answer. – Nkosi Oct 18 '17 at 13:02
  • 1
    Cannot accept own answers until few days passed, if I am not mystaken. Also your answer is probably more robust/best practise than manipulating the length of stream. – wondra Oct 18 '17 at 13:05
  • @wondra ok, I'll leave it be for the time being. Glad to help. Happy coding. – Nkosi Oct 18 '17 at 13:06