From 4af30a05b7cda839e66bde5e1ddaec55a6b23f87 Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Tue, 28 Sep 2021 16:04:50 +0300 Subject: [PATCH] User data backup (#27) * Skeleton implementation for user data backup * Get flipper file tree * Support file names with spaces * Working implementation of backup save * Skeleton implementation of backup restore * Working implementation of backup restore --- .../components/FlipperListDelegate.qml | 17 ++ application/screens/homescreen.qml | 45 ++++++ backend/backend.pro | 9 ++ backend/fileinfo.h | 20 +++ backend/firmwaredownloader.cpp | 23 ++- backend/firmwaredownloader.h | 5 +- .../operations/assetsdownloadoperation.cpp | 8 +- .../operations/getfiletreeoperation.cpp | 66 ++++++++ .../operations/getfiletreeoperation.h | 37 +++++ .../operations/userbackupoperation.cpp | 129 +++++++++++++++ .../operations/userbackupoperation.h | 40 +++++ .../operations/userrestoreoperation.cpp | 149 ++++++++++++++++++ .../operations/userrestoreoperation.h | 40 +++++ backend/flipperzero/storage/listoperation.cpp | 85 ++++++++++ backend/flipperzero/storage/listoperation.h | 36 +++++ .../flipperzero/storage/mkdiroperation.cpp | 2 +- backend/flipperzero/storage/readoperation.cpp | 2 +- .../flipperzero/storage/removeoperation.cpp | 2 +- backend/flipperzero/storage/statoperation.cpp | 2 +- .../flipperzero/storage/writeoperation.cpp | 2 +- backend/flipperzero/storagecontroller.cpp | 9 ++ backend/flipperzero/storagecontroller.h | 2 + 22 files changed, 716 insertions(+), 14 deletions(-) create mode 100644 backend/fileinfo.h create mode 100644 backend/flipperzero/operations/getfiletreeoperation.cpp create mode 100644 backend/flipperzero/operations/getfiletreeoperation.h create mode 100644 backend/flipperzero/operations/userbackupoperation.cpp create mode 100644 backend/flipperzero/operations/userbackupoperation.h create mode 100644 backend/flipperzero/operations/userrestoreoperation.cpp create mode 100644 backend/flipperzero/operations/userrestoreoperation.h create mode 100644 backend/flipperzero/storage/listoperation.cpp create mode 100644 backend/flipperzero/storage/listoperation.h diff --git a/application/components/FlipperListDelegate.qml b/application/components/FlipperListDelegate.qml index e871722e..10f2b496 100644 --- a/application/components/FlipperListDelegate.qml +++ b/application/components/FlipperListDelegate.qml @@ -14,6 +14,9 @@ Item { signal versionListRequested(var device) signal screenStreamRequested(var device) + signal backupRequested(var device) + signal restoreRequested(var device) + id: item width: parent.width height: 85 @@ -183,6 +186,20 @@ Item { MenuSeparator {} + Menu { + title: qsTr("Backup && Restore") + + MenuItem { + text: qsTr("Backup User Data...") + onTriggered: backupRequested(device) + } + + MenuItem { + text: qsTr("Restore User Data...") + onTriggered: restoreRequested(device) + } + } + Menu { title: qsTr("Expert options") diff --git a/application/screens/homescreen.qml b/application/screens/homescreen.qml index 8d84152e..95a44b5c 100644 --- a/application/screens/homescreen.qml +++ b/application/screens/homescreen.qml @@ -80,6 +80,29 @@ Item { } } + FileDialog { + id: dirDialog + title: qsTr("Please choose a directory") + folder: shortcuts.home + selectFolder: true + + function openWithConfirmation(onAcceptedFunc, messageObj = {}) { + const onDialogRejected = function() { + dirDialog.rejected.disconnect(onDialogRejected); + dirDialog.accepted.disconnect(onDialogAccepted); + } + + const onDialogAccepted = function() { + onDialogRejected(); + confirmationDialog.openWithMessage(onAcceptedFunc, messageObj) + } + + dirDialog.accepted.connect(onDialogAccepted); + dirDialog.rejected.connect(onDialogRejected); + dirDialog.open(); + } + } + ListView { id: deviceList model: deviceRegistry @@ -127,6 +150,28 @@ Item { }, messageObj); } + onBackupRequested: { + const messageObj = { + title : qsTr("Backup user data?"), + subtitle : qsTr("This will backup the contents of internal storage.") + }; + + dirDialog.openWithConfirmation(function() { + downloader.backupUserData(device, dirDialog.fileUrl); + }, messageObj); + } + + onRestoreRequested: { + const messageObj = { + title : qsTr("Restore user data?"), + subtitle : qsTr("This will restore the contents of internal storage.") + }; + + dirDialog.openWithConfirmation(function() { + downloader.restoreUserData(device, dirDialog.fileUrl); + }, messageObj); + } + onLocalAssetsUpdateRequested: { const messageObj = { title : qsTr("Update the databases?"), diff --git a/backend/backend.pro b/backend/backend.pro index e96f5652..f3568651 100644 --- a/backend/backend.pro +++ b/backend/backend.pro @@ -25,9 +25,13 @@ SOURCES += \ flipperzero/operations/fixbootissuesoperation.cpp \ flipperzero/operations/fixoptionbytesoperation.cpp \ flipperzero/operations/flipperzerooperation.cpp \ + flipperzero/operations/getfiletreeoperation.cpp \ + flipperzero/operations/userbackupoperation.cpp \ + flipperzero/operations/userrestoreoperation.cpp \ flipperzero/operations/wirelessstackdownloadoperation.cpp \ flipperzero/recoverycontroller.cpp \ flipperzero/remotecontroller.cpp \ + flipperzero/storage/listoperation.cpp \ flipperzero/storage/mkdiroperation.cpp \ flipperzero/storage/readoperation.cpp \ flipperzero/storage/removeoperation.cpp \ @@ -47,6 +51,7 @@ HEADERS += \ abstractserialoperation.h \ deviceregistry.h \ failable.h \ + fileinfo.h \ filenode.h \ firmwaredownloader.h \ flipperupdates.h \ @@ -61,9 +66,13 @@ HEADERS += \ flipperzero/operations/fixbootissuesoperation.h \ flipperzero/operations/fixoptionbytesoperation.h \ flipperzero/operations/flipperzerooperation.h \ + flipperzero/operations/getfiletreeoperation.h \ + flipperzero/operations/userbackupoperation.h \ + flipperzero/operations/userrestoreoperation.h \ flipperzero/operations/wirelessstackdownloadoperation.h \ flipperzero/recoverycontroller.h \ flipperzero/remotecontroller.h \ + flipperzero/storage/listoperation.h \ flipperzero/storage/mkdiroperation.h \ flipperzero/storage/readoperation.h \ flipperzero/storage/removeoperation.h \ diff --git a/backend/fileinfo.h b/backend/fileinfo.h new file mode 100644 index 00000000..38072a48 --- /dev/null +++ b/backend/fileinfo.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +enum class FileType { + Directory, + RegularFile, + Storage, + Unknown +}; + +struct FileInfo { + QByteArray name; + QByteArray absolutePath; + FileType type; + qint64 size; +}; + +using FileInfoList = QList; diff --git a/backend/firmwaredownloader.cpp b/backend/firmwaredownloader.cpp index 6cc4c7bb..48c616d3 100644 --- a/backend/firmwaredownloader.cpp +++ b/backend/firmwaredownloader.cpp @@ -6,11 +6,13 @@ #include #include "flipperzero/flipperzero.h" -#include "flipperzero/operations/wirelessstackdownloadoperation.h" -#include "flipperzero/operations/firmwaredownloadoperation.h" -#include "flipperzero/operations/fixoptionbytesoperation.h" -#include "flipperzero/operations/assetsdownloadoperation.h" +#include "flipperzero/operations/userbackupoperation.h" +#include "flipperzero/operations/userrestoreoperation.h" #include "flipperzero/operations/fixbootissuesoperation.h" +#include "flipperzero/operations/assetsdownloadoperation.h" +#include "flipperzero/operations/fixoptionbytesoperation.h" +#include "flipperzero/operations/firmwaredownloadoperation.h" +#include "flipperzero/operations/wirelessstackdownloadoperation.h" #include "remotefilefetcher.h" #include "macros.h" @@ -90,6 +92,16 @@ void FirmwareDownloader::downloadAssets(FlipperZero *device, const QString &file enqueueOperation(new Flipper::Zero::AssetsDownloadOperation(device, file, this)); } +void FirmwareDownloader::backupUserData(FlipperZero *device, const QString &backupPath) +{ + enqueueOperation(new Flipper::Zero::UserBackupOperation(device, backupPath, this)); +} + +void FirmwareDownloader::restoreUserData(FlipperZero *device, const QString &backupPath) +{ + enqueueOperation(new Flipper::Zero::UserRestoreOperation(device, backupPath, this)); +} + void FirmwareDownloader::processQueue() { if(m_operationQueue.isEmpty()) { @@ -102,7 +114,8 @@ void FirmwareDownloader::processQueue() connect(currentOperation, &AbstractOperation::finished, this, [=]() { info_msg(QStringLiteral("Operation '%1' finished with status: %2.").arg(currentOperation->description(), currentOperation->errorString())); currentOperation->deleteLater(); - processQueue(); + + QTimer::singleShot(0, this, &FirmwareDownloader::processQueue); }); currentOperation->start(); diff --git a/backend/firmwaredownloader.h b/backend/firmwaredownloader.h index 6deccdfe..e20e4856 100644 --- a/backend/firmwaredownloader.h +++ b/backend/firmwaredownloader.h @@ -5,9 +5,9 @@ #include #include "flipperupdates.h" -#include "abstractoperation.h" class QIODevice; +class AbstractOperation; namespace Flipper { @@ -36,6 +36,9 @@ public slots: void downloadAssets(Flipper::FlipperZero *device, const QString &filePath); + void backupUserData(Flipper::FlipperZero *device, const QString &backupPath); + void restoreUserData(Flipper::FlipperZero *device, const QString &backupPath); + private slots: void processQueue(); diff --git a/backend/flipperzero/operations/assetsdownloadoperation.cpp b/backend/flipperzero/operations/assetsdownloadoperation.cpp index 72fc456d..3f4846bf 100644 --- a/backend/flipperzero/operations/assetsdownloadoperation.cpp +++ b/backend/flipperzero/operations/assetsdownloadoperation.cpp @@ -305,16 +305,18 @@ bool AssetsDownloadOperation::deleteFiles() device()->setMessage(tr("Deleting unneeded files...")); - int i = m_delete.size(); + int numFiles = m_delete.size(); for(const auto &fileInfo : qAsConst(m_delete)) { - --i; + const auto isLastFile = (--numFiles == 0); const auto fileName = QByteArrayLiteral("/ext/") + fileInfo.absolutePath.toLocal8Bit(); + auto *op = device()->storage()->remove(fileName); + connect(op, &AbstractOperation::finished, this, [=]() { if(op->isError()) { finishWithError(op->errorString()); - } else if(i == 0) { + } else if(isLastFile) { QTimer::singleShot(0, this, &AssetsDownloadOperation::transitionToNextState); } }); diff --git a/backend/flipperzero/operations/getfiletreeoperation.cpp b/backend/flipperzero/operations/getfiletreeoperation.cpp new file mode 100644 index 00000000..ba0c7b3b --- /dev/null +++ b/backend/flipperzero/operations/getfiletreeoperation.cpp @@ -0,0 +1,66 @@ +#include "getfiletreeoperation.h" + +#include + +#include "flipperzero/flipperzero.h" +#include "flipperzero/storagecontroller.h" +#include "flipperzero/storage/listoperation.h" + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +GetFileTreeOperation::GetFileTreeOperation(FlipperZero *device, const QByteArray &rootPath, QObject *parent): + Operation(device, parent), + m_rootPath(rootPath), + m_pendingCount(0) +{} + +const QString GetFileTreeOperation::description() const +{ + return QStringLiteral("Get File Tree @%1").arg(QString(m_rootPath)); +} + +const FileInfoList &GetFileTreeOperation::result() const +{ + return m_result; +} + +void GetFileTreeOperation::transitionToNextState() +{ + if(state() == BasicState::Ready) { + setState(State::Running); + listDirectory(m_rootPath); + + } else if(state() == State::Running) { + --m_pendingCount; + auto *op = qobject_cast(sender()); + + if(op->isError()) { + finishWithError(op->errorString()); + return; + } + + for(const auto &fileInfo : qAsConst(op->result())) { + if(fileInfo.type == FileType::Directory) { + listDirectory(fileInfo.absolutePath); + } + + m_result.push_back(fileInfo); + } + + op->deleteLater(); + + if(!m_pendingCount) { + finish(); + } + } +} + +void GetFileTreeOperation::listDirectory(const QByteArray &path) +{ + ++m_pendingCount; + auto *op = device()->storage()->list(path); + connect(op, &AbstractOperation::finished, this, &GetFileTreeOperation::transitionToNextState); +} diff --git a/backend/flipperzero/operations/getfiletreeoperation.h b/backend/flipperzero/operations/getfiletreeoperation.h new file mode 100644 index 00000000..f9d0cf0d --- /dev/null +++ b/backend/flipperzero/operations/getfiletreeoperation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "flipperzerooperation.h" +#include "fileinfo.h" + +class QSerialPort; + +namespace Flipper { +namespace Zero { + +class GetFileTreeOperation : public Operation +{ + Q_OBJECT + + enum State { + Running = BasicState::User + }; + +public: + GetFileTreeOperation(FlipperZero *device, const QByteArray &rootPath, QObject *parent = nullptr); + const QString description() const override; + const FileInfoList &result() const; + +private slots: + void transitionToNextState() override; + +private: + void listDirectory(const QByteArray &path); + + QByteArray m_rootPath; + QByteArray m_currentPath; + FileInfoList m_result; + int m_pendingCount; +}; + +} +} diff --git a/backend/flipperzero/operations/userbackupoperation.cpp b/backend/flipperzero/operations/userbackupoperation.cpp new file mode 100644 index 00000000..70f0a70d --- /dev/null +++ b/backend/flipperzero/operations/userbackupoperation.cpp @@ -0,0 +1,129 @@ +#include "userbackupoperation.h" + +#include +#include +#include + +#include "flipperzero/flipperzero.h" +#include "flipperzero/storagecontroller.h" +#include "flipperzero/storage/readoperation.h" + +#include "getfiletreeoperation.h" +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +UserBackupOperation::UserBackupOperation(FlipperZero *device, const QString &backupPath, QObject *parent): + Operation(device, parent), + m_backupDir(QUrl(backupPath).toLocalFile()), + m_deviceDirName(QByteArrayLiteral("/int")) +{} + +const QString UserBackupOperation::description() const +{ + return QStringLiteral("Backup user data @%1 %2").arg(device()->model(), device()->name()); +} + +void UserBackupOperation::transitionToNextState() +{ + if(state() == BasicState::Ready) { + setState(State::CreatingDirectory); + + if(!m_deviceDirName.startsWith('/')) { + finishWithError(QStringLiteral("Expecting absolute path for device directory")); + } else if(!createBackupDirectory()) { + finishWithError(QStringLiteral("Failed to create backup directory")); + } else { + QTimer::singleShot(0, this, &UserBackupOperation::transitionToNextState); + } + + } else if(state() == State::CreatingDirectory) { + setState(State::GettingFileTree); + + auto *op = new GetFileTreeOperation(device(), m_deviceDirName, this); + + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } else { + m_fileList = op->result(); + QTimer::singleShot(0, this, &UserBackupOperation::transitionToNextState); + } + + op->deleteLater(); + }); + + op->start(); + + } else if(state() == State::GettingFileTree) { + setState(State::ReadingFiles); + + if(!readFiles()) { + finishWithError(QStringLiteral("Failed to read files from device")); + } + } +} + +bool UserBackupOperation::createBackupDirectory() +{ + const auto &subdir = device()->name(); + const QFileInfo targetDirInfo(m_backupDir, subdir); + + if(targetDirInfo.isDir()) { + QDir d(targetDirInfo.absoluteFilePath()); + + if(!d.removeRecursively()) { + return false; + } + + } else if(targetDirInfo.exists()) { + return false; + } + + return m_backupDir.mkpath(subdir + m_deviceDirName) && m_backupDir.cd(subdir); +} + +bool UserBackupOperation::readFiles() +{ + auto numFiles = std::count_if(m_fileList.cbegin(), m_fileList.cend(), [](const FileInfo &arg) { + return arg.type == FileType::RegularFile; + }); + + for(const auto &fileInfo: qAsConst(m_fileList)) { + const auto filePath = fileInfo.absolutePath.mid(1); + + if(fileInfo.type == FileType::Directory) { + if(!m_backupDir.mkdir(filePath)) { + return false; + } + + } else if(fileInfo.type == FileType::RegularFile) { + const auto isLastFile = (--numFiles == 0); + + auto *file = new QFile(m_backupDir.absoluteFilePath(filePath), this); + if(!file->open(QIODevice::WriteOnly)) { + file->deleteLater(); + return false; + } + + auto *op = device()->storage()->read(fileInfo.absolutePath, file); + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } + + op->deleteLater(); + + file->close(); + file->deleteLater(); + + if(isLastFile) { + finish(); + } + }); + } + } + + return true; +} diff --git a/backend/flipperzero/operations/userbackupoperation.h b/backend/flipperzero/operations/userbackupoperation.h new file mode 100644 index 00000000..4cca3fcf --- /dev/null +++ b/backend/flipperzero/operations/userbackupoperation.h @@ -0,0 +1,40 @@ +#pragma once + +#include "flipperzerooperation.h" + +#include + +#include "fileinfo.h" + +namespace Flipper { +namespace Zero { + +class UserBackupOperation : public Operation +{ + Q_OBJECT + + enum State { + CreatingDirectory = BasicState::User, + GettingFileTree, + ReadingFiles + }; + +public: + UserBackupOperation(FlipperZero *device, const QString &backupPath, QObject *parent = nullptr); + const QString description() const override; + +private slots: + void transitionToNextState() override; + +private: + bool createBackupDirectory(); + bool readFiles(); + + QDir m_backupDir; + QByteArray m_deviceDirName; + FileInfoList m_fileList; +}; + +} +} + diff --git a/backend/flipperzero/operations/userrestoreoperation.cpp b/backend/flipperzero/operations/userrestoreoperation.cpp new file mode 100644 index 00000000..10f3c164 --- /dev/null +++ b/backend/flipperzero/operations/userrestoreoperation.cpp @@ -0,0 +1,149 @@ +#include "userrestoreoperation.h" + +#include +#include +#include +#include + +#include "flipperzero/flipperzero.h" +#include "flipperzero/storagecontroller.h" +#include "flipperzero/storage/mkdiroperation.h" +#include "flipperzero/storage/writeoperation.h" +#include "flipperzero/storage/removeoperation.h" + +#include "macros.h" + +using namespace Flipper; +using namespace Zero; + +UserRestoreOperation::UserRestoreOperation(FlipperZero *device, const QString &backupPath, QObject *parent): + Operation(device, parent), + m_backupDir(QUrl(backupPath).toLocalFile()), + m_deviceDirName(QByteArrayLiteral("/int")) +{ + m_backupDir.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); + m_backupDir.setSorting(QDir::Name | QDir::DirsFirst); +} + +const QString UserRestoreOperation::description() const +{ + return QStringLiteral("Restore user data @%1 %2").arg(device()->model(), device()->name()); +} + +void UserRestoreOperation::transitionToNextState() +{ + if(state() == BasicState::Ready) { + setState(State::ReadingBackupDir); + if(!readBackupDir()) { + finishWithError(QStringLiteral("Failed to process backup directory")); + } else { + QTimer::singleShot(0, this, &UserRestoreOperation::transitionToNextState); + } + + } else if(state() == State::ReadingBackupDir) { + setState(State::DeletingFiles); + if(!deleteFiles()) { + finishWithError(QStringLiteral("Failed to delete old files")); + } + + } else if(state() == State::DeletingFiles) { + setState(State::WritingFiles); + if(!writeFiles()) { + finishWithError(QStringLiteral("Failed to write new files")); + } + + } else if(state() == State::WritingFiles) { + finish(); + } else {} +} + +bool UserRestoreOperation::readBackupDir() +{ + const auto subdir = device()->name() + m_deviceDirName; + + check_return_bool(m_backupDir.exists(subdir), "Requested directory not found"); + check_return_bool(m_backupDir.cd(subdir), "Access denied"); + + QDirIterator it(m_backupDir, QDirIterator::Subdirectories); + + while(it.hasNext()) { + it.next(); + m_files.append(it.fileInfo()); + } + + check_return_bool(!m_files.isEmpty(), "Backup directory is empty."); + return true; +} + +bool UserRestoreOperation::deleteFiles() +{ + device()->setMessage(QStringLiteral("Cleaning up...")); + + auto numFiles = m_files.size(); + for(auto it = m_files.crbegin(); it != m_files.crend(); ++it) { + check_return_bool(it->isFile() || it->isDir(), "Expected a file or directory"); + + const auto filePath = m_deviceDirName + QByteArrayLiteral("/") + m_backupDir.relativeFilePath(it->absoluteFilePath()).toLocal8Bit(); + const auto isLastFile = (--numFiles == 0); + + auto *op = device()->storage()->remove(filePath); + connect(op, &AbstractOperation::finished, this, [=](){ + if(op->isError()) { + finishWithError(op->errorString()); + } else if(isLastFile) { + QTimer::singleShot(0, this, &UserRestoreOperation::transitionToNextState); + } + + op->deleteLater(); + }); + } + + return true; +} + +bool UserRestoreOperation::writeFiles() +{ + device()->setMessage(QStringLiteral("Restoring backup...")); + + auto numFiles = m_files.size(); + + for(const auto &fileInfo: qAsConst(m_files)) { + const auto filePath = m_deviceDirName + QByteArrayLiteral("/") + m_backupDir.relativeFilePath(fileInfo.absoluteFilePath()).toLocal8Bit(); + const auto isLastFile = (--numFiles == 0); + + AbstractOperation *op; + + if(fileInfo.isFile()) { + auto *file = new QFile(fileInfo.absoluteFilePath(), this); + + if(!file->open(QIODevice::ReadOnly)) { + file->deleteLater(); + error_msg(QStringLiteral("Failed to open file for reading: %1.").arg(file->errorString())); + return false; + } + + op = device()->storage()->write(filePath, file); + connect(op, &AbstractOperation::finished, this, [=]() { + file->close(); + file->deleteLater(); + }); + + } else if(fileInfo.isDir()) { + op = device()->storage()->mkdir(filePath); + } else { + return false; + } + + connect(op, &AbstractOperation::finished, this, [=]() { + if(op->isError()) { + finishWithError(op->errorString()); + } else if(isLastFile) { + QTimer::singleShot(0, this, &UserRestoreOperation::transitionToNextState); + } + + op->deleteLater(); + }); + } + + return true; +} diff --git a/backend/flipperzero/operations/userrestoreoperation.h b/backend/flipperzero/operations/userrestoreoperation.h new file mode 100644 index 00000000..04d9cae2 --- /dev/null +++ b/backend/flipperzero/operations/userrestoreoperation.h @@ -0,0 +1,40 @@ +#pragma once + +#include "flipperzerooperation.h" + +#include +#include + +namespace Flipper { +namespace Zero { + +class UserRestoreOperation : public Operation +{ + Q_OBJECT + + enum State { + ReadingBackupDir = BasicState::User, + DeletingFiles, + WritingFiles + }; + +public: + UserRestoreOperation(FlipperZero *device, const QString &backupPath, QObject *parent = nullptr); + const QString description() const override; + +private slots: + void transitionToNextState() override; + +private: + QDir m_backupDir; + QByteArray m_deviceDirName; + QFileInfoList m_files; + + bool readBackupDir(); + bool deleteFiles(); + bool writeFiles(); +}; + +} +} + diff --git a/backend/flipperzero/storage/listoperation.cpp b/backend/flipperzero/storage/listoperation.cpp new file mode 100644 index 00000000..00095d6d --- /dev/null +++ b/backend/flipperzero/storage/listoperation.cpp @@ -0,0 +1,85 @@ +#include "listoperation.h" + +#include + +#include "macros.h" + +#define FILE_PREFIX QByteArrayLiteral("[F]") +#define DIRECTORY_PREFIX QByteArrayLiteral("[D]") + +using namespace Flipper; +using namespace Zero; + +ListOperation::ListOperation(QSerialPort *serialPort, const QByteArray &dirName, QObject *parent): + SimpleSerialOperation(serialPort, parent), + m_dirName(dirName) +{} + +const QString ListOperation::description() const +{ + return QStringLiteral("List @%1").arg(QString(m_dirName)); +} + +const FileInfoList &ListOperation::result() const +{ + return m_result; +} + +QByteArray ListOperation::endOfMessageToken() const +{ + return QByteArrayLiteral("\r\n\r\n>: \a"); +} + +QByteArray ListOperation::commandLine() const +{ + return QByteArrayLiteral("storage list \"") + m_dirName + QByteArrayLiteral("\"\r\n"); +} + +bool ListOperation::parseReceivedData() +{ + QBuffer buf; + + if(!buf.open(QIODevice::ReadWrite)) { + finishWithError(buf.errorString()); + return false; + } + + buf.write(receivedData()); + buf.seek(0); + + while(buf.canReadLine()) { + const auto line = buf.readLine().trimmed(); + if(line.startsWith(FILE_PREFIX)) { + parseFile(line); + } else if(line.startsWith(DIRECTORY_PREFIX)) { + parseDirectory(line); + } else {} + } + + return true; +} + +void ListOperation::parseDirectory(const QByteArray &line) +{ + FileInfo info; + info.name = line.mid(DIRECTORY_PREFIX.size() + 1); + info.absolutePath = m_dirName + QByteArrayLiteral("/") + info.name; + info.type = FileType::Directory; + info.size = 0; + + m_result.append(info); +} + +void ListOperation::parseFile(const QByteArray &line) +{ + const auto sizeIdx = line.lastIndexOf(' ') + 1; + const auto nameIdx = FILE_PREFIX.size() + 1; + + FileInfo info; + info.name = line.mid(nameIdx, sizeIdx - nameIdx - 1); + info.absolutePath = m_dirName + QByteArrayLiteral("/") + info.name; + info.type = FileType::RegularFile; + info.size = line.mid(sizeIdx, line.size() - sizeIdx - 1).toLongLong(nullptr, 10); + + m_result.append(info); +} diff --git a/backend/flipperzero/storage/listoperation.h b/backend/flipperzero/storage/listoperation.h new file mode 100644 index 00000000..94547b69 --- /dev/null +++ b/backend/flipperzero/storage/listoperation.h @@ -0,0 +1,36 @@ +#pragma once + +#include "simpleserialoperation.h" + +#include + +#include "fileinfo.h" + +namespace Flipper { +namespace Zero { + +class ListOperation : public SimpleSerialOperation +{ + Q_OBJECT + +public: + ListOperation(QSerialPort *serialPort, const QByteArray &dirName, QObject *parent = nullptr); + const QString description() const override; + const FileInfoList &result() const; + +private: + QByteArray endOfMessageToken() const override; + QByteArray commandLine() const override; + + bool parseReceivedData() override; + void parseDirectory(const QByteArray &line); + void parseFile(const QByteArray &line); + +private: + QByteArray m_dirName; + FileInfoList m_result; +}; + +} +} + diff --git a/backend/flipperzero/storage/mkdiroperation.cpp b/backend/flipperzero/storage/mkdiroperation.cpp index 183e07e0..e317a11e 100644 --- a/backend/flipperzero/storage/mkdiroperation.cpp +++ b/backend/flipperzero/storage/mkdiroperation.cpp @@ -21,7 +21,7 @@ QByteArray MkDirOperation::endOfMessageToken() const QByteArray MkDirOperation::commandLine() const { - return QByteArrayLiteral("storage mkdir ") + m_dirName + QByteArrayLiteral("\r\n"); + return QByteArrayLiteral("storage mkdir \"") + m_dirName + QByteArrayLiteral("\"\r\n"); } bool MkDirOperation::parseReceivedData() diff --git a/backend/flipperzero/storage/readoperation.cpp b/backend/flipperzero/storage/readoperation.cpp index 7047288e..15355dad 100644 --- a/backend/flipperzero/storage/readoperation.cpp +++ b/backend/flipperzero/storage/readoperation.cpp @@ -62,7 +62,7 @@ void ReadOperation::onSerialPortReadyRead() bool ReadOperation::begin() { - const auto cmdLine = QByteArrayLiteral("storage read_chunks ") + m_fileName + QByteArrayLiteral(" ") + + const auto cmdLine = QByteArrayLiteral("storage read_chunks \"") + m_fileName + QByteArrayLiteral("\" ") + QByteArray::number(CHUNK_SIZE, 10) + QByteArrayLiteral("\r"); const auto success = (serialPort()->write(cmdLine) == cmdLine.size()) && serialPort()->flush(); diff --git a/backend/flipperzero/storage/removeoperation.cpp b/backend/flipperzero/storage/removeoperation.cpp index d1b99b83..1ee853fb 100644 --- a/backend/flipperzero/storage/removeoperation.cpp +++ b/backend/flipperzero/storage/removeoperation.cpp @@ -22,7 +22,7 @@ QByteArray RemoveOperation::endOfMessageToken() const QByteArray RemoveOperation::commandLine() const { - return QByteArrayLiteral("storage remove ") + m_fileName + QByteArrayLiteral("\r\n"); + return QByteArrayLiteral("storage remove \"") + m_fileName + QByteArrayLiteral("\"\r\n"); } bool RemoveOperation::parseReceivedData() diff --git a/backend/flipperzero/storage/statoperation.cpp b/backend/flipperzero/storage/statoperation.cpp index 7df6a9b6..9ecd3ff4 100644 --- a/backend/flipperzero/storage/statoperation.cpp +++ b/backend/flipperzero/storage/statoperation.cpp @@ -50,7 +50,7 @@ QByteArray StatOperation::endOfMessageToken() const QByteArray StatOperation::commandLine() const { - return QByteArrayLiteral("storage stat ") + m_fileName + QByteArrayLiteral("\r\n"); + return QByteArrayLiteral("storage stat \"") + m_fileName + QByteArrayLiteral("\"\r\n"); } bool StatOperation::parseReceivedData() diff --git a/backend/flipperzero/storage/writeoperation.cpp b/backend/flipperzero/storage/writeoperation.cpp index beac7e23..8ec71ecf 100644 --- a/backend/flipperzero/storage/writeoperation.cpp +++ b/backend/flipperzero/storage/writeoperation.cpp @@ -77,7 +77,7 @@ bool WriteOperation::writeSetupCommand() { const auto bytesAvailable = m_file->bytesAvailable(); m_chunkSize = bytesAvailable < CHUNK_SIZE ? bytesAvailable : CHUNK_SIZE; - const auto cmdLine = QByteArrayLiteral("storage write_chunk ") + m_fileName + QByteArrayLiteral(" ") + + const auto cmdLine = QByteArrayLiteral("storage write_chunk \"") + m_fileName + QByteArrayLiteral("\" ") + QByteArray::number(m_chunkSize) + QByteArrayLiteral("\r"); const auto success = (serialPort()->write(cmdLine) == cmdLine.size()) && serialPort()->flush(); diff --git a/backend/flipperzero/storagecontroller.cpp b/backend/flipperzero/storagecontroller.cpp index a89be3e1..ce2aa767 100644 --- a/backend/flipperzero/storagecontroller.cpp +++ b/backend/flipperzero/storagecontroller.cpp @@ -9,6 +9,7 @@ #include "storage/writeoperation.h" #include "storage/readoperation.h" #include "storage/statoperation.h" +#include "storage/listoperation.h" #include "macros.h" @@ -24,6 +25,14 @@ StorageController::StorageController(const QSerialPortInfo &portInfo, QObject *p StorageController::~StorageController() {} +ListOperation *StorageController::list(const QByteArray &dirName) +{ + auto *op = new ListOperation(m_serialPort, dirName, this); + enqueueOperation(op); + return op; + +} + StatOperation *StorageController::stat(const QByteArray &fileName) { auto *op = new StatOperation(m_serialPort, fileName, this); diff --git a/backend/flipperzero/storagecontroller.h b/backend/flipperzero/storagecontroller.h index a7e519d7..db90dd77 100644 --- a/backend/flipperzero/storagecontroller.h +++ b/backend/flipperzero/storagecontroller.h @@ -13,6 +13,7 @@ class AbstractSerialOperation; namespace Flipper { namespace Zero { +class ListOperation; class StatOperation; class ReadOperation; class MkDirOperation; @@ -34,6 +35,7 @@ class StorageController : public SignalingFailable StorageController(const QSerialPortInfo &portInfo, QObject *parent = nullptr); ~StorageController(); + ListOperation *list(const QByteArray &dirName); StatOperation *stat(const QByteArray &fileName); ReadOperation *read(const QByteArray &fileName, QIODevice *file); MkDirOperation *mkdir(const QByteArray &dirName);