|
| 1 | +/* |
| 2 | + * Copyright (C) 2024 by Oleksandr Zolotov <alex@nextcloud.com> |
| 3 | + * |
| 4 | + * This program is free software; you can redistribute it and/or modify |
| 5 | + * it under the terms of the GNU General Public License as published by |
| 6 | + * the Free Software Foundation; either version 2 of the License, or |
| 7 | + * (at your option) any later version. |
| 8 | + * |
| 9 | + * This program is distributed in the hope that it will be useful, but |
| 10 | + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 11 | + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 12 | + * for more details. |
| 13 | + */ |
| 14 | + |
| 15 | +#include "bulkpropagatordownloadjob.h" |
| 16 | + |
| 17 | +#include "owncloudpropagator_p.h" |
| 18 | +#include "syncfileitem.h" |
| 19 | +#include "syncengine.h" |
| 20 | +#include "common/syncjournaldb.h" |
| 21 | +#include "common/syncjournalfilerecord.h" |
| 22 | +#include "propagatorjobs.h" |
| 23 | +#include "filesystem.h" |
| 24 | +#include "account.h" |
| 25 | +#include "networkjobs.h" |
| 26 | +#include "propagatedownloadencrypted.h" |
| 27 | + |
| 28 | +#include <QDir> |
| 29 | + |
| 30 | +namespace OCC { |
| 31 | + |
| 32 | +Q_LOGGING_CATEGORY(lcBulkPropagatorDownloadJob, "nextcloud.sync.propagator.bulkdownload", QtInfoMsg) |
| 33 | + |
| 34 | +BulkPropagatorDownloadJob::BulkPropagatorDownloadJob(OwncloudPropagator *propagator, |
| 35 | + PropagateDirectory *parentDirJob, |
| 36 | + const std::vector<SyncFileItemPtr> &items) |
| 37 | + : PropagatorJob(propagator) |
| 38 | + , _filesToDownload(items) |
| 39 | + , _parentDirJob(parentDirJob) |
| 40 | +{ |
| 41 | +} |
| 42 | + |
| 43 | +namespace |
| 44 | +{ |
| 45 | +static QString makeRecallFileName(const QString &fn) |
| 46 | +{ |
| 47 | + QString recallFileName(fn); |
| 48 | + // Add _recall-XXXX before the extension. |
| 49 | + int dotLocation = recallFileName.lastIndexOf('.'); |
| 50 | + // If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file) |
| 51 | + if (dotLocation <= recallFileName.lastIndexOf('/') + 1) { |
| 52 | + dotLocation = recallFileName.size(); |
| 53 | + } |
| 54 | + |
| 55 | + QString timeString = QDateTime::currentDateTimeUtc().toString("yyyyMMdd-hhmmss"); |
| 56 | + recallFileName.insert(dotLocation, "_.sys.admin#recall#-" + timeString); |
| 57 | + |
| 58 | + return recallFileName; |
| 59 | +} |
| 60 | + |
| 61 | +void handleRecallFile(const QString &filePath, const QString &folderPath, SyncJournalDb &journal) |
| 62 | +{ |
| 63 | + qCDebug(lcBulkPropagatorDownloadJob) << "handleRecallFile: " << filePath; |
| 64 | + |
| 65 | + FileSystem::setFileHidden(filePath, true); |
| 66 | + |
| 67 | + QFile file(filePath); |
| 68 | + if (!file.open(QIODevice::ReadOnly)) { |
| 69 | + qCWarning(lcBulkPropagatorDownloadJob) << "Could not open recall file" << file.errorString(); |
| 70 | + return; |
| 71 | + } |
| 72 | + QFileInfo existingFile(filePath); |
| 73 | + QDir baseDir = existingFile.dir(); |
| 74 | + |
| 75 | + while (!file.atEnd()) { |
| 76 | + QByteArray line = file.readLine(); |
| 77 | + line.chop(1); // remove trailing \n |
| 78 | + |
| 79 | + QString recalledFile = QDir::cleanPath(baseDir.filePath(line)); |
| 80 | + if (!recalledFile.startsWith(folderPath) || !recalledFile.startsWith(baseDir.path())) { |
| 81 | + qCWarning(lcBulkPropagatorDownloadJob) << "Ignoring recall of " << recalledFile; |
| 82 | + continue; |
| 83 | + } |
| 84 | + |
| 85 | + // Path of the recalled file in the local folder |
| 86 | + QString localRecalledFile = recalledFile.mid(folderPath.size()); |
| 87 | + |
| 88 | + SyncJournalFileRecord record; |
| 89 | + if (!journal.getFileRecord(localRecalledFile, &record) || !record.isValid()) { |
| 90 | + qCWarning(lcBulkPropagatorDownloadJob) << "No db entry for recall of" << localRecalledFile; |
| 91 | + continue; |
| 92 | + } |
| 93 | + |
| 94 | + qCInfo(lcBulkPropagatorDownloadJob) << "Recalling" << localRecalledFile << "Checksum:" << record._checksumHeader; |
| 95 | + |
| 96 | + QString targetPath = makeRecallFileName(recalledFile); |
| 97 | + |
| 98 | + qCDebug(lcBulkPropagatorDownloadJob) << "Copy recall file: " << recalledFile << " -> " << targetPath; |
| 99 | + // Remove the target first, QFile::copy will not overwrite it. |
| 100 | + FileSystem::remove(targetPath); |
| 101 | + QFile::copy(recalledFile, targetPath); |
| 102 | + } |
| 103 | +} |
| 104 | +} |
| 105 | + |
| 106 | +void BulkPropagatorDownloadJob::addDownloadItem(const SyncFileItemPtr &item) |
| 107 | +{ |
| 108 | + Q_ASSERT(item->isDirectory() || item->_type == ItemTypeVirtualFileDehydration || item->_type == ItemTypeVirtualFile); |
| 109 | + if (item->isDirectory() || (item->_type != ItemTypeVirtualFileDehydration && item->_type != ItemTypeVirtualFile)) { |
| 110 | + qCDebug(lcBulkPropagatorDownloadJob) << "Failed to process bulk download for a non-virtual file" << item->_originalFile; |
| 111 | + return; |
| 112 | + } |
| 113 | + _filesToDownload.push_back(item); |
| 114 | +} |
| 115 | + |
| 116 | +bool BulkPropagatorDownloadJob::scheduleSelfOrChild() |
| 117 | +{ |
| 118 | + if (_filesToDownload.empty()) { |
| 119 | + return false; |
| 120 | + } |
| 121 | + |
| 122 | + _state = Running; |
| 123 | + |
| 124 | + for (const auto &fileToDownload : _filesToDownload) { |
| 125 | + qCDebug(lcBulkPropagatorDownloadJob) << "Scheduling bulk propagator job:" << this << "and starting download of item" |
| 126 | + << "with file:" << fileToDownload->_file << "with size:" << fileToDownload->_size; |
| 127 | + _filesDownloading.push_back(fileToDownload); |
| 128 | + start(fileToDownload); |
| 129 | + } |
| 130 | + |
| 131 | + _filesToDownload.clear(); |
| 132 | + |
| 133 | + checkPropagationIsDone(); |
| 134 | + |
| 135 | + return true; |
| 136 | +} |
| 137 | + |
| 138 | +PropagatorJob::JobParallelism BulkPropagatorDownloadJob::parallelism() const |
| 139 | +{ |
| 140 | + return PropagatorJob::JobParallelism::FullParallelism; |
| 141 | +} |
| 142 | + |
| 143 | +void BulkPropagatorDownloadJob::startAfterIsEncryptedIsChecked(const SyncFileItemPtr &item) |
| 144 | +{ |
| 145 | + const auto &vfs = propagator()->syncOptions()._vfs; |
| 146 | + Q_ASSERT(vfs && vfs->mode() == Vfs::WindowsCfApi); |
| 147 | + Q_ASSERT(item->_type == ItemTypeVirtualFileDehydration || item->_type == ItemTypeVirtualFile); |
| 148 | + |
| 149 | + if (propagator()->localFileNameClash(item->_file)) { |
| 150 | + _parentDirJob->appendTask(item); |
| 151 | + finalizeOneFile(item); |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + // For virtual files just dehydrate or create the placeholder and be done |
| 156 | + if (item->_type == ItemTypeVirtualFileDehydration) { |
| 157 | + const auto fsPath = propagator()->fullLocalPath(item->_file); |
| 158 | + if (!FileSystem::verifyFileUnchanged(fsPath, item->_previousSize, item->_previousModtime)) { |
| 159 | + propagator()->_anotherSyncNeeded = true; |
| 160 | + item->_errorString = tr("File has changed since discovery"); |
| 161 | + abortWithError(item, SyncFileItem::SoftError, tr("File has changed since discovery")); |
| 162 | + return; |
| 163 | + } |
| 164 | + qCDebug(lcBulkPropagatorDownloadJob) << "dehydrating file" << item->_file; |
| 165 | + const auto r = vfs->dehydratePlaceholder(*item); |
| 166 | + if (!r) { |
| 167 | + qCCritical(lcBulkPropagatorDownloadJob) << "Could not dehydrate a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error(); |
| 168 | + abortWithError(item, SyncFileItem::NormalError, r.error()); |
| 169 | + return; |
| 170 | + } |
| 171 | + if (!propagator()->_journal->deleteFileRecord(item->_originalFile)) { |
| 172 | + qCWarning(lcBulkPropagatorDownloadJob) << "could not delete file from local DB" << item->_originalFile; |
| 173 | + abortWithError(item, SyncFileItem::NormalError, tr("Could not delete file record %1 from local DB").arg(item->_originalFile)); |
| 174 | + return; |
| 175 | + } |
| 176 | + } else if (item->_type == ItemTypeVirtualFile) { |
| 177 | + qCDebug(lcBulkPropagatorDownloadJob) << "creating virtual file" << item->_file; |
| 178 | + const auto r = vfs->createPlaceholder(*item); |
| 179 | + if (!r) { |
| 180 | + qCCritical(lcBulkPropagatorDownloadJob) << "Could not create a placholder for a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error(); |
| 181 | + abortWithError(item, SyncFileItem::NormalError, r.error()); |
| 182 | + return; |
| 183 | + } |
| 184 | + } else { |
| 185 | + // we should never get here, as BulkPropagatorDownloadJob must only ever be instantiated and only contain virtual files |
| 186 | + qCCritical(lcBulkPropagatorDownloadJob) << "File" << QDir::toNativeSeparators(item->_file) << "can not be downloaded because it is non virtual!"; |
| 187 | + abortWithError(item, SyncFileItem::NormalError, tr("File %1 can not be downloaded because it is non virtual!").arg(QDir::toNativeSeparators(item->_file))); |
| 188 | + return; |
| 189 | + } |
| 190 | + |
| 191 | + if (!updateMetadata(item)) { |
| 192 | + return; |
| 193 | + } |
| 194 | + |
| 195 | + if (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) { |
| 196 | + // make sure ReadOnly flag is preserved for placeholder, similarly to regular files |
| 197 | + FileSystem::setFileReadOnly(propagator()->fullLocalPath(item->_file), true); |
| 198 | + } |
| 199 | + finalizeOneFile(item); |
| 200 | +} |
| 201 | + |
| 202 | +void BulkPropagatorDownloadJob::finalizeOneFile(const SyncFileItemPtr &file) |
| 203 | +{ |
| 204 | + const auto foundIt = std::find_if(std::cbegin(_filesDownloading), std::cend(_filesDownloading), [&file](const auto &fileDownloading) { |
| 205 | + return fileDownloading == file; |
| 206 | + }); |
| 207 | + if (foundIt != std::cend(_filesDownloading)) { |
| 208 | + emit propagator()->itemCompleted(file, ErrorCategory::GenericError); |
| 209 | + _filesDownloading.erase(foundIt); |
| 210 | + } |
| 211 | + checkPropagationIsDone(); |
| 212 | +} |
| 213 | + |
| 214 | +void BulkPropagatorDownloadJob::checkPropagationIsDone() |
| 215 | +{ |
| 216 | + if (_filesToDownload.empty() && _filesDownloading.empty()) { |
| 217 | + qCInfo(lcBulkPropagatorDownloadJob) << "finished with status" << SyncFileItem::Status::Success; |
| 218 | + emit finished(SyncFileItem::Status::Success); |
| 219 | + propagator()->scheduleNextJob(); |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +void BulkPropagatorDownloadJob::start(const SyncFileItemPtr &item) |
| 224 | +{ |
| 225 | + if (propagator()->_abortRequested) { |
| 226 | + return; |
| 227 | + } |
| 228 | + |
| 229 | + qCDebug(lcBulkPropagatorDownloadJob) << item->_file << propagator()->_activeJobList.count(); |
| 230 | + |
| 231 | + const auto path = item->_file; |
| 232 | + const auto slashPosition = path.lastIndexOf('/'); |
| 233 | + const auto parentPath = slashPosition >= 0 ? path.left(slashPosition) : QString(); |
| 234 | + |
| 235 | + SyncJournalFileRecord parentRec; |
| 236 | + if (!propagator()->_journal->getFileRecord(parentPath, &parentRec)) { |
| 237 | + qCWarning(lcBulkPropagatorDownloadJob) << "could not get file from local DB" << parentPath; |
| 238 | + abortWithError(item, SyncFileItem::NormalError, tr("could not get file %1 from local DB").arg(parentPath)); |
| 239 | + return; |
| 240 | + } |
| 241 | + |
| 242 | + if (!propagator()->account()->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || !parentRec.isE2eEncrypted()) { |
| 243 | + startAfterIsEncryptedIsChecked(item); |
| 244 | + } else { |
| 245 | + _downloadEncryptedHelper = new PropagateDownloadEncrypted(propagator(), parentPath, item, this); |
| 246 | + connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::fileMetadataFound, [this, &item] { |
| 247 | + startAfterIsEncryptedIsChecked(item); |
| 248 | + }); |
| 249 | + connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::failed, [this, &item] { |
| 250 | + abortWithError( |
| 251 | + item, |
| 252 | + SyncFileItem::NormalError, |
| 253 | + tr("File %1 cannot be downloaded because encryption information is missing.").arg(QDir::toNativeSeparators(item->_file))); |
| 254 | + }); |
| 255 | + _downloadEncryptedHelper->start(); |
| 256 | + } |
| 257 | +} |
| 258 | + |
| 259 | +bool BulkPropagatorDownloadJob::updateMetadata(const SyncFileItemPtr &item) |
| 260 | +{ |
| 261 | + const auto fn = propagator()->fullLocalPath(item->_file); |
| 262 | + const auto result = propagator()->updateMetadata(*item); |
| 263 | + if (!result) { |
| 264 | + abortWithError(item, SyncFileItem::FatalError, tr("Error updating metadata: %1").arg(result.error())); |
| 265 | + return false; |
| 266 | + } else if (*result == Vfs::ConvertToPlaceholderResult::Locked) { |
| 267 | + abortWithError(item, SyncFileItem::SoftError, tr("The file %1 is currently in use").arg(item->_file)); |
| 268 | + return false; |
| 269 | + } |
| 270 | + |
| 271 | + propagator()->_journal->commit("download file start2"); |
| 272 | + |
| 273 | + // handle the special recall file |
| 274 | + if (!item->_remotePerm.hasPermission(RemotePermissions::IsShared) |
| 275 | + && (item->_file == QLatin1String(".sys.admin#recall#") || item->_file.endsWith(QLatin1String("/.sys.admin#recall#")))) { |
| 276 | + handleRecallFile(fn, propagator()->localPath(), *propagator()->_journal); |
| 277 | + } |
| 278 | + |
| 279 | + const auto isLockOwnedByCurrentUser = item->_lockOwnerId == propagator()->account()->davUser(); |
| 280 | + |
| 281 | + const auto isUserLockOwnedByCurrentUser = (item->_lockOwnerType == SyncFileItem::LockOwnerType::UserLock && isLockOwnedByCurrentUser); |
| 282 | + const auto isTokenLockOwnedByCurrentUser = (item->_lockOwnerType == SyncFileItem::LockOwnerType::TokenLock && isLockOwnedByCurrentUser); |
| 283 | + |
| 284 | + if (item->_locked == SyncFileItem::LockStatus::LockedItem && !isUserLockOwnedByCurrentUser && !isTokenLockOwnedByCurrentUser) { |
| 285 | + qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is locked: making it read only"; |
| 286 | + FileSystem::setFileReadOnly(fn, true); |
| 287 | + } else { |
| 288 | + qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is not locked: making it" << ((!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) |
| 289 | + ? "read only" |
| 290 | + : "read write"); |
| 291 | + FileSystem::setFileReadOnlyWeak(fn, (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite))); |
| 292 | + } |
| 293 | + return true; |
| 294 | +} |
| 295 | + |
| 296 | +void BulkPropagatorDownloadJob::done(const SyncFileItem::Status status) |
| 297 | +{ |
| 298 | + emit finished(status); |
| 299 | +} |
| 300 | + |
| 301 | +void BulkPropagatorDownloadJob::abortWithError(SyncFileItemPtr item, SyncFileItem::Status status, const QString &error) |
| 302 | +{ |
| 303 | + qCInfo(lcBulkPropagatorDownloadJob) << "finished with status" << status << error; |
| 304 | + abort(AbortType::Synchronous); |
| 305 | + if (item) { |
| 306 | + item->_errorString = error; |
| 307 | + item->_status = status; |
| 308 | + emit propagator()->itemCompleted(item, ErrorCategory::GenericError); |
| 309 | + } |
| 310 | + done(status); |
| 311 | +} |
| 312 | + |
| 313 | +} |
0 commit comments