rikyoz / bit7z

A C++ static library offering a clean and simple interface to the 7-zip shared libraries.
https://rikyoz.github.io/bit7z
Mozilla Public License 2.0
623 stars 113 forks source link

Using the 7zip SDK to add files to an archive which already contains files #48

Closed PMime closed 3 years ago

PMime commented 3 years ago

Hello Riccardo,

originally I wanted to use your library in our software. But my company insisted that I should take the 7zip SDK. Because I assume that you are experienced in the SDK, I address to you.

We have the following issue: We like to integrate the functionality of the 7zip SDK to add files to an archive. Files that are already in the archive should remain.

The SDK offers a nice C++ example: \CPP\7zip\UI\Client7z. It contains all things we aim for.

But there is an important problem. If the archive already exists and has some files in it, they disappear and only the new files are compressed in the archive.

I already have exchanged outFileStreamSpec->Create(archiveName, true) with outFileStreamSpec->Open(archiveName, OPEN_ALWAYS) in the Client7z example.

It would be nice, if you could explain how to solve this problem, best with code snippets.

Thanks in advance!

rikyoz commented 3 years ago

Hello PMime, First of all, sorry for the late reply!

originally I wanted to use your library in our software. But my company insisted that I should take the 7zip SDK.

Thank you in any case for having considered bit7z!

Because I assume that you are experienced in the SDK, I address to you.

We have the following issue: We like to integrate the functionality of the 7zip SDK to add files to an archive. Files that are already in the archive should remain.

The SDK offers a nice C++ example: \CPP\7zip\UI\Client7z. It contains all things we aim for.

But there is an important problem. If the archive already exists and has some files in it, they disappear and only the new files are compressed in the archive.

I already have exchanged outFileStreamSpec->Create(archiveName, true) with outFileStreamSpec->Open(archiveName, OPEN_ALWAYS) in the Client7z example.

It would be nice, if you could explain how to solve this problem, best with code snippets.

Sure, I'll try my best to explain how to implement the updating of archives. Unfortunately, the documentation of 7-zip is really scarce and incomplete in this sense. I managed to implement it in bit7z only after digging up many small pieces of information through the internet. And now, the internal code of bit7z has some abstraction levels that make it slightly tricky to understand how it works.

Essentially, the procedure of updating an archive consists of:

Unfortunately, I don't know of a way to do this without using a temporary file. As far as I know, this is the same way implemented in the 7-zip File Manager.

Anyway, a "bit" of code that I hope it can help you:

/* Relevant changes to include in your UpdateCallback */
class UpdateCallback : protected CMyUnknownImp, public IArchiveUpdateCallback { 
    public:
        UpdateCallback(const vector<FSItem>& new_items)
            : mOldArc{ nullptr }, mOldItemsCount{ 0 } /* other members */ {}

        virtual ~UpdateCallback() {
            Finalize();
        }

        MY_UNKNOWN_IMP1( IArchiveUpdateCallback )

        void setOldArc(IInArchive* old_arc) {
            if ( old_arc ) {
                mOldArc = old_arc;
                //Note: GetNumberOfItems may fail, here I omitted the check of its HRESULT return value!
                mOldArc->GetNumberOfItems(&mOldItemsCount);
            }
        }

        uint32_t itemsCount() { 
            /* Total number of items is equal to the number of items in the old archive 
               plus the new items to be added */
            return mOldItemsCount + mNewItems.size();
        }

        COM_DECLSPEC_NOTHROW STDMETHODIMP GetProperty(UInt32 index, PROPID propID, PROPVARIANT* value) {
            PROPVARIANT prop = {};
            if (propID == kpidIsAnti) {
                prop.vt = VT_BOOL;
                prop.boolVal = VARIANT_FALSE;
            } else if (index < mOldItemsCount) { //Index refers to an item in the old archive
                // Getting the property from the old archive reader
                mOldArc->GetProperty(index, propID, &prop);
            } else {
                // Here you should put the code for getting the properties of the new item, as usual
            }
            *value = prop;
            return S_OK;
        }

        COM_DECLSPEC_NOTHROW STDMETHODIMP GetStream(UInt32 index, ISequentialInStream** inStream) {
            RINOK(Finalize());

            if (index < mOldItemsCount) { // Old item in the archive, no need to create a stream
                return S_OK;
            }

            // Here you should put the code for creating a stream for the new file

            return S_OK;
        }

