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 }