File indexing completed on 2024-04-21 05:46:28

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "mergedvfs.h"
0006 #include "kupdaemon.h"
0007 #include "vfshelpers.h"
0008 #include "kupfiledigger_debug.h"
0009 
0010 #include <KLocalizedString>
0011 #include <KMessageBox>
0012 #include <kwidgetsaddons_version.h>
0013 
0014 #include <QDBusInterface>
0015 #include <QDir>
0016 #include <QGuiApplication>
0017 
0018 #include <utility>
0019 #include <git2/branch.h>
0020 
0021 using NameMap = QMap<QString, MergedNode *>;
0022 using NameMapIterator = QMapIterator<QString, MergedNode *>;
0023 
0024 git_repository *MergedNode::mRepository = nullptr;
0025 
0026 bool mergedNodeLessThan(const MergedNode *a, const MergedNode *b) {
0027     if(a->isDirectory() != b->isDirectory()) {
0028         return a->isDirectory();
0029     }
0030     return a->objectName() < b->objectName();
0031 }
0032 
0033 bool versionGreaterThan(const VersionData *a, const VersionData *b) {
0034     return a->mModifiedDate > b->mModifiedDate;
0035 }
0036 
0037 
0038 MergedNode::MergedNode(QObject *pParent, const QString &pName, uint pMode)
0039    :QObject(pParent)
0040 {
0041     mSubNodes = nullptr;
0042     setObjectName(pName);
0043     mMode = pMode;
0044 }
0045 
0046 void MergedNode::getBupUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath,
0047                            QString *pBranchName, qint64 *pCommitTime, QString *pPathInRepo) const {
0048     QList<const MergedNode *> lStack;
0049     const MergedNode *lNode = this;
0050     while(lNode != nullptr) {
0051         lStack.append(lNode);
0052         lNode = qobject_cast<const MergedNode *>(lNode->parent());
0053     }
0054     const auto lRepo = qobject_cast<const MergedRepository *>(lStack.takeLast());
0055     if(pComplete) {
0056         pComplete->setUrl("bup://" + lRepo->objectName() + lRepo->mBranchName + '/' +
0057                           vfsTimeToString(static_cast<git_time_t>(mVersionList.at(pVersionIndex)->mCommitTime)));
0058     }
0059     if(pRepoPath) {
0060         *pRepoPath = lRepo->objectName();
0061     }
0062     if(pBranchName) {
0063         *pBranchName = lRepo->mBranchName;
0064     }
0065     if(pCommitTime) {
0066         *pCommitTime = mVersionList.at(pVersionIndex)->mCommitTime;
0067     }
0068     if(pPathInRepo) {
0069         pPathInRepo->clear();
0070     }
0071     while(!lStack.isEmpty()) {
0072         QString lPathComponent = lStack.takeLast()->objectName();
0073         if(pComplete) {
0074             pComplete->setPath(pComplete->path() + '/' + lPathComponent);
0075         }
0076         if(pPathInRepo) {
0077             pPathInRepo->append(QLatin1Char('/'));
0078             pPathInRepo->append(lPathComponent);
0079         }
0080     }
0081 }
0082 
0083 MergedNodeList &MergedNode::subNodes() {
0084     if(mSubNodes == nullptr) {
0085         mSubNodes = new MergedNodeList();
0086         if(S_ISDIR(mMode)) {
0087             QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0088             generateSubNodes();
0089             QGuiApplication::restoreOverrideCursor();
0090         }
0091     }
0092     return *mSubNodes;
0093 }
0094 
0095 void MergedNode::askForIntegrityCheck() {
0096 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 101, 0)
0097     int lAnswer = KMessageBox::questionTwoActions(nullptr, xi18nc("@info messagebox",
0098                                                              "Could not read this backup archive. Perhaps some files "
0099                                                              "have become corrupted. Do you want to run an integrity "
0100                                                              "check to test this?"), QString(), KStandardGuiItem::ok(), KStandardGuiItem::cancel());
0101     if(lAnswer == KMessageBox::PrimaryAction) {
0102 #else
0103 
0104     int lAnswer = KMessageBox::questionYesNo(nullptr, xi18nc("@info messagebox",
0105                                                              "Could not read this backup archive. Perhaps some files "
0106                                                              "have become corrupted. Do you want to run an integrity "
0107                                                              "check to test this?"));
0108     if(lAnswer == KMessageBox::Yes) {
0109 #endif
0110         QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH);
0111         if(lInterface.isValid()) {
0112             lInterface.call(QStringLiteral("runIntegrityCheck"),
0113                             QDir::cleanPath(QString::fromLocal8Bit(git_repository_path(mRepository))));
0114         }
0115     }
0116 }
0117 
0118 void MergedNode::generateSubNodes() {
0119     NameMap lSubNodeMap;
0120     foreach(VersionData *lCurrentVersion, mVersionList) {
0121         git_tree *lTree;
0122         if(0 != git_tree_lookup(&lTree, mRepository, &lCurrentVersion->mOid)) {
0123             askForIntegrityCheck();
0124             continue; // try to be fault tolerant by not aborting...
0125         }
0126         git_blob *lMetadataBlob = nullptr;
0127         VintStream *lMetadataStream = nullptr;
0128         const git_tree_entry *lMetaDataTreeEntry = git_tree_entry_byname(lTree, ".bupm");
0129         if(lMetaDataTreeEntry != nullptr && 0 == git_blob_lookup(&lMetadataBlob, mRepository, git_tree_entry_id(lMetaDataTreeEntry))) {
0130             lMetadataStream = new VintStream(git_blob_rawcontent(lMetadataBlob), static_cast<int>(git_blob_rawsize(lMetadataBlob)), this);
0131             Metadata lMetadata;
0132             readMetadata(*lMetadataStream, lMetadata); // the first entry is metadata for the directory itself, discard it.
0133         }
0134 
0135         ulong lEntryCount = git_tree_entrycount(lTree);
0136         for(uint i = 0; i < lEntryCount; ++i) {
0137             uint lMode;
0138             const git_oid *lOid;
0139             QString lName;
0140             bool lChunked;
0141             const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lTree, i);
0142             getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName);
0143             if(lName == QStringLiteral(".bupm")) {
0144                 continue;
0145             }
0146 
0147             MergedNode *lSubNode = lSubNodeMap.value(lName, nullptr);
0148             if(lSubNode == nullptr) {
0149                 lSubNode = new MergedNode(this, lName, lMode);
0150                 lSubNodeMap.insert(lName, lSubNode);
0151                 mSubNodes->append(lSubNode);
0152             } else if((S_IFMT & lMode) != (S_IFMT & lSubNode->mMode)) {
0153                 if(S_ISDIR(lMode)) {
0154                     lName.append(xi18nc("added after folder name in some cases", " (folder)"));
0155                 } else if(S_ISLNK(lMode)) {
0156                     lName.append(xi18nc("added after file name in some cases", " (symlink)"));
0157                 } else {
0158                     lName.append(xi18nc("added after file name in some cases", " (file)"));
0159                 }
0160                 lSubNode = lSubNodeMap.value(lName, nullptr);
0161                 if(lSubNode == nullptr) {
0162                     lSubNode = new MergedNode(this, lName, lMode);
0163                     lSubNodeMap.insert(lName, lSubNode);
0164                     mSubNodes->append(lSubNode);
0165                 }
0166             }
0167             bool lAlreadySeen = false;
0168             foreach(VersionData *lVersion, lSubNode->mVersionList) {
0169                 if(lVersion->mOid == *lOid) {
0170                     lAlreadySeen = true;
0171                     break;
0172                 }
0173             }
0174             if(S_ISDIR(lMode)) {
0175                 if(!lAlreadySeen) {
0176                     lSubNode->mVersionList.append(new VersionData(lOid, lCurrentVersion->mCommitTime,
0177                                                                   lCurrentVersion->mModifiedDate, 0));
0178                 }
0179             } else {
0180                 qint64 lModifiedDate = lCurrentVersion->mModifiedDate;
0181                 qint64 lSize = -1;
0182                 Metadata lMetadata;
0183                 if(lMetadataStream != nullptr && 0 == readMetadata(*lMetadataStream, lMetadata)) {
0184                     lModifiedDate = lMetadata.mMtime;
0185                     lSize = lMetadata.mSize;
0186                 }
0187                 if(!lAlreadySeen) {
0188                     VersionData *lVersionData;
0189                     if(lSize >= 0) {
0190                         lVersionData = new VersionData(lOid, lCurrentVersion->mCommitTime, lModifiedDate, static_cast<quint64>(lSize));
0191                     } else {
0192                         lVersionData = new VersionData(lChunked, lOid, lCurrentVersion->mCommitTime, lModifiedDate);
0193                     }
0194                     lSubNode->mVersionList.append(lVersionData);
0195                 }
0196             }
0197         }
0198         if(lMetadataStream != nullptr) {
0199             delete lMetadataStream;
0200             git_blob_free(lMetadataBlob);
0201         }
0202         git_tree_free(lTree);
0203     }
0204     std::sort(mSubNodes->begin(), mSubNodes->end(), mergedNodeLessThan);
0205     foreach(MergedNode *lNode, *mSubNodes) {
0206         std::sort(lNode->mVersionList.begin(), lNode->mVersionList.end(), versionGreaterThan);
0207     }
0208 }
0209 
0210 MergedRepository::MergedRepository(QObject *pParent, const QString &pRepositoryPath, QString pBranchName)
0211    : MergedNode(pParent, pRepositoryPath, DEFAULT_MODE_DIRECTORY), mBranchName(std::move(pBranchName))
0212 {
0213     if(!objectName().endsWith(QLatin1Char('/'))) {
0214         setObjectName(objectName() + QLatin1Char('/'));
0215     }
0216 }
0217 
0218 MergedRepository::~MergedRepository() {
0219     if(mRepository != nullptr) {
0220         git_repository_free(mRepository);
0221     }
0222 }
0223 
0224 bool MergedRepository::open() {
0225     if(0 != git_repository_open(&mRepository, objectName().toLocal8Bit())) {
0226         qCWarning(KUPFILEDIGGER) << "could not open repository " << objectName();
0227         mRepository = nullptr;
0228         return false;
0229     }
0230     return true;
0231 }
0232 
0233 bool MergedRepository::readBranch() {
0234     if(mRepository == nullptr) {
0235         return false;
0236     }
0237     git_revwalk *lRevisionWalker;
0238     if(0 != git_revwalk_new(&lRevisionWalker, mRepository)) {
0239         qCWarning(KUPFILEDIGGER) << "could not create a revision walker in repository " << objectName();
0240         return false;
0241     }
0242 
0243     QString lCompleteBranchName = QStringLiteral("refs/heads/");
0244     lCompleteBranchName.append(mBranchName);
0245     if(0 != git_revwalk_push_ref(lRevisionWalker, lCompleteBranchName.toLocal8Bit())) {
0246         qCWarning(KUPFILEDIGGER) << "Unable to read branch " << mBranchName << " in repository " << objectName();
0247         git_revwalk_free(lRevisionWalker);
0248         return false;
0249     }
0250     bool lEmptyList = true;
0251     git_oid lOid;
0252     while(0 == git_revwalk_next(&lOid, lRevisionWalker)) {
0253         git_commit *lCommit;
0254         if(0 != git_commit_lookup(&lCommit, mRepository, &lOid)) {
0255             continue;
0256         }
0257         git_time_t lTime = git_commit_time(lCommit);
0258         mVersionList.append(new VersionData(git_commit_tree_id(lCommit), lTime, lTime, 0));
0259         lEmptyList = false;
0260         git_commit_free(lCommit);
0261     }
0262     git_revwalk_free(lRevisionWalker);
0263     return !lEmptyList;
0264 }
0265 
0266 bool MergedRepository::permissionsOk() {
0267     if(mRepository == nullptr) {
0268         return false;
0269     }
0270     QDir lRepoDir(objectName());
0271     if(!lRepoDir.exists()) {
0272         return false;
0273     }
0274     QList<QDir> lDirectories;
0275     lDirectories << lRepoDir;
0276     while(!lDirectories.isEmpty()) {
0277         QDir lDir = lDirectories.takeFirst();
0278         foreach(QFileInfo lFileInfo, lDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
0279             if(!lFileInfo.isReadable()) {
0280                 return false;
0281             }
0282             if(lFileInfo.isDir()) {
0283                 lDirectories << QDir(lFileInfo.absoluteFilePath());
0284             }
0285         }
0286     }
0287     return true;
0288 }
0289 
0290 uint qHash(git_oid pOid) {
0291     return qHash(QByteArray::fromRawData(reinterpret_cast<const char *>(pOid.id), GIT_OID_RAWSZ));
0292 }
0293 
0294 
0295 bool operator ==(const git_oid &pOidA, const git_oid &pOidB) {
0296     QByteArray a = QByteArray::fromRawData(reinterpret_cast<const char *>(pOidA.id), GIT_OID_RAWSZ);
0297     QByteArray b = QByteArray::fromRawData(reinterpret_cast<const char *>(pOidB.id), GIT_OID_RAWSZ);
0298     return a == b;
0299 }
0300 
0301 
0302 quint64 VersionData::size() {
0303     if(mSizeIsValid) {
0304         return mSize;
0305     }
0306     if(mChunkedFile) {
0307         mSize = calculateChunkFileSize(&mOid, MergedNode::mRepository);
0308     } else {
0309         git_blob *lBlob;
0310         if(0 == git_blob_lookup(&lBlob, MergedNode::mRepository, &mOid)) {
0311             mSize = static_cast<quint64>(git_blob_rawsize(lBlob));
0312             git_blob_free(lBlob);
0313         } else {
0314             mSize = 0;
0315         }
0316     }
0317     mSizeIsValid = true;
0318     return mSize;
0319 }