        COM_DECLSPEC_NOTHROW STDMETHODIMP GetUpdateItemInfo(UInt32 index,
            Int32* newData,
            Int32* newProperties,
            UInt32* indexInArchive) {

            bool is_old_item = index < mOldItemsCount;

            //Note: 0=false, i.e. no new data (old item). 1=true, i.e. new data (new item);
            if (newData != nullptr) {
                *newData = is_old_item ? 0 : 1; 
            }
            if (newProperties != nullptr) {
                *newProperties = is_old_item ? 0 : 1;
            }
            if (indexInArchive != nullptr) {
                *indexInArchive = is_old_item ? index : static_cast<uint32_t>(-1);
            }

            return S_OK;
        }

        // Other methods from IArchiveUpdateCallback

    private:
        IInArchive* mOldArc;
        uint32_t mOldItemsCount;
        // Other private members of your update callback class
};

int main() {
    std::cout << "Loading 7-zip DLL..." << std::endl;
    auto library = LoadLibrary("./7z.dll");
    if (library == nullptr) {
        std::cerr << "Could not load 7z.dll" << std::endl;
        return -1;
    }

    std::cout << "Getting CreateObject function..." << std::endl;
    auto create_object = reinterpret_cast<CreateObjectFunc>(GetProcAddress(library, "CreateObject"));
    if (create_object == nullptr) {
        std::cerr << "Could not get CreateObject function" << std::endl;
        return -1;
    }

    std::cout << "Initializing old archive reader..." << std::endl;
    CMyComPtr<IInArchive> old_arc;
    HRESULT res = create_object(&format_GUID, &::IID_IInArchive, reinterpret_cast<void**>(&old_arc));
    if ( res != S_OK ) {
        std::cerr << "Could not get class object for old_arc (error " << res << ")" << std::endl;
        return -1;
    }

    std::cout << "Creating old archive input stream..." << std::endl;
    CMyComPtr<CInFileStream> in_file_stream = new CInFileStream;
    if ( !in_file_stream->Open( L"archive.7z" ) ) {
        std::cerr << "Could not open input archive file stream" << std::endl;
        return -1;
    }

    std::cout << "Initializing OpenCallback for the old archive..." << std::endl;
    CMyComPtr<IArchiveOpenCallback> open_callback = new OpenCallback();
    res = old_arc->Open( in_file_stream, nullptr, open_callback );
    if ( res != S_OK ) {
        std::cerr << "Could not open archive file" << std::endl;
        return -1;
    }

    std::cout << "Initializing the new output archive from the input old one..." << std::endl;
    CMyComPtr<IOutArchive> new_arc;
    res = old_arc->QueryInterface(::IID_IOutArchive, reinterpret_cast<void**>(&new_arc));
    if ( res != S_OK ) {
        std::cerr << "Could not initialize new archive (error " << res << ")" << std::endl;
        return -1;
    }

    std::cout << "Creating output file stream..." << std::endl;
    CMyComPtr<COutFileStream> out_file_stream = new COutFileStream();
    if (!out_file_stream->Create(L"archive.7z.tmp", true)) {
        std::cerr << "Could not create temp file" << std::endl;
        return -1;
    }

    std::cout << "Initializing UpdateCallback..." << std::endl;
    /* New item that you want to add.
       Note: FSItem is a class that I use in bit7z, it's similar to the CDirItem in Client7z.cpp */
    FSItem item{L"7z.dll"}; 
    vector<FSItem> new_items = {item}; //new items vector
    CMyComPtr<UpdateCallback> update_callback = new UpdateCallback{new_items};
    /* This is the most important part: your update callback must have access 
       to the old archive reader so that it can:
        - distinguish between requests concerning old and new data;
        - have access to the properties of the old items.
    */
    update_callback->setOldArc(old_arc);

    std::cout << "Compressing new files..." << std::endl;
    res = new_arc->UpdateItems(out_file_stream, update_callback->itemsCount(), update_callback);

    if (res == E_NOTIMPL) {
        std::cerr << "Unsupported operation" << std::endl;
        return -1;
    }

    if (res != S_OK) {
        std::cerr << "Error while compressing" << std::endl;
        return -1;
    }

    // Releasing the streams so that we can rename the temporary archive back to the original file name
    in_file_stream.Release();
    out_file_stream.Release();

    // Closing the old archive so that we can delete it
    old_arc->Close();

    // Renaming the temporary archive back to the original file name
    std::error_code ec;
    std::filesystem::rename("archive.7z.tmp", "archive.7z", ec);
    if (ec) {
        std::cerr << "Could not restore the old archive file name (" << ec.message() << ")" << std::endl;
        return -1;
    }
    std::cout << "Operation completed!" << std::endl;
}
PMime commented 3 years ago

Thank you Riccardo! Especially the code is very helpful

rikyoz commented 3 years ago

Glad that I could be of assistance!