Delphi XE2 TZipFile: replace a file in zip archive
Asked Answered
R

1

9

I'd like to replace a file (= delete old and add new) in a zip archive with the Delphi XE2/XE3 standard System.Zip unit. But there are no replace/delete methods. Does anybody have an idea how it could be achieved without needing to extract all files and add them to a new archive?

I have this code, but it adds the "document.txt" once more if it's already present:

var
  ZipFile: TZipFile;
  SS: TStringStream;
const
  ZipDocument = 'E:\document.zip';
begin
  ZipFile := TZipFile.Create; //Zipfile: TZipFile
  SS := TStringStream.Create('hello');
  try
    if FileExists(ZipDocument) then
      ZipFile.Open(ZipDocument, zmReadWrite)
    else
      ZipFile.Open(ZipDocument, zmWrite);

    ZipFile.Add(SS, 'document.txt');

    ZipFile.Close;
  finally
    SS.Free;
    ZipFile.Free;
  end;
end;

Note: I used TPAbbrevia before (that did the job), but I'd like to use Delphi's Zip unit now. So please do not answer something like "use another library". Thank you.

Rosaleerosaleen answered 31/10, 2012 at 18:21 Comment(4)
You have answered your own question. The built in ZIP library doesn't support that functionality.Trowel
Maybe somebody wrote a hack that it does?Rosaleerosaleen
Why don't you use Abbrevia? I've been told that it is very good.Trowel
I do use it and it's great. But I've written a library that needs zip functionality and I do want to reduce the dependencies. Furthermore TPAbbrevia is a little bit tricky to install for unexperienced users if you need OSX plattform support.Rosaleerosaleen
P
13

I'd recommend Abbrevia because I'm biased :), you already know it, and it doesn't require any hacks. Barring that, here's your hack:

type
  TZipFileHelper = class helper for TZipFile
    procedure Delete(FileName: string);
  end;

{ TZipFileHelper }

procedure TZipFileHelper.Delete(FileName: string);
var
  i, j: Integer;
  StartOffset, EndOffset, Size: UInt32;
  Header: TZipHeader;
  Buf: TBytes;
begin
  i := IndexOf(FileName);
  if i <> -1 then begin
    // Find extents for existing file in the file stream
    StartOffset := Self.FFiles[i].LocalHeaderOffset;
    EndOffset := Self.FEndFileData;
    for j := 0 to Self.FFiles.Count - 1 do begin
      if (Self.FFiles[j].LocalHeaderOffset > StartOffset) and
         (Self.FFiles[j].LocalHeaderOffset <= EndOffset) then
        EndOffset := Self.FFiles[j].LocalHeaderOffset;
    end;
    Size := EndOffset - StartOffset;
    // Update central directory header data
    Self.FFiles.Delete(i);
    for j := 0 to Self.FFiles.Count - 1 do begin
      Header := Self.FFiles[j];
      if Header.LocalHeaderOffset > StartOffset then begin
        Header.LocalHeaderOffset := Header.LocalHeaderOffset - Size;
        Self.FFiles[j] := Header;
      end;
    end;
    // Remove existing file stream
    SetLength(Buf, Self.FEndFileData - EndOffset);
    Self.FStream.Position := EndOffset;
    if Length(Buf) > 0 then
      Self.FStream.Read(Buf[0], Length(Buf));
    Self.FStream.Size := StartOffset;
    if Length(Buf) > 0 then
      Self.FStream.Write(Buf[0], Length(Buf));
    Self.FEndFileData := Self.FStream.Position;
  end;
end;

Usage:

ZipFile.Delete('document.txt');
ZipFile.Add(SS, 'document.txt');
Problem answered 31/10, 2012 at 19:7 Comment(14)
+1 Does that hack actually remove the old file from the ZIP, or does it just remove it from the file table or whatever it is called?Trowel
No it does not physically remove the file (the zip file grows), but it's a good start. I'll try to extend the code and delete the old file from FStream and recalculate the header (new file positions).Rosaleerosaleen
@David good catch; I should have looked at the implementation closer. Updated with a more complete hack that does delete file content.Nebulosity
@CraigPeterson That's better. For very large files you might not want to load all the trailing stream into memory at once. But I'm sure oxo can worry about details like that.Trowel
@CraigPeterson Faster than me :) Thanks a lot! For my purposes the trailing stream size is not an issue (I do not expect huge files), but an extra loop for copying data is definitely not a problem for anybody. Craig has already done the hard part.Rosaleerosaleen
@David for "very large files" you would not want to have a temporary copy of the archive created as well. Worse, sometimes there maybe not enough volume on media to have two copies of archive at same time. So "log-FS-like" appending of new file version does have its benefits. It is interesting feature and i hope for it to remain, even if not default behavior.Rhinal
@Arioch'The I can't see anybody other than you talking about creating temporary copies of the archive. Perhaps you don't understand my comment.Trowel
@David how would you make new ZIP file ? You make temporary zip, copy the content into it, then delete old one then rename new one to old name. Or how would you do ?Rhinal
@Arioch It's all in Craig's answer. My comment concerned a possible optimisation of the final section of code from the answer.Trowel
Another optimization would be to store Self.FFiles[j].LocalHeaderOffset in a local variable.Out
There is a bug at the Stream.Read and Write because the first element of the Buf array is referenced but the size is zero if we delete the last file. These functions already support TBytes parameter: Read(Buf, Length(Buf); and Write(Buf, Length(Buf);Out
@hubalu The version I posted used the TBytes overloads, but they don't exist on (at least) Delphi XE2 and the bug was introduced in Yaniv's edit. I've corrected that. As for Self.FFiles[j].LocalHeaderOffset, that's really a micro optimization; the disk I/O is going to dwarf everything else, but if you wanted to do that, you could go a step further and just stick LocalHeaderOffset in a local variable instead.Nebulosity
Another issue is calling the Stream.Read and Stream.Write without checking the return values. It would be better to use ReadBuffer and WriteBuffer.Out
And a check would be necessary at the beginning: if not (Self.FMode in [zmReadWrite, zmWrite]) then raise EZipException.CreateRes(@SZipNoWrite);Out

© 2022 - 2024 — McMap. All rights reserved.