File indexing completed on 2024-05-05 17:54:18

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 "bupvfs.h"
0006 
0007 #include <QCoreApplication>
0008 #include <QFile>
0009 #include <QVarLengthArray>
0010 #include <QUrl>
0011 
0012 #include <KIO/WorkerBase>
0013 using namespace KIO;
0014 #include <KLocalizedString>
0015 #include <KProcess>
0016 
0017 #include <grp.h>
0018 #include <pwd.h>
0019 
0020 // Pseudo plugin class to embed meta data
0021 class KIOPluginForMetaData : public QObject
0022 {
0023     Q_OBJECT
0024     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.bup" FILE "bup.json")
0025 };
0026 
0027 class BupWorker : public WorkerBase
0028 {
0029 public:
0030     BupWorker(const QByteArray &pPoolSocket, const QByteArray &pAppSocket);
0031     ~BupWorker() override;
0032     KIO::WorkerResult close() override;
0033     KIO::WorkerResult get(const QUrl &pUrl) override;
0034     KIO::WorkerResult listDir(const QUrl &pUrl) override ;
0035     KIO::WorkerResult open(const QUrl &pUrl, QIODevice::OpenMode pMode) override;
0036     KIO::WorkerResult read(filesize_t pSize) override;
0037     KIO::WorkerResult seek(filesize_t pOffset) override;
0038     KIO::WorkerResult stat(const QUrl &pUrl) override;
0039     KIO::WorkerResult mimetype(const QUrl &pUrl) override;
0040 
0041 private:
0042     bool checkCorrectRepository(const QUrl &pUrl, QStringList &pPathInRepository);
0043     QString getUserName(uid_t pUid);
0044     QString getGroupName(gid_t pGid);
0045     void createUDSEntry(Node *pNode, KIO::UDSEntry & pUDSEntry, int pDetails);
0046 
0047     QHash<uid_t, QString> mUsercache;
0048     QHash<gid_t, QString> mGroupcache;
0049     Repository *mRepository;
0050     File *mOpenFile;
0051 };
0052 
0053 BupWorker::BupWorker(const QByteArray &pPoolSocket, const QByteArray &pAppSocket)
0054    : WorkerBase("bup", pPoolSocket, pAppSocket)
0055 {
0056     mRepository = nullptr;
0057     mOpenFile = nullptr;
0058     git_libgit2_init();
0059 }
0060 
0061 BupWorker::~BupWorker() {
0062     delete mRepository;
0063     git_libgit2_shutdown();
0064 }
0065 
0066 KIO::WorkerResult BupWorker::close() {
0067     mOpenFile = nullptr;
0068     return KIO::WorkerResult::pass();
0069 }
0070 
0071 KIO::WorkerResult BupWorker::get(const QUrl &pUrl) {
0072     QStringList lPathInRepo;
0073     if(!checkCorrectRepository(pUrl, lPathInRepo)) {
0074         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString()));
0075     }
0076 
0077     // Assume that a symlink should be followed.
0078     // Kio will never call get() on a symlink if it actually wants to copy a
0079     // symlink, it would just create a symlink on the destination kioworker using the
0080     // target it already got from calling stat() on this one.
0081     Node *lNode = mRepository->resolve(lPathInRepo, true);
0082     if(lNode == nullptr) {
0083         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/")));
0084     }
0085     File *lFile = qobject_cast<File *>(lNode);
0086     if(lFile == nullptr) {
0087         return KIO::WorkerResult::fail(KIO::ERR_IS_DIRECTORY, lPathInRepo.join(QStringLiteral("/")));
0088     }
0089 
0090     mimeType(lFile->mMimeType);
0091     // Emit total size AFTER mimetype
0092     totalSize(lFile->size());
0093 
0094     //make sure file is at the beginning
0095     lFile->seek(0);
0096     KIO::filesize_t lProcessedSize = 0;
0097     const QString lResumeOffset = metaData(QStringLiteral("resume"));
0098     if(!lResumeOffset.isEmpty()) {
0099         bool ok;
0100         quint64 lOffset = lResumeOffset.toULongLong(&ok);
0101         if (ok && lOffset < lFile->size()) {
0102             if(0 == lFile->seek(lOffset)) {
0103                 canResume();
0104                 lProcessedSize = lOffset;
0105             }
0106         }
0107     }
0108 
0109     QByteArray lResultArray;
0110     int lRetVal;
0111     while(0 == (lRetVal = lFile->read(lResultArray))) {
0112         data(lResultArray);
0113         lProcessedSize += static_cast<quint64>(lResultArray.length());
0114         processedSize(lProcessedSize);
0115     }
0116     if(lRetVal == KIO::ERR_NO_CONTENT) {
0117         data(QByteArray());
0118         processedSize(lProcessedSize);
0119         return KIO::WorkerResult::pass();
0120     } else {
0121         return KIO::WorkerResult::fail(lRetVal, lPathInRepo.join(QStringLiteral("/")));
0122     }
0123 }
0124 
0125 KIO::WorkerResult BupWorker::listDir(const QUrl &pUrl) {
0126     QStringList lPathInRepo;
0127     if(!checkCorrectRepository(pUrl, lPathInRepo)) {
0128         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString()));
0129     }
0130     Node *lNode = mRepository->resolve(lPathInRepo, true);
0131     if(lNode == nullptr) {
0132         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/")));
0133     }
0134     auto lDir = qobject_cast<Directory *>(lNode);
0135     if(lDir == nullptr) {
0136         return KIO::WorkerResult::fail(KIO::ERR_IS_FILE, lPathInRepo.join(QStringLiteral("/")));
0137     }
0138 
0139     // give the directory a chance to reload if necessary.
0140     lDir->reload();
0141 
0142     const QString sDetails = metaData(QStringLiteral("details"));
0143     const int lDetails = sDetails.isEmpty() ? 2 : sDetails.toInt();
0144 
0145     NodeMapIterator i(lDir->subNodes());
0146     UDSEntry lEntry;
0147     while(i.hasNext()) {
0148         createUDSEntry(i.next().value(), lEntry, lDetails);
0149         listEntry(lEntry);
0150     }
0151     return KIO::WorkerResult::pass();
0152 }
0153 
0154 KIO::WorkerResult BupWorker::open(const QUrl &pUrl, QIODevice::OpenMode pMode) {
0155     if(pMode & QIODevice::WriteOnly) {
0156         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_WRITING, pUrl.toDisplayString());
0157     }
0158 
0159     QStringList lPathInRepo;
0160     if(!checkCorrectRepository(pUrl, lPathInRepo)) {
0161         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString()));
0162     }
0163 
0164     Node *lNode = mRepository->resolve(lPathInRepo, true);
0165     if(lNode == nullptr) {
0166         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/")));
0167     }
0168 
0169     File *lFile = qobject_cast<File *>(lNode);
0170     if(lFile == nullptr) {
0171         return KIO::WorkerResult::fail(KIO::ERR_IS_DIRECTORY, lPathInRepo.join(QStringLiteral("/")));
0172     }
0173 
0174     if(0 != lFile->seek(0)) {
0175         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, pUrl.toDisplayString());
0176     }
0177 
0178     mOpenFile = lFile;
0179     mimeType(lFile->mMimeType);
0180     totalSize(lFile->size());
0181     position(0);
0182     return KIO::WorkerResult::pass();
0183 }
0184 
0185 KIO::WorkerResult BupWorker::read(filesize_t pSize) {
0186     if(mOpenFile == nullptr) {
0187         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_READ, QString());
0188     }
0189     QByteArray lResultArray;
0190     int lRetVal = 0;
0191     while(pSize > 0 && 0 == (lRetVal = mOpenFile->read(lResultArray, static_cast<int>(pSize)))) {
0192         pSize -= static_cast<quint64>(lResultArray.size());
0193         data(lResultArray);
0194     }
0195     if(lRetVal == 0) {
0196         data(QByteArray());
0197         return KIO::WorkerResult::pass();
0198     } else {
0199         return KIO::WorkerResult::fail(lRetVal, mOpenFile->completePath());
0200     }
0201 }
0202 
0203 KIO::WorkerResult BupWorker::seek(filesize_t pOffset) {
0204     if(mOpenFile == nullptr) {
0205         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_SEEK, QString());
0206     }
0207 
0208     if(0 != mOpenFile->seek(pOffset)) {
0209         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_SEEK, mOpenFile->completePath());
0210     }
0211     position(pOffset);
0212     return KIO::WorkerResult::pass();
0213 }
0214 
0215 KIO::WorkerResult BupWorker::stat(const QUrl &pUrl) {
0216     QStringList lPathInRepo;
0217     if(!checkCorrectRepository(pUrl, lPathInRepo)) {
0218         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString()));
0219     }
0220 
0221     Node *lNode = mRepository->resolve(lPathInRepo);
0222     if(lNode == nullptr) {
0223         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/")));
0224     }
0225 
0226     const QString sDetails = metaData(QStringLiteral("details"));
0227     const int lDetails = sDetails.isEmpty() ? 2 : sDetails.toInt();
0228 
0229     UDSEntry lUDSEntry;
0230     createUDSEntry(lNode, lUDSEntry, lDetails);
0231     statEntry(lUDSEntry);
0232     return KIO::WorkerResult::pass();
0233 }
0234 
0235 KIO::WorkerResult BupWorker::mimetype(const QUrl &pUrl) {
0236     QStringList lPathInRepo;
0237     if(!checkCorrectRepository(pUrl, lPathInRepo)) {
0238         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("No bup repository found.\n%1", pUrl.toDisplayString()));
0239     }
0240 
0241     Node *lNode = mRepository->resolve(lPathInRepo);
0242     if(lNode == nullptr) {
0243         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, lPathInRepo.join(QStringLiteral("/")));
0244     }
0245 
0246     mimeType(lNode->mMimeType);
0247     return KIO::WorkerResult::pass();
0248 }
0249 
0250 bool BupWorker::checkCorrectRepository(const QUrl &pUrl, QStringList &pPathInRepository) {
0251     // make this worker accept most URLs.. even incorrect ones. (no slash (wrong),
0252     // one slash (correct), two slashes (wrong), three slashes (correct))
0253     QString lPath;
0254     if(!pUrl.host().isEmpty()) {
0255         lPath = QStringLiteral("/") + pUrl.host() + pUrl.adjusted(QUrl::StripTrailingSlash).path() + '/';
0256     } else {
0257         lPath = pUrl.adjusted(QUrl::StripTrailingSlash).path() + '/';
0258         if(!lPath.startsWith(QLatin1Char('/'))) {
0259             lPath.prepend(QLatin1Char('/'));
0260         }
0261     }
0262 
0263     if(mRepository && mRepository->isValid()) {
0264         if(lPath.startsWith(mRepository->objectName())) {
0265             lPath.remove(0, mRepository->objectName().length());
0266             pPathInRepository = lPath.split(QLatin1Char('/'), Qt::SkipEmptyParts);
0267             return true;
0268         }
0269         delete mRepository;
0270         mRepository = nullptr;
0271     }
0272 
0273     pPathInRepository = lPath.split(QLatin1Char('/'), Qt::SkipEmptyParts);
0274     QString lRepoPath = QStringLiteral("/");
0275     while(!pPathInRepository.isEmpty()) {
0276         // make sure the repo path will end with a slash
0277         lRepoPath += pPathInRepository.takeFirst();
0278         lRepoPath += QStringLiteral("/");
0279         if((QFile::exists(lRepoPath + QStringLiteral("objects")) &&
0280             QFile::exists(lRepoPath + QStringLiteral("refs"))) ||
0281               (QFile::exists(lRepoPath + QStringLiteral(".git/objects")) &&
0282                QFile::exists(lRepoPath + QStringLiteral(".git/refs")))) {
0283             mRepository = new Repository(nullptr, lRepoPath);
0284             return mRepository->isValid();
0285         }
0286     }
0287     return false;
0288 }
0289 
0290 QString BupWorker::getUserName(uid_t pUid) {
0291     if(!mUsercache.contains(pUid)) {
0292         struct passwd *lUserInfo = getpwuid(pUid);
0293         if(lUserInfo) {
0294             mUsercache.insert(pUid, QString::fromLocal8Bit(lUserInfo->pw_name));
0295         }
0296         else {
0297             return QString::number(pUid);
0298         }
0299     }
0300     return mUsercache.value(pUid);
0301 }
0302 
0303 QString BupWorker::getGroupName(gid_t pGid) {
0304     if(!mGroupcache.contains(pGid)) {
0305         struct group *lGroupInfo = getgrgid(pGid);
0306         if(lGroupInfo) {
0307             mGroupcache.insert(pGid, QString::fromLocal8Bit(lGroupInfo->gr_name));
0308         }
0309         else {
0310             return QString::number( pGid );
0311         }
0312     }
0313     return mGroupcache.value(pGid);
0314 }
0315 
0316 void BupWorker::createUDSEntry(Node *pNode, UDSEntry &pUDSEntry, int pDetails) {
0317     pUDSEntry.clear();
0318     pUDSEntry.fastInsert(KIO::UDSEntry::UDS_NAME, pNode->objectName());
0319     if(!pNode->mSymlinkTarget.isEmpty()) {
0320         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, pNode->mSymlinkTarget);
0321         if(pDetails > 1) {
0322             Node *lNode = qobject_cast<Node *>(pNode->parent())->resolve(pNode->mSymlinkTarget, true);
0323             if(lNode != nullptr) { // follow symlink only if details > 1 and it leads to something
0324                 pNode = lNode;
0325             }
0326         }
0327     }
0328     pUDSEntry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, pNode->mMode & S_IFMT);
0329     pUDSEntry.fastInsert(KIO::UDSEntry::UDS_ACCESS, pNode->mMode & 07777);
0330     if(pDetails > 0) {
0331         quint64 lSize = 0;
0332         File *lFile = qobject_cast<File *>(pNode);
0333         if(lFile != nullptr) {
0334             lSize = lFile->size();
0335         }
0336         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_SIZE, static_cast<qint64>(lSize));
0337         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, pNode->mMimeType);
0338         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, pNode->mAtime);
0339         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, pNode->mMtime);
0340         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_USER, getUserName(static_cast<uint>(pNode->mUid)));
0341         pUDSEntry.fastInsert(KIO::UDSEntry::UDS_GROUP, getGroupName(static_cast<uint>(pNode->mGid)));
0342     }
0343 }
0344 
0345 extern "C" int Q_DECL_EXPORT kdemain(int pArgc, char **pArgv) {
0346     QCoreApplication lApp(pArgc, pArgv);
0347     QCoreApplication::setApplicationName(QStringLiteral("kio_bup"));
0348     KLocalizedString::setApplicationDomain("kup");
0349 
0350     if(pArgc != 4) {
0351         fprintf(stderr, "Usage: kio_bup protocol domain-socket1 domain-socket2\n");
0352         exit(-1);
0353     }
0354 
0355     BupWorker lWorker(pArgv[2], pArgv[3]);
0356     lWorker.dispatchLoop();
0357 
0358     return 0;
0359 }
0360 
0361 #include "bupworker.moc"