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