From 49373a1137f44293d50f71bb13f025c781c77adb Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 4 Jan 2026 12:05:28 +0100 Subject: [PATCH] Feature: Support compress files in file management dialog --- client/FileManager.cpp | 71 +++ common/ZstdArchive.h | 757 +++++++++++++++++++++++++++ common/commands.h | 2 + server/2015Remote/2015Remote.cpp | 143 +++++ server/2015Remote/2015Remote.h | 1 + server/2015Remote/2015Remote.rc | Bin 109382 -> 104972 bytes server/2015Remote/FileManagerDlg.cpp | 161 ++++++ server/2015Remote/FileManagerDlg.h | 7 + server/2015Remote/resource.h | 37 +- 9 files changed, 1147 insertions(+), 32 deletions(-) create mode 100644 common/ZstdArchive.h diff --git a/client/FileManager.cpp b/client/FileManager.cpp index 635cef0..3ea0090 100644 --- a/client/FileManager.cpp +++ b/client/FileManager.cpp @@ -4,6 +4,7 @@ #include "FileManager.h" #include +#include "ZstdArchive.h" typedef struct { DWORD dwSizeHigh; @@ -26,9 +27,79 @@ CFileManager::~CFileManager() m_UploadList.clear(); } +std::vector ParseMultiStringPath(const char* buffer, size_t size) +{ + std::vector 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 paths = ParseMultiStringPath((char*)lpBuffer + 1, nSize - 1); + zsta::Error err = zsta::CZstdArchive::Compress(std::vector(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 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; diff --git a/common/ZstdArchive.h b/common/ZstdArchive.h new file mode 100644 index 0000000..270a7b2 --- /dev/null +++ b/common/ZstdArchive.h @@ -0,0 +1,757 @@ +/** + * @file ZstdArchive.h + * @brief ZSTA 归档格式 - 基于 Zstd 的轻量级压缩模块 + * @version 1.0 + * + * 特性: + * - 支持单个或多个文件、文件夹压缩 + * - 保留目录结构 + * - UTF-8 路径支持 + * - 固定大小头部和条目表,便于快速索引 + * - 版本控制,支持向后兼容 + */ + +#ifndef ZSTD_ARCHIVE_H +#define ZSTD_ARCHIVE_H + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#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& srcPaths, + const std::string& outPath, + int level = 3) { + if (srcPaths.empty()) { + return Error::EmptyInput; + } + + // 收集所有文件 + std::vector 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(files.size()); + header.compressLevel = level; + header.createTime = static_cast(time(nullptr)); + + if (fwrite(&header, sizeof(header), 1, fOut) != 1) { + fclose(fOut); + return Error::FileWriteFailed; + } + + // 预留条目表空间 + long entryTablePos = ftell(fOut); + std::vector 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(EntryType::Directory) + : static_cast(EntryType::File); + e.modifyTime = f.modTime; + + if (f.isDir) { + e.method = static_cast(CompressMethod::Store); + e.originalSize = 0; + e.compressedSize = 0; + } else { + FILE* fIn = fopen(f.fullPath.c_str(), "rb"); + if (!fIn) { + e.method = static_cast(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 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 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(CompressMethod::Store); + e.compressedSize = e.originalSize; + fwrite(inBuf.data(), 1, e.originalSize, fOut); + } else { + e.method = static_cast(CompressMethod::Zstd); + e.compressedSize = compSize; + fwrite(outBuf.data(), 1, compSize, fOut); + } + } else { + e.method = static_cast(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{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 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(EntryType::Directory)) { + CreateDirectoryRecursive(fullPath); + } else { + // 创建父目录 + std::string parent = GetParentPath(fullPath); + if (!parent.empty()) { + CreateDirectoryRecursive(parent); + } + + if (e.compressedSize > 0) { + std::vector compBuf(e.compressedSize); + if (fread(compBuf.data(), 1, e.compressedSize, fIn) != e.compressedSize) { + result = Error::FileReadFailed; + break; + } + + std::vector origBuf(e.originalSize); + + if (e.method == static_cast(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 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& 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 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(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(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& 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(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 diff --git a/common/commands.h b/common/commands.h index ef5cea7..b90cc77 100644 --- a/common/commands.h +++ b/common/commands.h @@ -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, // 要求验证 diff --git a/server/2015Remote/2015Remote.cpp b/server/2015Remote/2015Remote.cpp index 010b832..32cae9d 100644 --- a/server/2015Remote/2015Remote.cpp +++ b/server/2015Remote/2015Remote.cpp @@ -101,6 +101,92 @@ std::string GetPwdHash(); // CMy2015RemoteApp 构造 +// 自定义压缩文件 +#include +#include +#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 diff --git a/server/2015Remote/2015Remote.h b/server/2015Remote/2015Remote.h index ed78adc..057da0d 100644 --- a/server/2015Remote/2015Remote.h +++ b/server/2015Remote/2015Remote.h @@ -85,6 +85,7 @@ public: { m_pSplash = pSplash; } + BOOL ProcessZstaCmd(); virtual BOOL InitInstance(); diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 82180e34fab734d602adfed6a89e9fdbb7dac44c..547d07190f2e1e6c237590b85c7f227c47815c1b 100644 GIT binary patch delta 395 zcmX?hjjd-3+XgmnR(pmO3@avcavK^hWLVCykl_V`J&=3@B!S{UQkS6y$X^a*fgnW9 zOEBxr>&)U3=s?=lRv&U1`5he4~t^t+br-uf@eF21fv8a*Q>@^(Vl+Y!J75c4MZ7@rU&RS zvI)B}cry3^HTp6*GWapL16e_n3qysc`vfxbOy7{gCqFM<`dg{H=bpBo`cbdElQ!3Qx6?Vd@^`4Uv{UasEYZp*z8RT~ zM|%YKj6w3Pj!x>Yz)Z?_Vy5N0gzfa_=kA`S$|@|@8=Vi*R1Vr{bQ@Z%5ol$)00d}g zI~Lo|{BUkPp*F>3i@-sS)yW_>Xjr6Ozi@H#_JHjbiPvRdy4l;rxp~$xcjCX(S0kmJ zg6US6EDuD%2Yv`a4~O#`oO<%Uo2HzY=Ex0wA776gfg=6d&u*Hnz*_6_QJ4^#0C~!| zKUdSyWD79MetufGYeO!qvVL)~Ld6RI3$mm%F(5>^hY!`2VVycjZ|uQ(y1D}kXsH?> zvYv{ZO$d2Zox9{lJ9bgWBj~2V0?ais8|ircx%a|;L^t(}7fxvx=HM}Yox;ZA)j%c+KMs_(CT)o8#L(0e5e zmB6Z8X^EC{{KZSRV_z#d8M1SEd0@?)t<=>ep=j5`=ymLd)&704)Pu`MB_D3R=y%+W zrmUbSfq^|rt!?}j7HNulK8iuljl>6s<%rQVO;jPu4sn1WIhwFcCRD3g$A9TN(y=@) zP>*8Ih%l+-;$=bhFs~WzaHzhIQL~5)|g8ND!UgFG+2yv zd;dety^u=7YC2)n_UQW6N;fSe0%KIU7uzY#g%&zljI|Y8QOKD6OcPYimBk=pmKjE= zcRyAu?LDxT#Q$JuWi}u4DF&4etE^Pb))bEvou|gfu#jHh4Mht((d5|NuPYipX}WbG z9@zxU+63eP9EPrGe3NdZWBz~q(xEXscuSN;Jr{E<`u diff --git a/server/2015Remote/FileManagerDlg.cpp b/server/2015Remote/FileManagerDlg.cpp index 14afc23..f093634 100644 --- a/server/2015Remote/FileManagerDlg.cpp +++ b/server/2015Remote/FileManagerDlg.cpp @@ -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 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 BuildMultiStringPath(const std::vector& paths); + +void CFileManagerDlg::OnRemoteCompress() +{ + std::vector 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 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 diff --git a/server/2015Remote/FileManagerDlg.h b/server/2015Remote/FileManagerDlg.h index fd2bd99..eea4ebf 100644 --- a/server/2015Remote/FileManagerDlg.h +++ b/server/2015Remote/FileManagerDlg.h @@ -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}} diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index f6ced72..1a87688 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -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