Refactor: Move FileManager to common, add macOS file management support

This commit is contained in:
yuanyuanxiang
2026-05-03 09:57:46 +02:00
parent a3611d9fc1
commit 36423b1c7c
11 changed files with 206 additions and 35 deletions

View File

@@ -1,807 +0,0 @@
#pragma once
#include <dirent.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <iconv.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include <vector>
#include <list>
#include <fstream>
#include <sstream>
#include <cerrno>
#include "FileTransferV2.h"
// 外部声明 clientID在 main.cpp 中定义)
extern uint64_t g_myClientID;
// ============== Linux File Manager ==============
// Implements file transfer between Windows server and Linux client
// Supports: browse, upload, download, delete, rename, create folder
#define MAX_SEND_BUFFER 65535
class FileManager : public IOCPManager
{
public:
FileManager(IOCPClient* client)
: m_client(client),
m_nTransferMode(TRANSFER_MODE_NORMAL),
m_nCurrentFileLength(0),
m_nCurrentUploadFileLength(0)
{
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
SendDriveList();
}
~FileManager()
{
m_uploadList.clear();
}
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (!szBuffer || ulLength == 0) return;
switch (szBuffer[0]) {
case COMMAND_LIST_FILES:
SendFilesList((char*)szBuffer + 1);
break;
case COMMAND_DELETE_FILE:
DeleteSingleFile((char*)szBuffer + 1);
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DELETE_DIRECTORY:
DeleteDirectoryRecursive((char*)szBuffer + 1);
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DOWN_FILES:
// Server wants to download files FROM Linux (upload from Linux perspective)
UploadToRemote((char*)szBuffer + 1);
break;
case CMD_DOWN_FILES_V2:
// V2 上传文件到服务端
FileTransferV2::HandleDownFilesV2(szBuffer, ulLength, m_client, g_myClientID);
break;
case COMMAND_CONTINUE:
// Server requests next chunk of file data
SendFileData(szBuffer + 1);
break;
case COMMAND_STOP:
StopTransfer();
break;
case COMMAND_CREATE_FOLDER:
CreateFolder((char*)szBuffer + 1);
break;
case COMMAND_RENAME_FILE:
RenameFile(szBuffer + 1);
break;
case COMMAND_SET_TRANSFER_MODE:
SetTransferMode(szBuffer + 1);
break;
case COMMAND_FILE_SIZE:
// Server wants to upload a file TO Linux (download from Linux perspective)
CreateLocalRecvFile(szBuffer + 1);
break;
case COMMAND_FILE_DATA:
// Server sends file data chunk
WriteLocalRecvFile(szBuffer + 1, ulLength - 1);
break;
default:
Mprintf("[FileManager] Unhandled command: %d\n", (int)szBuffer[0]);
break;
}
}
private:
IOCPClient* m_client;
// Upload state (Linux -> Windows)
std::list<std::string> m_uploadList;
char m_strCurrentUploadFile[4096];
int64_t m_nCurrentUploadFileLength;
// Download state (Windows -> Linux)
char m_strCurrentFileName[4096];
int64_t m_nCurrentFileLength;
int m_nTransferMode;
// ---- Send single byte token ----
void SendToken(BYTE token)
{
if (!m_client) return;
m_client->Send2Server((char*)&token, 1);
}
// ---- iconv encoding conversion ----
static std::string iconvConvert(const std::string& input, const char* from, const char* to)
{
if (input.empty()) return input;
iconv_t cd = iconv_open(to, from);
if (cd == (iconv_t)-1) return input;
size_t inLeft = input.size();
size_t outLeft = inLeft * 4;
std::string output(outLeft, '\0');
char* inPtr = const_cast<char*>(input.data());
char* outPtr = &output[0];
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
iconv_close(cd);
if (ret == (size_t)-1) return input;
output.resize(output.size() - outLeft);
return output;
}
static std::string gbkToUtf8(const std::string& gbk)
{
return iconvConvert(gbk, "GBK", "UTF-8");
}
static std::string utf8ToGbk(const std::string& utf8)
{
return iconvConvert(utf8, "UTF-8", "GBK");
}
// ---- Windows path -> Linux path ----
// Examples:
// /:\home\user\file.txt -> /home/user/file.txt
// /:\home\user\ -> /home/user/
// /:\ -> /
// /: -> /
// C:\Users\file.txt -> /Users/file.txt
static std::string winPathToLinux(const char* winPath)
{
if (!winPath || !*winPath) return "/";
std::string p(winPath);
// 1. Backslash -> forward slash
for (auto& c : p) {
if (c == '\\') c = '/';
}
// 2. Remove "X:/" or "X:" prefix (handles both /: and C: style)
if (p.size() >= 3 && p[1] == ':' && p[2] == '/') {
p = p.substr(3);
} else if (p.size() >= 2 && p[1] == ':') {
p = p.substr(2);
}
// 3. Ensure starts with /
if (p.empty() || p[0] != '/') {
p = "/" + p;
}
// 4. Merge consecutive slashes
std::string result;
result.reserve(p.size());
for (size_t i = 0; i < p.size(); i++) {
if (i > 0 && p[i] == '/' && p[i - 1] == '/')
continue;
result += p[i];
}
return result.empty() ? "/" : result;
}
// ---- Linux path -> Windows path (for sending to server) ----
// Converts /home/user/file.txt to /:\home\user\file.txt
static std::string linuxPathToWin(const std::string& linuxPath)
{
// Start with drive letter representation /: for Linux root
// Linux path /home/user/file.txt should become /:\home\user\file.txt
std::string result = "/:";
if (!linuxPath.empty()) {
result += linuxPath;
}
// Convert all forward slashes to backslashes (except the leading /:)
for (size_t i = 2; i < result.size(); ++i) {
if (result[i] == '/') result[i] = '\\';
}
return result;
}
// ---- Unix time_t -> Windows FILETIME ----
static uint64_t unixToFiletime(time_t t)
{
return (uint64_t)t * 10000000ULL + 116444736000000000ULL;
}
// ---- Get root filesystem type ----
static std::string getRootFsType()
{
std::ifstream f("/proc/mounts");
std::string line;
while (std::getline(f, line)) {
std::istringstream iss(line);
std::string dev, mp, fs;
if (iss >> dev >> mp >> fs) {
if (mp == "/") return fs;
}
}
return "ext4";
}
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
bool MakeSureDirectoryExists(const std::string& filePath)
{
// Get parent directory
std::string dir;
size_t lastSlash = filePath.rfind('/');
if (lastSlash != std::string::npos && lastSlash > 0) {
dir = filePath.substr(0, lastSlash);
} else {
// No parent directory to create
return true;
}
// Create directories recursively
std::string current;
for (size_t i = 0; i < dir.size(); ++i) {
current += dir[i];
if (dir[i] == '/' && current.size() > 1) {
mkdir(current.c_str(), 0755);
}
}
if (!current.empty() && current != "/") {
mkdir(current.c_str(), 0755);
}
return true;
}
// ---- Ensure directory path exists (mkdir -p for directory path) ----
bool MakeSureDirectoryPathExists(const std::string& dirPath)
{
std::string dir = dirPath;
// Remove trailing slash
while (!dir.empty() && dir.back() == '/') {
dir.pop_back();
}
// Create directories recursively
std::string current;
for (size_t i = 0; i < dir.size(); ++i) {
current += dir[i];
if (dir[i] == '/' && current.size() > 1) {
mkdir(current.c_str(), 0755);
}
}
if (!current.empty() && current != "/") {
mkdir(current.c_str(), 0755);
}
return true;
}
// ---- Send drive list (Linux: root partition /) ----
void SendDriveList()
{
if (!m_client) return;
BYTE buf[256];
buf[0] = (BYTE)TOKEN_DRIVE_LIST;
DWORD offset = 1;
buf[offset] = '/';
buf[offset + 1] = 3; // DRIVE_FIXED
unsigned long totalMB = 0, freeMB = 0;
struct statvfs sv;
if (statvfs("/", &sv) == 0) {
unsigned long long totalBytes = (unsigned long long)sv.f_blocks * sv.f_frsize;
unsigned long long freeBytes = (unsigned long long)sv.f_bfree * sv.f_frsize;
totalMB = (unsigned long)(totalBytes / 1024 / 1024);
freeMB = (unsigned long)(freeBytes / 1024 / 1024);
}
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
const char* typeName = "Linux";
int typeNameLen = strlen(typeName) + 1;
memcpy(buf + offset + 10, typeName, typeNameLen);
std::string fsType = getRootFsType();
int fsNameLen = fsType.size() + 1;
memcpy(buf + offset + 10 + typeNameLen, fsType.c_str(), fsNameLen);
offset += 10 + typeNameLen + fsNameLen;
m_client->Send2Server((char*)buf, offset);
}
// ---- Send file list for directory ----
void SendFilesList(const char* path)
{
if (!m_client) return;
m_nTransferMode = TRANSFER_MODE_NORMAL;
std::string linuxPath = winPathToLinux(gbkToUtf8(path).c_str());
std::vector<uint8_t> buf;
buf.reserve(64 * 1024);
buf.push_back((uint8_t)TOKEN_FILE_LIST);
DIR* dir = opendir(linuxPath.c_str());
if (!dir) {
Mprintf("[FileManager] opendir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
return;
}
struct dirent* ent;
while ((ent = readdir(dir)) != nullptr) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = linuxPath;
if (fullPath.back() != '/') fullPath += '/';
fullPath += ent->d_name;
struct stat st;
if (lstat(fullPath.c_str(), &st) != 0)
continue;
uint8_t isDir = S_ISDIR(st.st_mode) ? 0x10 : 0x00;
buf.push_back(isDir);
std::string gbkName = utf8ToGbk(ent->d_name);
buf.insert(buf.end(), gbkName.begin(), gbkName.end());
buf.push_back(0);
uint64_t fileSize = (uint64_t)st.st_size;
DWORD sizeHigh = (DWORD)(fileSize >> 32);
DWORD sizeLow = (DWORD)(fileSize & 0xFFFFFFFF);
const uint8_t* pH = (const uint8_t*)&sizeHigh;
const uint8_t* pL = (const uint8_t*)&sizeLow;
buf.insert(buf.end(), pH, pH + sizeof(DWORD));
buf.insert(buf.end(), pL, pL + sizeof(DWORD));
uint64_t ft = unixToFiletime(st.st_mtime);
const uint8_t* pFT = (const uint8_t*)&ft;
buf.insert(buf.end(), pFT, pFT + 8);
}
closedir(dir);
m_client->Send2Server((char*)buf.data(), (ULONG)buf.size());
}
// ============== File Upload (Linux -> Windows) ==============
// Build file list for directory upload
bool BuildUploadList(const std::string& pathName)
{
std::string linuxPath = winPathToLinux(gbkToUtf8(pathName).c_str());
DIR* dir = opendir(linuxPath.c_str());
if (!dir) return false;
struct dirent* ent;
while ((ent = readdir(dir)) != nullptr) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = linuxPath;
if (fullPath.back() != '/') fullPath += '/';
fullPath += ent->d_name;
struct stat st;
if (lstat(fullPath.c_str(), &st) != 0)
continue;
if (S_ISDIR(st.st_mode)) {
// Recursively add directory contents
std::string winPath = linuxPathToWin(fullPath);
BuildUploadList(winPath.c_str());
} else if (S_ISREG(st.st_mode)) {
// Add file to upload list
std::string winPath = linuxPathToWin(fullPath);
m_uploadList.push_back(utf8ToGbk(winPath));
}
}
closedir(dir);
return true;
}
// Start upload process
void UploadToRemote(const char* filePath)
{
// Clear any previous upload state
m_uploadList.clear();
m_nCurrentUploadFileLength = 0;
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
std::string path(filePath);
// Check if it's a directory (ends with backslash)
if (!path.empty() && path.back() == '\\') {
BuildUploadList(path);
Mprintf("[FileManager] Upload: %zu files from directory\n", m_uploadList.size());
if (m_uploadList.empty()) {
StopTransfer();
return;
}
} else {
m_uploadList.push_back(path);
}
// Send first file size
SendFileSize(m_uploadList.front().c_str());
}
// Send file size to initiate transfer
void SendFileSize(const char* fileName)
{
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
// Save current file being processed
strncpy(m_strCurrentUploadFile, linuxPath.c_str(), sizeof(m_strCurrentUploadFile) - 1);
m_strCurrentUploadFile[sizeof(m_strCurrentUploadFile) - 1] = '\0';
struct stat st;
if (stat(linuxPath.c_str(), &st) != 0) {
Mprintf("[FileManager] stat failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
// Skip this file and try next
if (!m_uploadList.empty()) {
m_uploadList.pop_front();
}
if (m_uploadList.empty()) {
SendToken(TOKEN_TRANSFER_FINISH);
} else {
SendFileSize(m_uploadList.front().c_str());
}
return;
}
uint64_t fileSize = (uint64_t)st.st_size;
m_nCurrentUploadFileLength = fileSize;
DWORD sizeHigh = (DWORD)(fileSize >> 32);
DWORD sizeLow = (DWORD)(fileSize & 0xFFFFFFFF);
Mprintf("[FileManager] Upload: %s (%lld bytes)\n", linuxPath.c_str(), (long long)fileSize);
// Build packet: TOKEN_FILE_SIZE + sizeHigh + sizeLow + fileName
int packetSize = strlen(fileName) + 10;
std::vector<BYTE> packet(packetSize, 0);
packet[0] = TOKEN_FILE_SIZE;
memcpy(&packet[1], &sizeHigh, sizeof(DWORD));
memcpy(&packet[5], &sizeLow, sizeof(DWORD));
memcpy(&packet[9], fileName, strlen(fileName) + 1);
m_client->Send2Server((char*)packet.data(), packetSize);
}
// Send file data chunk
void SendFileData(PBYTE lpBuffer)
{
// Parse offset from COMMAND_CONTINUE
// Format: [offsetHigh:4bytes][offsetLow:4bytes]
DWORD offsetHigh = 0, offsetLow = 0;
memcpy(&offsetHigh, lpBuffer, sizeof(DWORD));
memcpy(&offsetLow, lpBuffer + 4, sizeof(DWORD));
// Skip signal: offsetLow == 0xFFFFFFFF (server wants to skip this file)
if (offsetLow == 0xFFFFFFFF) {
UploadNext();
return;
}
int64_t offset = ((int64_t)offsetHigh << 32) | offsetLow;
// Check if we've reached the end of file
if (offset >= m_nCurrentUploadFileLength) {
UploadNext();
return;
}
FILE* fp = fopen(m_strCurrentUploadFile, "rb");
if (!fp) {
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentUploadFile, errno);
SendTransferFinishForCurrentFile();
return;
}
if (fseeko(fp, offset, SEEK_SET) != 0) {
Mprintf("[FileManager] fseeko failed (errno=%d)\n", errno);
fclose(fp);
SendTransferFinishForCurrentFile();
return;
}
const int headerLen = 9; // 1 + 4 + 4
const int maxDataSize = MAX_SEND_BUFFER - headerLen;
std::vector<BYTE> packet(MAX_SEND_BUFFER);
packet[0] = TOKEN_FILE_DATA;
memcpy(&packet[1], &offsetHigh, sizeof(DWORD));
memcpy(&packet[5], &offsetLow, sizeof(DWORD));
size_t bytesRead = fread(&packet[headerLen], 1, maxDataSize, fp);
int readErr = ferror(fp);
fclose(fp);
if (readErr) {
Mprintf("[FileManager] fread error (errno=%d)\n", errno);
SendTransferFinishForCurrentFile();
return;
}
if (bytesRead > 0) {
int packetSize = headerLen + bytesRead;
m_client->Send2Server((char*)packet.data(), packetSize);
} else {
// No more data, proceed to next file
UploadNext();
}
}
// Helper: finish current file transfer gracefully
void SendTransferFinishForCurrentFile()
{
if (!m_uploadList.empty()) {
m_uploadList.pop_front();
}
if (m_uploadList.empty()) {
SendToken(TOKEN_TRANSFER_FINISH);
} else {
SendFileSize(m_uploadList.front().c_str());
}
}
// Move to next file in upload queue
void UploadNext()
{
SendTransferFinishForCurrentFile();
}
void StopTransfer()
{
// Clear upload state
m_uploadList.clear();
m_nCurrentUploadFileLength = 0;
memset(m_strCurrentUploadFile, 0, sizeof(m_strCurrentUploadFile));
// Clear download state
m_nCurrentFileLength = 0;
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
SendToken(TOKEN_TRANSFER_FINISH);
}
// ============== File Download (Windows -> Linux) ==============
// Create local file for receiving
void CreateLocalRecvFile(PBYTE lpBuffer)
{
// Clear previous download state
memset(m_strCurrentFileName, 0, sizeof(m_strCurrentFileName));
DWORD sizeHigh = 0, sizeLow = 0;
memcpy(&sizeHigh, lpBuffer, sizeof(DWORD));
memcpy(&sizeLow, lpBuffer + 4, sizeof(DWORD));
m_nCurrentFileLength = ((int64_t)sizeHigh << 32) | sizeLow;
// Get file name (after 8 bytes of size)
const char* fileName = (const char*)(lpBuffer + 8);
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
strncpy(m_strCurrentFileName, linuxPath.c_str(), sizeof(m_strCurrentFileName) - 1);
m_strCurrentFileName[sizeof(m_strCurrentFileName) - 1] = '\0';
Mprintf("[FileManager] Download: %s (%lld bytes)\n",
m_strCurrentFileName, (long long)m_nCurrentFileLength);
// Create parent directories
MakeSureDirectoryExists(m_strCurrentFileName);
// Check if file exists
struct stat st;
bool fileExists = (stat(m_strCurrentFileName, &st) == 0);
// Determine effective transfer mode
int nTransferMode = m_nTransferMode;
switch (m_nTransferMode) {
case TRANSFER_MODE_OVERWRITE_ALL:
nTransferMode = TRANSFER_MODE_OVERWRITE;
break;
case TRANSFER_MODE_ADDITION_ALL:
nTransferMode = TRANSFER_MODE_ADDITION;
break;
case TRANSFER_MODE_JUMP_ALL:
nTransferMode = TRANSFER_MODE_JUMP;
break;
case TRANSFER_MODE_NORMAL:
// For Linux without UI, default to overwrite when file exists
nTransferMode = TRANSFER_MODE_OVERWRITE;
break;
}
BYTE response[9] = {0};
response[0] = TOKEN_DATA_CONTINUE;
if (fileExists) {
if (nTransferMode == TRANSFER_MODE_ADDITION) {
// Resume: send existing file size as offset
uint64_t existingSize = st.st_size;
DWORD existHigh = (DWORD)(existingSize >> 32);
DWORD existLow = (DWORD)(existingSize & 0xFFFFFFFF);
memcpy(response + 1, &existHigh, sizeof(DWORD));
memcpy(response + 5, &existLow, sizeof(DWORD));
} else if (nTransferMode == TRANSFER_MODE_JUMP) {
// Skip this file
DWORD skip = 0xFFFFFFFF;
memcpy(response + 5, &skip, sizeof(DWORD));
}
// TRANSFER_MODE_OVERWRITE: offset stays 0, file will be truncated
}
// Create or truncate file (unless skipping)
DWORD lowCheck = 0;
memcpy(&lowCheck, response + 5, sizeof(DWORD));
if (lowCheck != 0xFFFFFFFF) {
const char* mode = (nTransferMode == TRANSFER_MODE_ADDITION && fileExists) ? "r+b" : "wb";
FILE* fp = fopen(m_strCurrentFileName, mode);
if (fp) {
if (nTransferMode == TRANSFER_MODE_ADDITION && fileExists) {
// Seek to end for append mode
fseeko(fp, 0, SEEK_END);
}
fclose(fp);
} else {
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentFileName, errno);
}
}
m_client->Send2Server((char*)response, sizeof(response));
}
// Write received file data
void WriteLocalRecvFile(PBYTE lpBuffer, ULONG nSize)
{
if (nSize < 8) return;
DWORD offsetHigh = 0, offsetLow = 0;
memcpy(&offsetHigh, lpBuffer, sizeof(DWORD));
memcpy(&offsetLow, lpBuffer + 4, sizeof(DWORD));
int64_t offset = ((int64_t)offsetHigh << 32) | offsetLow;
ULONG dataLen = nSize - 8;
PBYTE data = lpBuffer + 8;
FILE* fp = fopen(m_strCurrentFileName, "r+b");
if (!fp) {
fp = fopen(m_strCurrentFileName, "wb");
}
if (!fp) {
Mprintf("[FileManager] fopen failed: %s (errno=%d)\n", m_strCurrentFileName, errno);
// Send error response - skip to end
BYTE response[9] = {0};
response[0] = TOKEN_DATA_CONTINUE;
DWORD skip = 0xFFFFFFFF;
memcpy(response + 5, &skip, sizeof(DWORD));
m_client->Send2Server((char*)response, sizeof(response));
return;
}
if (fseeko(fp, offset, SEEK_SET) != 0) {
Mprintf("[FileManager] fseeko failed (errno=%d)\n", errno);
fclose(fp);
// Send skip response to avoid server hanging
BYTE response[9] = {0};
response[0] = TOKEN_DATA_CONTINUE;
DWORD skip = 0xFFFFFFFF;
memcpy(response + 5, &skip, sizeof(DWORD));
m_client->Send2Server((char*)response, sizeof(response));
return;
}
size_t written = fwrite(data, 1, dataLen, fp);
if (written != dataLen) {
Mprintf("[FileManager] WriteLocalRecvFile: fwrite incomplete (%zu/%lu, errno=%d)\n",
written, (unsigned long)dataLen, errno);
}
fclose(fp);
// Calculate new offset with proper 64-bit arithmetic
int64_t newOffset = offset + written;
DWORD newOffsetHigh = (DWORD)(newOffset >> 32);
DWORD newOffsetLow = (DWORD)(newOffset & 0xFFFFFFFF);
// Send continue with new offset
BYTE response[9];
response[0] = TOKEN_DATA_CONTINUE;
memcpy(response + 1, &newOffsetHigh, sizeof(DWORD));
memcpy(response + 5, &newOffsetLow, sizeof(DWORD));
m_client->Send2Server((char*)response, sizeof(response));
}
void SetTransferMode(PBYTE lpBuffer)
{
memcpy(&m_nTransferMode, lpBuffer, sizeof(m_nTransferMode));
}
// ============== File Operations ==============
void DeleteSingleFile(const char* fileName)
{
std::string linuxPath = winPathToLinux(gbkToUtf8(fileName).c_str());
if (unlink(linuxPath.c_str()) != 0) {
Mprintf("[FileManager] unlink failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
}
}
void DeleteDirectoryRecursive(const char* dirPath)
{
std::string linuxPath = winPathToLinux(gbkToUtf8(dirPath).c_str());
// Remove trailing slash
while (!linuxPath.empty() && linuxPath.back() == '/') {
linuxPath.pop_back();
}
DeleteDirectoryRecursiveInternal(linuxPath);
}
// Internal recursive delete using Linux paths directly (no encoding conversion)
void DeleteDirectoryRecursiveInternal(const std::string& linuxPath)
{
DIR* dir = opendir(linuxPath.c_str());
if (!dir) {
Mprintf("[FileManager] opendir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
return;
}
struct dirent* ent;
while ((ent = readdir(dir)) != nullptr) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = linuxPath + "/" + ent->d_name;
struct stat st;
if (lstat(fullPath.c_str(), &st) != 0)
continue;
if (S_ISDIR(st.st_mode)) {
DeleteDirectoryRecursiveInternal(fullPath);
} else {
unlink(fullPath.c_str());
}
}
closedir(dir);
if (rmdir(linuxPath.c_str()) != 0) {
Mprintf("[FileManager] rmdir failed: %s (errno=%d)\n", linuxPath.c_str(), errno);
}
}
void CreateFolder(const char* folderPath)
{
std::string linuxPath = winPathToLinux(gbkToUtf8(folderPath).c_str());
MakeSureDirectoryPathExists(linuxPath);
SendToken(TOKEN_CREATEFOLDER_FINISH);
}
void RenameFile(PBYTE lpBuffer)
{
const char* oldName = (const char*)lpBuffer;
const char* newName = oldName + strlen(oldName) + 1;
std::string oldPath = winPathToLinux(gbkToUtf8(oldName).c_str());
std::string newPath = winPathToLinux(gbkToUtf8(newName).c_str());
if (rename(oldPath.c_str(), newPath.c_str()) != 0) {
Mprintf("[FileManager] rename failed: %s (errno=%d)\n", oldPath.c_str(), errno);
}
SendToken(TOKEN_RENAME_FINISH);
}
};

