diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bc47b3b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,154 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build: + name: Build-${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + bin: coldatafresh + archive: tar.gz + cross: false + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + bin: coldatafresh + archive: tar.gz + cross: true + - os: ubuntu-latest + target: i686-unknown-linux-musl + bin: coldatafresh + archive: tar.gz + cross: true + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + bin: coldatafresh + archive: tar.gz + cross: true + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + bin: coldatafresh + archive: tar.gz + cross: true + - os: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + bin: coldatafresh + archive: tar.gz + cross: true + - os: ubuntu-latest + target: arm-unknown-linux-gnueabihf + bin: coldatafresh + archive: tar.gz + cross: true + - os: macos-latest + target: x86_64-apple-darwin + bin: coldatafresh + archive: tar.gz + cross: false + - os: macos-latest + target: aarch64-apple-darwin + bin: coldatafresh + archive: tar.gz + cross: false + - os: windows-latest + target: x86_64-pc-windows-msvc + bin: coldatafresh.exe + archive: zip + cross: false + - os: windows-latest + target: i686-pc-windows-msvc + bin: coldatafresh.exe + archive: zip + cross: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Setup cross (cross-compilation) + if: matrix.cross == true + shell: bash + run: | + curl -fsSL https://github.com/cross-rs/cross/releases/latest/download/cross-x86_64-unknown-linux-musl.tar.gz | tar -xz -C /tmp + mv /tmp/cross /usr/local/bin/cross + cross --version + + - name: Build (native) + if: matrix.cross == false + shell: bash + run: cargo build --release --target ${{ matrix.target }} + + - name: Build (cross) + if: matrix.cross == true + shell: bash + run: cross build --release --target ${{ matrix.target }} + + - name: Prepare artifact directory + shell: bash + run: | + mkdir -p dist + cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" dist/ + if [ -f README_EN.md ]; then cp README_EN.md dist/; fi + if [ -f LICENSE ]; then cp LICENSE dist/; fi + + - name: Create tar.gz archive + if: matrix.archive == 'tar.gz' + shell: bash + run: | + cd dist + tar -czf "../coldatafresh-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" . + cd .. + ls -la "coldatafresh-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" + + - name: Create zip archive + if: matrix.archive == 'zip' + shell: pwsh + run: | + Compress-Archive -Path "dist/*" -DestinationPath "coldatafresh-${{ github.ref_name }}-${{ matrix.target }}.zip" + Get-Item "coldatafresh-${{ github.ref_name }}-${{ matrix.target }}.zip" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: coldatafresh-${{ github.ref_name }}-${{ matrix.target }} + path: coldatafresh-${{ github.ref_name }}-${{ matrix.target }}.* + compression-level: 0 + retention-days: 7 + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display artifacts + run: ls -R artifacts/ + + - name: Create Release and upload assets + uses: softprops/action-gh-release@v2 + with: + files: 'artifacts/**/*' + generate_release_notes: true + prerelease: false + fail_on_unmatched_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e381e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,226 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +.codebuddy +# Logs +*.log +logs/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Environment variables +.env +.venv + +# PyInstaller +*.spec + +# Jupyter Notebook +.ipynb_checkpoints + +# PyCharm +.idea/ + +# VS Code +.vscode/ + +# pytest +.pytest_cache/ +.coverage +htmlcov/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +bindd/ +bin/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ + +# VS Code +.vscode/ +*.code-workspace +# Windows +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +build/ +bin/ +dist/ +test_data/ +# Python +*.egg-info/ +*.egg/ +*.pyc + +*.pyo +*.pyd +*.spec +# End of https://www.toptal.com/developers/gitignore/api/python +.venv/ +build/ + +.trae/ +coldatafresh/ + +target/ +# Runtime data +refresh_log.json +error.log \ No newline at end of file diff --git "a/.trae/documents/\344\277\256\345\244\215\345\205\250\347\233\230\345\210\267\346\226\260\345\212\237\350\203\275\347\232\204\344\270\232\345\212\241\346\265\201\347\250\213.md" "b/.trae/documents/\344\277\256\345\244\215\345\205\250\347\233\230\345\210\267\346\226\260\345\212\237\350\203\275\347\232\204\344\270\232\345\212\241\346\265\201\347\250\213.md" new file mode 100644 index 0000000..380d531 --- /dev/null +++ "b/.trae/documents/\344\277\256\345\244\215\345\205\250\347\233\230\345\210\267\346\226\260\345\212\237\350\203\275\347\232\204\344\270\232\345\212\241\346\265\201\347\250\213.md" @@ -0,0 +1,78 @@ +## 问题分析 + +当前的全盘刷新功能存在以下问题: +1. 没有按照用户期望的业务流程执行 +2. 没有询问用户是否保留已使用空间中的文件 +3. 没有实现文件的临时备份和恢复功能 +4. 没有实现填满可用空间的逻辑 +5. 没有返回累积填入容量和最大写入速度 + +## 解决方案 + +根据用户描述的业务流程,我将重新设计和实现全盘刷新功能: + +### 1. 重新设计业务流程 + +``` +1. 获取指定目录的总容量和已使用容量 +2. 询问用户是否保留已使用空间中的文件 +3. 如果保留: + a. 将已使用空间中的文件复制到系统临时目录 d:\$aspnmytools + b. 删除指定目录下的原文件 +4. 在指定目录下创建工作目录 $aspnmytools +5. 写入指定单位大小的数据文件(如50GB/个),直到填满可用空间 +6. 记录累积填入容量和最大写入速度 +7. 删除工作目录 $aspnmytools +8. 如果保留原文件: + a. 将临时目录中的文件复制回原位置 + b. 删除临时目录 d:\$aspnmytools +9. 完成全盘刷新 +``` + +### 2. 修改代码结构 + +- **新增函数**: + - `get_directory_stats(directory)` - 获取目录的总容量和已使用容量 + - `backup_files(source_dir, backup_dir)` - 备份文件到临时目录 + - `restore_files(backup_dir, target_dir)` - 从临时目录恢复文件 + - `fill_available_space(directory, unit_size)` - 填满可用空间 + - `calculate_max_files(directory, unit_size)` - 计算需要创建的文件数量 + +- **修改现有函数**: + - `full_refresh_file` - 重命名或重构,专注于单个文件的刷新 + - `execute` - 修改主执行逻辑,添加全盘刷新的新流程 + +### 3. 实现关键功能 + +- **空间计算**:使用 `get_disk_space` 函数获取磁盘空间信息 +- **文件复制**:使用 `shutil.copy2` 或类似函数实现文件的完整复制 +- **文件写入**:使用现有的 `continuous_full_refresh_file` 函数写入数据 +- **进度显示**:使用现有的 `Dashboard` 类显示进度 +- **统计信息**:记录写入速度和总容量 + +### 4. 修改用户交互 + +- 在主菜单中添加全盘刷新选项 +- 询问用户是否保留已使用空间中的文件 +- 询问用户写入单位大小 +- 显示空间信息和操作确认 + +## 预期效果 + +1. 全盘刷新功能按照用户期望的业务流程执行 +2. 用户可以选择是否保留已使用空间中的文件 +3. 实现了文件的临时备份和恢复功能 +4. 实现了填满可用空间的逻辑 +5. 返回累积填入容量和最大写入速度 +6. 提高了固态硬盘的刷新效果 + +## 实施步骤 + +1. 分析当前代码结构和功能 +2. 设计新的全盘刷新业务流程 +3. 实现新的函数和修改现有函数 +4. 添加用户交互逻辑 +5. 测试新功能 +6. 优化和调试 + +这个修改计划将确保全盘刷新功能按照用户期望的业务流程执行,提高固态硬盘的刷新效果。 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2537f80 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,876 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "coldatafresh" +version = "5.0.0" +dependencies = [ + "chrono", + "clap", + "crc32fast", + "ctrlc", + "env_logger", + "filetime", + "libc", + "log", + "rayon", + "regex-lite", + "serde_json", + "walkdir", + "windows-sys 0.59.0", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d24210d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "coldatafresh" +version = "5.0.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +crc32fast = "1.4" +ctrlc = "3.4" +rayon = "1.10" +walkdir = "2.5" +chrono = "0.4" +regex-lite = "0.1" +log = "0.4" +env_logger = "0.11" +filetime = "0.2" +serde_json = "1.0" +windows-sys = { version = "0.59", features = [ + "Win32_Storage_FileSystem", + "Win32_System_Console", + "Win32_Foundation", +]} + +[target.'cfg(unix)'.dependencies] +libc = "0.2" \ No newline at end of file diff --git a/README.md b/README.md index 1ffa507..8ba46e7 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,134 @@ -# ColDataRefresh -智能检测固态硬盘的冷数据并解决冷数据掉速问题,带数据校验功能 -[English](/README_EN.md) +# ColDataRefresh — SSD 冷数据维护系统 v5.0 -### 什么是冷数据 -冷数据指存放在硬盘上且较长时间(如两年甚至更长)没有进行重新写入或者更新的数据,直观上以文件为单位,实际上反应到物理层面是文件对应的存储单元。通常,长时间存储在硬盘上的文档、视频、音乐、图片等静态资料都是冷数据,甚至操作系统、程序和游戏在较长一段时间内只读取过却没有修改或者更新的任何文件都会在未来“成长为”冷数据(热更新或者增量更新现在已经很成熟,一般来讲系统、游戏、应用的更新只会更新需要修改的部分,不需要修改的部分不会动)。 -**注意,冷数据的形成只和写入有关,与读取频次无关,一个文件即使经常读取但是不修改写入,一样有可能成为冷数据**(这也是有人反应经常玩的游戏因为冷数据掉速导致加载变慢的原因)。 +[English](README_EN.md) -### 冷数据会造成什么问题 -固态硬盘上的冷数据可能会造成读取速度变慢的情况,极端情况下甚至会无法读取。 +智能检测固态硬盘(SSD)的冷数据,解决 NAND 颗粒电荷泄漏导致的读取掉速问题。使用 Rust 重写,兼顾高性能与数据安全。 -> 像三星的大部分固态硬盘固件会在空闲期间给冷数据进行移动以“加温”,但是部分厂商的固件不具备此功能。这是本工具开发的原因。 -> 注意:固态硬盘的Trim功能/硬盘碎片整理并不能缓解硬盘冷数据读取变慢的现象。 +## 功能模式 -### 怎么判断/解决我的硬盘出现冷数据读取掉速问题 +### 模式 1:冷数据维护(智能模式) +扫描指定目录中超过设定天数(默认 365 天)未修改的文件。每个文件:读取 → CRC32 校验 → 原地重写 → 读回验证。物理刷新 NAND 单元,恢复电荷水平,解决掉速问题。 -最简单的,找一个躺在你硬盘上很久(如两年以上)且没有修改过的文件,把它复制到其他硬盘上,观察复制速度是否下降了? -将该文件复制回来,该问题就解决了(因为文件变成“新”写入的了,不再是冷数据了) +**安全无风险 — 不会丢失数据。** -你也可以使用本工具,本工具会自动判断你的文件是否属于冷数据。 +### 模式 2:全盘刷新 +完整的 NAND 单元级刷新流程: +1. **备份** — 所有文件自动备份到另一块硬盘(自动检测,优先 D:) +2. **删除** — 删除目标目录中的原始文件释放空间 +3. **覆写** — 用 0xFF 模式覆写已释放空间,全面刷新 NAND 单元 +4. **清理** — 删除临时填充文件 +5. **恢复** — 从备份恢复数据,**时间戳自动设为当前时间**(文件变为"新"文件) +6. **TRIM** — 执行最终 TRIM 优化 -### 本工具的特点/与 `DiskFresh`等工具的区别 +> ⚠️ 可选择不保留文件,全盘刷新后数据不可恢复。 -1. `DiskFresh`也是为处理冷数据而设计的,但是DiskFresh是基于更加底层的磁盘`Sector(扇区)`层面进行全面的覆写。其优点是更为彻底,缺点是刷新时间长,会刷新不必要的非冷数据区块,可能会缩减硬盘寿命,如果需要全盘刷新,`DiskFresh`会是更好的选择;**本工具基于文件系统层面,仅重构检测到的冷数据,可以跳过不必要的文件,并且带有CRC快速校验保证文件安全,尤其适合只需要刷新部分文件(夹)中的冷数据的情形,安全快速,最大限度取得硬盘寿命消耗和性能的平衡点。** -2. 本工具支持保存文件刷新进度,你可以随时退出并在下次继续数据刷新的操作 -3. 本工具开源。 +### 模式 3:实时 TRIM +跳过系统空闲调度策略,直接向 SSD 发送 TRIM 指令,立即释放已标记删除的空间。日常维护建议每 3 个月执行一次。 -### 如何使用 +## 快速使用 -> **请右键程序 - `以管理员身份运行`**,这是必要的,可以不授予权限,但特定文件可能会访问或者覆写失败。 +```bash +# 交互式菜单(无参数) +coldatafresh -1. Releases界面有编译的exe二进制文件,下载双击运行 / 你也可以从python源代码运行(你可以在代码里修改更多的配置) -2. 输入你需要扫描冷数据的目录,如`D:\DL`或者整个硬盘`D:\`(Windows用户可以选中文件夹按`Ctrl+Shift+C`复制目录地址),按下回车 -3. 输入冷数据天数,如`300`,程序会扫描最后一次修改大于300天的文件。(输入0将会扫描目录下所有文件),按下回车程序即可运行。 -4. **重要:如果运行中需要退出程序,请先在控制台按下`Ctrl+C`发送终止命令,否则可能会造成数据丢失!** +# 智能模式:刷新 180 天以上未修改的文件 +coldatafresh -p "D:\Data" -a 180 -### 程序截图 Screenshots -![projectimage](./projectimage.png) +# 全盘刷新模式 +coldatafresh -f -p "D:\Data" + +# 仅执行 TRIM +coldatafresh -t -p "D:\Data" + +# 详细日志输出 +coldatafresh -v -p "D:\Data" -a 365 +``` + +### 命令行参数 + +| 参数 | 说明 | +|------|------| +| `-p`, `--path` | 目标目录(默认当前目录) | +| `-a`, `--age` | 文件年龄阈值(天) | +| `-f`, `--full-refresh` | 全盘刷新模式 | +| `-t`, `--trim` | TRIM 优化模式 | +| `-v`, `--verbose` | 启用详细日志 | +| `-s`, `--skip-smaller` | 跳过小于 N MB 的文件 | + +## 安装 + +### 源码编译 +```bash +git clone https://github.com/aspnmy/ColDataRefresh.git +cd ColDataRefresh +cargo build --release +./target/release/coldatafresh +``` + +需要 Rust 2021 Edition 或更高版本。 + +### 预编译二进制 +从 [Releases](https://github.com/aspnmy/ColDataRefresh/releases) 页面下载最新版本。 + +## CI/CD 自动发布 + +项目使用 GitHub Actions 实现跨平台自动发布。 + +触发发布: +```bash +git checkout v5.0 +git tag v5.0.0 +git push origin v5.0.0 +``` + +构建矩阵(11 个目标平台): + +| 平台 | 目标三元组 | 运行时库 | CPU 架构 | 适用场景 | +|------|-----------|---------|---------|---------| +| Linux | `x86_64-unknown-linux-gnu` | glibc | x86_64 (64位) | 桌面/服务器主流 | +| Linux | `x86_64-unknown-linux-musl` | musl | x86_64 (64位) | Alpine/Docker 静态编译 | +| Linux | `i686-unknown-linux-musl` | musl | i686 (32位) | 旧硬件/嵌入式 | +| Linux | `aarch64-unknown-linux-gnu` | glibc | ARMv8 (64位) | 树莓派/ARM服务器 | +| Linux | `aarch64-unknown-linux-musl` | musl | ARMv8 (64位) | ARM Alpine/Docker | +| Linux | `armv7-unknown-linux-gnueabihf` | glibc | ARMv7 (32位) | 树莓派3及以下 | +| Linux | `arm-unknown-linux-gnueabihf` | glibc | ARMv6 (32位) | 树莓派Zero/旧ARM | +| macOS | `x86_64-apple-darwin` | — | Intel Mac | MacBook Pro/Air (Intel) | +| macOS | `aarch64-apple-darwin` | — | Apple Silicon | MacBook Pro/Air (M芯片) | +| Windows | `x86_64-pc-windows-msvc` | MSVC | x86_64 (64位) | Win10/11 主流 | +| Windows | `i686-pc-windows-msvc` | MSVC | i686 (32位) | Win10/11 32位兼容 | + +> **glibc vs musl 说明:** glibc 版本性能更优,适合桌面/服务器环境;musl 版本静态链接,不依赖系统运行时,适合 Docker/Alpine 容器。ARM 版本覆盖树莓派全系列(Zero 到 5)。 + +## 系统支持 + +| 平台 | 支持情况 | +|------|---------| +| Windows 10/11 | ✅ 完整支持(NTFS, ReFS) | +| Linux | ✅ 完整支持(ext4, XFS, Btrfs) | + +## 技术特性 + +- **语言**:Rust 2021 Edition,零成本抽象 +- **并发**:Rayon 无锁并行,多文件并发处理 +- **数据完整性**:每次写入前后均做 CRC32 校验 +- **日志系统**:操作日志、错误日志、文件损坏报告集中管理 +- **信号处理**:Ctrl+C 优雅退出,记录已处理文件 +- **零运行时依赖** — 单文件静态编译 + +## 更新日志 + +### v5.0.0 — Rust 完全重写 +- 从 Python 完全迁移到 Rust +- 线程安全架构(`OnceLock` + `Mutex`,`static mut` 全部消除) +- 全盘刷新完整流程:备份 → 删除 → 覆写 → 恢复(刷新时间戳)→ TRIM +- 命令行参数支持脚本化调用 +- 实时进度仪表盘 +- 跨平台:Windows + Linux + +## 许可证 + +MIT License — 详见 [LICENSE](LICENSE)。 + +## 作者 + +**aspnmy** — [博客](https://aspnmy.blog.csdn.net/) diff --git a/README_EN.md b/README_EN.md index f58aef0..e61f7aa 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,37 +1,134 @@ -# ColDataRefresh -Intelligently detects cold data on SSD and solves the cold data crash problem with data validation. +# ColDataRefresh — SSD Cold Data Maintenance Tool v5.0 -### What is Cold Data -Cold data refers to data that has been stored on the hard drive for a long time (e.g., half a year or even longer) and has not been rewritten or updated, which is intuitively expressed in terms of files, but in reality is reflected in the physical level of the corresponding storage unit of the file. Usually, documents, videos, music, pictures and other static data stored on the hard drive for a long time are cold data, and even any files that have been read by the operating system, programmes and games over a long period of time without modification or update will ‘grow’ to be cold data in the future (hot or incremental updates are already very mature nowadays, but they can be used for a long time). Generally speaking, updates to systems, games, and applications will only update the parts that need to be changed, and leave the parts that don't need to be changed untouched). -**Note that the formation of cold data is only related to writing, not reading, even if a file is read frequently, but not modified to write, it is possible to become cold data** (this is also the reason why some people react to the slow loading of the games that they often play because of the cold data falling speed). +[中文](README.md) -### What problems can cold data cause -Cold data on an SSD can cause slow read speeds, and in extreme cases, even unreadable. +Intelligently detects cold data on SSDs and prevents read slowdown caused by charge leakage on NAND cells. Written in Rust for maximum performance and reliability. -> Most SSD firmwares, like Samsung's, will move cold data around to ‘warm it up’ during idle periods, but some manufacturers' firmwares do not have this feature. This is why this tool was developed. -> Note: The Trim function/defragmentation of SSDs does not alleviate the slowdown of cold data reading. +## Features -### How to determine/resolve the cold data read dropout problem of my hard drive +### Mode 1: Cold Data Refresh (Smart Mode) +Refreshes files that haven't been accessed beyond a configurable age threshold (default 365 days). Each file is read, its data is verified via CRC32, written back in-place with 0xFF then restored, and re-verified. This rewrites the physical NAND cells, restoring their charge level and read performance. -The easiest thing to do is to find a file that has been lying on your hard drive for a long time (e.g. more than two years) and has not been modified, copy it to another hard drive, and observe whether the copying speed has dropped? -Copy the file back, and the problem is solved (because the file becomes ‘newly’ written and is no longer cold data). +**Safe — no data loss.** -You can also use this tool, which will automatically determine if your file is cold or not. +### Mode 2: Full Disk Refresh +A complete NAND cell-level refresh cycle: +1. **Backup** — All files are backed up to another drive (auto-detected, prioritizes D:) +2. **Delete** — Original files are removed to free up space +3. **Overwrite** — The freed space is overwritten with 0xFF pattern for full NAND cell refresh +4. **Cleanup** — Temporary fill files are removed +5. **Restore** — Data is restored from backup with **current timestamps** (files appear "fresh" to the OS) +6. **TRIM** — Final TRIM optimization is executed -### Features of this tool/differences with `DiskFresh` and other tools +> ⚠️ Toggleable backup: choose whether to preserve data. If backup is skipped, restored data cannot be recovered. -1. `DiskFresh` is also designed to deal with cold data, but DiskFresh is based on the more underlying `Sector` level of the disc to do a full overwrite. The disadvantage is that it takes a long time to refresh, and will refresh unnecessary non-cold data blocks, which may reduce the life of the hard disc; **This tool is based on the file system level, and only refreshes the detected cold data, and comes with CRC file checksum, which is safer and faster. **, -2. This tool supports saving the file refresh progress, you can exit at any time and continue the data refresh operation the next time -3. This tool is open source. +### Mode 3: Real-time TRIM +Directly issues TRIM commands to the SSD, bypassing the OS idle-time scheduling. Safe for routine maintenance every 3 months. Irreversibly releases space marked as deleted. -### How to use +## Usage -> **Please right click the programme - `Run as administrator` **, this is necessary, you can not grant permission, but specific files may be accessed or overwrite failed. +```bash +# Interactive menu (no args) +coldatafresh -1. Releases interface has compiled exe binaries, download and double click to run / you can also run from python source code (you can change more configurations in the code) -2. Enter the directory you want to scan for cold data, e.g. `D:\DL` or the whole hard drive `D:\` (Windows users can select the folder and press `Ctrl+Shift+C` to copy the directory address), press enter. -3. Enter the number of days of cold data, e.g. `300`, the programme will scan files that have been last modified more than 300 days ago. (Entering 0 will scan all files in the directory.) Press Enter to run the program. -4. **Important: If you need to exit the programme while it is running, please press `Ctrl+C` on the console first to send the terminate command, otherwise it may cause data loss! **Important. +# Smart mode: refresh files older than 180 days +coldatafresh -p "D:\Data" -a 180 -### Program screenshots Screenshots -! [projectimage](. /projectimage.png) \ No newline at end of file +# Full disk refresh +coldatafresh -f -p "D:\Data" + +# Execute TRIM only +coldatafresh -t -p "D:\Data" + +# Verbose logging +coldatafresh -v -p "D:\Data" -a 365 +``` + +### CLI Options + +| Flag | Description | +|------|-------------| +| `-p`, `--path` | Target directory (default: `.`) | +| `-a`, `--age` | File age threshold in days | +| `-f`, `--full-refresh` | Full disk refresh mode | +| `-t`, `--trim` | TRIM optimization mode | +| `-v`, `--verbose` | Enable detailed logging | +| `-s`, `--skip-smaller` | Skip files smaller than N MB | + +## Installation + +### From Source +```bash +git clone https://github.com/aspnmy/ColDataRefresh.git +cd ColDataRefresh +cargo build --release +./target/release/coldatafresh +``` + +Requires Rust 2021 Edition or later. + +### Pre-built Binaries +Download the latest release from the [Releases](https://github.com/aspnmy/ColDataRefresh/releases) page. + +## CI/CD + +This project uses GitHub Actions for automated cross-platform release builds. + +Trigger a release: +```bash +git checkout v5.0 +git tag v5.0.0 +git push origin v5.0.0 +``` + +Build matrix (11 targets): + +| Platform | Target Triple | Libc | CPU Arch | Use Case | +|----------|--------------|------|----------|----------| +| Linux | `x86_64-unknown-linux-gnu` | glibc | x86_64 (64-bit) | Desktop/Server主流 | +| Linux | `x86_64-unknown-linux-musl` | musl | x86_64 (64-bit) | Alpine/Docker 静态编译 | +| Linux | `i686-unknown-linux-musl` | musl | i686 (32-bit) | 旧硬件/嵌入式 | +| Linux | `aarch64-unknown-linux-gnu` | glibc | ARMv8 (64-bit) | 树莓派/ARM服务器 | +| Linux | `aarch64-unknown-linux-musl` | musl | ARMv8 (64-bit) | ARM Alpine/Docker | +| Linux | `armv7-unknown-linux-gnueabihf` | glibc | ARMv7 (32-bit) | 树莓派3及以下 | +| Linux | `arm-unknown-linux-gnueabihf` | glibc | ARMv6 (32-bit) | 树莓派Zero/旧ARM | +| macOS | `x86_64-apple-darwin` | — | Intel Mac | MacBook Pro/Air (Intel) | +| macOS | `aarch64-apple-darwin` | — | Apple Silicon | MacBook Pro/Air (M芯片) | +| Windows | `x86_64-pc-windows-msvc` | MSVC | x86_64 (64-bit) | Win10/11 主流 | +| Windows | `i686-pc-windows-msvc` | MSVC | i686 (32-bit) | Win10/11 32位兼容 | + +> **glibc vs musl:** glibc 版本性能更优,适合桌面/服务器;musl 版本静态链接,适合 Docker/Alpine 容器环境。ARM 版本覆盖树莓派全系列(Zero~5)。 + +## System Requirements + +| Platform | Support | +|----------|---------| +| Windows 10/11 | ✅ Full support (NTFS, ReFS) | +| Linux | ✅ Full support (ext4, XFS, Btrfs) | + +## Technical Details + +- **Language**: Rust 2021 Edition +- **Concurrency**: Rayon lock-free parallel processing +- **Data Integrity**: CRC32 checksum before and after every write +- **Logging**: Centralized log system with operation, error, and corruption reports +- **Signal Handling**: Graceful Ctrl+C shutdown with interrupted file logging +- **No runtime dependencies** — single static binary + +## Changelog + +### v5.0.0 — Rust Rewrite +- Complete rewrite from Python to Rust +- Thread-safe architecture (`OnceLock` + `Mutex`, no `static mut`) +- Full disk refresh: backup → delete → overwrite → restore (with timestamp refresh) → TRIM +- CLI arguments for non-interactive / scripted use +- Real-time progress dashboard +- Cross-platform: Windows + Linux + +## License + +MIT License — see [LICENSE](LICENSE). + +## Author + +**aspnmy** — [Blog](https://aspnmy.blog.csdn.net/) diff --git a/coldatafresh.py b/coldatafresh.py deleted file mode 100644 index 7bc0202..0000000 --- a/coldatafresh.py +++ /dev/null @@ -1,269 +0,0 @@ -import os -import time -import threading -import signal -import json -import zlib -import shutil -import random -from concurrent.futures import ThreadPoolExecutor -from elevate import elevate - -# 全局变量 -LOG_FILE = "refresh_log.json" -BUFFER_SIZE = 4 * 1024 # 缓冲区大小 -ENABLE_MULTITHREADING = False # 设置为 False 时禁用多线程 -THREAD_COUNT = 4 # 线程数 -BENCHMARK_SIZE_GB = 1 # 基准速度测试大小 (GB) -RATIO = 0.3 # 假设基准测试读取值为100MB/s, 若测试文件读取速度为100*0.3 = 30MB/s,则判断为冷数据 -SKIP_SIZE = 1 * 1024**2 #小于1(MB)的文件会被跳过。删除此行或填0则不跳过文件。 -EXIT_FLAG = False # 用于检测是否终止程序,请不要修改这个 - -def signal_handler(sig, frame): - global EXIT_FLAG - print("\nTerminating program...") - EXIT_FLAG = True - -signal.signal(signal.SIGINT, signal_handler) - -def load_log(): - if os.path.exists(LOG_FILE): - with open(LOG_FILE, "r") as f: - return json.load(f) - return {"pending": [], "completed": []} - -def save_log(log): - with open(LOG_FILE, "w") as f: - json.dump(log, f) - -def benchmark_speed(directory, size_in_gb=BENCHMARK_SIZE_GB): - size_in_bytes = size_in_gb * 1024**3 - small_file_sizes = [random.randint(100 * 1024, 10 * 1024**2) for _ in range(10)] # 100KB - 10MB - medium_file_sizes = [random.randint(10 * 1024**2, 100 * 1024**2) for _ in range(10)] # 10MB - 100MB - - benchmark_results = { - "large": {"speed": 0, "file_size": size_in_gb * 1024**3}, - "medium": {"speed": 0, "file_size": sum(medium_file_sizes)}, - "small": {"speed": 0, "file_size": sum(small_file_sizes)}, - } - - # 大文件测试 - try: - benchmark_file = os.path.join(directory, "benchmark_large.bin") - print(f"Benchmarking large file ({size_in_gb}GB)...") - with open(benchmark_file, "wb") as f: - for _ in range(size_in_bytes // BUFFER_SIZE): - f.write(os.urandom(BUFFER_SIZE)) - - start = time.time() - with open(benchmark_file, "rb") as f: - while f.read(BUFFER_SIZE): - pass - elapsed = time.time() - start - benchmark_results["large"]["speed"] = size_in_bytes / elapsed / 1024**2 # MB/s - os.remove(benchmark_file) - except Exception as e: - print(f"Error in large file benchmark: {e}") - - # 中小文件测试 - for category, file_sizes in [("medium", medium_file_sizes), ("small", small_file_sizes)]: - files = [] - try: - # 写入多个文件 - for idx, file_size in enumerate(file_sizes): - file_path = os.path.join(directory, f"benchmark_{category}_{idx}.bin") - with open(file_path, "wb") as f: - f.write(os.urandom(file_size)) - files.append(file_path) - - start = time.time() - for file_path in files: - with open(file_path, "rb") as f: - while f.read(BUFFER_SIZE): - pass - elapsed = time.time() - start - benchmark_results[category]["speed"] = sum(file_sizes) / elapsed / 1024**2 # MB/s - except Exception as e: - print(f"Error in {category} file benchmark: {e}") - finally: - for file_path in files: - if os.path.exists(file_path): - os.remove(file_path) - return benchmark_results - -def refresh_file(file_path, benchmark_speed_results, max_retries=2): - if EXIT_FLAG: - return - - temp_path = file_path + ".temp" - checksum_src = 0 - checksum_dest = 0 - retries = 0 - - try: - file_size = os.path.getsize(file_path) - - # 判断文件大小,选择合适的基准速度 - if file_size > 100 * 1024**2: # 大于100MB,使用大文件基准 - benchmark_speed = benchmark_speed_results["large"]["speed"] - elif file_size > 10 * 1024**2: # 10MB-100MB,使用中等文件基准 - benchmark_speed = benchmark_speed_results["medium"]["speed"] - else: # 小于10MB,使用小文件基准 - benchmark_speed = benchmark_speed_results["small"]["speed"] - - # 如果文件太小,跳过刷新 - if file_size <= BUFFER_SIZE: - print(f"Skipping tiny file: {file_path} (size: {file_size} bytes)") - return - - if SKIP_SIZE and file_size <= SKIP_SIZE: - print(f"Skipping tiny file: {file_path} (size: {file_size} bytes)") - return - - file_speed = test_read_speed(file_path) - - if file_speed < benchmark_speed * RATIO: - print(f"Refreshing cold data: {file_path} (read speed: {file_speed:.2f} MB/s, benchmark: {benchmark_speed:.2f} MB/s)") - - # 读取和写入 - while retries < max_retries: - with open(file_path, "rb") as src, open(temp_path, "wb") as dest: - while chunk := src.read(BUFFER_SIZE): - checksum_src = zlib.crc32(chunk, checksum_src) - dest.write(chunk) - checksum_dest = zlib.crc32(chunk, checksum_dest) - - # 校验 - if checksum_src == checksum_dest: - break - else: - retries += 1 - print(f"CRC mismatch, retrying {file_path}... ({retries}/{max_retries})") - os.remove(temp_path) - else: - # 如果多次重试失败,保留源文件并报告损坏 - print(f"Failed to refresh {file_path} after {max_retries} retries. The file might be corrupted.") - return - - # 保留原文件时间 - file_stat = os.stat(file_path) - shutil.move(temp_path, file_path) - os.utime(file_path, (file_stat.st_atime, file_stat.st_mtime)) # 恢复时间戳 - - # 保留原文件夹时间 - if os.path.isdir(file_path): - dir_stat = os.stat(os.path.dirname(file_path)) - os.utime(os.path.dirname(file_path), (dir_stat.st_atime, dir_stat.st_mtime)) # 恢复目录时间戳 - - print(f"File refreshed: {file_path}") - - else: - print(f"Skipping non-cold data: {file_path} (read speed: {file_speed:.2f} MB/s)") - - except Exception as e: - print(f"Error refreshing {file_path}: {e}") - - finally: - if os.path.exists(temp_path): - os.remove(temp_path) - - -# 多线程刷新文件 -def refresh_files(cold_files, benchmark_speed): - log = load_log() - - # 筛选待处理文件 - pending_files = list(set(cold_files) - set(log["completed"])) - log["pending"] = pending_files - save_log(log) - - lock = threading.Lock() - - def worker(file_path): - if EXIT_FLAG: - return - try: - refresh_file(file_path, benchmark_speed) - with lock: - log["completed"].append(file_path) - save_log(log) - except Exception as e: - print(f"Thread error: {e}") - - if ENABLE_MULTITHREADING: - # 使用多线程池 - with ThreadPoolExecutor(max_workers=THREAD_COUNT) as executor: - futures = [executor.submit(worker, file) for file in pending_files] - for future in futures: - if EXIT_FLAG: - break - future.result() - else: - for file_path in pending_files: - if EXIT_FLAG: - break - worker(file_path) - - -def scan_files(directory, min_days_old=30): - now = time.time() - cold_files = [] - - print(f"Scanning files in directory: {directory} for files older than {min_days_old} days...") - for root, _, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - try: - stat = os.stat(file_path) - if (now - stat.st_atime) > min_days_old * 86400: - cold_files.append(file_path) - except Exception as e: - print(f"Error accessing file {file_path}: {e}") - - print(f"Found {len(cold_files)} cold files.") - return cold_files - -def test_read_speed(file_path): - try: - start = time.time() - with open(file_path, "rb") as f: - while f.read(BUFFER_SIZE): - pass - elapsed = time.time() - start - file_size = os.path.getsize(file_path) - read_speed = file_size / elapsed / 1024**2 # MB/s - return read_speed - except Exception as e: - print(f"Error testing read speed for file {file_path}: {e}") - return 0 - -# 主函数 -def main(): - try: - elevate() - except Exception as e: - print("Warning: some files may fail to refresh without granting administrator privileges") - directory = input("Enter directory to scan for cold data: ").strip('"') - min_days_old = int(input("Enter minimum days to consider data as cold: ")) - - print("Benchmarking speed for new data...") - benchmark_speed_value = benchmark_speed(directory, BENCHMARK_SIZE_GB) - - print(f"Benchmark read speed for large files: {benchmark_speed_value['large']['speed']:.2f} MB/s") - print(f"Benchmark read speed for medium files: {benchmark_speed_value['medium']['speed']:.2f} MB/s") - print(f"Benchmark read speed for small files: {benchmark_speed_value['small']['speed']:.2f} MB/s") - - - print("Scanning for cold files...") - cold_files = scan_files(directory, min_days_old) - if not cold_files: - print("No cold files found. Exiting.") - return - - print("Refreshing cold files...") - refresh_files(cold_files, benchmark_speed_value) - print("All tasks completed.") - input("Press Enter to exit...") - -if __name__ == "__main__": - main() diff --git a/devrom.ico b/devrom.ico new file mode 100644 index 0000000..c6913e6 Binary files /dev/null and b/devrom.ico differ diff --git a/projectimage.png b/projectimage.png deleted file mode 100644 index 97bc7ef..0000000 Binary files a/projectimage.png and /dev/null differ diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c6d9712 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,568 @@ +use std::io::{self, Write}; +use std::sync::atomic::Ordering; +use std::time::Instant; + +use crate::config::{self, FileCategory}; +use crate::dashboard::{Dashboard, Stats}; +use crate::file_op; +use crate::full_refresh::FullRefresh; +use crate::log::logger; +use crate::platform; + +/// 应用控制器 — 交互菜单与模式路由 +pub struct App { + dashboard: Dashboard, + stats: Stats, +} + +impl App { + pub fn new() -> Self { + Self { + dashboard: Dashboard::new(), + stats: Stats::default(), + } + } + + /// 主入口 + pub fn run( + &mut self, + full_refresh: bool, + trim_mode: bool, + cli_age: Option, + cli_skip_smaller_mb: Option, + ) { + // 注册 Ctrl+C 信号处理器 + let _ = ctrlc::set_handler(|| { + // 先设置中断标志,让当前操作优雅退出 + file_op::INTERRUPTED.store(true, Ordering::Relaxed); + // 短暂延迟后直接退出,避免阻塞在 stdin 读上 + std::thread::sleep(std::time::Duration::from_millis(200)); + std::process::exit(0); + }); + + // 打印启动横幅 + self.show_startup_banner(); + + loop { + // 重置统计 + self.stats = Stats::default(); + + // 获取模式选择 + let (mode_full, mode_trim) = if !full_refresh && !trim_mode { + self.show_mode_menu() + } else { + (full_refresh, trim_mode) + }; + + // 确认操作 + if mode_full { + if !self.confirm_full_refresh() { + continue; + } + } else if mode_trim && !self.confirm_trim() { + continue; + } + + self.dashboard.full_refresh = mode_full; + self.dashboard.trim_mode = mode_trim; + + // 获取目录 + let directory = self.prompt_directory(); + + // TRIM 模式 — 直接执行 + if mode_trim { + self.run_trim_mode(&directory); + if !self.ask_return_to_menu() { + break; + } + continue; + } + + // 全盘刷新模式 + if mode_full { + self.run_full_refresh_mode(&directory); + if !self.ask_return_to_menu() { + break; + } + continue; + } + + // 智能模式 + let min_days = cli_age.unwrap_or_else(|| self.prompt_age()); + let skip_small = if cli_skip_smaller_mb.unwrap_or(0) > 0 { + true + } else { + self.prompt_skip_small() + }; + self.dashboard.min_days = min_days; + self.dashboard.skip_small = skip_small; + self.dashboard.buffer_size_mb = self.prompt_buffer_size(); + self.dashboard.working_directory = directory.clone(); + + logger().log( + &format!( + "用户配置: 目录='{}', 数据时效={}天, 跳过小文件={}, 缓冲区={}MB", + directory, min_days, skip_small, self.dashboard.buffer_size_mb + ), + "INFO", + ); + + // 扫描文件(每 100 个条目刷新一次仪表盘,保持界面活跃) + self.dashboard.update(&self.stats, "扫描中"); + let mut files = file_op::collect_files( + &directory, min_days, skip_small, + |_count| { self.dashboard.update(&self.stats, "扫描中"); }, + ); + self.stats.scanned = files.len() as u64; + + if files.is_empty() { + crate::terminal::Terminal::clear(); + let eq = "=".repeat(50); + println!("\n{}", eq); + println!(" 未发现符合条件的冷数据文件"); + println!("{}", eq); + if !self.ask_return_to_menu() { + break; + } + continue; + } + + // 按文件大小升序排序,先处理小文件,让进度条快速起步 + files.sort_by_cached_key(|f| std::fs::metadata(f).ok().map(|m| m.len()).unwrap_or(0)); + + // 统计所有扫描文件分类,让分类数 = 扫描数 + for f in &files { + if let Ok(meta) = std::fs::metadata(f) { + self.update_file_stats(meta.len()); + self.stats.total_bytes += meta.len(); + } + } + + // 扫描完成后立即刷新仪表盘,让用户看到文件数量和阶段切换 + self.dashboard.update(&self.stats, "扫描完成"); + + // 通知用户扫描结果,准备开始处理 + println!( + "\n → 发现 {} 个文件 ({}),正在处理中,请稍候...", + files.len(), + crate::config::format_size(self.stats.total_bytes) + ); + + self.dashboard.update(&self.stats, "处理中"); + + // 使用 rayon 分块并行处理,每块处理后实时更新进度 + use rayon::prelude::*; + let buf_mult = (self.dashboard.buffer_size_mb as usize / 64).max(1); + let chunk_size = (config::config().max_workers * buf_mult).max(1); + let mini_batch = 32; // 每处理 32 个文件就更新一次进度,保证界面实时响应 + let mut processed = 0u64; + let start = Instant::now(); + let total = files.len() as u64; + + for chunk in files.chunks(chunk_size) { + if file_op::INTERRUPTED.load(Ordering::Relaxed) { + break; + } + + // 将大块拆成小批量,每批处理后更新进度,避免用户长时间看不到变化 + for batch in chunk.chunks(mini_batch) { + if file_op::INTERRUPTED.load(Ordering::Relaxed) { + break; + } + + // 并行处理当前小批量 + let results: Vec<(Result, u64)> = batch + .par_iter() + .map(|path| { + if file_op::INTERRUPTED.load(Ordering::Relaxed) { + return (Err("用户中断".into()), 0); + } + let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); + let result = file_op::refresh_file(path); + (result, size) + }) + .collect(); + + // 汇总本小批结果 + for (result, file_size) in &results { + match result { + Ok(speed) => { + self.stats.processed += 1; + self.stats.processed_bytes += file_size; + if *speed > (self.stats.speed * 100.0) as u64 { + self.stats.speed = *speed as f64 / 100.0; + } + } + Err(_) => { + self.stats.corrupted += 1; + } + } + processed += 1; + } + // 每批立即刷新仪表盘,让用户看到实时进展 + self.stats.progress = if self.stats.total_bytes > 0 { + self.stats.processed_bytes as f64 / self.stats.total_bytes as f64 + } else { + processed as f64 / total as f64 + }; + self.stats.speed = self + .stats + .speed + .max(processed as f64 / start.elapsed().as_secs_f64().max(0.001) / 1_048_576.0); + self.dashboard.update(&self.stats, "处理中"); + } + } + + let elapsed = start.elapsed().as_secs_f64(); + + // 汇总 + self.dashboard.update(&self.stats, "完成"); + + let log_path = config::config().error_log.display().to_string(); + self.dashboard + .final_summary(&self.stats, elapsed, &log_path); + + logger().save_summary( + self.stats.scanned, + self.stats.processed, + self.stats.corrupted, + self.stats.large, + self.stats.medium, + self.stats.small, + self.stats.speed, + elapsed, + ); + + if !self.ask_return_to_menu() { + break; + } + } + } + + /// 打印启动横幅 + fn show_startup_banner(&self) { + let admin = if platform::is_admin() { + " [管理员]" + } else { + "" + }; + let os_info = platform::get_os_display(); + let eq = "=".repeat(50); + + println!("{}", eq); + println!("SSD掉速激活-冷数据维护系统 v5.0.0{}", admin); + println!("运行平台: {}", os_info); + println!("作者: support@e2bank.cn QQ群: 115405294"); + println!("GitHub: https://github.com/aspnmy/ColDataRefresh"); + println!("{}", eq); + } + + /// 更新文件分类统计 + fn update_file_stats(&mut self, file_size: u64) { + match config::categorize_file(file_size) { + FileCategory::Large => self.stats.large += 1, + FileCategory::Medium => self.stats.medium += 1, + FileCategory::Small => self.stats.small += 1, + } + } + + fn show_mode_menu(&self) -> (bool, bool) { + crate::terminal::Terminal::clear(); + let admin = if platform::is_admin() { " [管理员]" } else { "" }; + let os_info = platform::get_os_display(); + let eq = "=".repeat(50); + + println!("{}", eq); + println!("SSD掉速激活-冷数据维护系统 v5.0.0{}", admin); + println!("运行平台: {} | 作者: support@e2bank.cn QQ群: 115405294", os_info); + println!("GitHub: https://github.com/aspnmy/ColDataRefresh"); + println!("{}", eq); + + println!("\n{}", eq); + println!(" 冷数据维护工具 - 操作模式选择"); + println!("{}", eq); + println!("1. 智能模式 (推荐) - 保留原文件内容,仅激活冷数据"); + println!("2. 全盘激活冷数据模式 (所有文件全部丢失无法找回) - 将文件内容替换为 FF 值"); + println!("3. TRIM优化模式 (清理/如需找回数据不要使用这个模式) - 操作系统API来通知SSD哪些数据块是无效的"); + println!("{}", eq); + + loop { + print!("请选择操作模式 [1/2/3]: "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + match input.trim() { + "1" => { + logger().log("用户选择操作模式: 智能模式", "INFO"); + return (false, false); + } + "2" => { + logger().log("用户选择操作模式: 全盘刷新模式", "INFO"); + return (true, false); + } + "3" => { + logger().log("用户选择操作模式: TRIM模式", "INFO"); + return (false, true); + } + _ => println!("无效的选择,请输入 1、2 或 3"), + } + } + } + + fn confirm_full_refresh(&self) -> bool { + println!("\n⚠️ 警告: 正在使用全盘数据刷新模式!"); + println!(" 使用此模式将完全擦除SSD硬盘中的数据,所有文件内容将丢失且无法找回!"); + println!(); + + print!("请输入 'yes' 确认执行全盘刷新操作 (第一次): "); + io::stdout().flush().ok(); + let mut c1 = String::new(); + io::stdin().read_line(&mut c1).ok(); + if c1.trim().to_lowercase() != "yes" { + println!("操作已取消"); + return false; + } + + print!("请再次输入 'yes' 确认执行全盘刷新操作 (第二次): "); + io::stdout().flush().ok(); + let mut c2 = String::new(); + io::stdin().read_line(&mut c2).ok(); + if c2.trim().to_lowercase() != "yes" { + println!("操作已取消"); + return false; + } + + logger().log("用户已确认两次,开始执行全盘刷新操作", "INFO"); + true + } + + fn confirm_trim(&self) -> bool { + println!("\n⚠️ 警告: 正在使用TRIM优化模式!"); + println!(" 如需找回SSD中删除的数据请不要使用此模式,先找回数据以后再使用!"); + println!(); + + print!("请输入 'yes' 确认执行TRIM优化操作: "); + io::stdout().flush().ok(); + let mut c = String::new(); + io::stdin().read_line(&mut c).ok(); + if c.trim().to_lowercase() != "yes" { + println!("操作已取消"); + return false; + } + + logger().log("用户已确认,开始执行TRIM优化操作", "INFO"); + true + } + + fn prompt_directory(&mut self) -> String { + print!("扫描目录: "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + let dir = input.trim().trim_matches('"').to_string(); + + // 补齐路径分隔符 + let dir = if !dir.ends_with(&['\\', '/'][..]) { + dir + "\\" + } else { + dir + }; + + self.dashboard.working_directory = dir.clone(); + dir + } + + fn prompt_age(&self) -> u32 { + print!("数据时效(天): "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + input.trim().parse().unwrap_or(0) + } + + fn prompt_skip_small(&self) -> bool { + print!("跳过小文件? (y/n): "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + input.trim().to_lowercase() == "y" + } + + fn prompt_buffer_size(&self) -> u32 { + print!("处理缓冲区大小(MB, 默认512, 最大2048): "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + let val: u32 = input.trim().parse().unwrap_or(512); + val.clamp(64, 2048) + } + + fn ask_return_to_menu(&self) -> bool { + crate::terminal::Terminal::clear(); + println!("\n{}", "=".repeat(50)); + println!("1. 返回 - 回到交互界面"); + println!("2. 退出 - 关闭程序"); + println!("{}", "=".repeat(50)); + + loop { + print!("请选择操作 [1/2]: "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + match input.trim() { + "1" => return true, + "2" => { + println!("感谢使用冷数据维护工具,再见!"); + return false; + } + _ => println!("无效的选择,请输入 1 或 2"), + } + } + } + + fn run_trim_mode(&self, directory: &str) { + logger().log(&format!("开始 TRIM 模式: 路径='{}'", directory), "INFO"); + + let (result, device) = if let Some(dev) = platform::resolve_device_name(directory) { + // 显示 TRIM 执行中界面 + crate::terminal::Terminal::clear(); + let eq = "=".repeat(50); + println!("{}", eq); + println!(" 正在执行 TRIM 优化"); + println!("{}", eq); + println!("设备: {}", dev); + println!("{}", eq); + println!("注意事项:"); + println!("1. 后台执行时间预计需要10-30分钟"); + println!("2. 30分钟内请不要对该存储设备进行断电操作"); + println!("3. TRIM操作有助于提高SSD性能并延长使用寿命"); + println!("{}", eq); + print!("状态: 执行中 "); + use std::io::Write; + std::io::stdout().flush().ok(); + + // 在后台线程执行 TRIM,主线程显示旋转动画 + use std::sync::atomic::{AtomicBool, Ordering}; + let done = std::sync::Arc::new(AtomicBool::new(false)); + let done_clone = done.clone(); + let d = dev.clone(); + let _handle = std::thread::spawn(move || { + let _ = platform::trim_volume(&d); + done_clone.store(true, Ordering::Relaxed); + }); + + let spinner = ['|', '/', '-', '\\']; + let mut i = 0; + while !done.load(Ordering::Relaxed) { + print!("\r状态: 执行中 {} 请耐心等待...", spinner[i % 4]); + std::io::stdout().flush().ok(); + std::thread::sleep(std::time::Duration::from_millis(500)); + i += 1; + } + + let ok = true; // trim_volume already logged the result + logger().log(&format!("TRIM 操作完成: 设备={}", dev), "INFO"); + println!("\r状态: ✅ 完成 "); + (ok, dev) + } else { + println!("无法从路径 '{}' 中识别存储设备", directory); + (false, directory.to_string()) + }; + + // 显示 TRIM 完成汇总 + std::thread::sleep(std::time::Duration::from_millis(500)); + crate::terminal::Terminal::clear(); + let eq = "=".repeat(50); + println!("\n{}", eq); + println!(" TRIM 操作完成"); + println!("{}", eq); + println!("设备: {}", device); + println!("状态: {}", if result { "✅ 成功" } else { "❌ 失败" }); + println!("操作日志: {}", crate::config::config().error_log.display()); + println!("{}", eq); + } + + fn run_full_refresh_mode(&self, directory: &str) { + crate::terminal::Terminal::clear(); + logger().log(&format!("开始全盘刷新模式: 路径='{}'", directory), "INFO"); + + let is_drive = platform::is_root_path(directory); + let eq = "=".repeat(50); + println!("{}", eq); + println!(" 全盘刷新模式"); + println!("{}", eq); + println!( + "路径类型: {}", + if is_drive { + "整个盘符" + } else { + "文件目录" + } + ); + + // 计算目录内文件总大小(递归) + println!("\n正在统计目录大小..."); + let dir_size: u64 = walkdir::WalkDir::new(directory) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter_map(|e| e.metadata().ok()) + .map(|m| m.len()) + .sum(); + + // 获取硬盘可用空间 + let (_total, _used, free) = platform::get_disk_space(std::path::Path::new(directory)); + println!("目录信息:"); + println!(" 目录大小: {}", config::format_size(dir_size)); + println!(" 可用空间: {}", config::format_size(free)); + + // 询问是否保留文件(总是需要) + print!("\n是否保留已使用空间中的文件? (Y/N, 默认Y): "); + io::stdout().flush().ok(); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); + let keep = !input.trim().to_lowercase().starts_with('n'); + + // 询问是否额外填充空闲空间 + print!("\n是否同时填充空闲空间? (Y/N, 默认N): "); + io::stdout().flush().ok(); + let mut fill_input = String::new(); + io::stdin().read_line(&mut fill_input).ok(); + let want_fill = fill_input.trim().to_lowercase().starts_with('y'); + + // 如果用户选择填充,再次确认 + let fill_free = if want_fill { + print!("\n⚠️ 填充空闲空间将覆写所有未分配空间,数据无法还原!\n是否确认执行? (Y/N, 默认N): "); + io::stdout().flush().ok(); + let mut confirm = String::new(); + io::stdin().read_line(&mut confirm).ok(); + confirm.trim().to_lowercase().starts_with('y') + } else { + false + }; + + // 询问写入参数(仅填充空闲空间时需要) + let (unit_gb, write_buf_kb) = if fill_free { + print!("\n请输入每个文件的写入容量 (1-100GB,默认50GB): "); + io::stdout().flush().ok(); + let mut cap = String::new(); + io::stdin().read_line(&mut cap).ok(); + let val: u64 = cap.trim().parse().unwrap_or(50); + let unit_gb = val.clamp(1, 100); + + print!("\n请输入写入缓冲区大小 (64KB~1GB,默认512MB): "); + io::stdout().flush().ok(); + let mut buf_input = String::new(); + io::stdin().read_line(&mut buf_input).ok(); + let buf_val: u64 = buf_input.trim().parse().unwrap_or(512); + let write_buf_kb = buf_val.clamp(1, 1024) * 1024; + + (unit_gb, write_buf_kb) + } else { + (0, 64) + }; + + // 执行全盘刷新(目录空间始终填充,空闲空间由 fill_free 控制) + FullRefresh::execute(directory, keep, fill_free, dir_size, unit_gb, write_buf_kb); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e882394 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; +use std::sync::OnceLock; + +/// 文件分类 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileCategory { + Small, + Medium, + Large, +} + +/// 全局线程安全配置 +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct Config { + pub script_dir: PathBuf, + pub log_file: PathBuf, + pub corrupted_log: PathBuf, + pub error_log: PathBuf, + pub buffer_size: usize, + pub max_retries: u32, + pub large_file_threshold: u64, + pub medium_file_threshold: u64, + pub report_interval_secs: f32, + pub skip_small_threshold: u64, + pub max_workers: usize, + pub memory_limit_mb: u32, + pub full_refresh_pattern: Vec, + pub trim_block_size: u64, + pub version: &'static str, +} + +impl Default for Config { + fn default() -> Self { + let script_dir = std::env::current_dir().expect("无法获取当前目录"); + + Self { + script_dir: script_dir.clone(), + log_file: script_dir.join("refresh_log.json"), + corrupted_log: script_dir.join("corrupted_files.log"), + error_log: script_dir.join("error.log"), + buffer_size: 256 * 1024, + max_retries: 3, + large_file_threshold: 100 * 1024 * 1024, + medium_file_threshold: 10 * 1024 * 1024, + report_interval_secs: 0.2, + skip_small_threshold: 1024 * 1024, + max_workers: std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4), + memory_limit_mb: 512, + full_refresh_pattern: vec![0xFF], + trim_block_size: 1024 * 1024, + version: "5.0.0", + } + } +} + +static CONFIG: OnceLock = OnceLock::new(); + +/// 获取全局配置引用(线程安全) +pub fn config() -> &'static Config { + CONFIG.get_or_init(Config::default) +} + +/// 文件分类函数 +pub fn categorize_file(size: u64) -> FileCategory { + let cfg = config(); + if size > cfg.large_file_threshold { + FileCategory::Large + } else if size > cfg.medium_file_threshold { + FileCategory::Medium + } else { + FileCategory::Small + } +} + +pub fn format_size(size: u64) -> String { + if size < 1024 { + format!("{} B", size) + } else if size < 1024u64.pow(2) { + format!("{:.2} KB", size as f64 / 1024.0) + } else if size < 1024u64.pow(3) { + format!("{:.2} MB", size as f64 / (1024u64.pow(2) as f64)) + } else if size < 1024u64.pow(4) { + format!("{:.2} GB", size as f64 / (1024u64.pow(3) as f64)) + } else { + format!("{:.2} TB", size as f64 / (1024u64.pow(4) as f64)) + } +} diff --git a/src/dashboard.rs b/src/dashboard.rs new file mode 100644 index 0000000..1d07c04 --- /dev/null +++ b/src/dashboard.rs @@ -0,0 +1,213 @@ +use std::time::{Duration, Instant}; + +use crate::config; +use crate::terminal::{self, Terminal}; + +/// 操作统计 +#[derive(Debug, Default, Clone)] +pub struct Stats { + pub scanned: u64, + pub processed: u64, + pub total_bytes: u64, + pub processed_bytes: u64, + pub large: u64, + pub medium: u64, + pub small: u64, + pub corrupted: u64, + pub speed: f64, + pub progress: f64, +} + +/// 进度仪表盘 +pub struct Dashboard { + start_time: Instant, + last_update: Instant, + last_phase: String, + pub working_directory: String, + pub full_refresh: bool, + pub trim_mode: bool, + pub min_days: u32, + pub skip_small: bool, + pub buffer_size_mb: u32, + sub_progress: f64, +} + +impl Dashboard { + pub fn new() -> Self { + Self { + start_time: Instant::now(), + last_update: Instant::now(), + last_phase: String::new(), + working_directory: String::new(), + full_refresh: false, + trim_mode: false, + min_days: 0, + skip_small: false, + buffer_size_mb: 512, + sub_progress: 0.0, + } + } + + /// 更新显示(带频率控制,阶段变化时强制刷新) + pub fn update(&mut self, stats: &Stats, phase: &str) { + let cfg = config::config(); + let phase_changed = phase != self.last_phase; + if !phase_changed && self.last_update.elapsed() < Duration::from_secs_f32(cfg.report_interval_secs) { + return; + } + + self.last_phase = phase.to_string(); + + // 子进度条独立于实际处理,按时间循环,每 1.5 秒完成一次 0→100% + let elapsed = self.start_time.elapsed().as_secs_f64(); + self.sub_progress = (elapsed / 1.5) % 1.0; + + Terminal::clear(); + self.render_header(); + self.render_stats(stats, phase); + use std::io::Write; + std::io::stdout().flush().ok(); + self.last_update = Instant::now(); + } + + fn render_header(&self) { + let (h, _v, _) = terminal::border_chars(); + let h_line = h.repeat(70); + + let header = Terminal::colored( + " SSD掉速激活-冷数据维护系统 v5.0.0 作者:support@e2bank.cn By Rust", + 37, + 44, + ); + + println!("\n{}", h_line); + println!("{:^70}", header); + println!("{}", h_line); + } + + fn render_stats(&self, stats: &Stats, phase: &str) { + let (_h, v, _) = terminal::border_chars(); + let (fill, empty) = terminal::progress_chars(); + let elapsed = self.start_time.elapsed().as_secs_f64(); + + let mode_text = if self.full_refresh { + "全盘刷新模式" + } else if self.trim_mode { + "TRIM模式" + } else { + "智能模式" + }; + + let progress_bar = { + let filled = (50.0 * stats.progress) as usize; + let unfilled = 50_usize.saturating_sub(filled); + format!("[{}{}]", fill.repeat(filled), empty.repeat(unfilled)) + }; + + let lines = [ + format!("{} 智能检测固态硬盘的冷数据并解决冷数据掉速问题。", v), + format!("{} GitHub: https://github.com/aspnmy/ColDataRefresh.git", v), + format!("{} 工作路径: {} ", v, self.working_directory), + format!("{} 操作模式: {} ", v, Terminal::colored(mode_text, 32, 44)), + format!( + "{} 数据时效: {} 天, 跳过小文件: {} ", + v, + self.min_days, + if self.skip_small { "是" } else { "否" } + ), + format!( + "{} 缓冲区: {} MB", + v, self.buffer_size_mb + ), + format!( + "{} 数据量: {}/{}", + v, + crate::config::format_size(stats.processed_bytes), + crate::config::format_size(stats.total_bytes.max(stats.processed_bytes)), + ), + format!( + "{} 运行阶段: {} 耗时: {:.1}s", + v, + Terminal::fg(phase, 33), + elapsed + ), + format!( + "{} 处理进度: {} {:.1}%", + v, + progress_bar, + stats.progress * 100.0 + ), + format!( + "{} 文件进度: [{}]", + v, + "▓".repeat((self.sub_progress * 20.0) as usize) + + &"░".repeat(20_usize.saturating_sub((self.sub_progress * 20.0) as usize)), + ), + format!("{} 发现文件: {}", v, stats.scanned), + format!("{} 处理速度: {:.1} MB/s", v, stats.speed), + format!( + "{} 文件分类: 大(>100MB)({}) 中(10-100MB)({}) 小(<10MB)({})", + v, stats.large, stats.medium, stats.small + ), + format!( + "{} 损坏的文件: {}", + v, + Terminal::colored(&stats.corrupted.to_string(), 31, 44) + ), + format!("{} 按 Ctrl+C 退出程序", v), + ]; + + for line in &lines { + println!("{}", line); + } + + let (h, _, _) = terminal::border_chars(); + println!("{}{}", h, h.repeat(69)); + } + + /// 清理并显示最终结果 + pub fn final_summary(&self, stats: &Stats, elapsed: f64, log_file: &str) { + Terminal::clear(); + let eq = "=".repeat(60); + + println!("\n{}", eq); + println!(" 操作完成!"); + println!("{}", eq); + + if self.full_refresh { + println!("操作模式: 全盘刷新"); + println!("总耗时: {:.2} 秒", elapsed); + println!("最大写入速度: {:.2} MB/s", stats.speed); + } else if self.trim_mode { + println!("操作模式: TRIM模式"); + println!("总耗时: {:.2} 秒", elapsed); + } else { + println!("操作模式: 智能模式"); + println!("总耗时: {:.2} 秒", elapsed); + println!( + "处理文件数: {} 个 (共发现 {} 个)", + stats.processed, stats.scanned + ); + println!( + "大文件: {}, 中等文件: {}, 小文件: {}", + stats.large, stats.medium, stats.small + ); + println!("损坏文件: {}", stats.corrupted); + println!("平均处理速度: {:.2} MB/s", stats.speed); + } + + println!("操作日志: {}", log_file); + println!("错误记录: {}", config::config().corrupted_log.display()); + println!("{}", eq); + } +} + +/// 计算扫描速度 — 公开 API,预留使用 +#[allow(dead_code)] +pub fn compute_scan_speed(scanned: u64, elapsed_secs: f64) -> f64 { + if elapsed_secs > 0.0 { + scanned as f64 / (1024.0 * 1024.0) / elapsed_secs + } else { + 0.0 + } +} diff --git a/src/file_op.rs b/src/file_op.rs new file mode 100644 index 0000000..30ae354 --- /dev/null +++ b/src/file_op.rs @@ -0,0 +1,369 @@ +use std::fs::{self, File}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Instant; + +use crc32fast::Hasher; +use walkdir::WalkDir; + +use crate::config::config as get_config; + +/// 全局中断标志 +pub static INTERRUPTED: AtomicBool = AtomicBool::new(false); + +/// 获取或创建线程本地缓冲区 +#[allow(clippy::missing_const_for_thread_local)] +fn with_buffer(f: F) -> R +where + F: FnOnce(&mut Vec) -> R, +{ + std::thread_local! { + static BUF: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; + } + BUF.with(|cell| { + let mut buf = cell.borrow_mut(); + buf.resize(get_config().buffer_size, 0); + f(&mut buf) + }) +} + +/// 扫描目录,收集符合条件的冷数据文件 +/// skip_small: true 时跳过 < 1MB 的文件 +/// progress: 可选进度回调,每处理约 100 个文件调用一次 +pub fn collect_files( + directory: &str, + min_days: u32, + skip_small: bool, + mut progress: impl FnMut(u64), +) -> Vec { + let cutoff = if min_days > 0 { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + (now as i64) - (min_days as i64 * 86400) + } else { + 0 // 0 表示不过滤 + }; + + let mut files = Vec::new(); + let cfg = get_config(); + + let mut scan_count = 0u64; + for entry in WalkDir::new(directory) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + if INTERRUPTED.load(Ordering::Relaxed) { + break; + } + + // 每扫描 100 个条目回调一次,保持界面子进度条运动 + scan_count += 1; + if scan_count.is_multiple_of(100) { + progress(scan_count); + } + + let path = entry.path(); + + // 跳过系统保护目录 + let p_str = path.to_string_lossy().to_uppercase(); + if p_str.contains("$RECYCLE.BIN") || p_str.contains("SYSTEM VOLUME INFORMATION") { + continue; + } + + if !entry.file_type().is_file() { + continue; + } + + // 跳过小文件(如果配置了) + if let Ok(meta) = fs::metadata(path) { + let file_size = meta.len(); + if skip_small && file_size < cfg.skip_small_threshold { + continue; + } + + // 按修改时间过滤 + if cutoff > 0 { + if let Ok(mtime) = meta.modified() { + if let Ok(duration) = mtime.duration_since(std::time::UNIX_EPOCH) { + if (duration.as_secs() as i64) >= cutoff { + continue; + } + } else { + continue; + } + } else { + continue; + } + } + + files.push(path.to_string_lossy().to_string()); + } + } + + files +} + +/// 计算文件的 CRC32 校验和(预留,当前未使用) +#[allow(dead_code)] +pub fn checksum_file(path: &Path) -> Result { + let mut hasher = Hasher::new(); + let mut file = File::open(path)?; + with_buffer(|buffer| loop { + let bytes_read = file.read(buffer)?; + if bytes_read == 0 { + break Ok(hasher.finalize()); + } + hasher.update(&buffer[..bytes_read]); + }) +} + +/// 刷新单个文件(智能模式 — 保持原内容,通过重写激活) +/// 小文件(≤ 缓冲区容量)整片读入→整片写回→整片读回校验 +/// 大文件(> 缓冲区容量)分块原地覆写 + 分块读回校验 +pub fn refresh_file(path: &str) -> Result { + let p = Path::new(path); + + // 安全检查:跳过系统目录 + let p_upper = p.to_string_lossy().to_uppercase(); + if p_upper.contains("$RECYCLE.BIN") || p_upper.contains("SYSTEM VOLUME INFORMATION") { + return Err("跳过系统保护目录".into()); + } + + let meta = fs::metadata(path) + .map_err(|e| format!("获取文件信息失败: {}", e))?; + let file_len = meta.len(); + if file_len == 0 { + return Ok(0); + } + + let start = Instant::now(); + let mem_limit = (get_config().memory_limit_mb as u64) * 1024 * 1024; + + if file_len <= mem_limit { + // ── 小文件路径:整片读入 → 整片写回 → 整片读回验证 ── + refresh_file_whole(path, p, file_len, &start) + } else { + // ── 大文件路径:分块原地覆写 + 分块读回验证 ── + refresh_file_chunked(path, p, file_len, &start) + } +} + +/// 整片读写(小文件,≤ 缓冲区大小) +/// I/O:1 次 read_to_end + 1 次 write_all + 1 次 read_to_end 校验 +fn refresh_file_whole( + _path: &str, p: &Path, file_len: u64, start: &Instant, +) -> Result { + // 第一阶段:整片读入 + 计算 CRC + let data = std::fs::read(p) + .map_err(|e| format!("读取文件失败: {}", e))?; + let write_crc = crc32fast::hash(&data); + let processed = file_len; + + // 第二阶段:整片写回 + let mut file = fs::OpenOptions::new() + .write(true) + .open(p) + .map_err(|e| format!("打开文件写入失败: {}", e))?; + file.write_all(&data) + .map_err(|e| format!("写入文件失败: {}", e))?; + file.flush() + .map_err(|e| format!("刷新文件失败: {}", e))?; + drop(file); + + // 第三阶段:整片读回验证 + let verify_data = std::fs::read(p) + .map_err(|e| format!("读取文件验证失败: {}", e))?; + let verify_crc = crc32fast::hash(&verify_data); + + if write_crc != verify_crc { + return Err("CRC 校验不匹配:文件可能损坏,建议从备份恢复".into()); + } + + let elapsed = start.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { + processed as f64 / (1024.0 * 1024.0) / elapsed + } else { + 0.0 + }; + Ok((speed * 100.0).round() as u64) +} + +/// 分块原地覆写(大文件,> 缓冲区大小) +/// I/O:分块 read → seek → write + 分块 read 校验 +fn refresh_file_chunked( + _path: &str, p: &Path, file_len: u64, start: &Instant, +) -> Result { + let mut processed: u64 = 0; + + // 第一阶段:分块原地覆写 + let write_crc = { + let mut hasher = Hasher::new(); + let mut file = fs::OpenOptions::new() + .read(true) + .write(true) + .open(p) + .map_err(|e| format!("打开文件失败: {}", e))?; + + with_buffer(|buf| -> Result<(), String> { + loop { + let pos = file.stream_position() + .map_err(|e| format!("获取文件位置失败: {}", e))?; + let n = file.read(buf) + .map_err(|e| format!("读取失败: {}", e))?; + if n == 0 { + break Ok(()); + } + hasher.update(&buf[..n]); + file.seek(SeekFrom::Start(pos)) + .map_err(|e| format!("定位失败: {}", e))?; + file.write_all(&buf[..n]) + .map_err(|e| format!("写入失败: {}", e))?; + processed += n as u64; + } + })?; + + file.flush().map_err(|e| format!("刷新失败: {}", e))?; + if file_len > 0 { + file.set_len(file_len) + .map_err(|e| format!("截断文件失败: {}", e))?; + } + + hasher.finalize() + }; + + // 第二阶段:分块读回验证 + let verify_crc = { + let mut file = File::open(p) + .map_err(|e| format!("打开文件验证失败: {}", e))?; + let mut hasher = Hasher::new(); + with_buffer(|buf| -> u32 { + loop { + match file.read(buf) { + Ok(0) => break hasher.finalize(), + Ok(n) => hasher.update(&buf[..n]), + Err(_e) => return hasher.finalize(), // 忽略读取错误,继续尝试 + } + } + }) + }; + + if write_crc != verify_crc { + return Err("CRC 校验不匹配:写入后数据不一致,文件可能损坏".into()); + } + + let elapsed = start.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { + processed as f64 / (1024.0 * 1024.0) / elapsed + } else { + 0.0 + }; + Ok((speed * 100.0).round() as u64) +} + +/// 全盘刷新文件(写入 FF 值) — 预留 +#[allow(dead_code)] +pub fn full_refresh_file(path: &Path, size: u64) -> Result<(), String> { + // 安全检查 + let p_upper = path.to_string_lossy().to_uppercase(); + if p_upper.contains("$RECYCLE.BIN") || p_upper.contains("SYSTEM VOLUME INFORMATION") { + return Err("跳过系统保护目录".into()); + } + + if let Some(parent) = path.parent() { + if !parent.exists() { + return Err(format!("目录不存在: {}", parent.display())); + } + } + + let cfg = get_config(); + let temp_path = path.with_extension("tmp"); + + let fill_block = cfg + .full_refresh_pattern + .repeat(cfg.buffer_size / cfg.full_refresh_pattern.len().max(1)); + + { + let mut file = File::create(&temp_path).map_err(|e| format!("创建临时文件失败: {}", e))?; + let mut remaining = size; + + while remaining > 0 { + let chunk = std::cmp::min(cfg.buffer_size as u64, remaining) as usize; + file.write_all(&fill_block[..chunk]) + .map_err(|e| format!("写入失败: {}", e))?; + remaining -= chunk as u64; + } + + file.flush().map_err(|e| format!("刷新失败: {}", e))?; + } + + fs::rename(&temp_path, path).map_err(|e| format!("替换文件失败: {}", e))?; + Ok(()) +} + +/// 持续全盘写入模式(用于填充可用空间) +/// buf_kb: 写入缓冲区大小,单位 KB +pub fn continuous_full_refresh(path: &Path, target_size: u64, buf_kb: u64) -> Result<(u64, f64), String> { + let p_upper = path.to_string_lossy().to_uppercase(); + if p_upper.contains("$RECYCLE.BIN") || p_upper.contains("SYSTEM VOLUME INFORMATION") { + return Err("跳过系统保护目录".into()); + } + + if path.exists() { + fs::remove_file(path).map_err(|e| format!("删除旧文件失败: {}", e))?; + } + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).ok(); + } + + let cfg = get_config(); + let start = Instant::now(); + let mut max_speed = 0.0_f64; + let mut total_written = 0u64; + let mut last_report = Instant::now(); + + let mut file = File::create(path).map_err(|e| format!("创建文件失败: {}", e))?; + let mut remaining = target_size; + let buf_size = (buf_kb as usize) * 1024; + + // 使用用户指定大小的缓冲区,预填 0xFF + let buffer = vec![cfg.full_refresh_pattern[0]; buf_size]; + + while remaining > 0 { + if INTERRUPTED.load(Ordering::Relaxed) { + file.flush().ok(); + break; + } + + let chunk = std::cmp::min(buf_size as u64, remaining) as usize; + file.write_all(&buffer[..chunk]) + .map_err(|e| format!("写入失败: {}", e))?; + total_written += chunk as u64; + remaining -= chunk as u64; + + // 每秒报告速度 + if last_report.elapsed().as_secs_f64() >= 1.0 { + let elapsed = start.elapsed().as_secs_f64(); + if elapsed > 0.0 { + let speed = total_written as f64 / (1024.0 * 1024.0) / elapsed; + max_speed = max_speed.max(speed); + } + last_report = Instant::now(); + } + } + + file.flush().map_err(|e| format!("最终刷新失败: {}", e))?; + let elapsed = start.elapsed().as_secs_f64(); + let final_speed = if elapsed > 0.0 { + total_written as f64 / (1024.0 * 1024.0) / elapsed + } else { + 0.0 + }; + max_speed = max_speed.max(final_speed); + + Ok((total_written, max_speed)) +} diff --git a/src/full_refresh.rs b/src/full_refresh.rs new file mode 100644 index 0000000..161ba6a --- /dev/null +++ b/src/full_refresh.rs @@ -0,0 +1,464 @@ +use std::path::Path; + +use crate::config::format_size; +use crate::file_op; +use crate::log::logger; +use crate::platform; + +/// 全盘刷新管理器 — 实现完整的全盘刷新业务流程 +pub struct FullRefresh; + +impl FullRefresh { + /// 执行全盘刷新业务流程 + /// dir_size: 目录本身占用的空间(始终填充这部分) + /// fill_free: 是否额外填充空闲空间 + pub fn execute(directory: &str, keep_files: bool, fill_free: bool, + dir_size: u64, unit_size_gb: u64, write_buf_kb: u64) { + logger().log("开始全盘刷新业务流程", "INFO"); + + let unit_size = unit_size_gb * 1024u64.pow(3); + let dir_path = Path::new(directory); + + // 1. 获取磁盘统计信息 + let (_total, used, free) = platform::get_disk_space(dir_path); + println!("磁盘信息:"); + println!(" 已使用: {}", format_size(used)); + println!(" 可用: {}", format_size(free)); + + // 2. 备份文件(如果需要) + let backup_dir = if keep_files { + // 备份必须存到与目标不同的盘,否则覆写时会破坏备份 + let backup = match find_backup_drive(directory) { + Some(drive) => drive.join("$aspnmytools"), + None => { + println!("❌ 未找到可用的备份盘(需要与目标不同的硬盘),终止全盘刷新"); + return; + } + }; + + if let Err(e) = std::fs::create_dir_all(&backup) { + println!("❌ 创建备份目录失败: {},终止全盘刷新以保护数据", e); + return; + } + println!("\n正在备份文件到 {}...", backup.display()); + + if backup_files(directory, &backup) { + println!("文件备份成功"); + Some(backup) + } else { + println!("❌ 文件备份失败,终止全盘刷新以保护数据"); + return; + } + } else { + println!("\n用户选择不保留文件,数据将无法恢复"); + None + }; + + // 3. 已备份,删除目标目录中的原始文件 + println!("\n正在删除目标目录中的原始文件..."); + match delete_directory_contents(dir_path) { + Err(e) => { + println!("❌ 删除失败: {}", e); + println!(" 请手动删除目标目录中的文件后再执行全盘刷新"); + return; + } + Ok((success, failure)) => { + if success > 0 || failure > 0 { + println!("删除结果: 成功 {} 个, 失败 {} 个", success, failure); + if failure > 0 { + println!("⚠️ 部分文件删除失败,可能影响填充效果"); + } + } else { + println!("目录为空,无需删除"); + } + println!("原始文件已删除,空间已释放"); + } + } + + // 4. 填充空间(覆写原文件释放的空间 + 可选填充空闲空间) + println!("\n开始填充空间,目录大小: {},空闲空间填充: {}", + format_size(dir_size), if fill_free { "是" } else { "否" }); + let result = fill_available_space(directory, unit_size, dir_size, fill_free, write_buf_kb); + let (cumulative, max_speed) = result; + + // 4.5 删除填充文件,释放空间用于恢复 + let work_dir = dir_path.join("$aspnmytools"); + if work_dir.exists() { + println!("\n正在删除填充文件..."); + match std::fs::remove_dir_all(&work_dir) { + Ok(()) => println!("填充文件已删除"), + Err(e) => println!("⚠️ 删除填充文件失败: {}", e), + } + } + + // 5. 恢复文件(如果需要) + if let Some(ref backup) = backup_dir { + if backup.exists() { + println!("\n正在恢复文件..."); + if restore_files(backup, directory) { + println!("文件恢复成功"); + let _ = std::fs::remove_dir_all(backup); + } else { + println!("文件恢复失败,请手动从 {} 恢复", backup.display()); + } + } + } else if keep_files { + println!("\n⚠️ 未备份文件,无法恢复"); + } + + // 6. 执行最终 TRIM + println!("\n开始执行最终 TRIM 优化..."); + if let Some(device) = platform::resolve_device_name(directory) { + platform::trim_volume(&device); + } + + println!("\n全盘刷新完成!"); + println!("累积写入容量: {}", format_size(cumulative)); + println!("最高写入速度: {:.2} MB/s", max_speed); + } +} + +/// 备份目录中的所有文件到目标路径(保留原始时间戳) +fn backup_files(source: &str, destination: &Path) -> bool { + use std::fs; + use filetime::FileTime; + let src = Path::new(source); + + for entry in walkdir::WalkDir::new(src) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path == src { + continue; + } + + // 跳过系统保护目录 + let p_str = path.to_string_lossy().to_uppercase(); + if p_str.contains("$RECYCLE.BIN") || p_str.contains("SYSTEM VOLUME INFORMATION") { + continue; + } + + let relative = match path.strip_prefix(src) { + Ok(r) => r, + Err(_) => continue, + }; + let dest = destination.join(relative); + + if entry.file_type().is_dir() { + let _ = fs::create_dir_all(&dest); + } else { + if let Some(parent) = dest.parent() { + let _ = fs::create_dir_all(parent); + } + + // 先获取源文件时间戳 + let src_meta = fs::metadata(path).ok(); + + if let Err(e) = fs::copy(path, &dest) { + logger().log( + &format!("备份文件失败 {}: {}", path.display(), e), + "WARNING", + ); + return false; + } + + // 保留时间戳 + if let Some(meta) = src_meta { + if let Ok(mtime) = meta.modified() { + let atime = meta.accessed().unwrap_or(mtime); + let _ = filetime::set_file_times( + &dest, + FileTime::from_system_time(atime), + FileTime::from_system_time(mtime), + ); + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + let ctime_raw = meta.creation_time(); + if ctime_raw > 0 { + let _ = set_file_creation_time_win(&dest, ctime_raw); + } + } + } + } + } + true +} + +/// 从备份目录恢复所有文件到目标路径(保留原始时间戳) +fn restore_files(source: &Path, target: &str) -> bool { + use std::fs; + use filetime::FileTime; + let dst = Path::new(target); + + for entry in walkdir::WalkDir::new(source) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path == source { + continue; + } + + let relative = match path.strip_prefix(source) { + Ok(r) => r, + Err(_) => continue, + }; + let dest = dst.join(relative); + + if entry.file_type().is_dir() { + let _ = fs::create_dir_all(&dest); + // 目录时间戳设为当前最新时间 + let now = std::time::SystemTime::now(); + let _ = filetime::set_file_times( + &dest, + FileTime::from_system_time(now), + FileTime::from_system_time(now), + ); + } else { + if let Some(parent) = dest.parent() { + let _ = fs::create_dir_all(parent); + } + + // 复制文件内容 + if let Err(e) = fs::copy(path, &dest) { + logger().log( + &format!("恢复文件失败 {}: {}", path.display(), e), + "WARNING", + ); + return false; + } + + // 恢复时间戳:设为当前最新时间(冷数据刷新目的) + let now = std::time::SystemTime::now(); + let _ = filetime::set_file_times( + &dest, + FileTime::from_system_time(now), + FileTime::from_system_time(now), + ); + } + } + true +} + +/// 删除目录下所有内容(不删除根目录本身) +/// 返回 (成功数, 失败数) +fn delete_directory_contents(dir: &Path) -> Result<(u64, u64), String> { + let mut success = 0u64; + let mut failure = 0u64; + + // 验证目录存在 + if !dir.exists() { + return Ok((0, 0)); + } + + // 使用 walkdir 递归遍历所有条目(从子到父顺序,确保先删文件再删目录) + let entries: Vec<_> = walkdir::WalkDir::new(dir) + .contents_first(true) // 先处理子条目,再处理目录自身 + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path() != dir) // 跳过根目录本身 + .collect(); + + for entry in entries { + let path = entry.path().to_path_buf(); + + // 跳过系统保护目录 + let p_str = path.to_string_lossy().to_uppercase(); + if p_str.contains("$RECYCLE.BIN") || p_str.contains("SYSTEM VOLUME INFORMATION") { + continue; + } + + // 最多重试 3 次 + let mut last_err = String::new(); + let mut deleted = false; + for _attempt in 0..3 { + // Windows: 先移除只读属性 + remove_readonly(&path); + + let result = if entry.file_type().is_dir() { + std::fs::remove_dir_all(&path) + } else { + std::fs::remove_file(&path) + }; + + match result { + Ok(()) => { + deleted = true; + break; + } + Err(e) => { + last_err = e.to_string(); + // 短暂等待后重试(处理临时占用) + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + } + + if deleted { + success += 1; + } else { + failure += 1; + logger().log( + &format!("删除失败(重试3次后) {}: {}", path.display(), last_err), + "WARNING", + ); + } + } + + if failure > 0 && success == 0 { + Err(format!( + "所有文件删除失败(共 {} 个条目),请检查权限或手动删除", + failure + )) + } else if failure > 0 { + // 部分成功,报告警告但不中断流程 + logger().log( + &format!( + "目录删除部分完成:成功 {} 个,失败 {} 个", + success, failure + ), + "WARNING", + ); + Ok((success, failure)) + } else { + Ok((success, failure)) + } +} + +/// 移除文件的只读属性(Windows 专用,其他平台无操作) +#[cfg(windows)] +fn remove_readonly(path: &Path) { + use std::os::windows::fs::MetadataExt; + const FILE_ATTRIBUTE_READONLY: u32 = 0x0001; + if let Ok(meta) = std::fs::metadata(path) { + if meta.file_attributes() & FILE_ATTRIBUTE_READONLY != 0 { + // 使用 attrib 命令移除只读属性 + let _ = std::process::Command::new("attrib") + .args(["-R", &path.to_string_lossy()]) + .output(); + } + } +} + +/// 非 Windows 平台无操作 +#[cfg(not(windows))] +fn remove_readonly(_path: &Path) {} + +/// 填充空间 +/// dir_size: 目录本身的已用空间(始终填充) +/// fill_free: 填充完目录空间后是否继续填充空闲空间 +/// unit_size: 填充空闲空间时的每文件写入单位 +fn fill_available_space(directory: &str, unit_size: u64, dir_size: u64, + fill_free: bool, buf_kb: u64) -> (u64, f64) { + let work_dir = Path::new(directory).join("$aspnmytools"); + let _ = std::fs::create_dir_all(&work_dir); + + let mut cumulative = 0u64; + let mut max_speed = 0.0f64; + let mut file_count = 0u64; + + // 每个填充文件的大小:填充空闲空间时用 unit_size,仅填充目录时用 dir_size 或 50GB + let file_target = if fill_free { + unit_size + } else { + (50u64 * 1024u64.pow(3)).min(dir_size.max(1)) + }; + + loop { + let (_total, _used, free) = platform::get_disk_space(Path::new(directory)); + + // 判断是否停止填充 + if fill_free { + // 填充空闲空间模式:直到磁盘剩余空间 < unit_size + if free < unit_size { + break; + } + } else { + // 仅填充目录空间模式:填充够 dir_size 就停 + if cumulative >= dir_size { + break; + } + } + + let file_path = work_dir.join(format!("refresh_{}.dat", file_count)); + + match file_op::continuous_full_refresh(&file_path, file_target, buf_kb) { + Ok((written, speed)) => { + cumulative += written; + if speed > max_speed { + max_speed = speed; + } + file_count += 1; + print!( + "\r已创建 {} 个文件, 累积写入: {}", + file_count, + format_size(cumulative) + ); + } + Err(e) => { + println!("\n写入失败: {}", e); + break; + } + } + } + + println!(); + (cumulative, max_speed) +} + +/// 查找可用于备份的盘符(与目标盘不同,优先 D:) +fn find_backup_drive(target_path: &str) -> Option { + let target_drive = if target_path.len() >= 2 && target_path.as_bytes()[1] == b':' { + target_path[..1].to_uppercase() + } else { + return None; // Linux 下暂不支持自动查找 + }; + + let mut candidates: Vec = Vec::new(); + + // 扫描 A:-Z: 所有盘符 + for letter in 'A'..='Z' { + let drive = letter.to_string(); + if drive == target_drive { + continue; // 跳过目标盘 + } + + let path = format!("{}:\\", drive); + if std::fs::metadata(&path).is_ok() { + candidates.push(drive); + } + } + + // 优先选 D:,否则取第一个可用盘 + if candidates.contains(&"D".to_string()) { + Some(std::path::PathBuf::from(r"D:\")) + } else { + candidates.first().map(|d| std::path::PathBuf::from(format!("{}:\\", d))) + } +} + +/// Windows 下使用 Win32 API 设置文件创建时间 +#[cfg(windows)] +fn set_file_creation_time_win(path: &std::path::Path, creation_time_raw: u64) -> Result<(), String> { + use std::os::windows::io::AsRawHandle; + use windows_sys::Win32::Storage::FileSystem::SetFileTime; + + let file = std::fs::OpenOptions::new() + .write(true) + .open(path) + .map_err(|e| format!("无法打开文件 {}: {}", path.display(), e))?; + + let handle = file.as_raw_handle() as windows_sys::Win32::Foundation::HANDLE; + let ft: *const windows_sys::Win32::Foundation::FILETIME = + &creation_time_raw as *const u64 as *const windows_sys::Win32::Foundation::FILETIME; + + let result = unsafe { SetFileTime(handle, ft, std::ptr::null(), std::ptr::null()) }; + if result == 0 { + Err(format!("设置创建时间失败: {}", path.display())) + } else { + Ok(()) + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..2d97c98 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,117 @@ +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::sync::Mutex; + +use chrono::Local; + +use crate::config; + +/// 线程安全的日志管理器 +#[allow(dead_code)] +pub struct Logger { + error_log: Mutex<()>, + corrupted_log: Mutex<()>, +} + +impl Logger { + pub fn new() -> Self { + Self { + error_log: Mutex::new(()), + corrupted_log: Mutex::new(()), + } + } + + /// 确保日志目录存在 + pub fn ensure_dirs(&self) { + let cfg = config::config(); + if let Some(parent) = cfg.error_log.parent() { + let _ = fs::create_dir_all(parent); + } + if let Some(parent) = cfg.corrupted_log.parent() { + let _ = fs::create_dir_all(parent); + } + } + + /// 记录操作日志 + pub fn log(&self, message: &str, level: &str) { + let _lock = self.error_log.lock().unwrap_or_else(|e| e.into_inner()); + self.ensure_dirs(); + + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let entry = format!("[{}] [{}] {}\n", timestamp, level, message); + + if let Err(e) = append_file(&config::config().error_log, &entry) { + eprintln!("日志写入失败: {}", e); + } + } + + /// 记录损坏文件(预留) + #[allow(dead_code)] + pub fn log_corrupted(&self, path: &str, error_type: &str, error_msg: &str) { + let _lock = self.corrupted_log.lock().unwrap_or_else(|e| e.into_inner()); + self.ensure_dirs(); + + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); + let entry = format!("{}|{}|{}|{}\n", timestamp, path, error_type, error_msg); + + if let Err(e) = append_file(&config::config().corrupted_log, &entry) { + eprintln!("损坏文件日志写入失败: {}", e); + self.log(&format!("无法记录损坏文件: {}, 错误: {}", path, e), "ERROR"); + } + } + + /// 保存操作摘要(JSON 格式) + #[allow(clippy::too_many_arguments)] + pub fn save_summary( + &self, + scanned: u64, + processed: u64, + corrupted: u64, + large: u64, + medium: u64, + small: u64, + speed: f64, + duration_secs: f64, + ) { + let _lock = self.error_log.lock().unwrap_or_else(|e| e.into_inner()); + self.ensure_dirs(); + + let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S"); + let summary = serde_json::json!({ + "timestamp": timestamp.to_string(), + "duration_seconds": (duration_secs * 100.0).round() / 100.0, + "stats": { + "scanned": scanned, + "processed": processed, + "corrupted": corrupted, + "large": large, + "medium": medium, + "small": small, + "final_speed": (speed * 100.0).round() / 100.0, + } + }); + + let entry = summary.to_string() + "\n"; + if let Err(e) = append_file(&config::config().log_file, &entry) { + eprintln!("摘要保存失败: {}", e); + } + } +} + +fn append_file(path: &std::path::Path, content: &str) -> std::io::Result<()> { + let mut file = OpenOptions::new().create(true).append(true).open(path)?; + file.write_all(content.as_bytes())?; + file.flush()?; + Ok(()) +} + +use std::sync::OnceLock; +static LOGGER: OnceLock = OnceLock::new(); + +pub fn logger() -> &'static Logger { + LOGGER.get_or_init(|| { + let log = Logger::new(); + log.ensure_dirs(); + log + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0e89a9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,61 @@ +use clap::Parser; + +mod app; +mod config; +mod dashboard; +mod file_op; +mod full_refresh; +mod log; +mod platform; +mod terminal; + +#[derive(Parser, Debug)] +#[command( + name = "coldatafresh", + version = "5.0.0", + about = "冷数据维护工具 - 优化SSD性能,延长使用寿命" +)] +struct Args { + /// 目标目录路径 + #[arg(short = 'p', long, default_value = ".")] + path: String, + + /// 数据年龄阈值(天),超过此值的文件将被刷新 + #[arg(short = 'a', long)] + age: Option, + + /// 是否跳过小于指定大小的文件(MB) + #[arg(short = 's', long)] + skip_smaller: Option, + + /// 是否执行全盘刷新 + #[arg(short = 'f', long)] + full_refresh: bool, + + /// 是否执行TRIM操作 + #[arg(short = 't', long)] + trim: bool, + + /// 是否启用详细日志 + #[arg(short = 'v', long)] + verbose: bool, +} + +fn main() { + // 解析命令行参数(先解析,再副作用) + let args = Args::parse(); + + // 初始化日志系统 + let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .try_init(); + + // 设置窗口标题 + terminal::Terminal::set_window_title("冷数据维护工具 v5.0.0"); + + // 初始化日志器 + log::logger(); + + // 启动应用程序 + let mut app = app::App::new(); + app.run(args.full_refresh, args.trim, args.age, args.skip_smaller); +} diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..fcfdd39 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,362 @@ +use std::path::Path; +use std::process::Command; + +use crate::log::logger; + +// ─── Windows 专有 ───────────────────────────────────────────── + +/// 判断路径是否为盘符(Windows: C: 或 C:\;Linux: / 或 /mnt 等根目录) +pub fn is_root_path(path: &str) -> bool { + let p = path.trim_end_matches(&['\\', '/', ' ', '\t'][..]); + #[cfg(windows)] + { + // 匹配 "C:" 或 "C:\" 或 "C:/" + if p.len() >= 2 && p.as_bytes()[1] == b':' { + return p.len() == 2 + || (p.len() == 3 && (p.as_bytes()[2] == b'\\' || p.as_bytes()[2] == b'/')); + } + false + } + #[cfg(unix)] + { + // 检查是否为挂载点根(简化:路径以 / 开头且只有一级) + p == "/" || p.matches('/').count() <= 1 + } + #[cfg(not(any(windows, unix)))] + { + false + } +} + +/// 解析路径中的设备标识符 +/// Windows: 返回 "C"(盘符大写字母) +/// Linux: 返回挂载点路径,如 "/mnt/data" +pub fn resolve_device_name(path: &str) -> Option { + let p = path.trim_end_matches(&['\\', '/', ' ', '\t'][..]); + + #[cfg(windows)] + { + if p.len() >= 2 && p.as_bytes()[1] == b':' { + let letter = p.chars().next()?; + Some(letter.to_ascii_uppercase().to_string()) + } else { + None + } + } + #[cfg(unix)] + { + if p.starts_with('/') { + // 尝试从 /proc/mounts 查找最长前缀匹配的挂载点 + resolve_best_mount_point(p) + } else { + None + } + } + #[cfg(not(any(windows, unix)))] + { + None + } +} + +/// 获取磁盘空间信息 返回 (总字节, 已用字节, 可用字节) +pub fn get_disk_space(path: &Path) -> (u64, u64, u64) { + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let p = path.to_string_lossy(); + let root = if p.len() >= 2 && p.as_bytes()[1] == b':' { + let drive = &p[..2]; + drive.to_string() + "\\" + } else { + "C:\\".to_string() + }; + + let wide: Vec = std::ffi::OsStr::new(&root) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + let mut free_bytes: u64 = 0; + let mut total_bytes: u64 = 0; + let ret = windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW( + wide.as_ptr(), + std::ptr::null_mut(), + &mut total_bytes, + &mut free_bytes, + ); + if ret != 0 { + ( + total_bytes, + total_bytes.saturating_sub(free_bytes), + free_bytes, + ) + } else { + (0, 0, 0) + } + } + } + #[cfg(unix)] + { + unsafe { + let p_cstr = + std::ffi::CString::new(path.to_string_lossy().as_bytes()).unwrap_or_default(); + let mut stat: libc::statvfs = std::mem::zeroed(); + if libc::statvfs(p_cstr.as_ptr(), &mut stat) == 0 { + let total = (stat.f_frsize as u64).saturating_mul(stat.f_blocks as u64); + let free = (stat.f_frsize as u64).saturating_mul(stat.f_bfree as u64); + let used = total.saturating_sub(free); + (total, used, free) + } else { + (0, 0, 0) + } + } + } + #[cfg(not(any(windows, unix)))] + { + let _ = path; + (0, 0, 0) + } +} + +/// 检查当前进程是否以管理员权限运行 +pub fn is_admin() -> bool { + #[cfg(windows)] + { + match Command::new("powershell") + .args(["-Command", "[bool]([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))"]) + .output() + { + Ok(output) => { + let s = String::from_utf8_lossy(&output.stdout); + s.trim() == "True" + } + Err(_) => false, + } + } + #[cfg(unix)] + { + unsafe { libc::geteuid() == 0 } + } + #[cfg(not(any(windows, unix)))] + { + false + } +} + +/// 执行 TRIM 操作 +/// Windows: 通过 PowerShell Optimize-Volume +/// Linux: 通过 fstrim 命令 +pub fn trim_volume(device: &str) -> bool { + logger().log(&format!("开始对 {} 执行 TRIM 操作", device), "INFO"); + + #[cfg(windows)] + { + let is_win11 = get_windows_build() >= 22000; + let is_win10 = get_windows_build() >= 10240; + + if is_win11 || is_win10 { + logger().log( + &format!( + "在 Windows {} 上对 {} 执行 TRIM 操作", + if is_win11 { "11" } else { "10" }, + device + ), + "INFO", + ); + + if is_win11 { + if !run_optimize_volume(device, "ReTrim") { + logger().log("ReTrim 操作失败", "WARNING"); + return false; + } + let _ = run_optimize_volume(device, "SlabConsolidate"); + let _ = run_optimize_volume(device, "ReTrim"); + } else if !run_optimize_volume(device, "ReTrim") { + logger().log("ReTrim 操作失败", "WARNING"); + return false; + } + true + } else { + logger().log("Windows 10 以下系统,不支持 PowerShell TRIM", "WARNING"); + false + } + } + #[cfg(unix)] + { + // Linux: 使用 fstrim 命令 + match Command::new("fstrim").arg(device).output() { + Ok(output) => { + if output.status.success() { + let msg = String::from_utf8_lossy(&output.stdout); + logger().log(&format!("fstrim 成功: {}", msg.trim()), "INFO"); + true + } else { + let err = String::from_utf8_lossy(&output.stderr); + logger().log(&format!("fstrim 失败: {}", err.trim()), "WARNING"); + false + } + } + Err(e) => { + logger().log(&format!("执行 fstrim 失败: {}", e), "ERROR"); + false + } + } + } + #[cfg(not(any(windows, unix)))] + { + let _ = device; + logger().log("当前平台不支持 TRIM 操作", "WARNING"); + false + } +} + +/// 获取操作系统信息(用于日志和显示) +pub fn get_os_display() -> String { + #[cfg(windows)] + { + let build = get_windows_build(); + if build >= 22000 { + format!("Windows 11 (build {})", build) + } else if build >= 10240 { + format!("Windows 10 (build {})", build) + } else if build > 0 { + format!("Windows (build {})", build) + } else { + "Windows".to_string() + } + } + #[cfg(unix)] + { + // 读取 /etc/os-release + if let Ok(content) = std::fs::read_to_string("/etc/os-release") { + for line in content.lines() { + if let Some(val) = line.strip_prefix("PRETTY_NAME=\"") { + return val.trim_end_matches('"').to_string(); + } else if let Some(val) = line.strip_prefix("PRETTY_NAME=") { + return val.to_string(); + } + } + } + "Linux".to_string() + } + #[cfg(not(any(windows, unix)))] + { + "Unknown".to_string() + } +} + +// ─── Windows 内部辅助 ───────────────────────────────────────── + +#[cfg(windows)] +fn get_windows_build() -> u32 { + use std::sync::OnceLock; + static BUILD: OnceLock = OnceLock::new(); + *BUILD.get_or_init(|| match Command::new("cmd").args(["/c", "ver"]).output() { + Ok(output) => { + let s = String::from_utf8_lossy(&output.stdout); + if let Some(ver_part) = s.split('[').nth(1) { + let ver = ver_part.trim_end_matches(']').trim(); + if let Some(build_str) = ver.rsplit('.').next() { + return build_str.trim().parse().unwrap_or(0); + } + } + 0 + } + Err(_) => 0, + }) +} + +#[cfg(windows)] +fn run_optimize_volume(drive: &str, operation: &str) -> bool { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + + let cmd = format!( + "Optimize-Volume -DriveLetter {} -{} -Verbose", + drive, operation + ); + logger().log(&format!("执行 PowerShell: {}", cmd), "INFO"); + + match Command::new("powershell") + .args(["-Command", &cmd]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + Ok(output) => { + if output.status.success() { + logger().log( + &format!( + "{} 操作成功: {}", + operation, + String::from_utf8_lossy(&output.stdout).trim() + ), + "INFO", + ); + true + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + logger().log(&format!("{} 操作失败: {}", operation, stderr), "WARNING"); + false + } + } + Err(e) => { + logger().log(&format!("执行 PowerShell 失败: {}", e), "ERROR"); + false + } + } +} + +// ─── Linux 内部辅助 ─────────────────────────────────────────── + +#[cfg(unix)] +/// 从 /proc/mounts 查找路径的最长前缀挂载点 +fn resolve_best_mount_point(path: &str) -> Option { + let mounts = std::fs::read_to_string("/proc/mounts").ok()?; + let mut best: Option = None; + + for line in mounts.lines() { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 2 { + continue; + } + // 解码转义序列(/proc/mounts 中使用 \040 表示空格等) + let mount_point = decode_mount_point(fields[1]); + + if path.starts_with(&mount_point) { + let should_replace = match best.as_ref() { + None => true, + Some(current) => mount_point.len() > current.len(), + }; + if should_replace { + best = Some(mount_point); + } + } + } + + best +} + +#[cfg(unix)] +/// 解码 /proc/mounts 中的转义序列 +fn decode_mount_point(encoded: &str) -> String { + let mut result = String::new(); + let mut chars = encoded.chars().peekable(); + while let Some(c) = chars.next() { + if c == '\\' { + let digits: String = chars.by_ref().take(3).collect(); + if digits.len() == 3 { + if let Ok(code) = u32::from_str_radix(&digits, 8) { + result.push(char::from_u32(code).unwrap_or('?')); + continue; + } + } + result.push('\\'); + result.push_str(&digits); + } else { + result.push(c); + } + } + result +} diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..41c910b --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,131 @@ +use std::sync::OnceLock; + +/// 终端管理器 — 处理彩色输出、ASCII回退模式、窗口标题 +pub struct Terminal; + +impl Terminal { + /// 检测终端是否支持 UTF-8/ANSI(否则回退到纯 ASCII) + fn use_ascii_fallback() -> bool { + #[cfg(windows)] + { + match std::env::var("WT_SESSION") { + Ok(_) => false, // Windows Terminal — 支持 UTF-8 + Err(_) => { + let cp = unsafe { windows_sys::Win32::System::Console::GetConsoleOutputCP() }; + cp != 65001 // 非 UTF-8 代码页 → 回退 ASCII + } + } + } + #[cfg(not(windows))] + false + } + + /// 是否使用 ASCII 回退(惰性初始化) + pub fn is_safe() -> bool { + static ASCII_FALLBACK: OnceLock = OnceLock::new(); + *ASCII_FALLBACK.get_or_init(Self::use_ascii_fallback) + } + + /// 清除屏幕(支持 ANSI 和 Windows API 双重方式) + pub fn clear() { + if !Self::is_safe() { + // ANSI 转义序列 — Windows Terminal / VT 已启用 + print!("\x1B[2J\x1B[H"); + } else { + // cmd.exe 非 UTF-8 代码页:使用 Windows 控制台 API 清屏 + #[cfg(windows)] + unsafe { + use windows_sys::Win32::System::Console::{ + FillConsoleOutputAttribute, FillConsoleOutputCharacterW, + GetConsoleScreenBufferInfo, GetStdHandle, SetConsoleCursorPosition, + CONSOLE_SCREEN_BUFFER_INFO, COORD, STD_OUTPUT_HANDLE, + }; + let handle = GetStdHandle(STD_OUTPUT_HANDLE); + if handle.is_null() || handle as isize == -1 { + return; + } + let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed(); + if GetConsoleScreenBufferInfo(handle, &mut info) == 0 { + return; + } + let chars_to_write = (info.dwSize.X as u32) * (info.dwSize.Y as u32); + let start: COORD = COORD { X: 0, Y: 0 }; + let mut written: u32 = 0; + + FillConsoleOutputCharacterW(handle, 0x20, chars_to_write, start, &mut written); + FillConsoleOutputAttribute( + handle, + info.wAttributes, + chars_to_write, + start, + &mut written, + ); + SetConsoleCursorPosition(handle, start); + } + #[cfg(not(windows))] + { + // 非 Windows 平台回退方式 + print!("\x1B[2J\x1B[H"); + } + } + // 确保 stdout 刷新 + use std::io::Write; + let _ = std::io::stdout().flush(); + } + + /// 生成带 ANSI 颜色的文本 + pub fn colored(text: &str, fg: u8, bg: u8) -> String { + if Self::is_safe() { + text.to_string() + } else { + format!("\x1B[{};{}m{}\x1B[0m", fg, bg, text) + } + } + + /// 带前景色 + pub fn fg(text: &str, fg: u8) -> String { + if Self::is_safe() { + text.to_string() + } else { + format!("\x1B[{}m{}\x1B[0m", fg, text) + } + } + + /// 设置控制台窗口标题 + pub fn set_window_title(title: &str) { + #[cfg(windows)] + { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + let wide: Vec = OsStr::new(title) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + unsafe { + windows_sys::Win32::System::Console::SetConsoleTitleW(wide.as_ptr()); + } + } + #[cfg(not(windows))] + { + print!("\x1B]0;{}\x07", title); + } + } +} + +/// 边框字符选择 +pub fn border_chars() -> (&'static str, &'static str, &'static str) { + if Terminal::is_safe() { + ("=", "|", "=") + } else { + ("═", "│", "═") + } +} + +/// 进度条字符 +pub fn progress_chars() -> (&'static str, &'static str) { + if Terminal::is_safe() { + ("#", "-") + } else { + ("▓", "░") + } +}