diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index 599e696ce03..1612c824e36 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -1415,6 +1415,208 @@ bool LinuxDevice::writeFileContents(const FilePath &filePath, const QByteArray & return d->runInShell({"dd", {"of=" + filePath.path()}}, data); } +TransferDirection FileToTransfer::transferDirection() const +{ + if (m_source.needsDevice() == m_target.needsDevice()) + return TransferDirection::Invalid; + return m_source.needsDevice() ? TransferDirection::Download : TransferDirection::Upload; +} + +static TransferDirection transferDirection(const FilesToTransfer &files) +{ + if (files.isEmpty()) + return TransferDirection::Invalid; + const TransferDirection transferDirection = files.first().transferDirection(); + for (const FileToTransfer &file : files) { + if (file.transferDirection() != transferDirection) + return TransferDirection::Invalid; + } + return transferDirection; +} + +static bool isDeviceMatched(const FilePath &file, const QString &id) +{ + return (file.scheme() == "device") && (file.host() == id); +} + +static bool isDeviceMatched(const FilesToTransfer &files, const QString &id) +{ + for (const FileToTransfer &file : files) { + if (file.transferDirection() == TransferDirection::Upload && !isDeviceMatched(file.m_target, id)) + return false; + if (file.transferDirection() == TransferDirection::Download && !isDeviceMatched(file.m_source, id)) + return false; + } + return true; +} + +static FilePaths dirsToCreate(const FilesToTransfer &files) +{ + FilePaths dirs; + for (const FileToTransfer &file : files) { + FilePath parentDir = file.m_target.parentDir(); + while (true) { + if (dirs.contains(parentDir) || QDir(parentDir.path()).isRoot()) + break; + dirs << parentDir; + parentDir = parentDir.parentDir(); + } + } + sort(dirs); + return dirs; +} + +static QByteArray transferCommand(const TransferDirection transferDirection, bool link) +{ + if (transferDirection == TransferDirection::Upload) + return link ? "ln -s" : "put"; + if (transferDirection == TransferDirection::Download) + return "get"; + return {}; +} + +// TODO: Provide 2 subclasses that handle sftp and rsync respectively +class FileTransferInterface : public QObject +{ + Q_OBJECT + +public: + FileTransferInterface(const LinuxDevice::ConstPtr &device, const FilesToTransfer &files); + + void start(); + void stop() { m_process.close(); } + +signals: + void progress(const QString &progressMessage); + void done(const Utils::ProcessResultData &resultData); + +private: + LinuxDevice::ConstPtr m_device; + FilesToTransfer m_files; + + QTemporaryFile m_batchFile; + QtcProcess m_process; +}; + +FileTransferInterface::FileTransferInterface(const LinuxDevice::ConstPtr &device, const FilesToTransfer &files) + : m_device(device) + , m_files(files) + , m_batchFile(this) + , m_process(this) +{ + SshConnectionParameters::setupSshEnvironment(&m_process); + + connect(&m_process, &QtcProcess::readyReadStandardOutput, this, [this] { + emit progress(QString::fromLocal8Bit(m_process.readAllStandardOutput())); + }); + connect(&m_process, &QtcProcess::done, this, [this] { + ProcessResultData resultData = m_process.resultData(); + if (resultData.m_error == QProcess::FailedToStart) + resultData.m_errorString = tr("\"sftp\" failed to start: %1").arg(resultData.m_errorString); + else if (resultData.m_exitStatus != QProcess::NormalExit) + resultData.m_errorString = tr("\"sftp\" crashed."); + else if (resultData.m_exitCode != 0) + resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllStandardError()); + emit done(resultData); + }); +} + +void FileTransferInterface::start() +{ + stop(); + + auto startFailed = [this](const QString &errorString) { + emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString}); + }; + + if (m_files.isEmpty()) + return startFailed(tr("No files to transfer.")); + + const TransferDirection direction = transferDirection(m_files); + if (direction == TransferDirection::Invalid) + return startFailed(tr("Mixing different types on transfer in one go.")); + + if (!isDeviceMatched(m_files, m_device->id().toString())) + return startFailed(tr("Trying to transfer into / from not matching device.")); + + const FilePath sftpBinary = SshSettings::sftpFilePath(); + if (!sftpBinary.exists()) + return startFailed(tr("\"sftp\" binary \"%1\" does not exist.").arg(sftpBinary.toUserOutput())); + + if (!m_batchFile.isOpen() && !m_batchFile.open()) + return startFailed(tr("Could not create temporary file: %1").arg(m_batchFile.errorString())); + + const FilePaths dirs = dirsToCreate(m_files); + for (const FilePath &dir : dirs) { + if (direction == TransferDirection::Upload) { + m_batchFile.write("-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + + '\n'); + } else if (direction == TransferDirection::Download) { + if (!QDir::root().mkpath(dir.path())) { + return startFailed(tr("Failed to create local directory \"%1\".") + .arg(dir.toUserOutput())); + } + } + } + + for (const FileToTransfer &file : m_files) { + FilePath sourceFileOrLinkTarget = file.m_source; + bool link = false; + if (direction == TransferDirection::Upload) { + const QFileInfo fi(file.m_source.toFileInfo()); + if (fi.isSymLink()) { + link = true; + m_batchFile.write("-rm " + ProcessArgs::quoteArgUnix( + file.m_target.path()).toLocal8Bit() + '\n'); + // see QTBUG-5817. + sourceFileOrLinkTarget.setPath(fi.dir().relativeFilePath(fi.symLinkTarget())); + } + } + m_batchFile.write(transferCommand(direction, link) + ' ' + + ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path()).toLocal8Bit() + ' ' + + ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit() + '\n'); + } + m_batchFile.close(); + + m_process.setStandardInputFile(m_batchFile.fileName()); + + // TODO: Add support for shared ssh connection + const SshConnectionParameters params = displayless(m_device->sshParameters()); + m_process.setCommand(CommandLine(sftpBinary, + params.connectionOptions(sftpBinary) << params.host())); + m_process.start(); +} + +FileTransfer::FileTransfer(FileTransferInterface *transferInterface) + : d(transferInterface) +{ + d->setParent(this); + connect(d, &FileTransferInterface::progress, this, &FileTransfer::progress); + connect(d, &FileTransferInterface::done, this, &FileTransfer::done); +} + +FileTransfer::~FileTransfer() +{ + stop(); + delete d; +} + +void FileTransfer::start() +{ + d->start(); +} + +void FileTransfer::stop() +{ + d->stop(); +} + +FileTransfer *LinuxDevice::createFileTransfer(const FilesToTransfer &files) const +{ + return new FileTransfer(new FileTransferInterface( + sharedFromThis().dynamicCast(), files)); +} + namespace Internal { // Factory diff --git a/src/plugins/remotelinux/linuxdevice.h b/src/plugins/remotelinux/linuxdevice.h index 3272e46e575..8c5fd29fc8e 100644 --- a/src/plugins/remotelinux/linuxdevice.h +++ b/src/plugins/remotelinux/linuxdevice.h @@ -30,8 +30,48 @@ #include #include +namespace Utils { class ProcessResultData; } + namespace RemoteLinux { +enum class TransferDirection { + Upload, + Download, + Invalid +}; + +class FileToTransfer +{ +public: + Utils::FilePath m_source; + Utils::FilePath m_target; + TransferDirection transferDirection() const; +}; +using FilesToTransfer = QList; + +class FileTransferInterface; + +class REMOTELINUX_EXPORT FileTransfer : public QObject +{ + Q_OBJECT + +public: + ~FileTransfer(); + + void start(); + void stop(); + +signals: + void progress(const QString &progressMessage); + void done(const Utils::ProcessResultData &resultData); + +private: + FileTransfer(FileTransferInterface *transferInterface); + FileTransferInterface *d; + + friend class LinuxDevice; +}; + class REMOTELINUX_EXPORT LinuxDevice : public ProjectExplorer::IDevice { Q_DECLARE_TR_FUNCTIONS(RemoteLinux::Internal::LinuxDevice) @@ -87,6 +127,8 @@ public: QFileDevice::Permissions permissions(const Utils::FilePath &filePath) const override; bool setPermissions(const Utils::FilePath &filePath, QFileDevice::Permissions permissions) const override; + FileTransfer *createFileTransfer(const FilesToTransfer &files) const; + protected: LinuxDevice();