View File

@@ -1,688 +0,0 @@
#pragma once
#include "common/commands.h"
#include "common/file_upload.h"
#include "client/IOCPClient.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <iconv.h>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <fstream>
#include <thread>
// ============== Linux V2 File Transfer ==============
// Implements V2 file transfer protocol for Linux client
// Supports: receive files from server/C2C, send files to server/C2C
class FileTransferV2
{
public:
// V2 文件接收状态
struct RecvState {
uint64_t transferID;
uint64_t fileSize;
uint64_t receivedBytes;
std::string filePath;
int fd;
RecvState() : transferID(0), fileSize(0), receivedBytes(0), fd(-1) {}
~RecvState() { if (fd >= 0) close(fd); }
};
// 编码转换
static std::string gbkToUtf8(const std::string& gbk)
{
if (gbk.empty()) return gbk;
iconv_t cd = iconv_open("UTF-8", "GBK");
if (cd == (iconv_t)-1) return gbk;
size_t inLeft = gbk.size();
size_t outLeft = inLeft * 4;
std::string output(outLeft, '\0');
char* inPtr = const_cast<char*>(gbk.data());
char* outPtr = &output[0];
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
iconv_close(cd);
if (ret == (size_t)-1) return gbk;
output.resize(output.size() - outLeft);
return output;
}
static std::string utf8ToGbk(const std::string& utf8)
{
if (utf8.empty()) return utf8;
iconv_t cd = iconv_open("GBK", "UTF-8");
if (cd == (iconv_t)-1) return utf8;
size_t inLeft = utf8.size();
size_t outLeft = inLeft * 2;
std::string output(outLeft, '\0');
char* inPtr = const_cast<char*>(utf8.data());
char* outPtr = &output[0];
size_t ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft);
iconv_close(cd);
if (ret == (size_t)-1) return utf8;
output.resize(output.size() - outLeft);
return output;
}
// 递归创建目录
static bool MakeDirs(const std::string& path)
{
std::string dir;
size_t lastSlash = path.rfind('/');
if (lastSlash != std::string::npos && lastSlash > 0) {
dir = path.substr(0, lastSlash);
} else {
return true;
}
std::string current;
for (size_t i = 0; i < dir.size(); ++i) {
current += dir[i];
if (dir[i] == '/' && current.size() > 1) {
mkdir(current.c_str(), 0755);
}
}
if (!current.empty() && current != "/") {
mkdir(current.c_str(), 0755);
}
return true;
}
// 获取 C2C 目标目录(简化版:使用当前工作目录)
static std::string GetTargetDir(uint64_t transferID)
{
std::lock_guard<std::mutex> lock(s_targetDirMtx);
auto it = s_targetDirs.find(transferID);
if (it != s_targetDirs.end()) {
return it->second;
}
return "";
}
// 设置 C2C 目标目录
static void SetTargetDir(uint64_t transferID, const std::string& dir)
{
std::lock_guard<std::mutex> lock(s_targetDirMtx);
s_targetDirs[transferID] = dir;
Mprintf("[V2] SetTargetDir: transferID=%llu, dir=%s\n", transferID, dir.c_str());
}
// 处理 COMMAND_C2C_PREPARE - 在当前目录下创建子目录接收文件
static void HandleC2CPrepare(const uint8_t* data, size_t len, IOCPClient* client)
{
if (len < sizeof(C2CPreparePacket)) return;
const C2CPreparePacket* pkt = (const C2CPreparePacket*)data;
// 获取当前工作目录
char cwd[4096] = {};
if (!getcwd(cwd, sizeof(cwd))) {
strcpy(cwd, "/tmp");
}
// 创建接收子目录: recv_YYYYMM
time_t now = time(nullptr);
struct tm* t = localtime(&now);
char subdir[32];
snprintf(subdir, sizeof(subdir), "recv_%04d%02d",
t->tm_year + 1900, t->tm_mon + 1);
std::string targetDir = std::string(cwd) + "/" + subdir;
mkdir(targetDir.c_str(), 0755); // 已存在则忽略
SetTargetDir(pkt->transferID, targetDir);
Mprintf("[V2] C2C Prepare: transferID=%llu, targetDir=%s\n",
pkt->transferID, targetDir.c_str());
}
// 处理 V2 文件数据包
// 返回: 0=成功, >0=错误码
static int RecvFileChunkV2(const uint8_t* buf, size_t len, uint64_t myClientID)
{
// 检查是否是 FILE_COMPLETE_V2 包
if (len >= 1 && buf[0] == COMMAND_FILE_COMPLETE_V2) {
return HandleFileComplete(buf, len, myClientID);
}
if (len < sizeof(FileChunkPacketV2)) {
Mprintf("[V2 Recv] Invalid packet length: %zu\n", len);
return 2;
}
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)buf;
if (len < sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength) {
Mprintf("[V2 Recv] Incomplete packet: %zu < %zu\n", len,
sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength);
return 3;
}
// 验证目标
if (pkt->dstClientID != 0 && pkt->dstClientID != myClientID) {
Mprintf("[V2 Recv] Target mismatch: dst=%llu, my=%llu\n",
pkt->dstClientID, myClientID);
return 4;
}
// 提取文件名(服务端发送的是 GBK 编码)
std::string fileNameGbk((const char*)(pkt + 1), pkt->nameLength);
std::string fileName = gbkToUtf8(fileNameGbk);
// 构建保存路径
std::string savePath;
std::string targetDir = GetTargetDir(pkt->transferID);
if (!targetDir.empty()) {
// C2C 传输:使用预设的目标目录 + 相对路径(保留目录结构)
savePath = targetDir;
if (!savePath.empty() && savePath.back() != '/') {
savePath += '/';
}
// 转换 Windows 路径分隔符
std::string relPath = fileName;
for (char& c : relPath) {
if (c == '\\') c = '/';
}
// 移除盘符前缀 (如 C:/)
if (relPath.size() >= 3 && relPath[1] == ':' && relPath[2] == '/') {
relPath = relPath.substr(3);
} else if (relPath.size() >= 2 && relPath[1] == ':') {
relPath = relPath.substr(2);
}
// 移除开头的 /
while (!relPath.empty() && relPath[0] == '/') {
relPath = relPath.substr(1);
}
savePath += relPath;
} else {
// 服务端传输:使用相对路径
// 将 Windows 路径转换为 Linux 路径
savePath = fileName;
for (char& c : savePath) {
if (c == '\\') c = '/';
}
// 移除盘符前缀 (如 C:/)
if (savePath.size() >= 3 && savePath[1] == ':' && savePath[2] == '/') {
savePath = savePath.substr(2);
}
// 确保以 / 开头
if (savePath.empty() || savePath[0] != '/') {
savePath = "/" + savePath;
}
}
// 目录处理
if (pkt->flags & FFV2_DIRECTORY) {
MakeDirs(savePath + "/dummy");
Mprintf("[V2 Recv] Created directory: %s\n", savePath.c_str());
return 0;
}
// 创建父目录
MakeDirs(savePath);
// 获取或创建接收状态
uint64_t stateKey = pkt->transferID ^ ((uint64_t)pkt->fileIndex << 48);
RecvState* state = GetOrCreateState(stateKey, pkt, savePath);
if (!state) {
Mprintf("[V2 Recv] Failed to create state\n");
return 5;
}
// 写入数据
const char* data = (const char*)buf + sizeof(FileChunkPacketV2) + pkt->nameLength;
if (lseek(state->fd, pkt->offset, SEEK_SET) == -1) {
Mprintf("[V2 Recv] lseek failed: errno=%d\n", errno);
return 6;
}
size_t written = 0;
size_t toWrite = pkt->dataLength;
while (written < toWrite) {
ssize_t n = write(state->fd, data + written, toWrite - written);
if (n <= 0) {
if (errno == EINTR) continue;
Mprintf("[V2 Recv] write failed: errno=%d\n", errno);
return 7;
}
written += n;
}
state->receivedBytes += pkt->dataLength;
// 打印进度(每 10% 或最后一块)
bool isLast = (pkt->flags & FFV2_LAST_CHUNK) ||
(state->receivedBytes >= state->fileSize);
int progress = state->fileSize > 0 ?
(int)(100 * state->receivedBytes / state->fileSize) : 100;
static std::map<uint64_t, int> s_lastProgress;
if (isLast || progress >= s_lastProgress[stateKey] + 10) {
s_lastProgress[stateKey] = progress;
Mprintf("[V2 Recv] %s: %llu/%llu (%d%%)\n",
savePath.c_str(), state->receivedBytes, state->fileSize, progress);
}
// 文件完成
if (isLast) {
close(state->fd);
state->fd = -1;
Mprintf("[V2 Recv] File complete: %s\n", savePath.c_str());
// 清理状态
std::lock_guard<std::mutex> lock(s_statesMtx);
s_states.erase(stateKey);
s_lastProgress.erase(stateKey);
}
return 0;
}
// 处理文件完成校验包
static int HandleFileComplete(const uint8_t* buf, size_t len, uint64_t myClientID)
{
if (len < sizeof(FileCompletePacketV2)) {
return 1;
}
const FileCompletePacketV2* pkt = (const FileCompletePacketV2*)buf;
// 验证目标
if (pkt->dstClientID != 0 && pkt->dstClientID != myClientID) {
return 0; // 不是给我的,忽略
}
Mprintf("[V2] File complete verify: transferID=%llu, fileIndex=%u, size=%llu\n",
pkt->transferID, pkt->fileIndex, pkt->fileSize);
// TODO: 实现 SHA-256 校验
// 目前简单返回成功
return 0;
}
// ============== V2 文件发送 (Linux -> Server) ==============
// 生成传输 ID
static uint64_t GenerateTransferID()
{
static std::mutex s_mtx;
static uint64_t s_counter = 0;
std::lock_guard<std::mutex> lock(s_mtx);
uint64_t t = (uint64_t)time(nullptr) << 32;
return t | (++s_counter);
}
// 获取公共根目录 (Linux 路径)
static std::string GetCommonRoot(const std::vector<std::string>& files)
{
if (files.empty()) return "/";
if (files.size() == 1) {
size_t lastSlash = files[0].rfind('/');
if (lastSlash != std::string::npos) {
return files[0].substr(0, lastSlash + 1);
}
return "/";
}
std::string common = files[0];
for (size_t i = 1; i < files.size(); ++i) {
size_t j = 0;
while (j < common.size() && j < files[i].size() && common[j] == files[i][j]) {
++j;
}
common = common.substr(0, j);
}
// 截断到最后一个 /
size_t lastSlash = common.rfind('/');
if (lastSlash != std::string::npos) {
return common.substr(0, lastSlash + 1);
}
return "/";
}
// 递归收集目录中的文件和子目录
static void CollectFiles(const std::string& dir, std::vector<std::string>& files)
{
// 先添加目录本身(去掉末尾的斜杠)
std::string dirEntry = dir;
while (!dirEntry.empty() && dirEntry.back() == '/') {
dirEntry.pop_back();
}
if (!dirEntry.empty()) {
files.push_back(dirEntry + "/"); // 以 / 结尾表示目录
}
DIR* d = opendir(dir.c_str());
if (!d) return;
struct dirent* ent;
while ((ent = readdir(d)) != nullptr) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = dir;
if (fullPath.back() != '/') fullPath += '/';
fullPath += ent->d_name;
struct stat st;
if (lstat(fullPath.c_str(), &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
CollectFiles(fullPath, files);
} else if (S_ISREG(st.st_mode)) {
files.push_back(fullPath);
}
}
closedir(d);
}
// Windows 路径转 Linux 路径
static std::string WinPathToLinux(const std::string& winPath)
{
std::string p = winPath;
// 1. 反斜杠 -> 正斜杠
for (auto& c : p) {
if (c == '\\') c = '/';
}
// 2. 移除 "X:/" 或 "X:" 前缀
if (p.size() >= 3 && p[1] == ':' && p[2] == '/') {
p = p.substr(3);
} else if (p.size() >= 2 && p[1] == ':') {
p = p.substr(2);
}
// 3. 确保以 / 开头
if (p.empty() || p[0] != '/') {
p = "/" + p;
}
return p;
}
// 处理 CMD_DOWN_FILES_V2 请求 - 上传文件到服务端
// 格式: [cmd:1][targetDir\0][file1\0][file2\0]...[\0]
static void HandleDownFilesV2(const uint8_t* data, size_t len, IOCPClient* client, uint64_t myClientID)
{
if (len < 2) return;
// 解析目标目录 (GBK 编码)
const char* p = (const char*)(data + 1);
const char* end = (const char*)(data + len);
std::string targetDirGbk = p;
std::string targetDir = gbkToUtf8(targetDirGbk);
p += targetDirGbk.length() + 1;
// 解析文件列表
std::vector<std::string> remotePaths;
while (p < end && *p != '\0') {
std::string pathGbk = p;
remotePaths.push_back(gbkToUtf8(pathGbk));
p += pathGbk.length() + 1;
}
if (remotePaths.empty()) {
Mprintf("[V2 Send] No files to send\n");
return;
}
// 收集所有文件 (展开目录)
std::vector<std::string> allFiles;
std::vector<std::string> rootCandidates;
for (const auto& remotePath : remotePaths) {
std::string localPath = WinPathToLinux(remotePath);
struct stat st;
if (stat(localPath.c_str(), &st) != 0) {
Mprintf("[V2 Send] File not found: %s\n", localPath.c_str());
continue;
}
if (S_ISDIR(st.st_mode)) {
// 目录:递归收集
std::string dirPath = localPath;
if (dirPath.back() != '/') dirPath += '/';
// 使用父目录参与公共根计算
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
rootCandidates.push_back(parentPath);
CollectFiles(dirPath, allFiles);
} else {
// 文件
rootCandidates.push_back(localPath);
allFiles.push_back(localPath);
}
}
if (allFiles.empty()) {
Mprintf("[V2 Send] No files found\n");
return;
}
// 计算公共根目录
std::string commonRoot = GetCommonRoot(rootCandidates);
Mprintf("[V2 Send] Starting V2 transfer: %zu files, root=%s, target=%s\n",
allFiles.size(), commonRoot.c_str(), targetDir.c_str());
// 启动传输线程
std::thread([allFiles, targetDir, commonRoot, client, myClientID]() {
SendFilesV2(allFiles, targetDir, commonRoot, client, myClientID);
}).detach();
}
// V2 文件发送核心函数
static void SendFilesV2(const std::vector<std::string>& files,
const std::string& targetDir,
const std::string& commonRoot,
IOCPClient* client,
uint64_t myClientID)
{
const size_t CHUNK_SIZE = 60 * 1024; // 60KB per chunk
uint64_t transferID = GenerateTransferID();
uint32_t totalFiles = (uint32_t)files.size();
Mprintf("[V2 Send] TransferID=%llu, files=%u\n", transferID, totalFiles);
for (uint32_t fileIndex = 0; fileIndex < totalFiles; ++fileIndex) {
const std::string& filePath = files[fileIndex];
// 检查是否是目录项(以 / 结尾)
bool isDirectory = !filePath.empty() && filePath.back() == '/';
std::string actualPath = isDirectory ? filePath.substr(0, filePath.length() - 1) : filePath;
// 计算相对路径
std::string relativePath;
if (actualPath.find(commonRoot) == 0) {
relativePath = actualPath.substr(commonRoot.length());
} else {
size_t lastSlash = actualPath.rfind('/');
relativePath = (lastSlash != std::string::npos) ?
actualPath.substr(lastSlash + 1) : actualPath;
}
// 构建目标文件名 (服务端路径格式: targetDir + relativePath)
std::string targetName = targetDir;
if (!targetName.empty() && targetName.back() != '\\') {
targetName += '\\';
}
// 转换为 Windows 风格路径
for (char& c : relativePath) {
if (c == '/') c = '\\';
}
targetName += relativePath;
// 转为 GBK 编码
std::string targetNameGbk = utf8ToGbk(targetName);
// 目录项:发送单个包,不包含数据
if (isDirectory) {
std::vector<uint8_t> buffer(sizeof(FileChunkPacketV2) + targetNameGbk.length());
FileChunkPacketV2* pkt = (FileChunkPacketV2*)buffer.data();
memset(pkt, 0, sizeof(FileChunkPacketV2));
pkt->cmd = COMMAND_SEND_FILE_V2;
pkt->transferID = transferID;
pkt->srcClientID = myClientID;
pkt->dstClientID = 0; // 发送给服务端
pkt->fileIndex = fileIndex;
pkt->totalFiles = totalFiles;
pkt->fileSize = 0;
pkt->offset = 0;
pkt->dataLength = 0;
pkt->nameLength = targetNameGbk.length();
pkt->flags = FFV2_DIRECTORY | FFV2_LAST_CHUNK;
// 写入目录名
memcpy(buffer.data() + sizeof(FileChunkPacketV2),
targetNameGbk.c_str(), targetNameGbk.length());
Mprintf("[V2 Send] [%u/%u] DIR: %s -> %s\n",
fileIndex + 1, totalFiles, actualPath.c_str(), targetName.c_str());
if (!client->Send2Server((char*)buffer.data(), (ULONG)buffer.size())) {
Mprintf("[V2 Send] Send directory failed\n");
}
usleep(1000); // 1ms 间隔
continue;
}
// 获取文件信息
struct stat st;
if (stat(actualPath.c_str(), &st) != 0) {
Mprintf("[V2 Send] stat failed: %s\n", actualPath.c_str());
continue;
}
uint64_t fileSize = (uint64_t)st.st_size;
// 打开文件
int fd = open(actualPath.c_str(), O_RDONLY);
if (fd < 0) {
Mprintf("[V2 Send] open failed: %s, errno=%d\n", actualPath.c_str(), errno);
continue;
}
Mprintf("[V2 Send] [%u/%u] %s -> %s (%llu bytes)\n",
fileIndex + 1, totalFiles, actualPath.c_str(), targetName.c_str(), fileSize);
// 分块发送
uint64_t offset = 0;
std::vector<uint8_t> buffer;
buffer.reserve(sizeof(FileChunkPacketV2) + targetNameGbk.length() + CHUNK_SIZE);
while (offset < fileSize || (offset == 0 && fileSize == 0)) {
size_t toRead = std::min(CHUNK_SIZE, (size_t)(fileSize - offset));
bool isLast = (offset + toRead >= fileSize);
// 构建包头
buffer.resize(sizeof(FileChunkPacketV2) + targetNameGbk.length() + toRead);
FileChunkPacketV2* pkt = (FileChunkPacketV2*)buffer.data();
memset(pkt, 0, sizeof(FileChunkPacketV2));
pkt->cmd = COMMAND_SEND_FILE_V2;
pkt->transferID = transferID;
pkt->srcClientID = myClientID;
pkt->dstClientID = 0; // 发送给服务端
pkt->fileIndex = fileIndex;
pkt->totalFiles = totalFiles;
pkt->fileSize = fileSize;
pkt->offset = offset;
pkt->dataLength = toRead;
pkt->nameLength = targetNameGbk.length();
pkt->flags = isLast ? FFV2_LAST_CHUNK : FFV2_NONE;
// 写入文件名
memcpy(buffer.data() + sizeof(FileChunkPacketV2),
targetNameGbk.c_str(), targetNameGbk.length());
// 读取文件数据
if (toRead > 0) {
uint8_t* dataPtr = buffer.data() + sizeof(FileChunkPacketV2) + targetNameGbk.length();
ssize_t n = read(fd, dataPtr, toRead);
if (n < 0) {
Mprintf("[V2 Send] read failed: errno=%d\n", errno);
break;
}
if ((size_t)n < toRead) {
// 文件读取不完整,调整包大小
toRead = n;
pkt->dataLength = toRead;
buffer.resize(sizeof(FileChunkPacketV2) + targetNameGbk.length() + toRead);
isLast = true;
pkt->flags |= FFV2_LAST_CHUNK;
}
}
// 发送
if (!client->Send2Server((char*)buffer.data(), (ULONG)buffer.size())) {
Mprintf("[V2 Send] Send failed\n");
break;
}
offset += toRead;
// 空文件处理
if (fileSize == 0) break;
// 发送间隔 (避免发送过快)
usleep(10000); // 10ms
}
close(fd);
}
Mprintf("[V2 Send] Transfer complete: transferID=%llu\n", transferID);
}
private:
static RecvState* GetOrCreateState(uint64_t key, const FileChunkPacketV2* pkt,
const std::string& savePath)
{
std::lock_guard<std::mutex> lock(s_statesMtx);
auto it = s_states.find(key);
if (it != s_states.end()) {
return it->second.get();
}
// 创建新状态
std::unique_ptr<RecvState> state(new RecvState());
state->transferID = pkt->transferID;
state->fileSize = pkt->fileSize;
state->filePath = savePath;
state->receivedBytes = 0;
// 打开文件
state->fd = open(savePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (state->fd < 0) {
Mprintf("[V2 Recv] open failed: %s, errno=%d\n", savePath.c_str(), errno);
return nullptr;
}
RecvState* ptr = state.get();
s_states[key] = std::move(state);
return ptr;
}
// 接收状态存储
static inline std::map<uint64_t, std::unique_ptr<RecvState>> s_states;
static inline std::mutex s_statesMtx;
// C2C 目标目录存储
static inline std::map<uint64_t, std::string> s_targetDirs;
static inline std::mutex s_targetDirMtx;
};

View File

@@ -3,7 +3,7 @@
#include "client/IOCPClient.h"
#include "LinuxConfig.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include <dlfcn.h>
#include <sys/stat.h>
#include <thread>

View File

@@ -26,9 +26,9 @@
#include <cmath>
#include "ScreenHandler.h"
#include "SystemManager.h"
#include "FileManager.h"
#include "common/FileManager.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include "common/logger.h"
#define XXH_INLINE_ALL
#include "common/xxhash.h"