Feature: Support compress files in file management dialog

This commit is contained in:
yuanyuanxiang
2026-01-04 12:05:28 +01:00
parent 5e031523af
commit 49373a1137
9 changed files with 1147 additions and 32 deletions

View File

@@ -4,6 +4,7 @@
#include "FileManager.h"
#include <shellapi.h>
#include "ZstdArchive.h"
typedef struct {
DWORD dwSizeHigh;
@@ -26,9 +27,79 @@ CFileManager::~CFileManager()
m_UploadList.clear();
}
std::vector<std::string> ParseMultiStringPath(const char* buffer, size_t size)
{
std::vector<std::string> paths;
const char* p = buffer;
const char* end = buffer + size;
while (p < end)
{
size_t len = strlen(p);
if (len > 0)
{
paths.emplace_back(p, len);
}
p += len + 1;
}
return paths;
}
std::string GetExtractDir(const std::string& archivePath) {
if (archivePath.size() >= 5) {
std::string ext = archivePath.substr(archivePath.size() - 5);
for (char& c : ext) c = tolower(c);
if (ext == ".zsta") {
return archivePath.substr(0, archivePath.size() - 5);
}
}
return archivePath + "_extract";
}
std::string GetDirectory(const std::string& filePath) {
size_t pos = filePath.find_last_of("/\\");
if (pos != std::string::npos) {
return filePath.substr(0, pos);
}
return ".";
}
VOID CFileManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
{
switch (lpBuffer[0]) {
case CMD_COMPRESS_FILES: {
std::vector<std::string> paths = ParseMultiStringPath((char*)lpBuffer + 1, nSize - 1);
zsta::Error err = zsta::CZstdArchive::Compress(std::vector<std::string>(paths.begin() + 1, paths.end()), paths.at(0));
if (err != zsta::Error::Success) {
Mprintf("压缩失败: %s\n", zsta::CZstdArchive::GetErrorString(err));
}
else {
std::string dir = GetDirectory(paths.at(0));
SendFilesList((char*)dir.c_str());
}
break;
}
case CMD_UNCOMPRESS_FILES: {
std::string dir;
std::vector<std::string> paths = ParseMultiStringPath((char*)lpBuffer + 1, nSize - 1);
for (size_t i = 0; i < paths.size(); i++) {
const std::string& path = paths[i];
std::string destDir = GetExtractDir(path);
zsta::Error err = zsta::CZstdArchive::Extract(path, destDir);
if (err != zsta::Error::Success) {
Mprintf("解压失败: %s\n", zsta::CZstdArchive::GetErrorString(err));
}
else {
dir = GetDirectory(path);
}
}
if (!dir.empty()) {
SendFilesList((char*)dir.c_str());
}
break;
}
case COMMAND_LIST_FILES:// 获取文件列表
SendFilesList((char *)lpBuffer + 1);
break;

757
common/ZstdArchive.h Normal file
View File

@@ -0,0 +1,757 @@
/**
* @file ZstdArchive.h
* @brief ZSTA 归档格式 - 基于 Zstd 的轻量级压缩模块
* @version 1.0
*
* 特性:
* - 支持单个或多个文件、文件夹压缩
* - 保留目录结构
* - UTF-8 路径支持
* - 固定大小头部和条目表,便于快速索引
* - 版本控制,支持向后兼容
*/
#ifndef ZSTD_ARCHIVE_H
#define ZSTD_ARCHIVE_H
#include <zstd/zstd.h>
#include <string>
#include <vector>
#include <cstdint>
#include <cstring>
#include <cstdio>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <ctime>
#endif
/*
=============================================================================
ZSTA 归档格式规范 v1.0
=============================================================================
文件结构:
+---------------------------+
| 文件头 (ZstaHeader) | 32 字节
+---------------------------+
| 文件条目表 (ZstaEntry[]) | 每条 288 字节
+---------------------------+
| 压缩数据块 | 连续存放
+---------------------------+
文件头 (32 字节):
偏移 大小 描述
0 4 魔数 "ZSTA"
4 2 主版本号 (当前: 1)
6 2 次版本号 (当前: 0)
8 4 文件条目数量
12 4 压缩级别 (1-22)
16 8 创建时间戳 (Unix时间戳, 秒)
24 4 校验和 (CRC32, 保留)
28 4 保留
文件条目 (288 字节):
偏移 大小 描述
0 256 相对路径 (UTF-8, null 结尾)
256 1 类型: 0=文件, 1=目录
257 1 压缩方法: 0=存储, 1=zstd
258 2 文件属性 (保留)
260 4 CRC32 (保留)
264 8 原始大小
272 8 压缩大小 (目录为0)
280 8 修改时间戳 (Unix时间戳)
数据块:
按条目顺序连续存放每个文件的压缩数据
注意事项:
- 路径使用 '/' 作为分隔符
- 目录条目的路径以 '/' 结尾
- 所有多字节整数使用小端序
=============================================================================
*/
namespace zsta {
// 版本信息
constexpr uint16_t VERSION_MAJOR = 1;
constexpr uint16_t VERSION_MINOR = 0;
constexpr char MAGIC[4] = {'Z', 'S', 'T', 'A'};
// 条目类型
enum class EntryType : uint8_t {
File = 0,
Directory = 1
};
// 压缩方法
enum class CompressMethod : uint8_t {
Store = 0, // 不压缩
Zstd = 1 // Zstd 压缩
};
#pragma pack(push, 1)
/**
* @brief 文件头结构 (32 字节)
*/
struct ZstaHeader {
char magic[4]; // 魔数 "ZSTA"
uint16_t versionMajor; // 主版本号
uint16_t versionMinor; // 次版本号
uint32_t entryCount; // 条目数量
uint32_t compressLevel; // 压缩级别
uint64_t createTime; // 创建时间戳
uint32_t checksum; // 校验和 (保留)
uint32_t reserved; // 保留
};
/**
* @brief 文件条目结构 (288 字节)
*/
struct ZstaEntry {
char path[256]; // UTF-8 路径
uint8_t type; // 类型: 0=文件, 1=目录
uint8_t method; // 压缩方法
uint16_t attributes; // 文件属性 (保留)
uint32_t crc32; // CRC32 (保留)
uint64_t originalSize; // 原始大小
uint64_t compressedSize; // 压缩大小
uint64_t modifyTime; // 修改时间戳
};
#pragma pack(pop)
// 静态断言确保结构大小
static_assert(sizeof(ZstaHeader) == 32, "ZstaHeader must be 32 bytes");
static_assert(sizeof(ZstaEntry) == 288, "ZstaEntry must be 288 bytes");
/**
* @brief 归档信息
*/
struct ArchiveInfo {
uint16_t versionMajor;
uint16_t versionMinor;
uint32_t entryCount;
uint32_t compressLevel;
uint64_t createTime;
uint64_t totalOriginalSize;
uint64_t totalCompressedSize;
};
/**
* @brief 条目信息
*/
struct EntryInfo {
std::string path;
EntryType type;
uint64_t originalSize;
uint64_t compressedSize;
uint64_t modifyTime;
};
/**
* @brief 错误码
*/
enum class Error {
Success = 0,
FileOpenFailed,
FileReadFailed,
FileWriteFailed,
InvalidFormat,
UnsupportedVersion,
DecompressFailed,
CompressFailed,
PathTooLong,
EmptyInput
};
/**
* @brief ZSTA 归档类
*/
class CZstdArchive {
public:
/**
* @brief 压缩多个文件/文件夹
* @param srcPaths 源路径列表
* @param outPath 输出文件路径
* @param level 压缩级别 (1-22, 默认3)
* @return 错误码
*/
static Error Compress(const std::vector<std::string>& srcPaths,
const std::string& outPath,
int level = 3) {
if (srcPaths.empty()) {
return Error::EmptyInput;
}
// 收集所有文件
std::vector<FileInfo> files;
for (const auto& src : srcPaths) {
std::string baseName = GetFileName(src);
if (IsDirectory(src)) {
CollectFiles(src, baseName, files);
} else {
FileInfo fi;
fi.fullPath = src;
fi.relPath = baseName;
fi.isDir = false;
fi.size = GetFileSize(src);
fi.modTime = GetFileModTime(src);
files.push_back(fi);
}
}
if (files.empty()) {
return Error::EmptyInput;
}
FILE* fOut = fopen(outPath.c_str(), "wb");
if (!fOut) {
return Error::FileOpenFailed;
}
// 写文件头
ZstaHeader header = {};
memcpy(header.magic, MAGIC, 4);
header.versionMajor = VERSION_MAJOR;
header.versionMinor = VERSION_MINOR;
header.entryCount = static_cast<uint32_t>(files.size());
header.compressLevel = level;
header.createTime = static_cast<uint64_t>(time(nullptr));
if (fwrite(&header, sizeof(header), 1, fOut) != 1) {
fclose(fOut);
return Error::FileWriteFailed;
}
// 预留条目表空间
long entryTablePos = ftell(fOut);
std::vector<ZstaEntry> entries(files.size());
if (fwrite(entries.data(), sizeof(ZstaEntry), files.size(), fOut) != files.size()) {
fclose(fOut);
return Error::FileWriteFailed;
}
// 压缩并写入数据
ZSTD_CCtx* cctx = ZSTD_createCCtx();
Error result = Error::Success;
for (size_t i = 0; i < files.size(); i++) {
const auto& f = files[i];
auto& e = entries[i];
// 填充条目信息(路径转 UTF-8 存储)
std::string entryPath = LocalToUtf8(f.relPath);
if (f.isDir && !entryPath.empty() && entryPath.back() != '/') {
entryPath += '/';
}
if (entryPath.length() >= sizeof(e.path)) {
result = Error::PathTooLong;
break;
}
strncpy(e.path, entryPath.c_str(), sizeof(e.path) - 1);
e.path[sizeof(e.path) - 1] = '\0';
e.type = f.isDir ? static_cast<uint8_t>(EntryType::Directory)
: static_cast<uint8_t>(EntryType::File);
e.modifyTime = f.modTime;
if (f.isDir) {
e.method = static_cast<uint8_t>(CompressMethod::Store);
e.originalSize = 0;
e.compressedSize = 0;
} else {
FILE* fIn = fopen(f.fullPath.c_str(), "rb");
if (!fIn) {
e.method = static_cast<uint8_t>(CompressMethod::Store);
e.originalSize = 0;
e.compressedSize = 0;
continue;
}
// 获取文件大小
fseek(fIn, 0, SEEK_END);
e.originalSize = ftell(fIn);
fseek(fIn, 0, SEEK_SET);
if (e.originalSize > 0) {
std::vector<char> inBuf(e.originalSize);
if (fread(inBuf.data(), 1, e.originalSize, fIn) != e.originalSize) {
fclose(fIn);
continue;
}
size_t bound = ZSTD_compressBound(e.originalSize);
std::vector<char> outBuf(bound);
size_t compSize = ZSTD_compressCCtx(cctx, outBuf.data(), bound,
inBuf.data(), e.originalSize, level);
if (ZSTD_isError(compSize)) {
fclose(fIn);
result = Error::CompressFailed;
break;
}
// 如果压缩后更大,则存储原始数据
if (compSize >= e.originalSize) {
e.method = static_cast<uint8_t>(CompressMethod::Store);
e.compressedSize = e.originalSize;
fwrite(inBuf.data(), 1, e.originalSize, fOut);
} else {
e.method = static_cast<uint8_t>(CompressMethod::Zstd);
e.compressedSize = compSize;
fwrite(outBuf.data(), 1, compSize, fOut);
}
} else {
e.method = static_cast<uint8_t>(CompressMethod::Store);
e.compressedSize = 0;
}
fclose(fIn);
}
}
ZSTD_freeCCtx(cctx);
if (result == Error::Success) {
// 回写条目表
fseek(fOut, entryTablePos, SEEK_SET);
if (fwrite(entries.data(), sizeof(ZstaEntry), files.size(), fOut) != files.size()) {
result = Error::FileWriteFailed;
}
}
fclose(fOut);
if (result != Error::Success) {
remove(outPath.c_str());
}
return result;
}
/**
* @brief 压缩单个文件/文件夹
*/
static Error Compress(const std::string& srcPath, const std::string& outPath, int level = 3) {
return Compress(std::vector<std::string>{srcPath}, outPath, level);
}
/**
* @brief 解压归档
* @param archivePath 归档文件路径
* @param destDir 目标目录
* @return 错误码
*/
static Error Extract(const std::string& archivePath, const std::string& destDir) {
FILE* fIn = fopen(archivePath.c_str(), "rb");
if (!fIn) {
return Error::FileOpenFailed;
}
// 读取并验证文件头
ZstaHeader header;
memset(&header, 0, sizeof(header));
size_t bytesRead = fread(&header, 1, sizeof(header), fIn);
// 首先检查魔数(即使文件太短也要先检查已读取的部分)
if (bytesRead < 4 || memcmp(header.magic, MAGIC, 4) != 0) {
fclose(fIn);
return Error::InvalidFormat;
}
// 然后检查是否读取了完整的头部
if (bytesRead != sizeof(header)) {
fclose(fIn);
return Error::InvalidFormat;
}
if (header.versionMajor > VERSION_MAJOR) {
fclose(fIn);
return Error::UnsupportedVersion;
}
// 读取条目表
std::vector<ZstaEntry> entries(header.entryCount);
if (fread(entries.data(), sizeof(ZstaEntry), header.entryCount, fIn) != header.entryCount) {
fclose(fIn);
return Error::FileReadFailed;
}
CreateDirectoryRecursive(destDir);
ZSTD_DCtx* dctx = ZSTD_createDCtx();
Error result = Error::Success;
for (const auto& e : entries) {
// 路径从 UTF-8 转为本地编码
std::string localPath = Utf8ToLocal(e.path);
std::string fullPath = destDir + "/" + localPath;
NormalizePath(fullPath);
if (e.type == static_cast<uint8_t>(EntryType::Directory)) {
CreateDirectoryRecursive(fullPath);
} else {
// 创建父目录
std::string parent = GetParentPath(fullPath);
if (!parent.empty()) {
CreateDirectoryRecursive(parent);
}
if (e.compressedSize > 0) {
std::vector<char> compBuf(e.compressedSize);
if (fread(compBuf.data(), 1, e.compressedSize, fIn) != e.compressedSize) {
result = Error::FileReadFailed;
break;
}
std::vector<char> origBuf(e.originalSize);
if (e.method == static_cast<uint8_t>(CompressMethod::Zstd)) {
size_t ret = ZSTD_decompressDCtx(dctx, origBuf.data(), e.originalSize,
compBuf.data(), e.compressedSize);
if (ZSTD_isError(ret)) {
result = Error::DecompressFailed;
break;
}
} else {
// Store 方法,直接复制
memcpy(origBuf.data(), compBuf.data(), e.originalSize);
}
FILE* fOut = fopen(fullPath.c_str(), "wb");
if (fOut) {
fwrite(origBuf.data(), 1, e.originalSize, fOut);
fclose(fOut);
}
} else {
// 空文件
FILE* fOut = fopen(fullPath.c_str(), "wb");
if (fOut) fclose(fOut);
}
}
}
ZSTD_freeDCtx(dctx);
fclose(fIn);
return result;
}
/**
* @brief 获取归档信息
*/
static Error GetInfo(const std::string& archivePath, ArchiveInfo& info) {
FILE* fIn = fopen(archivePath.c_str(), "rb");
if (!fIn) {
return Error::FileOpenFailed;
}
ZstaHeader header;
if (fread(&header, sizeof(header), 1, fIn) != 1 ||
memcmp(header.magic, MAGIC, 4) != 0) {
fclose(fIn);
return Error::InvalidFormat;
}
info.versionMajor = header.versionMajor;
info.versionMinor = header.versionMinor;
info.entryCount = header.entryCount;
info.compressLevel = header.compressLevel;
info.createTime = header.createTime;
info.totalOriginalSize = 0;
info.totalCompressedSize = 0;
std::vector<ZstaEntry> entries(header.entryCount);
if (fread(entries.data(), sizeof(ZstaEntry), header.entryCount, fIn) == header.entryCount) {
for (const auto& e : entries) {
info.totalOriginalSize += e.originalSize;
info.totalCompressedSize += e.compressedSize;
}
}
fclose(fIn);
return Error::Success;
}
/**
* @brief 列出归档内容
*/
static Error List(const std::string& archivePath, std::vector<EntryInfo>& entries) {
FILE* fIn = fopen(archivePath.c_str(), "rb");
if (!fIn) {
return Error::FileOpenFailed;
}
ZstaHeader header;
if (fread(&header, sizeof(header), 1, fIn) != 1 ||
memcmp(header.magic, MAGIC, 4) != 0) {
fclose(fIn);
return Error::InvalidFormat;
}
std::vector<ZstaEntry> rawEntries(header.entryCount);
if (fread(rawEntries.data(), sizeof(ZstaEntry), header.entryCount, fIn) != header.entryCount) {
fclose(fIn);
return Error::FileReadFailed;
}
fclose(fIn);
entries.clear();
entries.reserve(header.entryCount);
for (const auto& e : rawEntries) {
EntryInfo info;
info.path = Utf8ToLocal(e.path); // 转为本地编码
info.type = static_cast<EntryType>(e.type);
info.originalSize = e.originalSize;
info.compressedSize = e.compressedSize;
info.modifyTime = e.modifyTime;
entries.push_back(info);
}
return Error::Success;
}
/**
* @brief 获取错误描述
*/
static const char* GetErrorString(Error err) {
switch (err) {
case Error::Success: return "Success";
case Error::FileOpenFailed: return "Failed to open file";
case Error::FileReadFailed: return "Failed to read file";
case Error::FileWriteFailed: return "Failed to write file";
case Error::InvalidFormat: return "Invalid archive format";
case Error::UnsupportedVersion: return "Unsupported archive version";
case Error::DecompressFailed: return "Decompression failed";
case Error::CompressFailed: return "Compression failed";
case Error::PathTooLong: return "Path too long";
case Error::EmptyInput: return "Empty input";
default: return "Unknown error";
}
}
private:
struct FileInfo {
std::string fullPath;
std::string relPath;
bool isDir;
uint64_t size;
uint64_t modTime;
};
#ifdef _WIN32
// MBCS -> UTF-8 (压缩时用,存入归档)
static std::string LocalToUtf8(const std::string& local) {
if (local.empty()) return "";
int wlen = MultiByteToWideChar(CP_ACP, 0, local.c_str(), -1, NULL, 0);
if (wlen <= 0) return local;
std::wstring wide(wlen - 1, 0);
MultiByteToWideChar(CP_ACP, 0, local.c_str(), -1, &wide[0], wlen);
int ulen = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, NULL, 0, NULL, NULL);
if (ulen <= 0) return local;
std::string utf8(ulen - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, &utf8[0], ulen, NULL, NULL);
return utf8;
}
// UTF-8 -> MBCS (解压时用,写入文件系统)
static std::string Utf8ToLocal(const std::string& utf8) {
if (utf8.empty()) return "";
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
if (wlen <= 0) return utf8;
std::wstring wide(wlen - 1, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &wide[0], wlen);
int llen = WideCharToMultiByte(CP_ACP, 0, wide.c_str(), -1, NULL, 0, NULL, NULL);
if (llen <= 0) return utf8;
std::string local(llen - 1, 0);
WideCharToMultiByte(CP_ACP, 0, wide.c_str(), -1, &local[0], llen, NULL, NULL);
return local;
}
#else
// Linux/macOS 默认 UTF-8
static std::string LocalToUtf8(const std::string& s) { return s; }
static std::string Utf8ToLocal(const std::string& s) { return s; }
#endif
static bool IsDirectory(const std::string& path) {
#ifdef _WIN32
DWORD attr = GetFileAttributesA(path.c_str());
return (attr != INVALID_FILE_ATTRIBUTES) && (attr & FILE_ATTRIBUTE_DIRECTORY);
#else
struct stat st;
if (stat(path.c_str(), &st) != 0) return false;
return S_ISDIR(st.st_mode);
#endif
}
static uint64_t GetFileSize(const std::string& path) {
#ifdef _WIN32
WIN32_FILE_ATTRIBUTE_DATA fad;
if (!GetFileAttributesExA(path.c_str(), GetFileExInfoStandard, &fad)) return 0;
return (static_cast<uint64_t>(fad.nFileSizeHigh) << 32) | fad.nFileSizeLow;
#else
struct stat st;
if (stat(path.c_str(), &st) != 0) return 0;
return st.st_size;
#endif
}
static uint64_t GetFileModTime(const std::string& path) {
#ifdef _WIN32
WIN32_FILE_ATTRIBUTE_DATA fad;
if (!GetFileAttributesExA(path.c_str(), GetFileExInfoStandard, &fad)) return 0;
// 转换 FILETIME 到 Unix 时间戳
ULARGE_INTEGER ull;
ull.LowPart = fad.ftLastWriteTime.dwLowDateTime;
ull.HighPart = fad.ftLastWriteTime.dwHighDateTime;
return (ull.QuadPart - 116444736000000000ULL) / 10000000ULL;
#else
struct stat st;
if (stat(path.c_str(), &st) != 0) return 0;
return st.st_mtime;
#endif
}
static std::string GetFileName(const std::string& path) {
// 先移除末尾的斜杠
std::string p = path;
while (!p.empty() && (p.back() == '/' || p.back() == '\\')) {
p.pop_back();
}
if (p.empty()) return "";
size_t pos = p.find_last_of("/\\");
if (pos != std::string::npos) {
return p.substr(pos + 1);
}
return p;
}
static std::string GetParentPath(const std::string& path) {
size_t pos = path.find_last_of("/\\");
if (pos != std::string::npos && pos > 0) {
return path.substr(0, pos);
}
return "";
}
static void NormalizePath(std::string& path) {
for (char& c : path) {
#ifdef _WIN32
if (c == '/') c = '\\';
#else
if (c == '\\') c = '/';
#endif
}
// 移除末尾的分隔符(除非是根目录)
while (path.size() > 1 && (path.back() == '/' || path.back() == '\\')) {
path.pop_back();
}
}
static void CollectFiles(const std::string& dir, const std::string& rel,
std::vector<FileInfo>& files) {
// 添加目录本身
FileInfo dirInfo;
dirInfo.fullPath = dir;
dirInfo.relPath = rel;
dirInfo.isDir = true;
dirInfo.size = 0;
dirInfo.modTime = GetFileModTime(dir);
files.push_back(dirInfo);
#ifdef _WIN32
WIN32_FIND_DATAA fd;
std::string pattern = dir + "\\*";
HANDLE h = FindFirstFileA(pattern.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
std::string fullPath = dir + "\\" + fd.cFileName;
std::string relPath = rel + "/" + fd.cFileName;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
CollectFiles(fullPath, relPath, files);
} else {
FileInfo fi;
fi.fullPath = fullPath;
fi.relPath = relPath;
fi.isDir = false;
fi.size = (static_cast<uint64_t>(fd.nFileSizeHigh) << 32) | fd.nFileSizeLow;
// 转换 FILETIME
ULARGE_INTEGER ull;
ull.LowPart = fd.ftLastWriteTime.dwLowDateTime;
ull.HighPart = fd.ftLastWriteTime.dwHighDateTime;
fi.modTime = (ull.QuadPart - 116444736000000000ULL) / 10000000ULL;
files.push_back(fi);
}
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
DIR* d = opendir(dir.c_str());
if (!d) return;
struct dirent* entry;
while ((entry = readdir(d)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
std::string fullPath = dir + "/" + entry->d_name;
std::string relPath = rel + "/" + entry->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
CollectFiles(fullPath, relPath, files);
} else {
FileInfo fi;
fi.fullPath = fullPath;
fi.relPath = relPath;
fi.isDir = false;
fi.size = st.st_size;
fi.modTime = st.st_mtime;
files.push_back(fi);
}
}
closedir(d);
#endif
}
static void CreateDirectoryRecursive(const std::string& path) {
if (path.empty()) return;
std::string normalized = path;
NormalizePath(normalized);
size_t pos = 0;
while ((pos = normalized.find_first_of("/\\", pos + 1)) != std::string::npos) {
std::string sub = normalized.substr(0, pos);
#ifdef _WIN32
CreateDirectoryA(sub.c_str(), nullptr);
#else
mkdir(sub.c_str(), 0755);
#endif
}
#ifdef _WIN32
CreateDirectoryA(normalized.c_str(), nullptr);
#else
mkdir(normalized.c_str(), 0755);
#endif
}
};
} // namespace zsta
#endif // ZSTD_ARCHIVE_H

View File

@@ -198,6 +198,8 @@ enum {
COMMAND_SWITCH_SCREEN = 69,
CMD_MULTITHREAD_COMPRESS = 70,
CMD_FPS = 71,
CMD_COMPRESS_FILES = 72, // 压缩文件
CMD_UNCOMPRESS_FILES = 73, // 解压文件
// 服务端发出的标识
TOKEN_AUTH = 100, // 要求验证

View File

@@ -101,6 +101,92 @@ std::string GetPwdHash();
// CMy2015RemoteApp 构造
// 自定义压缩文件
#include <windows.h>
#include <shlobj.h>
#include "ZstdArchive.h"
bool RegisterZstaMenu(const std::string& exePath) {
HKEY hKey;
const char* compressText = "压缩为 ZSTA 文件";
const char* extractText = "解压 ZSTA 文件";
const char* zstaDesc = "ZSTA 压缩文件";
const char* zstaExt = "ZstaArchive";
// 文件右键
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "*\\shell\\CompressToZsta", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)compressText, strlen(compressText) + 1);
RegCloseKey(hKey);
}
std::string compressCmd = "\"" + exePath + "\" -c \"%1\"";
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "*\\shell\\CompressToZsta\\command", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)compressCmd.c_str(), compressCmd.size() + 1);
RegCloseKey(hKey);
}
// 文件夹右键
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "Directory\\shell\\CompressToZsta", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)compressText, strlen(compressText) + 1);
RegCloseKey(hKey);
}
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "Directory\\shell\\CompressToZsta\\command", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)compressCmd.c_str(), compressCmd.size() + 1);
RegCloseKey(hKey);
}
// .zsta 文件关联
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, ".zsta", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)zstaExt, strlen(zstaExt) + 1);
RegCloseKey(hKey);
}
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "ZstaArchive", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)zstaDesc, strlen(zstaDesc) + 1);
RegCloseKey(hKey);
}
// .zsta 右键菜单
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "ZstaArchive\\shell\\extract", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)extractText, strlen(extractText) + 1);
RegCloseKey(hKey);
}
std::string extractCmd = "\"" + exePath + "\" -x \"%1\"";
if (RegCreateKeyExA(HKEY_CLASSES_ROOT, "ZstaArchive\\shell\\extract\\command", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)extractCmd.c_str(), extractCmd.size() + 1);
RegCloseKey(hKey);
}
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
return true;
}
bool UnregisterZstaMenu() {
RegDeleteTreeA(HKEY_CLASSES_ROOT, "*\\shell\\CompressToZsta");
RegDeleteTreeA(HKEY_CLASSES_ROOT, "Directory\\shell\\CompressToZsta");
RegDeleteTreeA(HKEY_CLASSES_ROOT, ".zsta");
RegDeleteTreeA(HKEY_CLASSES_ROOT, "ZstaArchive");
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
return true;
}
std::string RemoveTrailingSlash(const std::string& path) {
std::string p = path;
while (!p.empty() && (p.back() == '/' || p.back() == '\\')) {
p.pop_back();
}
return p;
}
std::string RemoveZstaExtension(const std::string& path) {
if (path.size() >= 5) {
std::string ext = path.substr(path.size() - 5);
for (char& c : ext) c = tolower(c);
if (ext == ".zsta") {
return path.substr(0, path.size() - 5);
}
}
return path + "_extract";
}
CMy2015RemoteApp::CMy2015RemoteApp()
{
// 支持重新启动管理器
@@ -237,8 +323,65 @@ BOOL LaunchAsAdmin(const char* szFilePath, const char* verb)
return ShellExecuteExA(&shExecInfo);
}
BOOL CMy2015RemoteApp::ProcessZstaCmd() {
// 检查是否已注册右键菜单
char exePath[MAX_PATH];
GetModuleFileNameA(NULL, exePath, MAX_PATH);
// 检查当前注册的路径是否是自己
HKEY hKey;
bool needRegister = false;
if (RegOpenKeyExA(HKEY_CLASSES_ROOT, "ZstaArchive\\shell\\extract\\command",
0, KEY_READ, &hKey) == ERROR_SUCCESS) {
char regPath[MAX_PATH * 2] = { 0 };
DWORD size = sizeof(regPath);
RegQueryValueExA(hKey, NULL, NULL, NULL, (BYTE*)regPath, &size);
RegCloseKey(hKey);
// 检查注册的路径是否包含当前程序路径
if (strstr(regPath, exePath) == NULL) {
needRegister = true; // 路径不同,需要重新注册
}
}
else {
needRegister = true; // 未注册
}
if (needRegister) {
RegisterZstaMenu(exePath);
}
// 处理自定义压缩和解压命令
if (__argc >= 3) {
std::string cmd = __argv[1];
std::string path = __argv[2];
// 压缩
if (cmd == "-c") {
std::string src = RemoveTrailingSlash(path);
std::string dst = src + ".zsta";
auto b = (zsta::CZstdArchive::Compress(src, dst) == zsta::Error::Success);
Mprintf("压缩%s: %s -> %s\n", b ? "成功" : "失败", src.c_str(), dst.c_str());
return FALSE;
}
// 解压
if (cmd == "-x") {
std::string dst = RemoveZstaExtension(path);
auto b = (zsta::CZstdArchive::Extract(path, dst) == zsta::Error::Success);
Mprintf("解压%s: %s -> %s\n", b ? "成功" : "失败", path.c_str(), dst.c_str());
return FALSE;
}
}
return TRUE;
}
BOOL CMy2015RemoteApp::InitInstance()
{
if (!ProcessZstaCmd()) {
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
return FALSE;
}
#if _DEBUG
BOOL runNormal = TRUE;
#else

View File

@@ -85,6 +85,7 @@ public:
{
m_pSplash = pSplash;
}
BOOL ProcessZstaCmd();
virtual BOOL InitInstance();

Binary file not shown.

View File

@@ -6,6 +6,8 @@
#include "FileManagerDlg.h"
#include "FileTransferModeDlg.h"
#include "InputDlg.h"
#include "ZstdArchive.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
@@ -130,6 +132,8 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
ON_NOTIFY(NM_RCLICK, IDC_LIST_REMOTE, OnRclickListRemote)
ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)
//}}AFX_MSG_MAP
ON_COMMAND(ID_FILEMANGER_COMPRESS, &CFileManagerDlg::OnFilemangerCompress)
ON_COMMAND(ID_FILEMANGER_UNCOMPRESS, &CFileManagerDlg::OnFilemangerUncompress)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
@@ -1330,6 +1334,78 @@ void CFileManagerDlg::OnLocalCopy()
SendUploadJob();
}
void CFileManagerDlg::OnLocalCompress()
{
std::vector<std::string> paths;
POSITION pos = m_list_local.GetFirstSelectedItemPosition();
while (pos) {
int nItem = m_list_local.GetNextSelectedItem(pos);
CString file = m_Local_Path + m_list_local.GetItemText(nItem, 0);
// 如果是目录
if (m_list_local.GetItemData(nItem)) {
file += '\\';
}
paths.push_back(file.GetBuffer(0));
}
std::string target = m_Local_Path.GetString() + ToPekingDateTime(0) + ".zsta";
zsta::Error err = zsta::CZstdArchive::Compress(paths, target);
if (err != zsta::Error::Success) {
::MessageBoxA(m_hWnd, "压缩失败: " + CString(zsta::CZstdArchive::GetErrorString(err)),
"错误", MB_OK | MB_ICONERROR);
}
else {
FixedLocalFileList(".");
}
}
bool HasZstaExtension(const std::string& path) {
if (path.size() < 5) return false;
std::string ext = path.substr(path.size() - 5);
// 转小写比较
for (char& c : ext) c = tolower(c);
return ext == ".zsta";
}
std::string GetExtractDir(const std::string& archivePath) {
if (archivePath.size() >= 5) {
std::string ext = archivePath.substr(archivePath.size() - 5);
for (char& c : ext) c = tolower(c);
if (ext == ".zsta") {
return archivePath.substr(0, archivePath.size() - 5);
}
}
return archivePath + "_extract";
}
void CFileManagerDlg::OnLocalUnCompress()
{
BOOL needRefresh = FALSE;
POSITION pos = m_list_local.GetFirstSelectedItemPosition();
while (pos) {
int nItem = m_list_local.GetNextSelectedItem(pos);
CString file = m_Local_Path + m_list_local.GetItemText(nItem, 0);
// 如果是目录
if (m_list_local.GetItemData(nItem)) {
continue;
}
else if (HasZstaExtension(file.GetString())){
std::string path(file.GetBuffer(0));
std::string destDir = GetExtractDir(path);
zsta::Error err = zsta::CZstdArchive::Extract(path, destDir);
if (err != zsta::Error::Success) {
::MessageBoxA(m_hWnd, "解压失败: " + CString(zsta::CZstdArchive::GetErrorString(err)),
"错误", MB_OK | MB_ICONERROR);
}
else {
needRefresh = TRUE;
}
}
}
if (needRefresh) {
FixedLocalFileList(".");
}
}
//////////////// 文件传输操作 ////////////////
// 只管发出了下载的文件
// 一个一个发,接收到下载完成时,下载第二个文件 ...
@@ -1360,6 +1436,66 @@ void CFileManagerDlg::OnRemoteCopy()
SendDownloadJob();
}
std::vector<char> BuildMultiStringPath(const std::vector<std::string>& paths);
void CFileManagerDlg::OnRemoteCompress()
{
std::vector<std::string> paths;
std::string target = m_Remote_Path.GetString() + ToPekingDateTime(0) + ".zsta";
paths.push_back(target);
POSITION pos = m_list_remote.GetFirstSelectedItemPosition();
while (pos) {
int nItem = m_list_remote.GetNextSelectedItem(pos);
CString file = m_Remote_Path + m_list_remote.GetItemText(nItem, 0);
// 如果是目录
if (m_list_remote.GetItemData(nItem)) {
file += '\\';
}
paths.push_back(file.GetBuffer(0));
}
if (paths.size() <= 1) {
::MessageBoxA(m_hWnd, "请先选择要压缩的文件或文件夹!", "提示", MB_OK | MB_ICONWARNING);
return;
}
auto pathsMultiString = BuildMultiStringPath(paths);
BYTE* bPacket = (BYTE*)LocalAlloc(LPTR, pathsMultiString.size() + 1);
if (bPacket) {
memcpy(bPacket + 1, pathsMultiString.data(), pathsMultiString.size());
bPacket[0] = CMD_COMPRESS_FILES;
m_ContextObject->Send2Client(bPacket, (DWORD)(pathsMultiString.size() + 1));
LocalFree(bPacket);
}
}
void CFileManagerDlg::OnRemoteUnCompress()
{
std::vector<std::string> paths;
POSITION pos = m_list_remote.GetFirstSelectedItemPosition();
while (pos) {
int nItem = m_list_remote.GetNextSelectedItem(pos);
CString file = m_Remote_Path + m_list_remote.GetItemText(nItem, 0);
// 如果是目录
if (m_list_remote.GetItemData(nItem)) {
continue;
}
else if (HasZstaExtension(file.GetString())) {
paths.push_back(file.GetBuffer(0));
}
}
if (paths.empty()) {
::MessageBoxA(m_hWnd, "请先选择要解压的.zsta文件!", "提示", MB_OK | MB_ICONWARNING);
return;
}
auto pathsMultiString = BuildMultiStringPath(paths);
BYTE* bPacket = (BYTE*)LocalAlloc(LPTR, pathsMultiString.size() + 1);
if (bPacket) {
memcpy(bPacket + 1, pathsMultiString.data(), pathsMultiString.size());
bPacket[0] = CMD_UNCOMPRESS_FILES;
m_ContextObject->Send2Client(bPacket, (DWORD)(pathsMultiString.size() + 1));
LocalFree(bPacket);
}
}
// 发出一个下载任务
BOOL CFileManagerDlg::SendDownloadJob()
{
@@ -2048,6 +2184,31 @@ void CFileManagerDlg::OnTransfer()
}
}
void CFileManagerDlg::OnFilemangerCompress()
{
POINT pt;
GetCursorPos(&pt);
if (GetFocus()->m_hWnd == m_list_local.m_hWnd) {
OnLocalCompress();
}
else {
OnRemoteCompress();
}
}
void CFileManagerDlg::OnFilemangerUncompress()
{
POINT pt;
GetCursorPos(&pt);
if (GetFocus()->m_hWnd == m_list_local.m_hWnd) {
OnLocalUnCompress();
}
else {
OnRemoteUnCompress();
}
}
void CFileManagerDlg::OnRename()
{
// TODO: Add your command handler code here

View File

@@ -164,6 +164,10 @@ protected:
afx_msg void OnUpdateLocalNewfolder(CCmdUI* pCmdUI);
afx_msg void OnRemoteCopy();
afx_msg void OnLocalCopy();
afx_msg void OnRemoteCompress();
afx_msg void OnLocalCompress();
afx_msg void OnRemoteUnCompress();
afx_msg void OnLocalUnCompress();
afx_msg void OnLocalDelete();
afx_msg void OnRemoteDelete();
afx_msg void OnRemoteStop();
@@ -203,6 +207,9 @@ private:
bool DeleteDirectory(LPCTSTR lpszDirectory);
void EnableControl(BOOL bEnable = TRUE);
float m_fScalingFactor;
public:
afx_msg void OnFilemangerCompress();
afx_msg void OnFilemangerUncompress();
};
//{{AFX_INSERT_LOCATION}}

