File indexing completed on 2024-04-28 04:58:02

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003     SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
0004 */
0005 
0006 #pragma once
0007 
0008 #include <optional>
0009 
0010 #include <QFileInfo>
0011 
0012 #include <kio/ioworker_defaults.h>
0013 
0014 #include "kio_smb.h"
0015 
0016 // Carries the context of a file transfer.
0017 struct TransferContext {
0018     // When resuming a file. This is false when starting a new .part!
0019     // To establish if a partial file is used the completeDestination should be compared with the partDestination.
0020     const bool resuming;
0021     // The intermediate destination
0022     const SMBUrl destination;
0023     // The part destination. This is null when not using a partial file.
0024     const SMBUrl partDestination;
0025     // The complete destination i.e. the final destination i.e. the place where the file will be once all is said and done
0026     const SMBUrl completeDestination;
0027 
0028     // The offest to resume from in the destination. Naturally only should be used when resuming is true.
0029     const off_t destinationOffset = -1;
0030 };
0031 
0032 // Simple encapsulation for SMB resume IO for use with shouldResume.
0033 // This hides the specific IO concern from the resume logic such that it can be used with either SMB IO or local IO.
0034 class SMBResumeIO
0035 {
0036 public:
0037     explicit SMBResumeIO(const SMBUrl &url)
0038         : m_url(url)
0039         // m_stat implicitly init'd by the stat for m_exists
0040         , m_exists(SMBWorker::cache_stat(m_url, &m_stat) == 0)
0041     {
0042     }
0043 
0044     bool exists() const
0045     {
0046         return m_exists;
0047     }
0048 
0049     off_t size() const
0050     {
0051         return m_stat.st_size;
0052     }
0053 
0054     bool isDir() const
0055     {
0056         return S_ISDIR(m_stat.st_mode);
0057     }
0058 
0059     bool remove()
0060     {
0061         return smbc_unlink(m_url.toSmbcUrl());
0062     }
0063 
0064     bool renameTo(const SMBUrl &newUrl)
0065     {
0066         smbc_unlink(newUrl.toSmbcUrl());
0067         if (smbc_rename(m_url.toSmbcUrl(), newUrl.toSmbcUrl()) < 0) {
0068             qCDebug(KIO_SMB_LOG) << "SMB failed to rename" << m_url << "to" << newUrl << "->" << strerror(errno);
0069             return false;
0070         }
0071         return true;
0072     }
0073 
0074 private:
0075     const SMBUrl m_url;
0076     struct stat m_stat {
0077     };
0078     bool m_exists;
0079 };
0080 
0081 // Simple encapsulation for local resume IO for use with shouldResume.
0082 // This hides the specific IO concern from the resume logic such that it can be used with either SMB IO or local IO.
0083 class QFileResumeIO : public QFileInfo
0084 {
0085 public:
0086     explicit QFileResumeIO(const SMBUrl &url)
0087         : QFileInfo(url.path())
0088     {
0089         qDebug() << url.path();
0090     }
0091 
0092     bool remove()
0093     {
0094         return QFile::remove(filePath());
0095     }
0096 
0097     bool renameTo(const SMBUrl &newUrl)
0098     {
0099         QFile::remove(newUrl.path());
0100         if (!QFile::rename(filePath(), newUrl.path())) {
0101             qCDebug(KIO_SMB_LOG) << "failed to rename" << filePath() << "to" << newUrl.path();
0102             return false;
0103         }
0104         return true;
0105     }
0106 
0107 private:
0108     const SMBUrl m_url;
0109 };
0110 
0111 namespace Transfer
0112 {
0113 
0114 // Check if we should resume the upload to destination.
0115 // This returns nullopt when an error has ocurred. The error() function is called internally.
0116 // NB: WorkerInterface is intentionally duck-typed so we can unit test with a mock entity that looks like a WorkerBase but isn't one.
0117 //     Similarly ResumeIO is duck-typed so we can use QFileInfo as as base class in one implementation but not the other,
0118 //     allowing us to cut down on boilerplate call-forwarding code.
0119 template<typename ResumeIO, typename WorkerInterface>
0120 Q_REQUIRED_RESULT std::variant<TransferContext, WorkerResult> shouldResume(const SMBUrl &destination, KIO::JobFlags flags, WorkerInterface *worker)
0121 {
0122     // Resumption has two presentations:
0123     // a) partial resumption - when a .part file is left behind and we pick up where that part left off
0124     // b) in-place resumption - when we are expected to append to the actual destination file without
0125     //   .part temporary in between (FIXME behavior is largely unclear and the below logic is possibly not correct
0126     //   https://invent.kde.org/frameworks/kio/-/issues/9)
0127     const bool markPartial = worker->configValue(QStringLiteral("MarkPartial"), true);
0128 
0129     if (const ResumeIO destIO(destination); destIO.exists()) {
0130         if (const bool resume = static_cast<bool>(flags & KIO::Resume); resume && destIO.exists()) {
0131             // We are resuming the destination file directly!
0132             return TransferContext{resume, destination, destination, destination, destIO.size()};
0133         }
0134 
0135         // Not a resume operation -> if we also were not told to overwrite then we can't process this copy at all
0136         // because the ultimate destination already exists.
0137         if (!(flags & KIO::Overwrite)) {
0138             return WorkerResult::fail(destIO.isDir() ? KIO::ERR_IS_DIRECTORY : KIO::ERR_FILE_ALREADY_EXIST, destination.toDisplayString());
0139         }
0140     }
0141 
0142     if (markPartial) {
0143         const SMBUrl partUrl = destination.partUrl();
0144         if (ResumeIO partIO(partUrl); partIO.exists() && worker->canResume(partIO.size())) {
0145             return TransferContext{true, partUrl, partUrl, destination, partIO.size()};
0146         }
0147 
0148         return TransferContext{false, partUrl, partUrl, destination}; // new part file without offsets or resume
0149     }
0150 
0151     // The part file is not enabled or present, neither is KIO::Resume enabled and the dest file present -> regular
0152     // transfer without resuming of anything.
0153     return TransferContext{false, destination, QUrl(), destination};
0154 }
0155 
0156 // Concludes the resuming. This ought to be called after writing to the destination has
0157 // completed. Destination should be closed. isError is the potential error state. When isError is true,
0158 // the partial file may get discarded (depending on it existing and having an insufficient size).
0159 // The return value is true when an error has occurred. When isError was true this can only ever return true.
0160 template<typename ResumeIO, typename WorkerInterface>
0161 Q_REQUIRED_RESULT WorkerResult concludeResumeHasError(const WorkerResult &result, const TransferContext &resume, WorkerInterface *worker)
0162 {
0163     qDebug() << "concluding" << resume.destination << resume.partDestination << resume.completeDestination;
0164 
0165     if (resume.destination == resume.completeDestination) {
0166         return result;
0167     }
0168 
0169     // Handle error condition.
0170     if (!result.success()) {
0171         const off_t minimumSize = worker->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE);
0172         // TODO should this be partdestination?
0173         if (ResumeIO destIO(resume.destination); destIO.exists() && destIO.size() < minimumSize) {
0174             destIO.remove();
0175         }
0176         return result;
0177     }
0178 
0179     // Rename partial file to its original name. The ResumeIO takes care of potential removing of the destination.
0180     if (ResumeIO partIO(resume.partDestination); !partIO.renameTo(resume.completeDestination)) {
0181         return WorkerResult::fail(ERR_CANNOT_RENAME_PARTIAL, resume.partDestination.toDisplayString());
0182     }
0183 
0184     return result;
0185 }
0186 
0187 } // namespace Transfer