FileTransfer: Make the usage more convenient

Prepare the API for rsync implementation.

Change-Id: I13b7def31c2e2b1460d18340f6bd7cbd8e0e9434
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
Jarek Kobus
2022-05-11 19:23:43 +02:00
parent 15665680df
commit 7cca6cd718
8 changed files with 343 additions and 165 deletions

View File

@@ -7,6 +7,7 @@ add_qtc_plugin(RemoteLinux
abstractremotelinuxdeploystep.cpp abstractremotelinuxdeploystep.h
abstractuploadandinstallpackageservice.cpp abstractuploadandinstallpackageservice.h
deploymenttimeinfo.cpp deploymenttimeinfo.h
filetransfer.h
genericdirectuploadservice.cpp genericdirectuploadservice.h
genericdirectuploadstep.cpp genericdirectuploadstep.h
genericlinuxdeviceconfigurationwidget.cpp genericlinuxdeviceconfigurationwidget.h genericlinuxdeviceconfigurationwidget.ui

View File

@@ -25,10 +25,11 @@
#include "abstractuploadandinstallpackageservice.h"
#include "linuxdevice.h"
#include "filetransfer.h"
#include "remotelinuxpackageinstaller.h"
#include <projectexplorer/deployablefile.h>
#include <projectexplorer/devicesupport/idevice.h>
#include <utils/processinterface.h>
#include <utils/qtcassert.h>
@@ -47,7 +48,7 @@ class AbstractUploadAndInstallPackageServicePrivate
{
public:
State state = Inactive;
std::unique_ptr<FileTransfer> uploader;
FileTransfer uploader;
Utils::FilePath packageFilePath;
};
@@ -58,6 +59,10 @@ using namespace Internal;
AbstractUploadAndInstallPackageService::AbstractUploadAndInstallPackageService()
: d(new AbstractUploadAndInstallPackageServicePrivate)
{
connect(&d->uploader, &FileTransfer::done, this,
&AbstractUploadAndInstallPackageService::handleUploadFinished);
connect(&d->uploader, &FileTransfer::progress, this,
&AbstractUploadAndInstallPackageService::progressMessage);
}
AbstractUploadAndInstallPackageService::~AbstractUploadAndInstallPackageService()
@@ -98,17 +103,12 @@ void AbstractUploadAndInstallPackageService::doDeploy()
d->state = Uploading;
LinuxDevice::ConstPtr linuxDevice = deviceConfiguration().dynamicCast<const LinuxDevice>();
QTC_ASSERT(linuxDevice, return);
const QString remoteFilePath = uploadDir() + QLatin1Char('/') + d->packageFilePath.fileName();
const FilesToTransfer files {{d->packageFilePath,
deviceConfiguration()->filePath(remoteFilePath)}};
d->uploader.reset(linuxDevice->createFileTransfer(files));
connect(d->uploader.get(), &FileTransfer::done, this,
&AbstractUploadAndInstallPackageService::handleUploadFinished);
connect(d->uploader.get(), &FileTransfer::progress, this,
&AbstractUploadAndInstallPackageService::progressMessage);
d->uploader->start();
d->uploader.setDevice(deviceConfiguration());
d->uploader.setFilesToTransfer(files);
d->uploader.start();
}
void AbstractUploadAndInstallPackageService::stopDeployment()
@@ -118,7 +118,7 @@ void AbstractUploadAndInstallPackageService::stopDeployment()
qWarning("%s: Unexpected state 'Inactive'.", Q_FUNC_INFO);
break;
case Uploading:
d->uploader->stop();
d->uploader.stop();
setFinished();
break;
case Installing:
@@ -167,7 +167,7 @@ void AbstractUploadAndInstallPackageService::handleInstallationFinished(const QS
void AbstractUploadAndInstallPackageService::setFinished()
{
d->state = Inactive;
d->uploader->stop();
d->uploader.stop();
disconnect(packageInstaller(), nullptr, this, nullptr);
handleDeploymentDone();
}

View File

@@ -0,0 +1,85 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include "remotelinux_export.h"
#include <projectexplorer/devicesupport/idevicefwd.h>
#include <utils/filepath.h>
namespace Utils { class ProcessResultData; }
namespace RemoteLinux {
enum class TransferDirection {
Upload,
Download,
Invalid
};
class REMOTELINUX_EXPORT FileToTransfer
{
public:
Utils::FilePath m_source;
Utils::FilePath m_target;
TransferDirection transferDirection() const;
};
using FilesToTransfer = QList<FileToTransfer>;
enum class FileTransferMethod {
Sftp,
Rsync,
Default = Sftp
};
class FileTransferPrivate;
class REMOTELINUX_EXPORT FileTransfer : public QObject
{
Q_OBJECT
public:
FileTransfer();
~FileTransfer();
void setDevice(const ProjectExplorer::IDeviceConstPtr &device);
void setTransferMethod(FileTransferMethod method);
void setFilesToTransfer(const FilesToTransfer &files);
void setRsyncFlags(const QString &flags);
void start();
void stop();
signals:
void progress(const QString &progressMessage);
void done(const Utils::ProcessResultData &resultData);
private:
FileTransferPrivate *d;
};
} // namespace RemoteLinux

View File

@@ -24,9 +24,11 @@
****************************************************************************/
#include "genericdirectuploadservice.h"
#include "linuxdevice.h"
#include "filetransfer.h"
#include <projectexplorer/deployablefile.h>
#include <projectexplorer/devicesupport/idevice.h>
#include <utils/hostosinfo.h>
#include <utils/processinterface.h>
#include <utils/qtcassert.h>
@@ -69,7 +71,7 @@ public:
QQueue<DeployableFile> filesToStat;
State state = Inactive;
QList<DeployableFile> filesToUpload;
std::unique_ptr<FileTransfer> uploader;
FileTransfer uploader;
QList<DeployableFile> deployableFiles;
};
@@ -80,6 +82,20 @@ using namespace Internal;
GenericDirectUploadService::GenericDirectUploadService(QObject *parent)
: AbstractRemoteLinuxDeployService(parent), d(new GenericDirectUploadServicePrivate)
{
connect(&d->uploader, &FileTransfer::done, this, [this](const ProcessResultData &result) {
QTC_ASSERT(d->state == Uploading, return);
if (result.m_error != QProcess::UnknownError) {
emit errorMessage(result.m_errorString);
setFinished();
handleDeploymentDone();
return;
}
d->state = PostProcessing;
chmod();
queryFiles();
});
connect(&d->uploader, &FileTransfer::progress,
this, &GenericDirectUploadService::progressMessage);
}
GenericDirectUploadService::~GenericDirectUploadService()
@@ -250,7 +266,7 @@ void GenericDirectUploadService::setFinished()
it.key()->terminate();
}
d->remoteProcs.clear();
d->uploader->stop();
d->uploader.stop();
d->filesToUpload.clear();
}
@@ -306,25 +322,10 @@ void GenericDirectUploadService::uploadFiles()
files.append({file.localFilePath(),
deviceConfiguration()->filePath(file.remoteFilePath())});
}
LinuxDevice::ConstPtr linuxDevice = deviceConfiguration().dynamicCast<const LinuxDevice>();
QTC_ASSERT(linuxDevice, return);
d->uploader.reset(linuxDevice->createFileTransfer(files));
connect(d->uploader.get(), &FileTransfer::done, this, [this](const ProcessResultData &result) {
QTC_ASSERT(d->state == Uploading, return);
if (result.m_error != QProcess::UnknownError) {
emit errorMessage(result.m_errorString);
setFinished();
handleDeploymentDone();
return;
}
d->state = PostProcessing;
chmod();
queryFiles();
});
connect(d->uploader.get(), &FileTransfer::progress,
this, &GenericDirectUploadService::progressMessage);
d->uploader->start();
d->uploader.setDevice(deviceConfiguration());
d->uploader.setFilesToTransfer(files);
d->uploader.start();
}
void GenericDirectUploadService::chmod()