View File

@@ -28,7 +28,6 @@
#define IDC_CURSOR_DRAG 144
#define IDC_CURSOR_MDRAG 145
#define IDD_DIALOG_TRANSMODE 146
#define IDR_MENU_FILE_OPERATION 147
#define IDD_DIALOG_FILE_COMPRESS 148
#define IDD_DIALOG_TALK 149
#define IDD_DIALOG_SHELL 150
@@ -237,7 +236,6 @@
#define IDR_GH0STTYPE 2003
#define IDD_BUILD 2004
#define IDD_FILE 2005
#define IDR_LIST 2006
#define IDC_DRAG 2007
#define IDC_MUTI_DRAG 2008
#define IDC_CURSOR3 2009
@@ -268,7 +266,6 @@
#define IDI_KEYBOARD 2034
#define IDI_SYSTEM 2035
#define IDC_DOT 2036
#define IDR_MINIMIZE 2037
#define IDD_AUDIO 2038
#define IDI_AUDIO 2039
#define IDC_LIST_REMOTE 2040
@@ -316,9 +313,6 @@
#define ID_LOCAL_TOOLBAR 2082
#define ID_REMOTE_TOOLBAR 2083
#define ID_APP_PWD 2084
#define IDM_FILEMANAGER 2085
#define IDM_SHUTDOWN 2086
#define IDM_REBOOT 2087
#define IDM_TRANSFER 2088
#define IDM_RENAME 2089
#define IDM_DELETE 2090
@@ -344,32 +338,13 @@
#define IDM_REMOTE_SMALLICON 2110
#define IDM_REMOTE_LIST 2111
#define IDM_REMOTE_REPORT 2112
#define IDM_SCREENSPY 2113
#define IDM_DOWNEXEC 2114
#define IDM_WEBCAM 2115
#define IDM_REMOVE 2116
#define IDM_KEYBOARD 2117
#define IDM_REFRESH 2118
#define IDM_SYSTEM 2119
#define IDM_KILLPROCESS 2120
#define IDM_REFRESHPSLIST 2121
#define IDM_REMOTESHELL 2122
#define IDM_LOGOFF 2123
#define IDM_SELECT_ALL 2124
#define IDM_UNSELECT_ALL 2125
#define IDM_OPEN_URL_SHOW 2126
#define IDM_OPEN_URL_HIDE 2127
#define IDM_CLEANEVENT 2128
#define IDM_HIDE 2129
#define IDM_SHOW 2130
#define IDM_EXIT 2131
#define IDM_RENAME_REMARK 2132
#define IDM_UPDATE_SERVER 2133
#define IDM_LOCAL_OPEN 2134
#define IDM_REMOTE_OPEN_SHOW 2135
#define IDM_REMOTE_OPEN_HIDE 2136
#define IDM_AUDIO_LISTEN 2137
#define IDM_DISCONNECT 2138
#define ID_STAUTSTIP 2139
#define ID_STAUTSSPEED 2140
#define ID_STAUTSPORT 2141
@@ -477,12 +452,6 @@
#define ID_VIEW_BIG_ICON 32806
#define ID_VIEW_SMALL_ICON 32807
#define ID_VIEW_DETAIL 32808
#define ID_OPERATION_RENAME 32813
#define ID_OPERATION_CLIENT_SHOW_RUN 32819
#define ID_OPERATION_CLIENT_HIDE_RUN 32820
#define ID_OPERATION_SERVER_RUN 32821
#define ID_OPERATION_COMPRESS 32823
#define ID_OPERATION_DECOMPRESS 32825
#define ID_PLIST_KILL 32828
#define ID_PLIST_REFRESH 32829
#define ID_WLIST_REFRESH 32836
@@ -628,6 +597,10 @@
#define ID_HOOK_WIN 32986
#define ID_32987 32987
#define ID_RUNAS_SERVICE 32988
#define ID_FILEMANGER_32989 32989
#define ID_FILEMANGER_COMPRESS 32990
#define ID_FILEMANGER_32991 32991
#define ID_FILEMANGER_UNCOMPRESS 32992
#define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects
@@ -635,7 +608,7 @@
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 320
#define _APS_NEXT_COMMAND_VALUE 32989
#define _APS_NEXT_COMMAND_VALUE 32993
#define _APS_NEXT_CONTROL_VALUE 2213
#define _APS_NEXT_SYMED_VALUE 101
#endif