From a8cb3c3b1ffba683c831711e1fd5b413b2f2d858 Mon Sep 17 00:00:00 2001 From: liang-ys Date: Tue, 24 Mar 2026 17:22:54 +0800 Subject: [PATCH] =?UTF-8?q?[add]=E6=B7=BB=E5=8A=A0=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- product/src/sys/sys.pro | 3 +- .../sys_data_sync_server/datasyncserver.cpp | 742 ++++++++++++++++++ .../sys/sys_data_sync_server/datasyncserver.h | 53 ++ product/src/sys/sys_data_sync_server/main.cpp | 17 + .../scripts/remote_sys_ctrl.bat | 41 + .../scripts/remote_sys_ctrl.sh | 40 + .../sys_data_sync_server.pro | 53 ++ 7 files changed, 948 insertions(+), 1 deletion(-) create mode 100644 product/src/sys/sys_data_sync_server/datasyncserver.cpp create mode 100644 product/src/sys/sys_data_sync_server/datasyncserver.h create mode 100644 product/src/sys/sys_data_sync_server/main.cpp create mode 100644 product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.bat create mode 100644 product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.sh create mode 100644 product/src/sys/sys_data_sync_server/sys_data_sync_server.pro diff --git a/product/src/sys/sys.pro b/product/src/sys/sys.pro index 8862f4f2..4acf3059 100644 --- a/product/src/sys/sys.pro +++ b/product/src/sys/sys.pro @@ -8,7 +8,8 @@ SUBDIRS += \ sys_file_sync \ sys_startup \ sys_svn_file_sync_api \ - sys_svn_file_sync + sys_svn_file_sync \ + sys_data_sync_server sys_file_sync.depends = sys_file_sync_api sys_svn_file_sync.depends = sys_svn_file_sync_api diff --git a/product/src/sys/sys_data_sync_server/datasyncserver.cpp b/product/src/sys/sys_data_sync_server/datasyncserver.cpp new file mode 100644 index 00000000..6bdf0fb5 --- /dev/null +++ b/product/src/sys/sys_data_sync_server/datasyncserver.cpp @@ -0,0 +1,742 @@ +#include "datasyncserver.h" + +DataSyncServer::DataSyncServer(QObject *parent) : QTcpServer(parent) +{ + +} + +/** + * @brief 处理新连接 + * @param handle 新连接的socket描述符 + * + * 创建QTcpSocket对象,初始化请求缓冲区,并连接相关信号槽 + */ +void DataSyncServer::incomingConnection(qintptr handle) +{ + QTcpSocket *socket = new QTcpSocket(this); + socket->setSocketDescriptor(handle); + + requestBuffers[socket] = QByteArray(); + + connect(socket, &QTcpSocket::readyRead, this, &DataSyncServer::onSocketReadyRead); + connect(socket, &QTcpSocket::disconnected, this, &DataSyncServer::onSocketDisconnected); + connect(socket, &QTcpSocket::bytesWritten, this, &DataSyncServer::onSocketBytesWritten); + +} + +/** + * @brief Socket数据就绪槽函数 + * + * 累积接收数据到缓冲区,尝试解析HTTP请求 + * 如果请求完整则处理,否则继续等待数据,限制最大请求大小为50MB + */ +void DataSyncServer::onSocketReadyRead() +{ + QTcpSocket *socket = qobject_cast(sender()); + if (!socket || socket->state() != QAbstractSocket::ConnectedState) return; + + requestBuffers[socket].append(socket->readAll()); + + QString method; + QString path; + QByteArray body; + int contentLength = -1; + + //尝试解析HTTP请求 + if (!parseHttpRequest(requestBuffers[socket], method, path, body, contentLength)) { + //请求尚未完整接收,等待更多数据 + if (requestBuffers[socket].size() > 1024 * 1024 *50) { //限制最大请求大小为50MB + qDebug()<<"请求过大,关闭连接"; + sendErrorResponse(socket, 413, "请求实体body过大!最大限制为50M"); + socket->disconnectFromHost(); + //清理缓冲区 + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + return; + } + + //请求解析成功,处理请求(注意:此时不清除缓冲区,因为可能需要异步处理) + handleRequest(socket); +} + +/** + * @brief Socket数据写入完成槽函数 + * @param bytes 已写入的字节数 + * + * 预留接口,目前为空实现,可用于拓展写入完成后的处理逻辑 + */ +void DataSyncServer::onSocketBytesWritten(qint64 bytes) +{ + //可以在这里添加写入完成的回调处理,目前为空实现 + Q_UNUSED(bytes); + +} + + +/** + * @brief Socket断开连接槽函数 + * + * 清理该Socket相关的所有数据:请求缓冲区、临时文件映射、操作类型标记等。 + */ +void DataSyncServer::onSocketDisconnected() +{ + QTcpSocket *socket = qobject_cast(sender()); + if (!socket) return; + + //清理该 socket 的所有相关数据 + requestBuffers.remove(socket); + socketTempFiles.remove(socket); + socketOperationType.remove(socket); + socket->deleteLater(); + +} + +/** + * @brief 解析HTTP请求 + * @param data 完整的HTTP请求数据 + * @param method 输出参数:HTTP方法 + * @param path 输出参数: 请求路径 + * @param body 输出参数:请求体内容 + * @param contentLength 输出参数:Content-Length值 + * @return 如果请求完整返回true,否则返回false + * + * 解析HTTP请求行、请求头和请求体。处理查询参数,根据Content-Length判断body是否完整 + */ +bool DataSyncServer::parseHttpRequest(const QByteArray &data, QString &method, QString &path, QByteArray &body, int &contentLength) +{ + //查找请求头和请求体的分隔符 + int headerEnd = data.indexOf("\r\n\r\n"); + if (headerEnd < 0) { + return false; //头部尚未完整接收 + } + + //解析请求头 + QByteArray headerData = data.left(headerEnd); + QList headerLines = headerData.split('\n'); + + if (headerLines.isEmpty()) { + return false; + } + + //解析请求行;格式为"METHOD /path HTTP/1.x" + QByteArray requestLine = headerLines[0].trimmed(); + QList requestParts = requestLine.split(' '); + if (requestParts.size() < 2) { + return false; + } + + method = QString::fromLatin1(requestParts[0]); + path = QString::fromLatin1(requestParts[1]); + + //提取查询数据(如果有) + int queryIndex = path.indexOf('?'); + if (queryIndex >= 0) { + path = path.left(queryIndex); + } + + //查找 Content-Length + contentLength = -1; + for (int i = 1; i < headerLines.size(); ++i) { + QByteArray line = headerLines[i].trimmed(); + if (QString(line).startsWith("Content-Length:", Qt::CaseInsensitive)) { + bool ok; + contentLength = line.mid(15).trimmed().toInt(&ok); + if (!ok) contentLength = -1; + break; + } + } + + //提取请求体 + int bodyStart = headerEnd + 4; + QByteArray receivedBody = data.mid(bodyStart); + + if (method.toUpper() == "POST" || method.toUpper() == "PUT") { + if (contentLength > 0) { + //需要接收完整的body + if (receivedBody.size() < contentLength) { + return false; //body 尚未完整接收 + } + body = receivedBody.left(contentLength); + } + else { + // 没有Content-Length, 假设所有剩余数据都是body + body = receivedBody; + } + } + + return true; +} + +/** + * @brief 处理HTTP请求 + * @param socket 客户端socket指针 + * + * 解析HTTP请求并根据方法和路径分发到相应的处理函数 + * 支持的路径: + * - GET /backup -> 备份操作 + * - POST /restore -> 恢复操作 + * - 其他 -> 404错误 + */ +void DataSyncServer::handleRequest(QTcpSocket *socket) +{ + QByteArray fullRequest = requestBuffers[socket]; + + QString method; + QString path; + QByteArray body; + int contentLength = -1; + + if (!parseHttpRequest(fullRequest, method, path, body, contentLength)) { + sendErrorResponse(socket, 400, "错误请求!"); + socket->disconnectFromHost(); + return; + } + + qDebug()<<"收到请求:"<disconnectFromHost(); + // 请求处理完成,清理缓冲区 + requestBuffers.remove(socket); + socketOperationType.remove(socket); + } + +} + +/** + * @brief 处理备份请求 + * @param socket 客户端socket指针 + * + * 执行数据备份操作: + * 1. 检查数据目录是否存在 + * 2. 生成唯一的临时文件名避免并发冲突 + * 3. 使用tar命令异步打包data目录 + * 4. 打包完成后在onTarProcessFinished中发送文件给客户端 + */ +void DataSyncServer::handleBackup(QTcpSocket *socket) +{ + //检查socket是否仍然连接 + if (!socket || socket->state() != QAbstractSocket::ConnectedState) { + qDebug() << "Socket已断开,取消备份操作"; + if (socket) { + requestBuffers.remove(socket); + socketOperationType.remove(socket); + } + return; + } + + //确保目录存在 + QString appDir = QCoreApplication::applicationDirPath() + "/../.."; + QString tmpDir = QDir(appDir).filePath("tmp"); + QString dataDir = QDir(appDir).filePath("data"); + + if (!ensureDirectoryExists(tmpDir)) { + qDebug() << "无法创建临时目录:" << tmpDir; + sendErrorResponse(socket, 500, "无法创建临时目录tmp"); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + //检查数据目录是否存在 + if (!QDir(dataDir).exists()) { + qDebug() << "数据目录不存在:" << dataDir; + sendErrorResponse(socket, 500, "data目录不存在!"); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + //生成唯一的临时文件名,避免并发冲突 + QString uniqueId = QUuid::createUuid().toString().remove('{').remove('}'); + QString outputFile = tmpDir + QDir::separator() + "data_" + uniqueId + ".zip"; + + // 使用QProcess 异步执行tar命令,避免阻塞 + QProcess *process = new QProcess(this); + processSockets[process] = socket; + + // 设置tar命令参数(使用setProgram/setArguments避免路径问题) + process->setProgram("zip"); + process->setWorkingDirectory(appDir); + QStringList arguments; + arguments << "-r" << outputFile << "data"; + process->setArguments(arguments); + + connect(process, QOverload::of(&QProcess::finished), this, &DataSyncServer::onTarProcessFinished); + connect(process, &QProcess::errorOccurred, this, &DataSyncServer::onTarProcessError); + + qDebug()<< "start backup command:tar" << arguments.join(" "); + + // 保存输出文件路径,供后续使用 + socketTempFiles[socket] = outputFile; + + process->start(); + + if (!process->waitForStarted(3000)) { + qDebug() << "tar 进程启动失败:" << process->errorString(); + sendErrorResponse(socket, 500, "无法启动tar进程!"); + processSockets.remove(process); + socketTempFiles.remove(socket); + process->deleteLater(); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + +} + +/** + * @brief 处理恢复请求 + * @param socket 客户端socket指针 + * @param body HTTP请求体,包含tar文件数据 + * + * 执行数据恢复操作: + * 1. 验证请求体不为空 + * 2. 将接收到的tar数据保存到临时文件 + * 3. 删除旧的data目录 + * 4. 使用tar命令异步解包到data目录 + * 5. 解包完成后发送成功相应 + */ +void DataSyncServer::handleRestore(QTcpSocket *socket, const QByteArray &body) +{ + // 检查socket是否仍然连接 + if (!socket || socket->state() != QAbstractSocket::ConnectedState) { + qDebug() << "Socket已断开,取消恢复操作"; + if (socket) { + requestBuffers.remove(socket); + socketOperationType.remove(socket); + } + return; + } + + if (body.isEmpty()) { + qDebug() << "恢复请求的 body 为空"; + sendErrorResponse(socket, 400, "错误请求!请求实体body为空!"); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + //确保目录存在 + QString appDir = QCoreApplication::applicationDirPath() + "/../.."; + QString tmpDir = QDir(appDir).filePath("tmp"); + QString restoreDir = QDir(appDir).filePath("data"); + + if (!ensureDirectoryExists(tmpDir)) { + qDebug() << "无法创建临时目录:" << tmpDir; + sendErrorResponse(socket, 500, "无法创建临时目录tmp!"); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + //生成唯一的临时文件名,避免并发冲突 + QString uniqueId = QUuid::createUuid().toString().remove('{').remove('}'); + QString saveFile = tmpDir + QDir::separator() + "data_" + uniqueId + ".tar"; + + //保存上传的文件 + QFile file(saveFile); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "无法创建临时文件:" << saveFile << file.errorString(); + sendErrorResponse(socket, 500, "无法创建临时文件!"); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + qint64 written = file.write(body); + file.close(); + + if (written != body.size()) { + qDebug() << "文件写入不完整,期望:" << body.size() << "实际:" <disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + + qDebug() << "已保存上传文件:" << saveFile << "大小:" << written; + + // 删除旧目录(如果存在) + if (QDir(restoreDir).exists()) { + qDebug() << "删除旧目录:" << restoreDir; + if (!QDir(restoreDir).removeRecursively()) { + qDebug() << "警告:无法完全删除旧目录:" << restoreDir; + } + } + + QProcess *process = new QProcess(this); + processSockets[process] = socket; + + process->setProgram("tar"); + QStringList arguments; + arguments << "xf" << saveFile << "-C" << appDir; + process->setArguments(arguments); + + connect(process, QOverload::of(&QProcess::finished), this, &DataSyncServer::onTarProcessFinished); + connect(process, &QProcess::errorOccurred, this, &DataSyncServer::onTarProcessError); + + qDebug() << "开始执行恢复命令:tar" << arguments.join(" "); + + //保存临时文件路径,成功后删除 + socketTempFiles[socket] = saveFile; + + process->start(); + + if (!process->waitForStarted(3000)) { + qDebug() << "tar 进程启动失败:" << process->errorString(); + sendErrorResponse(socket, 500, "无法启动tar进程!"); + processSockets.remove(process); + socketTempFiles.remove(socket); + //清理临时文件 + if (QFile::exists(saveFile)) { + QFile::remove(saveFile); + } + process->deleteLater(); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + return; + } + +} + +/** + * @brief tar进程完成槽函数 + * @param exitCode 进程退出码,0代表成功 + * @param exitStatus 进程退出状态,NormalExit表示正常退出 + * + * 当tar命令执行完成时调用: + * - 如果失败:发送错误响应并清理资源 + * - 如果成功且是备份操作:分块读取tar文件并发送给客户端 + * - 如果成功且是恢复操作:发送成功响应消息 + */ +void DataSyncServer::onTarProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *process = qobject_cast(sender()); + if (!process) return; + + QTcpSocket *socket = processSockets.value(process); + if (!socket) { + process->deleteLater(); + return; + } + + // 检查socket是否仍然连接 + if (socket->state() != QAbstractSocket::ConnectedState) { + qDebug() << "Socket已断开,清理资源"; + QString tempFile = socketTempFiles.value(socket); + if (!tempFile.isEmpty() && QFile::exists(tempFile)) { + QFile::remove(tempFile); + } + processSockets.remove(process); + socketTempFiles.remove(socket); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + process->deleteLater(); + return; + } + + processSockets.remove(process); + QString tempFile = socketTempFiles.value(socket); + bool isBackup = socketOperationType.value(socket, false); + + if (exitStatus != QProcess::NormalExit || exitCode != 0) { + QByteArray errorOutput = process->readAllStandardError(); + qDebug() << "tar 命令执行失败,退出码:" << exitCode << "错误:" << errorOutput; + sendErrorResponse(socket, 500, "tar进程执行失败!"); + + //清理临时文件 + if (!tempFile.isEmpty() && QFile::exists(tempFile)) { + QFile::remove(tempFile); + } + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + socketTempFiles.remove(socket); + process->deleteLater(); + return; + } + + // tar 命令执行成功 + QByteArray standardOutput = process->readAllStandardOutput(); + QByteArray errorOutput = process->readAllStandardError(); + + if (!standardOutput.isEmpty()) { + qDebug() << "tar 标准输出:" << standardOutput; + } + if (!errorOutput.isEmpty()) { + qDebug() << "tar 标准错误:" << errorOutput; + } + + //根据操作类型处理 + if (isBackup && !tempFile.isEmpty() && QFile::exists(tempFile)) { + //备份操作;分块发送文件,避免大文件占用过多内存 + QFile file(tempFile); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "无法打开备份文件:" << tempFile; + sendErrorResponse(socket, 500, "无法打开临时文件!"); + QFile::remove(tempFile); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + socketTempFiles.remove(socket); + process->deleteLater(); + return; + } + + qint64 fileSize = file.size(); + if (fileSize == 0) { + qDebug() << "备份文件为空"; + sendErrorResponse(socket, 500, "临时文件为空!"); + file.close(); + QFile::remove(tempFile); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + socketTempFiles.remove(socket); + process->deleteLater(); + return; + } + + qDebug() << "send backup file,size:" << fileSize; + + // 发送HTTP响应头 + QByteArray header = "HTTP/1.1 200 OK\r\n"; + header += "Content-Type: application/octet-stream\r\n"; + header += "Content-Length: " + QByteArray::number(fileSize) + "\r\n"; + header += "Connection: close\r\n"; + header += "Date: " + QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'").toLatin1() + "\r\n"; + header += "Server: DataSyncServer/1.0\r\n"; + header += "\r\n"; + + socket->write(header); + + // 分块读取并发送文件 + const qint64 chunkSize = 64 * 1024; // 64KB 块 + while (!file.atEnd()) { + QByteArray chunk = file.read(chunkSize); + if (chunk.isEmpty() && !file.atEnd()) { + qDebug() << "文件读取错误"; + file.close(); + QFile::remove(tempFile); + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + socketTempFiles.remove(socket); + process->deleteLater(); + return; + } + socket->write(chunk); + socket->waitForBytesWritten(30000); + } + file.close(); + + // 清理临时文件 + QFile::remove(tempFile); + } + else { + // 恢复操作成功 + qDebug() << "恢复操作成功完成"; + sendSuccessResponse(socket, QByteArray("文件接收并解压成功!"), "text/plain; charset=utf-8"); + + // 清理临时文件 + if (!tempFile.isEmpty() && QFile::exists(tempFile)) { + QFile::remove(tempFile); + } + } + + socket->disconnectFromHost(); + requestBuffers.remove(socket); + socketOperationType.remove(socket); + socketTempFiles.remove(socket); + process->deleteLater(); + +} + +/** + * @brief tar进程错误槽函数 + * @param error 进程错误类型 + * + * 当tar命令执行出错时被调用,根据错误类型生成相应的错误消息 + * 发送错误响应给客户端并清理所有相关资源(临时文件、缓冲区等)。 + */ +void DataSyncServer::onTarProcessError(QProcess::ProcessError error) +{ + QProcess *process = qobject_cast(sender()); + if (!process) return; + + QTcpSocket *socket = processSockets.value(process); + if (!socket) { + process->deleteLater(); + return; + } + + QString errorString; + switch (error) { + case QProcess::FailedToStart: + errorString = "tar command failed to start"; + break; + case QProcess::Crashed: + errorString = "tar command crashed"; + break; + case QProcess::Timedout: + errorString = "tar command timed out"; + break; + case QProcess::WriteError: + errorString = "tar command write error"; + break; + case QProcess::ReadError: + errorString = "tar command read error"; + break; + default: + errorString = "tar command unknown error"; + break; + } + + qDebug() << "tar 进程错误:" << errorString; + + QString tempFile = socketTempFiles.value(socket); + + // 清理临时文件 + if (!tempFile.isEmpty() && QFile::exists(tempFile)) { + QFile::remove(tempFile); + } + + if (socket && socket->state() == QAbstractSocket::ConnectedState) { + sendErrorResponse(socket, 500, errorString.toLatin1()); + socket->disconnectFromHost(); + } + + requestBuffers.remove(socket); + socketOperationType.remove(socket); + process->deleteLater(); + +} + +/** + * @brief 发送HTTP错误响应 + * @param socket 客户端socket指针 + * @param statusCode HTTP状态码(400,404,413,500等) + * @param message 错误消息内容 + * + * 构造并发送完整的HTTP错误响应,包含状态行、响应头和错误消息体。 + * 自动添加Data和Server等标准HTTP响应头字段 + */ +void DataSyncServer::sendErrorResponse(QTcpSocket *socket, int statusCode, const QByteArray &message) +{ + if (!socket || socket->state() != QAbstractSocket::ConnectedState) { + return; + } + + QByteArray response = "HTTP/1.1 " + QByteArray::number(statusCode) + " "; + + switch (statusCode) { + case 400: response += "Bad Request"; break; + case 404: response += "Not Found"; break; + case 413: response += "Request Entity Too Large"; break; + case 500: response += "Internal Server Error"; break; + default: response += "Error"; break; + } + + response += "\r\n"; + response += "Content-Type: text/plain; charset=utf-8\r\n"; + response += "Content-Length: " + QByteArray::number(message.size()) + "\r\n"; + response += "Connection: close\r\n"; + response += "Date: " + QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'").toLatin1() + "\r\n"; + response += "Server: DataSyncServer/1.0\r\n"; + response += "\r\n"; + response += message; + + socket->write(response); + +} + +/** + * @brief 发送HTTP成功响应 + * @param socket 客户端socket指针 + * @param content 响应内容 + * @param contentType Content-Type响应头,默认为"application/octet-stream" + * + * 构造并发送完整的HTTP 200成功响应,包含状态行、响应头和响应体 + * 自动添加Data和Server等标准HTTP响应头字段。 + */ +void DataSyncServer::sendSuccessResponse(QTcpSocket *socket, const QByteArray &content, const QByteArray &contentType) +{ + if (!socket || socket->state() != QAbstractSocket::ConnectedState) { + return; + } + + QByteArray response = "HTTP/1.1 200 OK\r\n"; + response += "Content-Type: " + contentType + "\r\n"; + response += "Content-Length: " + QByteArray::number(content.size()) + "\r\n"; + response += "Connection: close\r\n"; + response += "Date: " + QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'").toLatin1() + "\r\n"; + response += "Server: DataSyncServer/1.0\r\n"; + response += "\r\n"; + response += content; + + socket->write(response); + +} + +/** + * @brief 确保目录存在 + * @param path 目录路径 + * @return 如果目录存在或创建成功返回true,否则返回false + * + * 检查指定目录是否存在,如果不存在则递归创建该目录及其所有父目录 + */ +bool DataSyncServer::ensureDirectoryExists(const QString &path) +{ + QDir dir; + if (!dir.exists(path)) { + return dir.mkpath(path); + } + return true; +} + +/** + * @brief 清理路径字符串 + * @param path 原始路径字符串 + * @return 清理后的路径字符串 + * + * 移除路径中的潜在危险字符,防止路径遍历攻击 + * - 移除".."路径遍历符号 + * - 移除重复的路径分隔符 + * + * 注意:在当前实现中可能不需要,但保留此接口用于未来拓展 + */ +QString DataSyncServer::sanitizePath(const QString &path) +{ + // 移除潜在的路径遍历攻击(虽然在这个实现中可能不需要,但保留接口) + QString sanitized = path; + sanitized.replace("..", ""); + QString sep = QDir::separator(); + sanitized.replace(sep + sep, sep); + return sanitized; +} + diff --git a/product/src/sys/sys_data_sync_server/datasyncserver.h b/product/src/sys/sys_data_sync_server/datasyncserver.h new file mode 100644 index 00000000..a112a997 --- /dev/null +++ b/product/src/sys/sys_data_sync_server/datasyncserver.h @@ -0,0 +1,53 @@ +#ifndef DATASYNCSERVER_H +#define DATASYNCSERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DataSyncServer : public QTcpServer +{ + Q_OBJECT +public: + explicit DataSyncServer(QObject *parent = nullptr); + +signals: + +public slots: + +protected: + void incomingConnection(qintptr handle); + +private slots: + void onSocketReadyRead(); + void onSocketBytesWritten(qint64 bytes); + void onSocketDisconnected(); + void onTarProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onTarProcessError(QProcess::ProcessError error); + +private: + // 为每个socket 维护请求缓冲区,处理分包问题 + QHash requestBuffers; + QHash processSockets; + QHash socketTempFiles; + //标记每个socket的操作类型;true=备份,false=恢复 + QHash socketOperationType; + + + bool parseHttpRequest(const QByteArray &data, QString &method, QString &path, QByteArray &body, int &contentLength); + void handleRequest(QTcpSocket *socket); + void handleBackup(QTcpSocket *socket); + void handleRestore(QTcpSocket *socket, const QByteArray &body); + void sendErrorResponse(QTcpSocket *socket, int statusCode, const QByteArray &message); + void sendSuccessResponse(QTcpSocket *socket, const QByteArray &content, const QByteArray &contentType); + bool ensureDirectoryExists(const QString &path); + QString sanitizePath(const QString &path); +}; + +#endif // DATASYNCSERVER_H diff --git a/product/src/sys/sys_data_sync_server/main.cpp b/product/src/sys/sys_data_sync_server/main.cpp new file mode 100644 index 00000000..e8fa4c7a --- /dev/null +++ b/product/src/sys/sys_data_sync_server/main.cpp @@ -0,0 +1,17 @@ +#include "datasyncserver.h" + +#include + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + + DataSyncServer data_sync_server; + if (!data_sync_server.listen(QHostAddress::Any, 8080)) { + qFatal("Listen failed"); + } + + qDebug("data_sync_server started at 8080"); + + return a.exec(); +} diff --git a/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.bat b/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.bat new file mode 100644 index 00000000..23fbf82b --- /dev/null +++ b/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.bat @@ -0,0 +1,41 @@ +@echo off +setlocal + +REM ̶û +set username=admin + +echo ԶIPַ +set /p ip= + +if "%ip%"=="" ( + echo IPַΪգ + pause + exit +) + +echo. +echo ѡ +echo 1 - ϵͳ +echo 2 - ֹͣϵͳ +set /p choice= 1 2 + +if "%choice%"=="1" ( + set command=/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl +) else if "%choice%"=="2" ( + set command=/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl -s +) else ( + echo Чѡ + pause + exit +) + +echo. +echo %ip% ... +echo Ҫ룬ʾ롣 +echo. + +ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL %username%@%ip% %command% + +echo. +echo ִɡ +pause \ No newline at end of file diff --git a/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.sh b/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.sh new file mode 100644 index 00000000..333fb39b --- /dev/null +++ b/product/src/sys/sys_data_sync_server/scripts/remote_sys_ctrl.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# 固定用户名 +username="admin" + +echo "请输入远程主机IP地址:" +read ip + +if [ -z "$ip" ]; then + echo "IP地址不能为空!" + read -p "按回车键退出..." + exit 1 +fi + +echo +echo "请选择操作:" +echo "1 - 启动系统" +echo "2 - 停止系统" +read -p "请输入 1 或 2:" choice + +if [ "$choice" = "1" ]; then + command="/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl" +elif [ "$choice" = "2" ]; then + command="/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl -s" +else + echo "无效选择!" + read -p "按回车键退出..." + exit 1 +fi + +echo +echo "正在连接 $ip ..." +echo "如果需要密码,请根据提示输入。" +echo + +ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${username}@${ip} "$command" + +echo +echo "执行完成。" +read -p "按回车键退出..." \ No newline at end of file diff --git a/product/src/sys/sys_data_sync_server/sys_data_sync_server.pro b/product/src/sys/sys_data_sync_server/sys_data_sync_server.pro new file mode 100644 index 00000000..911b2b5f --- /dev/null +++ b/product/src/sys/sys_data_sync_server/sys_data_sync_server.pro @@ -0,0 +1,53 @@ +QT -= gui + +QT += network + +CONFIG += c++11 console +CONFIG -= app_bundle + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += main.cpp \ + datasyncserver.cpp + +HEADERS += \ + datasyncserver.h + +#------------------------------------------------------------------- +COMMON_PRI=$$PWD/../../common.pri +exists($$COMMON_PRI) { + include($$COMMON_PRI) +}else { + error("FATAL error: can not find common.pri") +} + +# ------------------------------------------------------------------ +# 脚本目录 +SCRIPT_DIR = $$PWD/scripts + +win32 { + SCRIPT_SRC = $$shell_path($$SCRIPT_DIR/remote_sys_ctrl.bat) + SCRIPT_DST = $$shell_path($$DESTDIR/remote_sys_ctrl.bat) + + generate_script.commands = $$QMAKE_COPY "$$SCRIPT_SRC" "$$SCRIPT_DST" +} + +unix:!macx { #Linux + SCRIPT_SRC = $$SCRIPT_DIR/remote_sys_ctrl.sh + SCRIPT_DST = $$DESTDIR/remote_sys_ctrl.sh + + generate_script.commands = cp "$$SCRIPT_SRC" "$$SCRIPT_DST" && chmod +x "$$SCRIPT_DST" +} + +QMAKE_EXTRA_TARGETS += generate_script +POST_TARGETDEPS += generate_script +# ------------------------------------------------------------------