View File

@@ -25,6 +25,7 @@
#include "linuxdevice.h"
#include "filetransfer.h"
#include "genericlinuxdeviceconfigurationwidget.h"
#include "genericlinuxdeviceconfigurationwizard.h"
#include "linuxdevicetester.h"
@@ -1458,60 +1459,196 @@ static QByteArray transferCommand(const TransferDirection transferDirection, boo
return {};
}
// TODO: Provide 2 subclasses that handle sftp and rsync respectively
static QString methodName(FileTransferMethod method)
{
switch (method) {
case FileTransferMethod::Sftp: return FileTransfer::tr("sftp"); break;
case FileTransferMethod::Rsync: return FileTransfer::tr("rsync"); break;
}
QTC_CHECK(false);
return {};
}
class FileTransferInterface : public QObject
{
Q_OBJECT
public:
void setDevice(const ProjectExplorer::IDeviceConstPtr &device) { m_device = device; }
void setFilesToTransfer(const FilesToTransfer &files, TransferDirection direction)
{
m_files = files;
m_direction = direction;
}
void start() { startImpl(); }
signals:
void progress(const QString &progressMessage);
void done(const Utils::ProcessResultData &resultData);
protected:
FileTransferInterface(FileTransferMethod method)
: m_method(method)
, 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, &FileTransferInterface::doneImpl);
}
void startFailed(const QString &errorString)
{
emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString});
}
bool handleError()
{
ProcessResultData resultData = m_process.resultData();
if (resultData.m_error == QProcess::FailedToStart) {
resultData.m_errorString = tr("\"%1\" failed to start: %2")
.arg(methodName(m_method), resultData.m_errorString);
} else if (resultData.m_exitStatus != QProcess::NormalExit) {
resultData.m_errorString = tr("\"%1\" crashed.").arg(methodName(m_method));
} else if (resultData.m_exitCode != 0) {
resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllStandardError());
} else {
return false;
}
return true;
}
void handleDone()
{
if (!handleError())
emit done(m_process.resultData());
}
FileTransferMethod m_method = FileTransferMethod::Default;
IDevice::ConstPtr m_device;
FilesToTransfer m_files;
QtcProcess m_process;
TransferDirection m_direction;
private:
virtual void startImpl() = 0;
virtual void doneImpl() = 0;
};
class SftpTransferImpl : public FileTransferInterface
{
public:
SftpTransferImpl() : FileTransferInterface(FileTransferMethod::Sftp) { }
private:
void startImpl()
{
const FilePath sftpBinary = SshSettings::sftpFilePath();
if (!sftpBinary.exists()) {
startFailed(tr("\"sftp\" binary \"%1\" does not exist.").arg(sftpBinary.toUserOutput()));
return;
}
m_batchFile.reset(new QTemporaryFile(this));
if (!m_batchFile->isOpen() && !m_batchFile->open()) {
startFailed(tr("Could not create temporary file: %1").arg(m_batchFile->errorString()));
return;
}
const FilePaths dirs = dirsToCreate(m_files);
for (const FilePath &dir : dirs) {
if (m_direction == TransferDirection::Upload) {
m_batchFile->write("-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit()
+ '\n');
} else if (m_direction == TransferDirection::Download) {
if (!QDir::root().mkpath(dir.path())) {
startFailed(tr("Failed to create local directory \"%1\".")
.arg(QDir::toNativeSeparators(dir.path())));
return;
}
}
}
for (const FileToTransfer &file : m_files) {
FilePath sourceFileOrLinkTarget = file.m_source;
bool link = false;
if (m_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(m_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();
}
void doneImpl() final { handleDone(); }
std::unique_ptr<QTemporaryFile> m_batchFile;
};
class RsyncTransferImpl : public FileTransferInterface
{
public:
RsyncTransferImpl(const QString &flags)
: FileTransferInterface(FileTransferMethod::Sftp)
, m_flags(flags)
{ }
private:
void startImpl() {}
void doneImpl() {}
QString m_flags;
};
class FileTransferPrivate : public QObject
{
Q_OBJECT
public:
FileTransferInterface(const LinuxDevice::ConstPtr &device, const FilesToTransfer &files);
void setDevice(const ProjectExplorer::IDeviceConstPtr &device) { m_device = device; }
void setTransferMethod(FileTransferMethod method) { m_method = method; }
void setFilesToTransfer(const FilesToTransfer &files) { m_files = files; }
void setRsyncFlags(const QString &flags) { m_rsyncFlags = flags; }
void start();
void stop() { m_process.close(); }
void stop();
signals:
void progress(const QString &progressMessage);
void done(const Utils::ProcessResultData &resultData);
private:
LinuxDevice::ConstPtr m_device;
FilesToTransfer m_files;
void startFailed(const QString &errorString);
QTemporaryFile m_batchFile;
QtcProcess m_process;
FileTransferMethod m_method = FileTransferMethod::Default;
IDevice::ConstPtr m_device;
FilesToTransfer m_files;
QString m_rsyncFlags = {"-av"}; // TODO: see RsyncDeployStep::defaultFlags(), reuse it
std::unique_ptr<FileTransferInterface> m_transfer;
};
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()
void FileTransferPrivate::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."));
@@ -1522,60 +1659,40 @@ void FileTransferInterface::start()
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()));
}
}
switch (m_method) {
case FileTransferMethod::Sftp:
m_transfer.reset(new SftpTransferImpl());
break;
case FileTransferMethod::Rsync:
m_transfer.reset(new RsyncTransferImpl(m_rsyncFlags));
break;
}
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();
QTC_ASSERT(m_transfer, startFailed(tr("Missing transfer implementation.")));
m_transfer->setDevice(m_device);
m_transfer->setFilesToTransfer(m_files, direction);
connect(m_transfer.get(), &FileTransferInterface::progress,
this, &FileTransferPrivate::progress);
connect(m_transfer.get(), &FileTransferInterface::done,
this, &FileTransferPrivate::done);
m_transfer->start();
}
FileTransfer::FileTransfer(FileTransferInterface *transferInterface)
: d(transferInterface)
void FileTransferPrivate::stop()
{
m_transfer.reset();
}
void FileTransferPrivate::startFailed(const QString &errorString)
{
emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString});
}
FileTransfer::FileTransfer()
: d(new FileTransferPrivate)
{
d->setParent(this);
connect(d, &FileTransferInterface::progress, this, &FileTransfer::progress);
connect(d, &FileTransferInterface::done, this, &FileTransfer::done);
connect(d, &FileTransferPrivate::progress, this, &FileTransfer::progress);
connect(d, &FileTransferPrivate::done, this, &FileTransfer::done);
}
FileTransfer::~FileTransfer()
@@ -1584,6 +1701,26 @@ FileTransfer::~FileTransfer()
delete d;
}
void FileTransfer::setDevice(const ProjectExplorer::IDeviceConstPtr &device)
{
d->setDevice(device);
}
void FileTransfer::setTransferMethod(FileTransferMethod method)
{
d->setTransferMethod(method);
}
void FileTransfer::setFilesToTransfer(const FilesToTransfer &files)
{
d->setFilesToTransfer(files);
}
void FileTransfer::setRsyncFlags(const QString &flags)
{
d->setRsyncFlags(flags);
}
void FileTransfer::start()
{
d->start();
@@ -1594,12 +1731,6 @@ void FileTransfer::stop()
d->stop();
}
FileTransfer *LinuxDevice::createFileTransfer(const FilesToTransfer &files) const
{
return new FileTransfer(new FileTransferInterface(
sharedFromThis().dynamicCast<const LinuxDevice>(), files));
}
namespace Internal {
// Factory

View File

@@ -30,48 +30,8 @@
#include <projectexplorer/devicesupport/idevice.h>
#include <projectexplorer/devicesupport/idevicefactory.h>
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<FileToTransfer>;
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)
@@ -127,8 +87,6 @@ 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();

View File

@@ -24,6 +24,7 @@ Project {
"abstractuploadandinstallpackageservice.h",
"deploymenttimeinfo.cpp",
"deploymenttimeinfo.h",
"filetransfer.h",
"genericdirectuploadservice.cpp",
"genericdirectuploadservice.h",
"genericdirectuploadstep.cpp",

View File

@@ -161,6 +161,7 @@ void RsyncDeployService::deployNextFile()
}
}
// TODO: consider adding "--progress" option for reporting the progress.
const QStringList args = QStringList(cmdLine.options)
<< (localFilePath + (file.localFilePath().isDir() ? "/" : QString()))
<< (cmdLine.remoteHostSpec + ':' + file.remoteFilePath());