/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL SPDX-FileCopyrightText: 2021-2022 Harald Sitter */ #pragma once #include #include #include #include "kio_smb.h" // Carries the context of a file transfer. struct TransferContext { // When resuming a file. This is false when starting a new .part! // To establish if a partial file is used the completeDestination should be compared with the partDestination. const bool resuming; // The intermediate destination const SMBUrl destination; // The part destination. This is null when not using a partial file. const SMBUrl partDestination; // The complete destination i.e. the final destination i.e. the place where the file will be once all is said and done const SMBUrl completeDestination; // The offest to resume from in the destination. Naturally only should be used when resuming is true. const off_t destinationOffset = -1; }; // Simple encapsulation for SMB resume IO for use with shouldResume. // This hides the specific IO concern from the resume logic such that it can be used with either SMB IO or local IO. class SMBResumeIO { public: explicit SMBResumeIO(const SMBUrl &url) : m_url(url) // m_stat implicitly init'd by the stat for m_exists , m_exists(SMBWorker::cache_stat(m_url, &m_stat) == 0) { } bool exists() const { return m_exists; } off_t size() const { return m_stat.st_size; } bool isDir() const { return S_ISDIR(m_stat.st_mode); } bool remove() { return smbc_unlink(m_url.toSmbcUrl()); } bool renameTo(const SMBUrl &newUrl) { smbc_unlink(newUrl.toSmbcUrl()); if (smbc_rename(m_url.toSmbcUrl(), newUrl.toSmbcUrl()) < 0) { qCDebug(KIO_SMB_LOG) << "SMB failed to rename" << m_url << "to" << newUrl << "->" << strerror(errno); return false; } return true; } private: const SMBUrl m_url; struct stat m_stat { }; bool m_exists; }; // Simple encapsulation for local resume IO for use with shouldResume. // This hides the specific IO concern from the resume logic such that it can be used with either SMB IO or local IO. class QFileResumeIO : public QFileInfo { public: explicit QFileResumeIO(const SMBUrl &url) : QFileInfo(url.path()) { qDebug() << url.path(); } bool remove() { return QFile::remove(filePath()); } bool renameTo(const SMBUrl &newUrl) { QFile::remove(newUrl.path()); if (!QFile::rename(filePath(), newUrl.path())) { qCDebug(KIO_SMB_LOG) << "failed to rename" << filePath() << "to" << newUrl.path(); return false; } return true; } private: const SMBUrl m_url; }; namespace Transfer { // Check if we should resume the upload to destination. // This returns nullopt when an error has ocurred. The error() function is called internally. // NB: WorkerInterface is intentionally duck-typed so we can unit test with a mock entity that looks like a WorkerBase but isn't one. // Similarly ResumeIO is duck-typed so we can use QFileInfo as as base class in one implementation but not the other, // allowing us to cut down on boilerplate call-forwarding code. template Q_REQUIRED_RESULT std::variant shouldResume(const SMBUrl &destination, KIO::JobFlags flags, WorkerInterface *worker) { // Resumption has two presentations: // a) partial resumption - when a .part file is left behind and we pick up where that part left off // b) in-place resumption - when we are expected to append to the actual destination file without // .part temporary in between (FIXME behavior is largely unclear and the below logic is possibly not correct // https://invent.kde.org/frameworks/kio/-/issues/9) const bool markPartial = worker->configValue(QStringLiteral("MarkPartial"), true); if (const ResumeIO destIO(destination); destIO.exists()) { if (const bool resume = static_cast(flags & KIO::Resume); resume && destIO.exists()) { // We are resuming the destination file directly! return TransferContext{resume, destination, destination, destination, destIO.size()}; } // Not a resume operation -> if we also were not told to overwrite then we can't process this copy at all // because the ultimate destination already exists. if (!(flags & KIO::Overwrite)) { return WorkerResult::fail(destIO.isDir() ? KIO::ERR_IS_DIRECTORY : KIO::ERR_FILE_ALREADY_EXIST, destination.toDisplayString()); } } if (markPartial) { const SMBUrl partUrl = destination.partUrl(); if (ResumeIO partIO(partUrl); partIO.exists() && worker->canResume(partIO.size())) { return TransferContext{true, partUrl, partUrl, destination, partIO.size()}; } return TransferContext{false, partUrl, partUrl, destination}; // new part file without offsets or resume } // The part file is not enabled or present, neither is KIO::Resume enabled and the dest file present -> regular // transfer without resuming of anything. return TransferContext{false, destination, QUrl(), destination}; } // Concludes the resuming. This ought to be called after writing to the destination has // completed. Destination should be closed. isError is the potential error state. When isError is true, // the partial file may get discarded (depending on it existing and having an insufficient size). // The return value is true when an error has occurred. When isError was true this can only ever return true. template Q_REQUIRED_RESULT WorkerResult concludeResumeHasError(const WorkerResult &result, const TransferContext &resume, WorkerInterface *worker) { qDebug() << "concluding" << resume.destination << resume.partDestination << resume.completeDestination; if (resume.destination == resume.completeDestination) { return result; } // Handle error condition. if (!result.success()) { const off_t minimumSize = worker->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE); // TODO should this be partdestination? if (ResumeIO destIO(resume.destination); destIO.exists() && destIO.size() < minimumSize) { destIO.remove(); } return result; } // Rename partial file to its original name. The ResumeIO takes care of potential removing of the destination. if (ResumeIO partIO(resume.partDestination); !partIO.renameTo(resume.completeDestination)) { return WorkerResult::fail(ERR_CANNOT_RENAME_PARTIAL, resume.partDestination.toDisplayString()); } return result; } } // namespace Transfer