Thursday, June 2, 2011

Qt Folder Compression

I needed a compression module that does not depend on anything but Qt, and I mean a module to compress a complete folder with its children. This is because I wanted the compression module to work on any platform supported by Qt with no other dependencies. So I made this modules that only uses only qCompress() & qUncompress() methods plus some logic.

It is mainly a recursive algorithm that serializes data into a single file, each with its relative path to the parent folder. And deserializing is just taking binary data, creating needed subfolders, and saving the file.

Download: QtFolderCompressor.zip

[FolderCompressor.h]
#ifndef FOLDERCOMPRESSOR_H
#define FOLDERCOMPRESSOR_H

#include <QFile>
#include <QObject>
#include <QDir>

class FolderCompressor : public QObject
{
    Q_OBJECT
public:
    explicit FolderCompressor(QObject *parent = 0);

    //A recursive function that scans all files inside the source folder
    //and serializes all files in a row of file names and compressed
    //binary data in a single file
    bool compressFolder(QString sourceFolder, QString destinationFile);

    //A function that deserializes data from the compressed file and
    //creates any needed subfolders before saving the file
    bool decompressFolder(QString sourceFile, QString destinationFolder);

private:
    QFile file;
    QDataStream dataStream;

    bool compress(QString sourceFolder, QString prefex);
};

#endif // FOLDERCOMPRESSOR_H


[FolderCompressor.cpp]
#include "FolderCompressor.h"

FolderCompressor::FolderCompressor(QObject *parent) :
    QObject(parent)
{
}

bool FolderCompressor::compressFolder(QString sourceFolder, QString destinationFile)
{
    QDir src(sourceFolder);
    if(!src.exists())//folder not found
    {
        return false;
    }

    file.setFileName(destinationFile);
    if(!file.open(QIODevice::WriteOnly))//could not open file
    {
        return false;
    }

    dataStream.setDevice(&file);

    bool success = compress(sourceFolder, "");
    file.close();

    return success;
}

bool FolderCompressor::compress(QString sourceFolder, QString prefex)
{
    QDir dir(sourceFolder);
    if(!dir.exists())
        return false;

    //1 - list all folders inside the current folder
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs);
    QFileInfoList foldersList = dir.entryInfoList();

    //2 - For each folder in list: call the same function with folders' paths
    for(int i=0; i<foldersList.length(); i++)
    {
        QString folderName = foldersList.at(i).fileName();
        QString folderPath = dir.absolutePath()+"/"+folderName;
        QString newPrefex = prefex+"/"+folderName;

        compress(folderPath, newPrefex);
    }

    //3 - List all files inside the current folder
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Files);
    QFileInfoList filesList = dir.entryInfoList();

    //4- For each file in list: add file path and compressed binary data
    for(int i=0; i<filesList.length(); i++)
    {
        QFile file(dir.absolutePath()+"/"+filesList.at(i).fileName());
        if(!file.open(QIODevice::ReadOnly))//couldn't open file
        {
            return false;
        }

        dataStream << QString(prefex+"/"+filesList.at(i).fileName());
        dataStream << qCompress(file.readAll());

        file.close();
    }

    return true;
}

bool FolderCompressor::decompressFolder(QString sourceFile, QString destinationFolder)
{
    //validation
    QFile src(sourceFile);
    if(!src.exists())
    {//file not found, to handle later
        return false;
    }
    QDir dir;
    if(!dir.mkpath(destinationFolder))
    {//could not create folder
        return false;
    }

    file.setFileName(sourceFile);
    if(!file.open(QIODevice::ReadOnly))
        return false;

    dataStream.setDevice(&file);

    while(!dataStream.atEnd())
    {
        QString fileName;
        QByteArray data;

        //extract file name and data in order
        dataStream >> fileName >> data;

        //create any needed folder
        QString subfolder;
        for(int i=fileName.length()-1; i>0; i--)
        {
            if((QString(fileName.at(i)) == QString("\\")) || (QString(fileName.at(i)) == QString("/")))
            {
                subfolder = fileName.left(i);
                dir.mkpath(destinationFolder+"/"+subfolder);
                break;
            }
        }

        QFile outFile(destinationFolder+"/"+fileName);
        if(!outFile.open(QIODevice::WriteOnly))
        {
            file.close();
            return false;
        }
        outFile.write(qUncompress(data));
        outFile.close();
    }

    file.close();
    return true;
}

9 comments:

  1. Nice :-) Thanks a lot!!

    ReplyDelete
  2. Two features that you missed. Hidden folders and files prefixed with "." and empty folders.

    To include hidden files change filters to:
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Hidden | QDir::Dirs);
    dir.setFilter(QDir::NoDotAndDotDot | QDir::Hidden | QDir::Files);

    To add empty folders:

    In compress():
    if(filesList.length() == 0) {
    qDebug() << "Compressing folder: " << prefex << "/";
    dataStream << QString(prefex + "/");
    dataStream << " ";
    }

    In decompress():

    if(fileName.endsWith("/") == false) {
    ...decompress code here...
    } else {
    dir.mkpath(destinationFolder + "/" + fileName);
    }

    ReplyDelete
  3. Very nice, thank you so much!

    ReplyDelete
  4. Do you know an easy way to compress to ZIp format with this class?

    ReplyDelete
    Replies
    1. I remember that you need to use a specific lib for zip format.

      Delete
  5. Thanks for your answer. I expected that you had another answer :)

    With this class I can't export my compressed files to an external application, only to applications that have this class to uncompress! It is a pity...

    By the way, do you have any suggestion to ZIP code in C++ classes/library to easily integrate with Qt?

    ReplyDelete
  6. Thank you very much!!!

    ReplyDelete