├── .cargo └── config ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── build.rs ├── compact.ico ├── hashfilter ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs └── src ├── backend.rs ├── background.rs ├── compact.rs ├── compression.rs ├── config.rs ├── console.rs ├── folder.rs ├── gui.rs ├── main.rs ├── persistence.rs └── ui ├── app.js ├── cash.min.js ├── index.html └── style.css /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-Ctarget-feature=+crt-static"] 3 | [target.i686-pc-windows-msvc] 4 | rustflags = ["-Ctarget-feature=+crt-static"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.10.1] - 2020-12-22 4 | 5 | ### Fixed 6 | 7 | - Avoid high CPU usage in GUI loop ([#42]) 8 | 9 | ## [0.10.0] - 2020-12-19 10 | 11 | ### Changed 12 | 13 | - Update dependencies 14 | - Minor UI tweaks due to changes in DPI handling 15 | - Migrate to new dialog crates 16 | - More small internal improvements by @Dr-Emann, thanks! ([#30], [#32]) 17 | 18 | ### Fixed 19 | 20 | - Exclusively lock files prior to compaction (should fix [#40], thanks @A-H-M) 21 | 22 | ## [0.9.0] - 2020-03-03 23 | 24 | ### Added 25 | 26 | - Preserve file timestamps following compression/decompression ([#16]) 27 | 28 | ## [0.8.0] - 2020-02-29 29 | 30 | ### Added 31 | 32 | - Excluded directories now get skipped entirely ([#8]) 33 | 34 | ### Changed 35 | 36 | - Paused jobs no longer poll ([#10], @Dr-Emann) 37 | - Less refcounting ([#9], @Dr-Emann) 38 | 39 | ### Fixed 40 | 41 | - Tests ([#11], @Dr-Emann) 42 | 43 | ### Removed 44 | 45 | - WofUtil.dll version check ([#6]) 46 | 47 | ## [0.7.1] - 2019-07-17 48 | 49 | ### Added 50 | 51 | - Initial release 52 | 53 | [0.7.1]: https://github.com/Freaky/Compactor/releases/tag/v0.7.1 54 | [0.8.0]: https://github.com/Freaky/Compactor/releases/tag/v0.8.0 55 | [0.9.0]: https://github.com/Freaky/Compactor/releases/tag/v0.9.0 56 | [0.10.0]: https://github.com/Freaky/Compactor/releases/tag/v0.10.0 57 | [0.10.1]: https://github.com/Freaky/Compactor/releases/tag/v0.10.1 58 | [#6]: https://github.com/Freaky/Compactor/issues/6 59 | [#8]: https://github.com/Freaky/Compactor/issues/8 60 | [#9]: https://github.com/Freaky/Compactor/pull/9 61 | [#10]: https://github.com/Freaky/Compactor/pull/10 62 | [#11]: https://github.com/Freaky/Compactor/pull/11 63 | [#16]: https://github.com/Freaky/Compactor/issues/16 64 | [#30]: https://github.com/Freaky/Compactor/pull/30 65 | [#32]: https://github.com/Freaky/Compactor/pull/32 66 | [#40]: https://github.com/Freaky/Compactor/issues/40 67 | [#42]: https://github.com/Freaky/Compactor/issues/42 -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "addr2line" 5 | version = "0.14.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" 8 | dependencies = [ 9 | "gimli", 10 | ] 11 | 12 | [[package]] 13 | name = "adler" 14 | version = "0.2.3" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" 17 | 18 | [[package]] 19 | name = "aho-corasick" 20 | version = "0.7.15" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 23 | dependencies = [ 24 | "memchr", 25 | ] 26 | 27 | [[package]] 28 | name = "arrayref" 29 | version = "0.3.6" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 32 | 33 | [[package]] 34 | name = "arrayvec" 35 | version = "0.5.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 38 | 39 | [[package]] 40 | name = "atk-sys" 41 | version = "0.10.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" 44 | dependencies = [ 45 | "glib-sys", 46 | "gobject-sys", 47 | "libc", 48 | "system-deps", 49 | ] 50 | 51 | [[package]] 52 | name = "autocfg" 53 | version = "1.0.1" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 56 | 57 | [[package]] 58 | name = "backtrace" 59 | version = "0.3.55" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" 62 | dependencies = [ 63 | "addr2line", 64 | "cfg-if 1.0.0", 65 | "libc", 66 | "miniz_oxide", 67 | "object", 68 | "rustc-demangle", 69 | ] 70 | 71 | [[package]] 72 | name = "base64" 73 | version = "0.13.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 76 | 77 | [[package]] 78 | name = "bitflags" 79 | version = "1.2.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 82 | 83 | [[package]] 84 | name = "blake2b_simd" 85 | version = "0.5.11" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 88 | dependencies = [ 89 | "arrayref", 90 | "arrayvec", 91 | "constant_time_eq", 92 | ] 93 | 94 | [[package]] 95 | name = "boxfnonce" 96 | version = "0.1.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" 99 | 100 | [[package]] 101 | name = "bstr" 102 | version = "0.2.14" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" 105 | dependencies = [ 106 | "memchr", 107 | ] 108 | 109 | [[package]] 110 | name = "cairo-sys-rs" 111 | version = "0.10.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" 114 | dependencies = [ 115 | "libc", 116 | "system-deps", 117 | ] 118 | 119 | [[package]] 120 | name = "cc" 121 | version = "1.0.66" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" 124 | 125 | [[package]] 126 | name = "cfg-if" 127 | version = "0.1.10" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 130 | 131 | [[package]] 132 | name = "cfg-if" 133 | version = "1.0.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 136 | 137 | [[package]] 138 | name = "chrono" 139 | version = "0.4.19" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 142 | dependencies = [ 143 | "libc", 144 | "num-integer", 145 | "num-traits", 146 | "time", 147 | "winapi", 148 | ] 149 | 150 | [[package]] 151 | name = "compactor" 152 | version = "0.10.1" 153 | dependencies = [ 154 | "backtrace", 155 | "compresstimator", 156 | "crossbeam-channel", 157 | "ctrlc", 158 | "directories", 159 | "dirs-sys", 160 | "filesize", 161 | "filetime", 162 | "fs2", 163 | "glob", 164 | "globset", 165 | "hashfilter", 166 | "humansize", 167 | "lazy_static", 168 | "open", 169 | "serde", 170 | "serde_derive", 171 | "serde_json", 172 | "siphasher", 173 | "tempdir", 174 | "tinyfiledialogs", 175 | "vergen", 176 | "walkdir", 177 | "web-view", 178 | "wfd", 179 | "winapi", 180 | "winres", 181 | ] 182 | 183 | [[package]] 184 | name = "compresstimator" 185 | version = "0.1.0" 186 | source = "git+https://github.com/Freaky/compresstimator.git?rev=26ddd3f499bc46f2c8b3ce814e9723ed41b47919#26ddd3f499bc46f2c8b3ce814e9723ed41b47919" 187 | dependencies = [ 188 | "lz4", 189 | ] 190 | 191 | [[package]] 192 | name = "constant_time_eq" 193 | version = "0.1.5" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 196 | 197 | [[package]] 198 | name = "crossbeam-channel" 199 | version = "0.5.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" 202 | dependencies = [ 203 | "cfg-if 1.0.0", 204 | "crossbeam-utils", 205 | ] 206 | 207 | [[package]] 208 | name = "crossbeam-utils" 209 | version = "0.8.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 212 | dependencies = [ 213 | "autocfg", 214 | "cfg-if 1.0.0", 215 | "lazy_static", 216 | ] 217 | 218 | [[package]] 219 | name = "ctrlc" 220 | version = "3.1.7" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "b57a92e9749e10f25a171adcebfafe72991d45e7ec2dcb853e8f83d9dafaeb08" 223 | dependencies = [ 224 | "nix", 225 | "winapi", 226 | ] 227 | 228 | [[package]] 229 | name = "directories" 230 | version = "2.0.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" 233 | dependencies = [ 234 | "cfg-if 0.1.10", 235 | "dirs-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "dirs-sys" 240 | version = "0.3.5" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 243 | dependencies = [ 244 | "libc", 245 | "redox_users", 246 | "winapi", 247 | ] 248 | 249 | [[package]] 250 | name = "filesize" 251 | version = "0.2.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" 254 | dependencies = [ 255 | "winapi", 256 | ] 257 | 258 | [[package]] 259 | name = "filetime" 260 | version = "0.2.13" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" 263 | dependencies = [ 264 | "cfg-if 1.0.0", 265 | "libc", 266 | "redox_syscall", 267 | "winapi", 268 | ] 269 | 270 | [[package]] 271 | name = "fnv" 272 | version = "1.0.7" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 275 | 276 | [[package]] 277 | name = "fs2" 278 | version = "0.4.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" 281 | dependencies = [ 282 | "libc", 283 | "winapi", 284 | ] 285 | 286 | [[package]] 287 | name = "fuchsia-cprng" 288 | version = "0.1.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 291 | 292 | [[package]] 293 | name = "gdk-pixbuf-sys" 294 | version = "0.10.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" 297 | dependencies = [ 298 | "gio-sys", 299 | "glib-sys", 300 | "gobject-sys", 301 | "libc", 302 | "system-deps", 303 | ] 304 | 305 | [[package]] 306 | name = "gdk-sys" 307 | version = "0.10.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" 310 | dependencies = [ 311 | "cairo-sys-rs", 312 | "gdk-pixbuf-sys", 313 | "gio-sys", 314 | "glib-sys", 315 | "gobject-sys", 316 | "libc", 317 | "pango-sys", 318 | "pkg-config", 319 | "system-deps", 320 | ] 321 | 322 | [[package]] 323 | name = "getrandom" 324 | version = "0.1.15" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 327 | dependencies = [ 328 | "cfg-if 0.1.10", 329 | "libc", 330 | "wasi 0.9.0+wasi-snapshot-preview1", 331 | ] 332 | 333 | [[package]] 334 | name = "gimli" 335 | version = "0.23.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" 338 | 339 | [[package]] 340 | name = "gio-sys" 341 | version = "0.10.1" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" 344 | dependencies = [ 345 | "glib-sys", 346 | "gobject-sys", 347 | "libc", 348 | "system-deps", 349 | "winapi", 350 | ] 351 | 352 | [[package]] 353 | name = "glib-sys" 354 | version = "0.10.1" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" 357 | dependencies = [ 358 | "libc", 359 | "system-deps", 360 | ] 361 | 362 | [[package]] 363 | name = "glob" 364 | version = "0.3.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 367 | 368 | [[package]] 369 | name = "globset" 370 | version = "0.4.6" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" 373 | dependencies = [ 374 | "aho-corasick", 375 | "bstr", 376 | "fnv", 377 | "log", 378 | "regex", 379 | ] 380 | 381 | [[package]] 382 | name = "gobject-sys" 383 | version = "0.10.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" 386 | dependencies = [ 387 | "glib-sys", 388 | "libc", 389 | "system-deps", 390 | ] 391 | 392 | [[package]] 393 | name = "gtk-sys" 394 | version = "0.10.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" 397 | dependencies = [ 398 | "atk-sys", 399 | "cairo-sys-rs", 400 | "gdk-pixbuf-sys", 401 | "gdk-sys", 402 | "gio-sys", 403 | "glib-sys", 404 | "gobject-sys", 405 | "libc", 406 | "pango-sys", 407 | "system-deps", 408 | ] 409 | 410 | [[package]] 411 | name = "hashfilter" 412 | version = "0.1.0" 413 | dependencies = [ 414 | "fs2", 415 | "siphasher", 416 | "tempdir", 417 | ] 418 | 419 | [[package]] 420 | name = "heck" 421 | version = "0.3.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 424 | dependencies = [ 425 | "unicode-segmentation", 426 | ] 427 | 428 | [[package]] 429 | name = "humansize" 430 | version = "1.1.0" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" 433 | 434 | [[package]] 435 | name = "itoa" 436 | version = "0.4.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 439 | 440 | [[package]] 441 | name = "javascriptcore-rs-sys" 442 | version = "0.2.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "3f46ada8a08dcd75a10afae872fbfb51275df4a8ae0d46b8cc7c708f08dd2998" 445 | dependencies = [ 446 | "libc", 447 | ] 448 | 449 | [[package]] 450 | name = "lazy_static" 451 | version = "1.4.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 454 | 455 | [[package]] 456 | name = "libc" 457 | version = "0.2.81" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" 460 | 461 | [[package]] 462 | name = "log" 463 | version = "0.4.11" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 466 | dependencies = [ 467 | "cfg-if 0.1.10", 468 | ] 469 | 470 | [[package]] 471 | name = "lz4" 472 | version = "1.23.2" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "aac20ed6991e01bf6a2e68cc73df2b389707403662a8ba89f68511fb340f724c" 475 | dependencies = [ 476 | "libc", 477 | "lz4-sys", 478 | ] 479 | 480 | [[package]] 481 | name = "lz4-sys" 482 | version = "1.9.2" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "dca79aa95d8b3226213ad454d328369853be3a1382d89532a854f4d69640acae" 485 | dependencies = [ 486 | "cc", 487 | "libc", 488 | ] 489 | 490 | [[package]] 491 | name = "memchr" 492 | version = "2.3.4" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 495 | 496 | [[package]] 497 | name = "miniz_oxide" 498 | version = "0.4.3" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" 501 | dependencies = [ 502 | "adler", 503 | "autocfg", 504 | ] 505 | 506 | [[package]] 507 | name = "nix" 508 | version = "0.18.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" 511 | dependencies = [ 512 | "bitflags", 513 | "cc", 514 | "cfg-if 0.1.10", 515 | "libc", 516 | ] 517 | 518 | [[package]] 519 | name = "num-integer" 520 | version = "0.1.44" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 523 | dependencies = [ 524 | "autocfg", 525 | "num-traits", 526 | ] 527 | 528 | [[package]] 529 | name = "num-traits" 530 | version = "0.2.14" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 533 | dependencies = [ 534 | "autocfg", 535 | ] 536 | 537 | [[package]] 538 | name = "object" 539 | version = "0.22.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" 542 | 543 | [[package]] 544 | name = "open" 545 | version = "1.4.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "7c283bf0114efea9e42f1a60edea9859e8c47528eae09d01df4b29c1e489cc48" 548 | dependencies = [ 549 | "winapi", 550 | ] 551 | 552 | [[package]] 553 | name = "pango-sys" 554 | version = "0.10.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" 557 | dependencies = [ 558 | "glib-sys", 559 | "gobject-sys", 560 | "libc", 561 | "system-deps", 562 | ] 563 | 564 | [[package]] 565 | name = "pkg-config" 566 | version = "0.3.19" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 569 | 570 | [[package]] 571 | name = "proc-macro2" 572 | version = "1.0.24" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 575 | dependencies = [ 576 | "unicode-xid", 577 | ] 578 | 579 | [[package]] 580 | name = "quote" 581 | version = "1.0.7" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 584 | dependencies = [ 585 | "proc-macro2", 586 | ] 587 | 588 | [[package]] 589 | name = "rand" 590 | version = "0.4.6" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 593 | dependencies = [ 594 | "fuchsia-cprng", 595 | "libc", 596 | "rand_core 0.3.1", 597 | "rdrand", 598 | "winapi", 599 | ] 600 | 601 | [[package]] 602 | name = "rand_core" 603 | version = "0.3.1" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 606 | dependencies = [ 607 | "rand_core 0.4.2", 608 | ] 609 | 610 | [[package]] 611 | name = "rand_core" 612 | version = "0.4.2" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 615 | 616 | [[package]] 617 | name = "rdrand" 618 | version = "0.4.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 621 | dependencies = [ 622 | "rand_core 0.3.1", 623 | ] 624 | 625 | [[package]] 626 | name = "redox_syscall" 627 | version = "0.1.57" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 630 | 631 | [[package]] 632 | name = "redox_users" 633 | version = "0.3.5" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 636 | dependencies = [ 637 | "getrandom", 638 | "redox_syscall", 639 | "rust-argon2", 640 | ] 641 | 642 | [[package]] 643 | name = "regex" 644 | version = "1.4.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" 647 | dependencies = [ 648 | "aho-corasick", 649 | "memchr", 650 | "regex-syntax", 651 | "thread_local", 652 | ] 653 | 654 | [[package]] 655 | name = "regex-syntax" 656 | version = "0.6.21" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" 659 | 660 | [[package]] 661 | name = "remove_dir_all" 662 | version = "0.5.3" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 665 | dependencies = [ 666 | "winapi", 667 | ] 668 | 669 | [[package]] 670 | name = "rust-argon2" 671 | version = "0.8.3" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 674 | dependencies = [ 675 | "base64", 676 | "blake2b_simd", 677 | "constant_time_eq", 678 | "crossbeam-utils", 679 | ] 680 | 681 | [[package]] 682 | name = "rustc-demangle" 683 | version = "0.1.18" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" 686 | 687 | [[package]] 688 | name = "ryu" 689 | version = "1.0.5" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 692 | 693 | [[package]] 694 | name = "same-file" 695 | version = "1.0.6" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 698 | dependencies = [ 699 | "winapi-util", 700 | ] 701 | 702 | [[package]] 703 | name = "serde" 704 | version = "1.0.118" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" 707 | 708 | [[package]] 709 | name = "serde_derive" 710 | version = "1.0.118" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" 713 | dependencies = [ 714 | "proc-macro2", 715 | "quote", 716 | "syn", 717 | ] 718 | 719 | [[package]] 720 | name = "serde_json" 721 | version = "1.0.60" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" 724 | dependencies = [ 725 | "itoa", 726 | "ryu", 727 | "serde", 728 | ] 729 | 730 | [[package]] 731 | name = "siphasher" 732 | version = "0.3.3" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" 735 | 736 | [[package]] 737 | name = "soup-sys" 738 | version = "0.10.0" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "c3c7adf08565630bbb71f955f11f8a68464817ded2703a3549747c235b58a13e" 741 | dependencies = [ 742 | "bitflags", 743 | "gio-sys", 744 | "glib-sys", 745 | "gobject-sys", 746 | "libc", 747 | "pkg-config", 748 | "system-deps", 749 | ] 750 | 751 | [[package]] 752 | name = "strum" 753 | version = "0.18.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" 756 | 757 | [[package]] 758 | name = "strum_macros" 759 | version = "0.18.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" 762 | dependencies = [ 763 | "heck", 764 | "proc-macro2", 765 | "quote", 766 | "syn", 767 | ] 768 | 769 | [[package]] 770 | name = "syn" 771 | version = "1.0.54" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" 774 | dependencies = [ 775 | "proc-macro2", 776 | "quote", 777 | "unicode-xid", 778 | ] 779 | 780 | [[package]] 781 | name = "system-deps" 782 | version = "1.3.2" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" 785 | dependencies = [ 786 | "heck", 787 | "pkg-config", 788 | "strum", 789 | "strum_macros", 790 | "thiserror", 791 | "toml", 792 | "version-compare", 793 | ] 794 | 795 | [[package]] 796 | name = "tempdir" 797 | version = "0.3.7" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 800 | dependencies = [ 801 | "rand", 802 | "remove_dir_all", 803 | ] 804 | 805 | [[package]] 806 | name = "thiserror" 807 | version = "1.0.22" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" 810 | dependencies = [ 811 | "thiserror-impl", 812 | ] 813 | 814 | [[package]] 815 | name = "thiserror-impl" 816 | version = "1.0.22" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" 819 | dependencies = [ 820 | "proc-macro2", 821 | "quote", 822 | "syn", 823 | ] 824 | 825 | [[package]] 826 | name = "thread_local" 827 | version = "1.0.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 830 | dependencies = [ 831 | "lazy_static", 832 | ] 833 | 834 | [[package]] 835 | name = "time" 836 | version = "0.1.44" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 839 | dependencies = [ 840 | "libc", 841 | "wasi 0.10.0+wasi-snapshot-preview1", 842 | "winapi", 843 | ] 844 | 845 | [[package]] 846 | name = "tinyfiledialogs" 847 | version = "3.3.10" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "c45fb26c3f37d9a8b556e51f6d7f13f685af766017030af56e9247e638aa6194" 850 | dependencies = [ 851 | "cc", 852 | "libc", 853 | ] 854 | 855 | [[package]] 856 | name = "toml" 857 | version = "0.5.7" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" 860 | dependencies = [ 861 | "serde", 862 | ] 863 | 864 | [[package]] 865 | name = "unicode-segmentation" 866 | version = "1.7.1" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 869 | 870 | [[package]] 871 | name = "unicode-xid" 872 | version = "0.2.1" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 875 | 876 | [[package]] 877 | name = "urlencoding" 878 | version = "1.1.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "c9232eb53352b4442e40d7900465dfc534e8cb2dc8f18656fcb2ac16112b5593" 881 | 882 | [[package]] 883 | name = "vergen" 884 | version = "3.1.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "4ce50d8996df1f85af15f2cd8d33daae6e479575123ef4314a51a70a230739cb" 887 | dependencies = [ 888 | "bitflags", 889 | "chrono", 890 | ] 891 | 892 | [[package]] 893 | name = "version-compare" 894 | version = "0.0.10" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" 897 | 898 | [[package]] 899 | name = "walkdir" 900 | version = "2.3.1" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 903 | dependencies = [ 904 | "same-file", 905 | "winapi", 906 | "winapi-util", 907 | ] 908 | 909 | [[package]] 910 | name = "wasi" 911 | version = "0.9.0+wasi-snapshot-preview1" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 914 | 915 | [[package]] 916 | name = "wasi" 917 | version = "0.10.0+wasi-snapshot-preview1" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 920 | 921 | [[package]] 922 | name = "web-view" 923 | version = "0.7.2" 924 | source = "git+https://github.com/Freaky/web-view?branch=blocking-step#30a35ca2f0a5e86b4de53789b6e05827491ca4e9" 925 | dependencies = [ 926 | "boxfnonce", 927 | "tinyfiledialogs", 928 | "urlencoding", 929 | "webview-sys", 930 | ] 931 | 932 | [[package]] 933 | name = "webkit2gtk-sys" 934 | version = "0.12.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "389e5138c85a0d111b9bda05b59efa8562315e1d657d72451410e12c858f0619" 937 | dependencies = [ 938 | "atk-sys", 939 | "bitflags", 940 | "cairo-sys-rs", 941 | "gdk-pixbuf-sys", 942 | "gdk-sys", 943 | "gio-sys", 944 | "glib-sys", 945 | "gobject-sys", 946 | "gtk-sys", 947 | "javascriptcore-rs-sys", 948 | "libc", 949 | "pango-sys", 950 | "pkg-config", 951 | "soup-sys", 952 | ] 953 | 954 | [[package]] 955 | name = "webview-sys" 956 | version = "0.6.1" 957 | source = "git+https://github.com/Freaky/web-view?branch=blocking-step#30a35ca2f0a5e86b4de53789b6e05827491ca4e9" 958 | dependencies = [ 959 | "cc", 960 | "gdk-sys", 961 | "gio-sys", 962 | "glib-sys", 963 | "gobject-sys", 964 | "gtk-sys", 965 | "javascriptcore-rs-sys", 966 | "libc", 967 | "pkg-config", 968 | "webkit2gtk-sys", 969 | ] 970 | 971 | [[package]] 972 | name = "wfd" 973 | version = "0.1.6" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "caf31c6b850cadedd58ee7d891df80fb9a652b5e363a72f2aa79d489bd7b020e" 976 | dependencies = [ 977 | "libc", 978 | "winapi", 979 | ] 980 | 981 | [[package]] 982 | name = "winapi" 983 | version = "0.3.9" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 986 | dependencies = [ 987 | "winapi-i686-pc-windows-gnu", 988 | "winapi-x86_64-pc-windows-gnu", 989 | ] 990 | 991 | [[package]] 992 | name = "winapi-i686-pc-windows-gnu" 993 | version = "0.4.0" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 996 | 997 | [[package]] 998 | name = "winapi-util" 999 | version = "0.1.5" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1002 | dependencies = [ 1003 | "winapi", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "winapi-x86_64-pc-windows-gnu" 1008 | version = "0.4.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1011 | 1012 | [[package]] 1013 | name = "winres" 1014 | version = "0.1.11" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc" 1017 | dependencies = [ 1018 | "toml", 1019 | ] 1020 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compactor" 3 | version = "0.10.1" 4 | authors = ["Thomas Hurst "] 5 | homepage = "https://github.com/Freaky/Compactor" 6 | description = "An interface to Windows 10 filesystem compression" 7 | edition = "2018" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | backtrace = "0.3.32" 12 | compresstimator = { git = "https://github.com/Freaky/compresstimator.git", rev = "26ddd3f499bc46f2c8b3ce814e9723ed41b47919" } 13 | crossbeam-channel = "0.5" 14 | ctrlc = "3.1" 15 | directories = "2.0.1" 16 | dirs-sys = "0.3.3" 17 | filesize = "0.2" 18 | fs2 = "0.4.3" 19 | glob = "0.3" 20 | globset = "0.4" 21 | hashfilter = { path = "hashfilter" } 22 | humansize = "1.1.0" 23 | lazy_static = "1.4.0" 24 | open = "1.4" 25 | serde = "1.0" 26 | serde_derive = "1.0" 27 | serde_json = "1.0" 28 | siphasher = "0.3.0" 29 | walkdir = "2.3" 30 | web-view = { git = "https://github.com/Freaky/web-view", branch = "blocking-step" } 31 | winapi = { version = "0.3.7", features = [ "combaseapi", "ioapiset", "knownfolders", "shellscalingapi", "shlobj", "shtypes", "winbase", "winerror", "winioctl", "winver"] } 32 | filetime = "0.2.8" 33 | tinyfiledialogs = "3.3.10" 34 | wfd = "0.1.6" 35 | 36 | [[bin]] 37 | name = "Compactor" 38 | path = "src/main.rs" 39 | 40 | [build-dependencies] 41 | vergen = "3" 42 | winres = "0.1" 43 | 44 | [profile.release] 45 | opt-level = "s" 46 | lto = true 47 | codegen-units = 1 48 | debug = false 49 | 50 | [dev-dependencies] 51 | tempdir = "0.3.7" 52 | 53 | [workspace] 54 | members = [ 55 | "hashfilter" 56 | ] 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Thomas Hurst 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compactor 2 | 3 | ## A friendly user interface to Windows 10 filesystem compression 4 | 5 | With modern lightweight compression algorithms running at gigabytes per second per core, it's practically a no-brainer to apply them to filesystems to make better use of storage and IO. 6 | 7 | Half-recognising this, Windows 10 ships with a reworked compression system that, while fast and effective, is only exposed to users via a command-line tool — [`compact.exe`]. 8 | 9 | Compactor is here to plug that gap, with a simple GUI utility anyone can use. 10 | 11 | ![](https://i.imgur.com/A9si8Zh.png) 12 | 13 | ## Installation [![v0.10.1](https://img.shields.io/github/release-pre/Freaky/Compactor.svg)](https://github.com/Freaky/Compactor/releases/tag/v0.10.1) [![Downloads](https://img.shields.io/github/downloads/Freaky/Compactor/total.svg)](https://github.com/Freaky/Compactor/releases) 14 | 15 | Downloads are available from the [Github Releases](https://github.com/Freaky/Compactor/releases) page under *Assets*, or you can use these direct links: 16 | 17 | * [v0.10.1 32-bit](https://github.com/Freaky/Compactor/releases/download/v0.10.1/Compactor-0.10.1-i686.zip) 18 | * [v0.10.1 64-bit](https://github.com/Freaky/Compactor/releases/download/v0.10.1/Compactor-0.10.1.zip) 19 | 20 | The 64-bit version is recommended for most users. 21 | 22 | If you get "*Windows protected your PC*" trying to run it, it's just [SmartScreen](https://www.pcworld.com/article/3197443/how-to-get-past-windows-defender-smartscreen-in-windows-10.html) upset the binaries aren't (yet) signed. Click "*More info*" and "*Run anyway*" if you judge things to be above-board. 23 | 24 | Note this is beta software and comes with no warranty. 25 | 26 | ## Features 27 | 28 | ### Real-time Progress Updates 29 | 30 | Compactor's directory analysis updates as it goes. You too can experience the satisfaction of watching the disk-space used counter tick down with each file compressed. 31 | 32 | ### Pause, Resume, Stop 33 | 34 | All operations can be paused and interrupted safely at any time. Compactor will finish off what it's doing and stop, or restart where it left off. 35 | 36 | ### Compresstimation 37 | 38 | Compactor performs a statistical compressibility check on larger files before passing them off to Windows for compaction. A large incompressible file can be skipped in less than a second instead of tying up your disk for minutes for zero benefit. 39 | 40 | ### Machine Learning 41 | 42 | Using advanced condition-based AI logic, Compactor can skip over files that have been previously found to be incompressible, making re-running Compactor on a previously compressed folder much quicker. 43 | 44 | (Yes, it's an if statement and a trivial hash database, hush) 45 | 46 | ### Scalable and Fast 47 | 48 | Written in [Rust], a modern compiled systems programming language from Mozilla, Compactor can cope easily with large folders containing millions of files. 49 | 50 | ![](https://i.imgur.com/VxyJmgR.png) 51 | 52 | ## Caveats 53 | 54 | ### Beta Software 55 | 56 | While it has been used successfully by thousands of people, Compactor should be used with care. It is intended for compressing replacable software, not precious files. 57 | 58 | **Make backups**. Report bugs. Be nice. You are reminded: 59 | 60 | ``` 61 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 62 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 63 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 64 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 65 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 66 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 67 | SOFTWARE. 68 | ``` 69 | 70 | It's in shouty legal text so you know I mean it. 71 | 72 | ### Data Corruption 73 | 74 | There has been [one report][#40] of data corruption with open SQLite database files. The author has been unable to reproduce this, but file locking was added to version 0.10 which should prevent them from being modified. 75 | 76 | ### Permissions 77 | 78 | Compactor currently has no mechanism to elevate its privileges using UAC for protected files. If you're using a limited account you may need to run the program with elevated permissions. 79 | 80 | Be careful what you compress. System files should be skipped automatically, and the Windows folder should be in the list of default exclusions (if you want to compact Windows, check out its [CompactOS] feature), but you almost certainly don't want to blindly run this across your entire `C:\` drive. 81 | 82 | ### Modifiable Files 83 | 84 | Compaction is designed for **files that rarely change** — any modifications result in the file being uncompressed in its entirety. In fact, simply opening a file in write mode will *hang* until the file is uncompressed, even if no changes are made. 85 | 86 | This generally doesn't matter much for application folders, but it's not great for databases, logs, virtual machine images, and various other things that *hopefully* mostly live elsewhere. 87 | 88 | If a game uses large files and in-place binary patching for updates, it might be worth adding to the exclusions list. 89 | 90 | ### Compatibility with Other Operating Systems 91 | 92 | Compaction is only supported on Windows 10 - earlier versions of Windows will be unable to access compressed files, though the rest of the filesystem should remain fully accessible. 93 | 94 | Linux and other operating systems will experience similar issues if the NTFS driver they're using lacks support. As of time of writing, NTFS-3G has a [third-party plugin for the feature](https://github.com/ebiggers/ntfs-3g-system-compression). 95 | 96 | This is available on FreeBSD under [`sysutils/fusefs-ntfs-compression`](https://www.freshports.org/sysutils/fusefs-ntfs-compression/), while users of lesser platforms may need to [manually install it](https://wiki.archlinux.org/index.php/NTFS-3G#Compressed_files) like savages. 97 | 98 | ## Compression Results 99 | 100 | A totally-not-cherry-picked sample of compression results with the default settings: 101 | 102 | | Program | Size | Compacted | Ratio | 103 | |-|-:|-:|-| 104 | | AI War 2 | 2.43 GiB | 1.42 GiB | 0.59x | 105 | | Big Pharma | 1.1 GiB | 711 MiB | 0.37x | 106 | | Crusader Kings 2 | 2.19 GiB | 1.29 GiB | 0.59x | 107 | | Deus Ex MD | 41.31 GiB | 28.06 GiB | 0.68x | 108 | | Infinifactory | 1.71 GiB | 742 MiB | 0.58x | 109 | | Satisfactory | 15.82 GiB | 10.45 GiB | 0.66x | 110 | | Space Engineers | 16.28 GiB | 9.4 GiB | 0.58x | 111 | | Stellaris | 7.76 GiB | 5.21 GiB | 0.67x | 112 | | Subnautica BZ | 10.62 GiB | 6.40 GiB | 0.60x | 113 | | The Long Dark | 7.42 GiB | 5.64 GiB | 0.76x | 114 | | Microsoft SDKs | 5.91 GiB | 2.45 GiB | 0.41x | 115 | | Visual Studio 2017 | 9.63 GiB | 4.77 GiB | 0.50x | 116 | | Windows Kits | 5.38 GiB | 2.03 GiB | 0.38x | 117 | 118 | A more comprehensive database of results is [maintained by the CompactGUI project](https://docs.google.com/spreadsheets/d/14CVXd6PTIYE9XlNpRsxJUGaoUzhC5titIC1rzQHI4yI/edit#gid=0). 119 | 120 | ## Future 121 | 122 | There are many things I want to do with Compactor in future. These include, but are certainly not limited to: 123 | 124 | * Make analysis optional. It isn't fundamentally needed. 125 | * Multithreaded analysis/compaction for SSDs. 126 | * GUI rework of some description. The longer I leave this the better Rust should get at it :P 127 | * Installer. Why does this involve so much XML oh god. 128 | * Sign the binaries/installer. This appears to involve money. 129 | * Scheduled task or a background service for set-it-and-forget-it operation. 130 | 131 | Feature requests can be discussed in the [forum](https://github.com/Freaky/Compactor/discussions), or you may open [an issue](https://github.com/Freaky/Compactor/issues). 132 | 133 | ## Alternatives 134 | 135 | * [`compact.exe`] is a command-line tool that ships with Windows 10. If you're familiar with the command line and batch files, maybe you'd prefer that. Weirdo. 136 | * [CompactGUI] is a popular Visual Basic program that shells out to `compact.exe` to do its work, instead of using the Windows API directly as Compactor does. It has some... performance issues, particularly with larger folders. 137 | * NTFS has supported [LZNT1 compression][lznt1] since 1995, hidden behind a checkbox under `Properties` → `Advanced Attributes`. It's less flexible and has a reputation for poor performance and issues with fragmentation, but is more set-it-and-forget-it. 138 | 139 | Are you aware of any others? Do let me know. 140 | 141 | ## Nerdy Technical Stuff 142 | 143 | Compactor is primarily written in [Rust]. The front-end is basically an embedded website driven by the [web-view] crate. It does *not* depend on any remote resources or open any ports. 144 | 145 | Under the hood it uses [`DeviceIoControl`] with [`FSCTL_SET_EXTERNAL_BACKING`] and [`FSCTL_DELETE_EXTERNAL_BACKING`], and a few functions from [WofApi] (Windows Overlay Filesystem). This is, of course, in part thanks to the [winapi] crate. Eventually I hope to get around to finishing off some of my bindings and contributing them back. 146 | 147 | Compresstimation uses a simple linear sampling algorithm, passing blocks through LZ4 level 1 as a compressibility check and averaging across the entire file. The code is [available on Github][compresstimator]. 148 | 149 | The incompressible-files database is simply an append-only list of SipHash128 path hashes. It should be safe to share between multiple instances if you want to compress different drives at the same time. It lives in `%APPDATA%\Local\Freaky\Compactor`. 150 | 151 | ## Author 152 | 153 | Compactor is written by [Thomas Hurst], a nerdy, aloof weirdo from the north-east of England, and a programmer for about 25 years. 154 | 155 | He mostly works with FreeBSD and focuses on Unix platforms, but uses Windows because he plays games instead of having a social life. 156 | 157 | You can find him on Mastodon at [@Freaky@hachyderm.io], or bug him on IRC as `Freaky` on [libera.chat]. 158 | 159 | [`compact.exe`]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/compact 160 | [Rust]: https://www.rust-lang.org/ 161 | [CompactGUI]: https://github.com/ImminentFate/CompactGUI 162 | [web-view]: https://github.com/Boscop/web-view 163 | [webview]: https://github.com/Freaky/webview/tree/various-fixes 164 | [`DeviceIoControl`]: https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol 165 | [`FSCTL_SET_EXTERNAL_BACKING`]: https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/fsctl-set-external-backing 166 | [`FSCTL_DELETE_EXTERNAL_BACKING`]: https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/fsctl-delete-external-backing 167 | [WofApi]: https://docs.microsoft.com/en-us/windows/desktop/api/wofapi/ 168 | [Compression API]: https://docs.microsoft.com/en-gb/windows/desktop/cmpapi/using-the-compression-api 169 | [winapi]: https://github.com/retep998/winapi-rs 170 | [CompactOS]: https://technet.microsoft.com/en-us/windows/dn940129(v=vs.60) 171 | [Thomas Hurst]: https://hur.st/ 172 | [@Freaky@hachyderm.io]: https://hachyderm.io/@Freaky 173 | [libera.chat]: https://libera.chat/ 174 | [overlapped IO]: https://docs.microsoft.com/en-us/windows/desktop/sync/synchronization-and-overlapped-input-and-output 175 | [compresstimator]: https://github.com/Freaky/compresstimator 176 | [lznt1]: https://en.wikipedia.org/wiki/NTFS#File_compression 177 | [#40]: https://github.com/Freaky/Compactor/issues/40 178 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{generate_cargo_keys, ConstantsFlags}; 2 | use winres; 3 | 4 | fn main() { 5 | generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); 6 | 7 | let mut res = winres::WindowsResource::new(); 8 | res.set_icon("compact.ico"); 9 | res.compile().unwrap(); 10 | } 11 | -------------------------------------------------------------------------------- /compact.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freaky/Compactor/b7dd5a767569263b6af65d16cc6f58c13def811f/compact.ico -------------------------------------------------------------------------------- /hashfilter/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "fs2" 5 | version = "0.4.3" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 10 | ] 11 | 12 | [[package]] 13 | name = "fuchsia-cprng" 14 | version = "0.1.1" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | 17 | [[package]] 18 | name = "hashfilter" 19 | version = "0.1.0" 20 | dependencies = [ 21 | "fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "siphasher 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 24 | ] 25 | 26 | [[package]] 27 | name = "libc" 28 | version = "0.2.58" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | 31 | [[package]] 32 | name = "rand" 33 | version = "0.4.6" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | dependencies = [ 36 | "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 37 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 40 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 41 | ] 42 | 43 | [[package]] 44 | name = "rand_core" 45 | version = "0.3.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | dependencies = [ 48 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 49 | ] 50 | 51 | [[package]] 52 | name = "rand_core" 53 | version = "0.4.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | 56 | [[package]] 57 | name = "rdrand" 58 | version = "0.4.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | dependencies = [ 61 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 62 | ] 63 | 64 | [[package]] 65 | name = "remove_dir_all" 66 | version = "0.5.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | dependencies = [ 69 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 70 | ] 71 | 72 | [[package]] 73 | name = "siphasher" 74 | version = "0.3.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | 77 | [[package]] 78 | name = "tempdir" 79 | version = "0.3.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | dependencies = [ 82 | "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "winapi" 88 | version = "0.3.7" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | dependencies = [ 91 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "winapi-i686-pc-windows-gnu" 97 | version = "0.4.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | 100 | [[package]] 101 | name = "winapi-x86_64-pc-windows-gnu" 102 | version = "0.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | 105 | [metadata] 106 | "checksum fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" 107 | "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 108 | "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" 109 | "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 110 | "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 111 | "checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" 112 | "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 113 | "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" 114 | "checksum siphasher 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9913c75df657d84a03fa689c016b0bb2863ff0b497b26a8d6e9703f8d5df03a8" 115 | "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 116 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 117 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 118 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 119 | -------------------------------------------------------------------------------- /hashfilter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hashfilter" 3 | version = "0.1.0" 4 | authors = ["Thomas Hurst "] 5 | edition = "2018" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | fs2 = "0.4.3" 10 | siphasher = "0.3.0" 11 | 12 | [dev-dependencies] 13 | tempdir = "0.3.7" 14 | -------------------------------------------------------------------------------- /hashfilter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fs::{File, OpenOptions}; 3 | use std::hash::Hash; 4 | use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | 8 | use fs2::FileExt; 9 | use siphasher::sip128::{Hasher128, SipHasher}; 10 | 11 | #[derive(Debug, Default)] 12 | pub struct HashFilter { 13 | path: Option, 14 | last_offset: u64, 15 | filter: HashSet, 16 | pending: Vec, 17 | } 18 | 19 | impl HashFilter { 20 | pub fn open>(path: P) -> Self { 21 | Self { 22 | path: Some(path.as_ref().to_owned()), 23 | ..Self::default() 24 | } 25 | } 26 | 27 | pub fn set_backing>(&mut self, path: P) { 28 | self.path = Some(path.as_ref().to_owned()); 29 | self.last_offset = 0; 30 | } 31 | 32 | pub fn load(&mut self) -> io::Result<()> { 33 | if self.path.is_none() { 34 | return Ok(()); 35 | } 36 | 37 | let mut file = match File::open(self.path.as_ref().unwrap()) { 38 | Ok(file) => file, 39 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), 40 | Err(e) => return Err(e), 41 | }; 42 | file.lock_shared()?; 43 | 44 | if self.last_offset > 0 { 45 | file.seek(SeekFrom::Start(self.last_offset))?; 46 | } 47 | 48 | let mut file = BufReader::new(file); 49 | let mut buf = [0; 16]; 50 | loop { 51 | match file.read_exact(&mut buf) { 52 | Ok(()) => self.filter.insert(u128::from_le_bytes(buf)), 53 | Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()), 54 | Err(e) => return Err(e), 55 | }; 56 | 57 | self.last_offset += 16; 58 | } 59 | } 60 | 61 | pub fn save(&mut self) -> io::Result<()> { 62 | if self.path.is_none() || self.pending.is_empty() { 63 | return Ok(()); 64 | } 65 | 66 | if let Some(dir) = self.path.as_ref().and_then(|p| p.parent()) { 67 | std::fs::create_dir_all(dir)?; 68 | } 69 | 70 | let mut file = OpenOptions::new() 71 | .write(true) 72 | .create(true) 73 | .open(self.path.as_ref().unwrap())?; 74 | file.lock_exclusive()?; 75 | 76 | let end = file.metadata()?.len(); 77 | if end % 16 != 0 { 78 | file.set_len(end - (end % 16))?; 79 | } 80 | file.seek(SeekFrom::End(0))?; 81 | 82 | let mut file = BufWriter::new(file); 83 | 84 | while let Some(key) = self.pending.pop() { 85 | file.write_all(&key.to_le_bytes())?; 86 | } 87 | 88 | if end == self.last_offset { 89 | self.last_offset = file.seek(SeekFrom::End(0))?; 90 | } 91 | 92 | file.into_inner()?.sync_all()?; 93 | 94 | Ok(()) 95 | } 96 | 97 | pub fn insert(&mut self, data: H) -> bool { 98 | let key = Self::key_for(data); 99 | 100 | if self.filter.insert(key) { 101 | self.pending.push(key); 102 | return true; 103 | } 104 | 105 | false 106 | } 107 | 108 | pub fn contains(&self, data: H) -> bool { 109 | self.filter.contains(&Self::key_for(data)) 110 | } 111 | 112 | fn key_for(data: H) -> u128 { 113 | let mut hash = SipHasher::new(); 114 | data.hash(&mut hash); 115 | let h = hash.finish128(); 116 | (u128::from(h.h1) << 64) | u128::from(h.h2) 117 | } 118 | } 119 | 120 | #[test] 121 | fn it_seems_to_work() { 122 | let dir = tempdir::TempDir::new("hashfilter-test").unwrap(); 123 | let db = dir.path().join("test.dat"); 124 | let mut hf = HashFilter::open(&db); 125 | let _ = hf.load(); 126 | 127 | let paths = vec![ 128 | PathBuf::from("/path/to/some/file0"), 129 | PathBuf::from("/path/to/some/file1"), 130 | PathBuf::from("/path/to/some/file2"), 131 | PathBuf::from("/path/to/some/file3"), 132 | PathBuf::from("/path/to/some/file4"), 133 | PathBuf::from("/path/to/some/file5"), 134 | PathBuf::from("/path/to/some/file6"), 135 | PathBuf::from("/path/to/some/file7"), 136 | PathBuf::from("/path/to/some/file8"), 137 | PathBuf::from("/path/to/some/file9"), 138 | ]; 139 | 140 | for p in &paths { 141 | hf.insert(p); 142 | } 143 | 144 | hf.save().unwrap(); 145 | 146 | let mut hf2 = HashFilter::open(&db); 147 | hf2.load().unwrap(); 148 | hf2.insert(PathBuf::from("/path/to/some/file10")); 149 | hf2.save().unwrap(); 150 | 151 | for p in &paths { 152 | assert!(hf2.contains(p)); 153 | } 154 | 155 | hf.load().unwrap(); 156 | assert!(hf.contains(PathBuf::from("/path/to/some/file10"))); 157 | } 158 | -------------------------------------------------------------------------------- /src/backend.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::PathBuf; 3 | use std::time::Duration; 4 | use std::time::Instant; 5 | 6 | use crossbeam_channel::{bounded, Receiver, RecvTimeoutError}; 7 | use filesize::PathExt; 8 | 9 | use crate::background::BackgroundHandle; 10 | use crate::compression::BackgroundCompactor; 11 | use crate::folder::{FileKind, FolderInfo, FolderScan}; 12 | use crate::gui::{GuiRequest, GuiWrapper}; 13 | use crate::persistence::{config, pathdb}; 14 | 15 | pub struct Backend { 16 | gui: GuiWrapper, 17 | msg: Receiver, 18 | info: Option, 19 | } 20 | 21 | fn format_size(size: u64, decimal: bool) -> String { 22 | use humansize::{file_size_opts as options, FileSize}; 23 | 24 | size.file_size(if decimal { 25 | options::DECIMAL 26 | } else { 27 | options::BINARY 28 | }) 29 | .expect("file size") 30 | } 31 | 32 | impl Backend { 33 | pub fn new(gui: GuiWrapper, msg: Receiver) -> Self { 34 | Self { 35 | gui, 36 | msg, 37 | info: None, 38 | } 39 | } 40 | 41 | pub fn run(&mut self) { 42 | loop { 43 | match self.msg.recv() { 44 | Ok(GuiRequest::ChooseFolder) => { 45 | let path = self.gui.choose_folder().recv().ok().flatten(); 46 | 47 | if let Some(path) = path { 48 | self.gui.folder(&path); 49 | self.scan_loop(path); 50 | } 51 | } 52 | Ok(GuiRequest::Analyse) if self.info.is_some() => { 53 | let path = self.info.take().unwrap().path; 54 | self.gui.folder(&path); 55 | self.scan_loop(path); 56 | } 57 | Ok(GuiRequest::Compress) if self.info.is_some() => { 58 | self.compress_loop(); 59 | } 60 | Ok(GuiRequest::Decompress) if self.info.is_some() => { 61 | self.uncompress_loop(); 62 | } 63 | Ok(msg) => { 64 | eprintln!("Backend: Ignored message: {:?}", msg); 65 | } 66 | Err(_) => { 67 | eprintln!("Backend: exit run loop"); 68 | break; 69 | } 70 | } 71 | } 72 | } 73 | 74 | fn scan_loop(&mut self, path: PathBuf) { 75 | let excludes = config().read().unwrap().current().globset().expect("globs"); 76 | 77 | let scanner = FolderScan::new(path, excludes); 78 | let task = BackgroundHandle::spawn(scanner); 79 | let start = Instant::now(); 80 | 81 | self.gui.status("Scanning", None); 82 | loop { 83 | let msg = self.msg.recv_timeout(Duration::from_millis(25)); 84 | 85 | match msg { 86 | Ok(GuiRequest::Pause) => { 87 | task.pause(); 88 | self.gui.status("Paused", Some(0.5)); 89 | self.gui.paused(); 90 | } 91 | Ok(GuiRequest::Resume) => { 92 | task.resume(); 93 | self.gui.status("Scanning", None); 94 | self.gui.resumed(); 95 | } 96 | Ok(GuiRequest::Stop) | Err(RecvTimeoutError::Disconnected) => { 97 | task.cancel(); 98 | } 99 | Ok(msg) => { 100 | eprintln!("Ignored message: {:?}", msg); 101 | } 102 | Err(RecvTimeoutError::Timeout) => (), 103 | } 104 | 105 | match task.wait_timeout(Duration::from_millis(25)) { 106 | Some(Ok(info)) => { 107 | self.gui 108 | .status(format!("Scanned in {:.2?}", start.elapsed()), Some(1.0)); 109 | self.gui.summary(info.summary()); 110 | self.gui.scanned(); 111 | self.info = Some(info); 112 | break; 113 | } 114 | Some(Err(info)) => { 115 | self.gui.status( 116 | format!("Scan stopped after {:.2?}", start.elapsed()), 117 | Some(0.5), 118 | ); 119 | self.gui.summary(info.summary()); 120 | self.gui.stopped(); 121 | self.info = Some(info); 122 | break; 123 | } 124 | None => { 125 | if let Some(status) = task.status() { 126 | self.gui 127 | .status(format!("Scanning: {}", status.0.display()), None); 128 | self.gui.summary(status.1); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | // Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn. 136 | fn compress_loop(&mut self) { 137 | let (send_file, send_file_rx) = bounded::<(PathBuf, u64)>(1); 138 | let (recv_result_tx, recv_result) = bounded::<(PathBuf, io::Result)>(1); 139 | 140 | let compression = Some(config().read().unwrap().current().compression); 141 | let compactor = BackgroundCompactor::new(compression, send_file_rx, recv_result_tx); 142 | let task = BackgroundHandle::spawn(compactor); 143 | let start = Instant::now(); 144 | 145 | let mut folder = self.info.take().expect("fileinfo"); 146 | let total = folder.len(FileKind::Compressible); 147 | let mut done = 0; 148 | 149 | let mut last_update = Instant::now(); 150 | let mut last_write = Instant::now(); 151 | let mut paused = false; 152 | let mut stopped = false; 153 | 154 | let old_size = folder.physical_size; 155 | let compressible_size = folder.summary().compressible.physical_size; 156 | 157 | let incompressible = pathdb(); 158 | let mut incompressible = incompressible.write().unwrap(); 159 | let _ = incompressible.load(); 160 | 161 | self.gui.compacting(); 162 | 163 | self.gui.status("Compacting".to_string(), Some(0.0)); 164 | loop { 165 | while paused && !stopped { 166 | self.gui 167 | .status("Paused".to_string(), Some(done as f32 / total as f32)); 168 | 169 | self.gui.summary(folder.summary()); 170 | 171 | match self.msg.recv() { 172 | Ok(GuiRequest::Pause) => { 173 | paused = true; 174 | } 175 | Ok(GuiRequest::Resume) => { 176 | self.gui 177 | .status("Compacting".to_string(), Some(done as f32 / total as f32)); 178 | self.gui.resumed(); 179 | paused = false; 180 | last_update = Instant::now(); 181 | } 182 | Ok(GuiRequest::Stop) => { 183 | stopped = true; 184 | break; 185 | } 186 | Ok(_) => (), 187 | Err(_) => { 188 | stopped = true; 189 | break; 190 | } 191 | } 192 | } 193 | 194 | if stopped { 195 | break; 196 | } 197 | 198 | if last_write.elapsed() > Duration::from_secs(60) { 199 | let _ = incompressible.save(); 200 | last_write = Instant::now(); 201 | } 202 | 203 | let mut displayed = false; 204 | 205 | if let Some(mut fi) = folder.pop(FileKind::Compressible) { 206 | send_file 207 | .send((folder.path.join(&fi.path), fi.logical_size)) 208 | .expect("send_file"); 209 | 210 | if !displayed && last_update.elapsed() > Duration::from_millis(50) { 211 | self.gui.status( 212 | format!("Compacting: {}", fi.path.display()), 213 | Some(done as f32 / total as f32), 214 | ); 215 | last_update = Instant::now(); 216 | displayed = true; 217 | } 218 | 219 | loop { 220 | if let Ok((path, result)) = recv_result.recv_timeout(Duration::from_millis(25)) 221 | { 222 | done += 1; 223 | match result { 224 | Ok(true) => { 225 | fi.physical_size = path.size_on_disk().unwrap_or(fi.physical_size); 226 | 227 | // Irritatingly Windows can return success when it fails. 228 | if fi.physical_size == fi.logical_size { 229 | incompressible.insert(path); 230 | folder.push(FileKind::Skipped, fi); 231 | } else { 232 | folder.push(FileKind::Compressed, fi); 233 | } 234 | } 235 | Ok(false) => { 236 | incompressible.insert(path); 237 | folder.push(FileKind::Skipped, fi); 238 | } 239 | Err(err) => { 240 | self.gui.status( 241 | format!("Error: {}, {}", err, fi.path.display()), 242 | Some(done as f32 / total as f32), 243 | ); 244 | folder.push(FileKind::Skipped, fi); 245 | } 246 | } 247 | 248 | if last_update.elapsed() > Duration::from_millis(50) { 249 | self.gui.summary(folder.summary()); 250 | } 251 | 252 | break; 253 | } 254 | 255 | if !displayed && last_update.elapsed() > Duration::from_millis(50) { 256 | self.gui.status( 257 | format!("Compacting: {}", fi.path.display()), 258 | Some(done as f32 / total as f32), 259 | ); 260 | 261 | displayed = true; 262 | } 263 | 264 | match self.msg.try_recv() { 265 | Ok(GuiRequest::Pause) if !paused => { 266 | self.gui.status( 267 | format!("Pausing after {}", fi.path.display()), 268 | Some(done as f32 / total as f32), 269 | ); 270 | self.gui.paused(); 271 | paused = true; 272 | } 273 | Ok(GuiRequest::Resume) => { 274 | self.gui.resumed(); 275 | paused = false; 276 | stopped = false; 277 | } 278 | Ok(GuiRequest::Stop) if !stopped => { 279 | self.gui.status( 280 | format!("Stopping after {}", fi.path.display()), 281 | Some(done as f32 / total as f32), 282 | ); 283 | stopped = true; 284 | } 285 | Ok(_) => (), 286 | Err(_) => (), 287 | } 288 | } 289 | } else { 290 | break; 291 | } 292 | } 293 | 294 | drop(send_file); 295 | task.wait(); 296 | 297 | let _ = incompressible.save(); 298 | 299 | let new_size = folder.physical_size; 300 | let decimal = config().read().unwrap().current().decimal; 301 | 302 | let msg = format!( 303 | "Compacted {} in {} files, saving {} in {:.2?}", 304 | format_size(compressible_size, decimal), 305 | done, 306 | format_size(old_size - new_size, decimal), 307 | start.elapsed() 308 | ); 309 | 310 | self.gui.status(msg, Some(done as f32 / total as f32)); 311 | self.gui.summary(folder.summary()); 312 | self.gui.scanned(); 313 | 314 | self.info = Some(folder); 315 | } 316 | 317 | // Oh no, not again. 318 | fn uncompress_loop(&mut self) { 319 | let (send_file, send_file_rx) = bounded::<(PathBuf, u64)>(1); 320 | let (recv_result_tx, recv_result) = bounded::<(PathBuf, io::Result)>(1); 321 | 322 | let compactor = BackgroundCompactor::new(None, send_file_rx, recv_result_tx); 323 | let task = BackgroundHandle::spawn(compactor); 324 | let start = Instant::now(); 325 | 326 | let mut folder = self.info.take().expect("fileinfo"); 327 | let total = folder.len(FileKind::Compressed); 328 | let mut done = 0; 329 | 330 | let mut last_update = Instant::now(); 331 | let mut paused = false; 332 | let mut stopped = false; 333 | 334 | let old_size = folder.physical_size; 335 | 336 | self.gui.compacting(); 337 | 338 | self.gui.status("Expanding".to_string(), Some(0.0)); 339 | loop { 340 | while paused && !stopped { 341 | self.gui 342 | .status("Paused".to_string(), Some(done as f32 / total as f32)); 343 | 344 | self.gui.summary(folder.summary()); 345 | 346 | match self.msg.recv() { 347 | Ok(GuiRequest::Pause) => { 348 | paused = true; 349 | } 350 | Ok(GuiRequest::Resume) => { 351 | self.gui 352 | .status("Expanding".to_string(), Some(done as f32 / total as f32)); 353 | self.gui.resumed(); 354 | paused = false; 355 | last_update = Instant::now(); 356 | } 357 | Ok(GuiRequest::Stop) => { 358 | stopped = true; 359 | break; 360 | } 361 | Ok(_) => (), 362 | Err(_) => { 363 | stopped = true; 364 | break; 365 | } 366 | } 367 | } 368 | 369 | if stopped { 370 | break; 371 | } 372 | 373 | if last_update.elapsed() > Duration::from_millis(50) { 374 | self.gui 375 | .status("Expanding".to_string(), Some(done as f32 / total as f32)); 376 | last_update = Instant::now(); 377 | 378 | self.gui.summary(folder.summary()); 379 | } 380 | 381 | if let Some(mut fi) = folder.pop(FileKind::Compressed) { 382 | send_file 383 | .send((folder.path.join(&fi.path), fi.logical_size)) 384 | .expect("send_file"); 385 | 386 | let mut waiting = false; 387 | loop { 388 | if let Ok((_path, result)) = recv_result.recv_timeout(Duration::from_millis(25)) 389 | { 390 | done += 1; 391 | match result { 392 | Ok(_) => { 393 | fi.physical_size = fi.logical_size; 394 | folder.push(FileKind::Compressible, fi); 395 | } 396 | Err(err) => { 397 | self.gui.status( 398 | format!("Error: {}, {}", err, fi.path.display()), 399 | Some(done as f32 / total as f32), 400 | ); 401 | folder.push(FileKind::Skipped, fi); 402 | } 403 | } 404 | 405 | break; 406 | } 407 | 408 | if !waiting && last_update.elapsed() > Duration::from_millis(50) { 409 | self.gui.status( 410 | format!("Expanding: {}", fi.path.display()), 411 | Some(done as f32 / total as f32), 412 | ); 413 | 414 | last_update = Instant::now(); 415 | waiting = true; 416 | } 417 | 418 | match self.msg.try_recv() { 419 | Ok(GuiRequest::Pause) if !paused => { 420 | self.gui.status( 421 | format!("Pausing after {}", fi.path.display()), 422 | Some(done as f32 / total as f32), 423 | ); 424 | self.gui.paused(); 425 | paused = true; 426 | } 427 | Ok(GuiRequest::Resume) => { 428 | self.gui.resumed(); 429 | paused = false; 430 | stopped = false; 431 | } 432 | Ok(GuiRequest::Stop) if !stopped => { 433 | self.gui.status( 434 | format!("Stopping after {}", fi.path.display()), 435 | Some(done as f32 / total as f32), 436 | ); 437 | stopped = true; 438 | } 439 | Ok(_) => (), 440 | Err(_) => (), 441 | } 442 | } 443 | } else { 444 | break; 445 | } 446 | } 447 | 448 | drop(send_file); 449 | task.wait(); 450 | 451 | let new_size = folder.physical_size; 452 | 453 | let msg = format!( 454 | "Expanded {} files wasting {} in {:.2?}", 455 | done, 456 | format_size( 457 | new_size - old_size, 458 | config().read().unwrap().current().decimal 459 | ), 460 | start.elapsed() 461 | ); 462 | 463 | self.gui.status(msg, Some(done as f32 / total as f32)); 464 | self.gui.summary(folder.summary()); 465 | self.gui.scanned(); 466 | 467 | self.info = Some(folder); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/background.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel::{Receiver, RecvTimeoutError, TryRecvError}; 2 | use std::panic::{catch_unwind, UnwindSafe}; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::Arc; 5 | use std::sync::Mutex; 6 | use std::thread; 7 | /// Tiny thread-backed background job thing 8 | /// 9 | /// This is very similar to ffi_helper's Task 10 | /// https://github.com/Michael-F-Bryan/ffi_helpers 11 | use std::time::Duration; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct ControlToken(Arc>); 15 | 16 | #[derive(Debug, Default)] 17 | pub struct ControlTokenInner { 18 | cancel: AtomicBool, 19 | pause: AtomicBool, 20 | status: Mutex>, 21 | } 22 | 23 | impl ControlToken { 24 | pub fn new() -> Self { 25 | Self(Arc::new(ControlTokenInner { 26 | cancel: AtomicBool::new(false), 27 | pause: AtomicBool::new(false), 28 | status: Mutex::new(None), 29 | })) 30 | } 31 | 32 | pub fn cancel(&self) { 33 | self.0.cancel.store(true, Ordering::SeqCst); 34 | } 35 | 36 | pub fn pause(&self) { 37 | self.0.pause.store(true, Ordering::SeqCst); 38 | } 39 | 40 | pub fn resume(&self) { 41 | self.0.pause.store(false, Ordering::SeqCst); 42 | } 43 | 44 | pub fn is_cancelled(&self) -> bool { 45 | self.0.cancel.load(Ordering::SeqCst) 46 | } 47 | 48 | pub fn is_paused(&self) -> bool { 49 | self.0.pause.load(Ordering::SeqCst) 50 | } 51 | 52 | pub fn is_cancelled_with_pause(&self) -> bool { 53 | self.is_cancelled() || (self.handle_pause() && self.is_cancelled()) 54 | } 55 | 56 | pub fn handle_pause(&self) -> bool { 57 | let mut paused = false; 58 | 59 | while self.is_paused() && !self.is_cancelled() { 60 | paused = true; 61 | thread::park(); 62 | } 63 | 64 | paused 65 | } 66 | 67 | pub fn set_status(&self, status: S) { 68 | let mut previous = self.0.status.lock().expect("status lock"); 69 | previous.replace(status); 70 | } 71 | 72 | pub fn get_status(&self) -> Option { 73 | let mut current = self.0.status.lock().expect("status lock"); 74 | current.take() 75 | } 76 | 77 | pub fn result(&self) -> Result<(), ()> { 78 | if self.is_cancelled() { 79 | Err(()) 80 | } else { 81 | Ok(()) 82 | } 83 | } 84 | } 85 | 86 | impl Default for ControlToken { 87 | fn default() -> Self { 88 | Self::new() 89 | } 90 | } 91 | 92 | pub struct BackgroundHandle { 93 | result: Receiver>, 94 | control: ControlToken, 95 | thread: thread::Thread, 96 | } 97 | 98 | impl BackgroundHandle { 99 | pub fn spawn(task: K) -> BackgroundHandle 100 | where 101 | K: Background + UnwindSafe + Send + Sync + 'static, 102 | T: Send + Sync + 'static, 103 | S: Send + Sync + Clone + 'static, 104 | { 105 | let (tx, rx) = crossbeam_channel::unbounded(); 106 | let control = ControlToken::new(); 107 | let inner_control = control.clone(); 108 | 109 | let handle = thread::spawn(move || { 110 | let response = catch_unwind(move || task.run(&inner_control)); 111 | let _ = tx.send(response); 112 | }); 113 | 114 | let thread = handle.thread().clone(); 115 | 116 | BackgroundHandle { 117 | result: rx, 118 | control, 119 | thread, 120 | } 121 | } 122 | 123 | pub fn poll(&self) -> Option { 124 | match self.result.try_recv() { 125 | Ok(value) => Some(value.unwrap()), 126 | Err(TryRecvError::Empty) => None, 127 | Err(e) => panic!("{:?}", e), 128 | } 129 | } 130 | 131 | pub fn wait_timeout(&self, wait: Duration) -> Option { 132 | match self.result.recv_timeout(wait) { 133 | Ok(value) => Some(value.unwrap()), 134 | Err(RecvTimeoutError::Timeout) => None, 135 | Err(e) => panic!("{:?}", e), 136 | } 137 | } 138 | 139 | pub fn wait(self) -> T { 140 | match self.result.recv() { 141 | Ok(value) => value.unwrap(), 142 | Err(e) => panic!("{:?}", e), 143 | } 144 | } 145 | 146 | pub fn cancel(&self) { 147 | self.control.cancel(); 148 | self.thread.unpark(); 149 | } 150 | 151 | pub fn is_cancelled(&self) -> bool { 152 | self.control.is_cancelled() 153 | } 154 | 155 | pub fn status(&self) -> Option { 156 | self.control.get_status() 157 | } 158 | 159 | pub fn pause(&self) { 160 | self.control.pause(); 161 | } 162 | 163 | pub fn resume(&self) { 164 | self.control.resume(); 165 | self.thread.unpark(); 166 | } 167 | 168 | pub fn is_paused(&self) -> bool { 169 | self.control.is_paused() 170 | } 171 | } 172 | 173 | impl Drop for BackgroundHandle { 174 | fn drop(&mut self) { 175 | self.cancel(); 176 | } 177 | } 178 | 179 | pub trait Background: Send + Sync { 180 | type Output: Send + Sync; 181 | type Status: Send + Sync; 182 | 183 | fn run(self, control: &ControlToken) -> Self::Output; 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use super::*; 189 | use std::time::Duration; 190 | 191 | #[derive(Debug, Clone, Copy)] 192 | pub struct Tick; 193 | 194 | impl Background for Tick { 195 | type Output = Result; 196 | type Status = u32; 197 | 198 | fn run(self, control: &ControlToken) -> Self::Output { 199 | let mut ticks = 0; 200 | 201 | while ticks < 100 && !control.is_cancelled_with_pause() { 202 | ticks += 1; 203 | control.set_status(ticks); 204 | 205 | thread::sleep(Duration::from_millis(10)); 206 | } 207 | 208 | control.result().map(|_| ticks).map_err(|_| ticks) 209 | } 210 | } 211 | 212 | #[test] 213 | fn it_cancels() { 214 | let task = Tick; 215 | 216 | let handle = BackgroundHandle::spawn(task); 217 | 218 | for _ in 0..10 { 219 | thread::sleep(Duration::from_millis(10)); 220 | let got = handle.poll(); 221 | assert!(got.is_none()); 222 | } 223 | 224 | handle.cancel(); 225 | 226 | let ret = handle.wait(); 227 | assert!(ret.is_err()); 228 | let ticks = ret.unwrap_err(); 229 | assert!(9 <= ticks && ticks <= 12); 230 | } 231 | 232 | #[test] 233 | fn it_pauses() { 234 | let task = Tick; 235 | 236 | let handle = BackgroundHandle::spawn(task); 237 | 238 | handle.pause(); 239 | 240 | for _ in 0..10 { 241 | thread::sleep(Duration::from_millis(10)); 242 | let got = handle.poll(); 243 | assert!(got.is_none()); 244 | } 245 | 246 | handle.resume(); 247 | 248 | for _ in 0..10 { 249 | thread::sleep(Duration::from_millis(10)); 250 | let got = handle.poll(); 251 | assert!(got.is_none()); 252 | } 253 | 254 | handle.cancel(); 255 | 256 | let ret = handle.wait(); 257 | assert!(ret.is_err()); 258 | let ticks = ret.unwrap_err(); 259 | assert!(9 <= ticks && ticks <= 12); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/compact.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_snake_case, dead_code)] 2 | 3 | use std::convert::TryFrom; 4 | use std::ffi::{CString, OsStr}; 5 | use std::os::windows::ffi::OsStrExt; 6 | use std::os::windows::io::AsRawHandle; 7 | use std::path::Path; 8 | use std::str::FromStr; 9 | 10 | use serde_derive::{Deserialize, Serialize}; 11 | 12 | use winapi::shared::minwindef::{BOOL, DWORD, PBOOL, PULONG, ULONG}; 13 | use winapi::shared::ntdef::PVOID; 14 | use winapi::shared::winerror::{HRESULT_CODE, SUCCEEDED}; 15 | use winapi::um::ioapiset::DeviceIoControl; 16 | use winapi::um::winioctl::{FSCTL_DELETE_EXTERNAL_BACKING, FSCTL_SET_EXTERNAL_BACKING}; 17 | use winapi::um::winnt::{HANDLE, HRESULT, LPCWSTR}; 18 | use winapi::um::winver::{GetFileVersionInfoA, GetFileVersionInfoSizeA, VerQueryValueA}; 19 | use winapi::STRUCT; 20 | 21 | STRUCT! { 22 | struct _WOF_FILE_COMPRESSION_INFO_V1 { 23 | Algorithm: ULONG, 24 | Flags: ULONG, 25 | } 26 | } 27 | 28 | STRUCT! { 29 | struct _WOF_EXTERNAL_INFO { 30 | Version: ULONG, 31 | Provider: ULONG, 32 | } 33 | } 34 | 35 | STRUCT! { 36 | struct _FILE_PROVIDER_EXTERNAL_INFO_V1 { 37 | Version: ULONG, 38 | Algorithm: ULONG, 39 | Flags: ULONG, 40 | } 41 | } 42 | 43 | STRUCT! { 44 | struct VS_FIXEDFILEINFO { 45 | dwSignature: DWORD, 46 | dwStrucVersion: DWORD, 47 | dwFileVersionMS: DWORD, 48 | dwFileVersionLS: DWORD, 49 | dwProductVersionMS: DWORD, 50 | dwProductVersionLS: DWORD, 51 | dwFileFlagsMask: DWORD, 52 | dwFileFlags: DWORD, 53 | dwFileOS: DWORD, 54 | dwFileType: DWORD, 55 | dwFileSubtype: DWORD, 56 | dwFileDateMS: DWORD, 57 | dwFileDateLS: DWORD, 58 | } 59 | } 60 | 61 | const VS_FIXEDFILEINFO_SIGNATURE: DWORD = 0xFEEF_04BD; 62 | 63 | const FILE_PROVIDER_COMPRESSION_XPRESS4K: ULONG = 0; 64 | const FILE_PROVIDER_COMPRESSION_LZX: ULONG = 1; 65 | const FILE_PROVIDER_COMPRESSION_XPRESS8K: ULONG = 2; 66 | const FILE_PROVIDER_COMPRESSION_XPRESS16K: ULONG = 3; 67 | 68 | const ERROR_SUCCESS: HRESULT = 0; 69 | const ERROR_COMPRESSION_NOT_BENEFICIAL: HRESULT = 344; 70 | 71 | const FILE_PROVIDER_CURRENT_VERSION: ULONG = 1; 72 | const WOF_CURRENT_VERSION: ULONG = 1; 73 | const WOF_PROVIDER_FILE: ULONG = 2; 74 | 75 | impl Default for _FILE_PROVIDER_EXTERNAL_INFO_V1 { 76 | fn default() -> Self { 77 | Self { 78 | Version: FILE_PROVIDER_CURRENT_VERSION, 79 | Algorithm: Compression::default().into(), 80 | Flags: 0, 81 | } 82 | } 83 | } 84 | 85 | impl Default for _WOF_EXTERNAL_INFO { 86 | fn default() -> Self { 87 | Self { 88 | Version: WOF_CURRENT_VERSION, 89 | Provider: WOF_PROVIDER_FILE, 90 | } 91 | } 92 | } 93 | 94 | impl From for _FILE_PROVIDER_EXTERNAL_INFO_V1 { 95 | fn from(compression: Compression) -> Self { 96 | Self { 97 | Version: FILE_PROVIDER_CURRENT_VERSION, 98 | Algorithm: compression.into(), 99 | Flags: 0, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] 105 | pub enum Compression { 106 | Xpress4k, 107 | Xpress8k, 108 | Xpress16k, 109 | Lzx, 110 | } 111 | 112 | impl Default for Compression { 113 | fn default() -> Self { 114 | Compression::Xpress8k 115 | } 116 | } 117 | 118 | impl std::fmt::Display for Compression { 119 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 120 | match self { 121 | Compression::Xpress4k => write!(f, "XPRESS4k"), 122 | Compression::Xpress8k => write!(f, "XPRESS8K"), 123 | Compression::Xpress16k => write!(f, "XPRESS16K"), 124 | Compression::Lzx => write!(f, "LZX"), 125 | } 126 | } 127 | } 128 | 129 | impl FromStr for Compression { 130 | type Err = (); 131 | 132 | fn from_str(s: &str) -> Result { 133 | match s { 134 | "XPRESS4K" => Ok(Compression::Xpress4k), 135 | "XPRESS8K" => Ok(Compression::Xpress8k), 136 | "XPRESS16K" => Ok(Compression::Xpress16k), 137 | "LZX" => Ok(Compression::Lzx), 138 | _ => Err(()), 139 | } 140 | } 141 | } 142 | 143 | impl TryFrom for Compression { 144 | type Error = (); 145 | 146 | fn try_from(value: ULONG) -> Result { 147 | match value { 148 | FILE_PROVIDER_COMPRESSION_XPRESS4K => Ok(Compression::Xpress4k), 149 | FILE_PROVIDER_COMPRESSION_XPRESS8K => Ok(Compression::Xpress8k), 150 | FILE_PROVIDER_COMPRESSION_XPRESS16K => Ok(Compression::Xpress16k), 151 | FILE_PROVIDER_COMPRESSION_LZX => Ok(Compression::Lzx), 152 | _ => Err(()), 153 | } 154 | } 155 | } 156 | 157 | impl From for ULONG { 158 | fn from(value: Compression) -> Self { 159 | match value { 160 | Compression::Xpress4k => FILE_PROVIDER_COMPRESSION_XPRESS4K, 161 | Compression::Xpress8k => FILE_PROVIDER_COMPRESSION_XPRESS8K, 162 | Compression::Xpress16k => FILE_PROVIDER_COMPRESSION_XPRESS16K, 163 | Compression::Lzx => FILE_PROVIDER_COMPRESSION_LZX, 164 | } 165 | } 166 | } 167 | 168 | pub fn system_supports_compression() -> std::io::Result { 169 | let dll = CString::new("WofUtil.dll").unwrap(); 170 | let path = CString::new("\\").unwrap(); 171 | let mut handle = 0; 172 | 173 | let len = unsafe { GetFileVersionInfoSizeA(dll.as_ptr(), &mut handle) }; 174 | 175 | if len == 0 { 176 | return Err(std::io::Error::last_os_error()); 177 | } 178 | 179 | let mut buf = vec![0u8; len as usize]; 180 | 181 | let ret = unsafe { 182 | GetFileVersionInfoA( 183 | dll.as_ptr(), 184 | handle, 185 | len, 186 | buf.as_mut_ptr() as *mut _ as PVOID, 187 | ) 188 | }; 189 | 190 | if ret == 0 { 191 | return Err(std::io::Error::last_os_error()); 192 | } 193 | 194 | let mut pinfo: PVOID = std::ptr::null_mut(); 195 | let mut pinfo_size = 0; 196 | 197 | let ret = unsafe { 198 | VerQueryValueA( 199 | buf.as_mut_ptr() as *mut _ as PVOID, 200 | path.as_ptr(), 201 | &mut pinfo, 202 | &mut pinfo_size, 203 | ) 204 | }; 205 | 206 | if ret == 0 { 207 | return Err(std::io::Error::last_os_error()); 208 | } 209 | 210 | assert!(pinfo_size as usize >= std::mem::size_of::()); 211 | assert!(!pinfo.is_null()); 212 | 213 | let pinfo: &VS_FIXEDFILEINFO = unsafe { &*(pinfo as *const VS_FIXEDFILEINFO) }; 214 | assert!(pinfo.dwSignature == VS_FIXEDFILEINFO_SIGNATURE); 215 | 216 | Ok((pinfo.dwFileVersionMS >> 16) & 0xffff >= 10) 217 | } 218 | 219 | pub fn file_supports_compression>(path: P) -> std::io::Result { 220 | let file = std::fs::File::open(path)?; 221 | let mut version: ULONG = 0; 222 | 223 | let ret = unsafe { 224 | WofGetDriverVersion( 225 | file.as_raw_handle() as HANDLE, 226 | WOF_PROVIDER_FILE, 227 | &mut version, 228 | ) 229 | }; 230 | 231 | if SUCCEEDED(ret) && version > 0 { 232 | Ok(true) 233 | } else { 234 | Ok(false) 235 | } 236 | } 237 | 238 | pub fn detect_compression>(path: P) -> std::io::Result> { 239 | let mut p: Vec = path.as_ref().encode_wide().collect(); 240 | p.push(0); 241 | 242 | let mut is_external: BOOL = 0; 243 | let mut provider: ULONG = 0; 244 | let mut file_info: _WOF_FILE_COMPRESSION_INFO_V1 = unsafe { std::mem::zeroed() }; 245 | let mut len: ULONG = std::mem::size_of::<_WOF_FILE_COMPRESSION_INFO_V1>() as ULONG; 246 | 247 | let ret = unsafe { 248 | WofIsExternalFile( 249 | p.as_ptr(), 250 | &mut is_external, 251 | &mut provider, 252 | &mut file_info as *mut _ as PVOID, 253 | &mut len, 254 | ) 255 | }; 256 | 257 | if SUCCEEDED(ret) { 258 | if is_external > 0 && provider == WOF_PROVIDER_FILE { 259 | Ok(Compression::try_from(file_info.Algorithm).ok()) 260 | } else { 261 | Ok(None) 262 | } 263 | } else { 264 | Err(std::io::Error::from_raw_os_error(HRESULT_CODE(ret))) 265 | } 266 | } 267 | 268 | unsafe fn as_byte_slice(p: &T) -> &[u8] { 269 | std::slice::from_raw_parts((p as *const T) as *const u8, std::mem::size_of::()) 270 | } 271 | 272 | pub fn compress_file>(path: P, compression: Compression) -> std::io::Result { 273 | let file = std::fs::File::open(path)?; 274 | compress_file_handle(&file, compression) 275 | } 276 | 277 | pub fn compress_file_handle( 278 | file: &std::fs::File, 279 | compression: Compression, 280 | ) -> std::io::Result { 281 | const LEN: usize = std::mem::size_of::<_WOF_EXTERNAL_INFO>() 282 | + std::mem::size_of::<_FILE_PROVIDER_EXTERNAL_INFO_V1>(); 283 | 284 | let mut data = [0u8; LEN]; 285 | let (wof, inf) = data.split_at_mut(std::mem::size_of::<_WOF_EXTERNAL_INFO>()); 286 | unsafe { 287 | wof.copy_from_slice(as_byte_slice(&_WOF_EXTERNAL_INFO::default())); 288 | inf.copy_from_slice(as_byte_slice(&_FILE_PROVIDER_EXTERNAL_INFO_V1::from( 289 | compression, 290 | ))); 291 | } 292 | 293 | let mut bytes_returned: DWORD = 0; 294 | 295 | let ret = unsafe { 296 | DeviceIoControl( 297 | file.as_raw_handle() as HANDLE, 298 | FSCTL_SET_EXTERNAL_BACKING, 299 | &mut data as *mut _ as PVOID, 300 | data.len() as DWORD, 301 | std::ptr::null_mut(), 302 | 0, 303 | &mut bytes_returned, 304 | std::ptr::null_mut(), 305 | ) 306 | }; 307 | 308 | // BOOL my arse 309 | if SUCCEEDED(ret) { 310 | Ok(true) 311 | } else { 312 | let e = HRESULT_CODE(ret); 313 | 314 | if e == ERROR_COMPRESSION_NOT_BENEFICIAL { 315 | Ok(false) 316 | } else { 317 | Err(std::io::Error::from_raw_os_error(e)) 318 | } 319 | } 320 | } 321 | 322 | pub fn uncompress_file>(path: P) -> std::io::Result<()> { 323 | let file = std::fs::File::open(path)?; 324 | uncompress_file_handle(&file) 325 | } 326 | 327 | pub fn uncompress_file_handle(file: &std::fs::File) -> std::io::Result<()> { 328 | let mut bytes_returned: DWORD = 0; 329 | 330 | let ret = unsafe { 331 | DeviceIoControl( 332 | file.as_raw_handle() as HANDLE, 333 | FSCTL_DELETE_EXTERNAL_BACKING, 334 | std::ptr::null_mut(), 335 | 0, 336 | std::ptr::null_mut(), 337 | 0, 338 | &mut bytes_returned, 339 | std::ptr::null_mut(), 340 | ) 341 | }; 342 | 343 | if SUCCEEDED(ret) { 344 | Ok(()) 345 | } else { 346 | Err(std::io::Error::from_raw_os_error(HRESULT_CODE(ret))) 347 | } 348 | } 349 | 350 | #[link(name = "WofUtil")] 351 | extern "system" { 352 | pub fn WofGetDriverVersion( 353 | file_or_volume_handle: HANDLE, 354 | provider: ULONG, 355 | version: PULONG, 356 | ) -> HRESULT; 357 | 358 | pub fn WofIsExternalFile( 359 | file_path: LPCWSTR, 360 | is_external_file: PBOOL, 361 | provider: PULONG, 362 | external_file_info: PVOID, 363 | length: PULONG, 364 | ) -> HRESULT; 365 | 366 | // This is a slightly simpler way of setting file backing. 367 | pub fn WofSetFileDataLocation( 368 | file_handle: HANDLE, 369 | provider: ULONG, 370 | external_file_info: PVOID, 371 | length: ULONG, 372 | ) -> HRESULT; 373 | } 374 | 375 | #[test] 376 | fn compact_works_i_guess() { 377 | let path = std::path::PathBuf::from("Cargo.lock"); 378 | 379 | let supported = system_supports_compression().expect("system_supports_compression"); 380 | 381 | if supported && file_supports_compression(&path).expect("file_supports_compression") { 382 | uncompress_file(&path).expect("uncompress_file"); 383 | assert_eq!(None, detect_compression(&path).expect("detect_compression")); 384 | compress_file(&path, Compression::default()).expect("compress_file"); 385 | assert_eq!( 386 | Some(Compression::default()), 387 | detect_compression(&path).expect("detect_compression") 388 | ); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /src/compression.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::os::windows::fs::OpenOptionsExt; 3 | use std::path::PathBuf; 4 | 5 | use compresstimator::Compresstimator; 6 | use crossbeam_channel::{Receiver, Sender}; 7 | use filetime::FileTime; 8 | use fs2::FileExt; 9 | use winapi::um::winnt::{FILE_READ_DATA, FILE_WRITE_ATTRIBUTES}; 10 | 11 | use crate::background::Background; 12 | use crate::background::ControlToken; 13 | use crate::compact::{self, Compression}; 14 | 15 | #[derive(Debug)] 16 | pub struct BackgroundCompactor { 17 | compression: Option, 18 | files_in: Receiver<(PathBuf, u64)>, 19 | files_out: Sender<(PathBuf, io::Result)>, 20 | } 21 | 22 | impl BackgroundCompactor { 23 | pub fn new( 24 | compression: Option, 25 | files_in: Receiver<(PathBuf, u64)>, 26 | files_out: Sender<(PathBuf, io::Result)>, 27 | ) -> Self { 28 | Self { 29 | compression, 30 | files_in, 31 | files_out, 32 | } 33 | } 34 | } 35 | 36 | fn handle_file(file: &PathBuf, compression: Option) -> io::Result { 37 | let est = Compresstimator::with_block_size(8192); 38 | let meta = std::fs::metadata(&file)?; 39 | let handle = std::fs::OpenOptions::new() 40 | .access_mode(FILE_WRITE_ATTRIBUTES | FILE_READ_DATA) 41 | .open(&file)?; 42 | 43 | handle.try_lock_exclusive()?; 44 | 45 | let ret = match compression { 46 | Some(compression) => match est.compresstimate(&handle, meta.len()) { 47 | Ok(ratio) if ratio < 0.95 => compact::compress_file_handle(&handle, compression), 48 | Ok(_) => Ok(false), 49 | Err(e) => Err(e), 50 | }, 51 | None => compact::uncompress_file_handle(&handle).map(|_| true), 52 | }; 53 | 54 | let _ = filetime::set_file_handle_times( 55 | &handle, 56 | Some(FileTime::from_last_access_time(&meta)), 57 | Some(FileTime::from_last_modification_time(&meta)), 58 | ); 59 | 60 | handle.unlock()?; 61 | 62 | ret 63 | } 64 | 65 | impl Background for BackgroundCompactor { 66 | type Output = (); 67 | type Status = (); 68 | 69 | fn run(self, control: &ControlToken) -> Self::Output { 70 | for file in &self.files_in { 71 | if control.is_cancelled_with_pause() { 72 | break; 73 | } 74 | 75 | let file = file.0; 76 | let ret = handle_file(&file, self.compression); 77 | if self.files_out.send((file, ret)).is_err() { 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use globset::{Glob, GlobSet, GlobSetBuilder}; 5 | use serde_derive::{Deserialize, Serialize}; 6 | 7 | use crate::compact::Compression; 8 | 9 | #[derive(Debug, Default)] 10 | pub struct ConfigFile { 11 | backing: Option, 12 | config: Config, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct Config { 17 | pub decimal: bool, 18 | pub compression: Compression, 19 | pub excludes: Vec, 20 | } 21 | 22 | impl Default for Config { 23 | fn default() -> Self { 24 | Self { 25 | decimal: false, 26 | compression: Compression::default(), 27 | excludes: vec![ 28 | "*:\\Windows*", 29 | "*:\\System Volume Information*", 30 | "*:\\$*", 31 | "*.7z", 32 | "*.aac", 33 | "*.avi", 34 | "*.ba", 35 | "*.{bik,bk2,bnk,pc_binkvid}", 36 | "*.br", 37 | "*.bz2", 38 | "*.cab", 39 | "*.dl_", 40 | "*.docx", 41 | "*.flac", 42 | "*.flv", 43 | "*.gif", 44 | "*.gz", 45 | "*.jpeg", 46 | "*.jpg", 47 | "*.log", 48 | "*.lz4", 49 | "*.lzma", 50 | "*.lzx", 51 | "*.m[24]v", 52 | "*.m4a", 53 | "*.mkv", 54 | "*.mp[234]", 55 | "*.mpeg", 56 | "*.mpg", 57 | "*.ogg", 58 | "*.onepkg", 59 | "*.png", 60 | "*.pptx", 61 | "*.rar", 62 | "*.upk", 63 | "*.vob", 64 | "*.vs[st]x", 65 | "*.wem", 66 | "*.webm", 67 | "*.wm[afv]", 68 | "*.xap", 69 | "*.xnb", 70 | "*.xlsx", 71 | "*.xz", 72 | "*.zst", 73 | "*.zstd", 74 | ] 75 | .into_iter() 76 | .map(String::from) 77 | .collect(), 78 | } 79 | } 80 | } 81 | 82 | impl ConfigFile { 83 | pub fn new>(path: P) -> Self { 84 | Self { 85 | backing: Some(path.as_ref().to_owned()), 86 | config: std::fs::read(path) 87 | .and_then(|data| { 88 | serde_json::from_slice::(&data) 89 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 90 | }) 91 | .unwrap_or_default(), 92 | } 93 | } 94 | 95 | pub fn save(&self) -> io::Result<()> { 96 | match &self.backing { 97 | Some(path) => { 98 | if let Some(dir) = path.parent() { 99 | std::fs::create_dir_all(dir)?; 100 | } 101 | 102 | let data = serde_json::to_string_pretty(&self.config).expect("Serialize"); 103 | std::fs::write(path, &data) 104 | } 105 | None => Ok(()), 106 | } 107 | } 108 | 109 | pub fn current(&self) -> Config { 110 | self.config.clone() 111 | } 112 | 113 | pub fn replace(&mut self, c: Config) { 114 | self.config = c; 115 | } 116 | } 117 | 118 | impl Config { 119 | pub fn globset(&self) -> Result { 120 | let mut globs = GlobSetBuilder::new(); 121 | for glob in &self.excludes { 122 | globs.add(Glob::new(glob).map_err(|e| e.to_string())?); 123 | } 124 | globs.build().map_err(|e| e.to_string()) 125 | } 126 | } 127 | 128 | #[test] 129 | fn test_config() { 130 | let s = Config::default(); 131 | 132 | assert!(s.globset().is_ok()); 133 | let gs = s.globset().unwrap(); 134 | 135 | assert!(gs.is_match("C:\\foo\\bar\\hmm.rar")); 136 | assert!(gs.is_match("C:\\Windows\\System32\\floop\\bla.txt")); 137 | assert!(gs.is_match("C:\\x.lz4")); 138 | } 139 | -------------------------------------------------------------------------------- /src/console.rs: -------------------------------------------------------------------------------- 1 | // Helper functions for handling the Windows console from a GUI context. 2 | // 3 | // Windows subsystem applications must explicitly attach to an existing console 4 | // before stdio works, and if not available, create their own if they wish to 5 | // print anything. 6 | // 7 | // These functions enable that, primarily for the purposes of displaying Rust 8 | // panics. 9 | 10 | use winapi::um::consoleapi::AllocConsole; 11 | use winapi::um::wincon::{AttachConsole, FreeConsole, GetConsoleWindow, ATTACH_PARENT_PROCESS}; 12 | 13 | /// Check if we're attached to an existing Windows console 14 | pub fn is_attached() -> bool { 15 | unsafe { !GetConsoleWindow().is_null() } 16 | } 17 | 18 | /// Try to attach to an existing Windows console, if necessary. 19 | /// 20 | /// It's normally a no-brainer to call this - it just makes println! and friends 21 | /// work as expected, without cluttering the screen with a console in the general 22 | /// case. 23 | pub fn attach() -> bool { 24 | if is_attached() { 25 | return true; 26 | } 27 | 28 | unsafe { AttachConsole(ATTACH_PARENT_PROCESS) != 0 } 29 | } 30 | 31 | /// Try to attach to a console, and if not, allocate ourselves a new one. 32 | pub fn alloc() -> bool { 33 | if attach() { 34 | return true; 35 | } 36 | 37 | unsafe { AllocConsole() != 0 } 38 | } 39 | 40 | /// Free any allocated console, if any. 41 | pub fn free() { 42 | unsafe { FreeConsole() }; 43 | } 44 | -------------------------------------------------------------------------------- /src/folder.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::os::windows::fs::MetadataExt; 3 | use std::path::{Path, PathBuf}; 4 | use std::time::{Duration, Instant}; 5 | 6 | use filesize::PathExt; 7 | use globset::GlobSet; 8 | use serde_derive::Serialize; 9 | use walkdir::WalkDir; 10 | use winapi::um::winnt::{ 11 | FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM, 12 | FILE_ATTRIBUTE_TEMPORARY, 13 | }; 14 | 15 | use crate::background::{Background, ControlToken}; 16 | use crate::persistence::pathdb; 17 | 18 | #[derive(Debug, Clone, Serialize)] 19 | pub struct FileInfo { 20 | pub path: PathBuf, 21 | pub logical_size: u64, 22 | pub physical_size: u64, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Default)] 26 | pub struct GroupInfo { 27 | pub files: VecDeque, 28 | pub logical_size: u64, 29 | pub physical_size: u64, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize)] 33 | pub struct FolderInfo { 34 | pub path: PathBuf, 35 | pub logical_size: u64, 36 | pub physical_size: u64, 37 | pub compressible: GroupInfo, 38 | pub compressed: GroupInfo, 39 | pub skipped: GroupInfo, 40 | } 41 | 42 | #[derive(Debug, Clone, Serialize, Default)] 43 | pub struct FolderSummary { 44 | pub logical_size: u64, 45 | pub physical_size: u64, 46 | pub compressible: GroupSummary, 47 | pub compressed: GroupSummary, 48 | pub skipped: GroupSummary, 49 | } 50 | 51 | #[derive(Debug, Clone, Serialize, Default)] 52 | pub struct GroupSummary { 53 | pub count: usize, 54 | pub logical_size: u64, 55 | pub physical_size: u64, 56 | } 57 | 58 | #[derive(Debug, Clone, Copy)] 59 | pub enum FileKind { 60 | Compressed, 61 | Compressible, 62 | Skipped, 63 | } 64 | 65 | impl FolderInfo { 66 | pub fn new>(path: P) -> Self { 67 | Self { 68 | path: path.as_ref().to_owned(), 69 | logical_size: 0, 70 | physical_size: 0, 71 | compressible: GroupInfo::default(), 72 | compressed: GroupInfo::default(), 73 | skipped: GroupInfo::default(), 74 | } 75 | } 76 | 77 | pub fn summary(&self) -> FolderSummary { 78 | FolderSummary { 79 | logical_size: self.logical_size, 80 | physical_size: self.physical_size, 81 | compressible: self.compressible.summary(), 82 | compressed: self.compressed.summary(), 83 | skipped: self.skipped.summary(), 84 | } 85 | } 86 | 87 | pub fn len(&mut self, kind: FileKind) -> usize { 88 | match kind { 89 | FileKind::Compressible => self.compressible.files.len(), 90 | FileKind::Compressed => self.compressed.files.len(), 91 | FileKind::Skipped => self.skipped.files.len(), 92 | } 93 | } 94 | 95 | pub fn pop(&mut self, kind: FileKind) -> Option { 96 | let ret = match kind { 97 | FileKind::Compressible => self.compressible.pop(), 98 | FileKind::Compressed => self.compressed.pop(), 99 | FileKind::Skipped => self.skipped.pop(), 100 | }; 101 | 102 | if let Some(fi) = ret { 103 | self.logical_size -= fi.logical_size; 104 | self.physical_size -= fi.physical_size; 105 | 106 | Some(fi) 107 | } else { 108 | None 109 | } 110 | } 111 | 112 | pub fn push(&mut self, kind: FileKind, fi: FileInfo) { 113 | self.logical_size += fi.logical_size; 114 | self.physical_size += fi.physical_size; 115 | 116 | match kind { 117 | FileKind::Compressible => self.compressible.push(fi), 118 | FileKind::Compressed => self.compressed.push(fi), 119 | FileKind::Skipped => self.skipped.push(fi), 120 | }; 121 | } 122 | } 123 | 124 | impl GroupInfo { 125 | pub fn summary(&self) -> GroupSummary { 126 | GroupSummary { 127 | count: self.files.len(), 128 | logical_size: self.logical_size, 129 | physical_size: self.physical_size, 130 | } 131 | } 132 | 133 | fn pop(&mut self) -> Option { 134 | let ret = self.files.pop_front(); 135 | 136 | if let Some(fi) = ret { 137 | self.logical_size -= fi.logical_size; 138 | self.physical_size -= fi.physical_size; 139 | 140 | Some(fi) 141 | } else { 142 | None 143 | } 144 | } 145 | 146 | fn push(&mut self, fi: FileInfo) { 147 | self.logical_size += fi.logical_size; 148 | self.physical_size += fi.physical_size; 149 | self.files.push_back(fi); 150 | } 151 | } 152 | 153 | #[derive(Debug)] 154 | pub struct FolderScan { 155 | path: PathBuf, 156 | excludes: GlobSet, 157 | } 158 | 159 | impl FolderScan { 160 | pub fn new>(path: P, excludes: GlobSet) -> Self { 161 | Self { 162 | path: path.as_ref().to_path_buf(), 163 | excludes, 164 | } 165 | } 166 | } 167 | 168 | impl Background for FolderScan { 169 | type Output = Result; 170 | type Status = (PathBuf, FolderSummary); 171 | 172 | fn run(self, control: &ControlToken) -> Self::Output { 173 | let FolderScan { path, excludes } = self; 174 | let mut ds = FolderInfo::new(&path); 175 | let incompressible = pathdb(); 176 | let mut incompressible = incompressible.write().unwrap(); 177 | let _ = incompressible.load(); 178 | 179 | let mut last_status = Instant::now(); 180 | 181 | // 1. Handle excludes separately for directories to allow pruning, while 182 | // still recording accurate sizes for files. 183 | // 2. Ignore errors - consider recording them somewhere in future. 184 | // 3. Only process files. 185 | // 4. Grab metadata - should be infallible on Windows, it comes with the 186 | // DirEntry. 187 | // 5. GetCompressedFileSizeW() or skip. 188 | let walker = WalkDir::new(&path) 189 | .into_iter() 190 | .filter_entry(|e| e.file_type().is_file() || !excludes.is_match(e.path())) 191 | .filter_map(|e| e.map_err(|e| eprintln!("Error: {:?}", e)).ok()) 192 | .filter(|e| e.file_type().is_file()) 193 | .filter_map(|e| e.metadata().map(|md| (e, md)).ok()) 194 | .filter_map(|(e, md)| e.path().size_on_disk().map(|s| (e, md, s)).ok()) 195 | .enumerate(); 196 | 197 | for (count, (entry, metadata, physical)) in walker { 198 | let shortname = entry 199 | .path() 200 | .strip_prefix(&path) 201 | .unwrap_or_else(|_e| entry.path()) 202 | .to_path_buf(); 203 | 204 | let fi = FileInfo { 205 | path: shortname, 206 | logical_size: metadata.len().max(physical), 207 | physical_size: physical, 208 | }; 209 | 210 | if count % 8 == 0 { 211 | if control.is_cancelled_with_pause() { 212 | return Err(ds); 213 | } 214 | 215 | if last_status.elapsed() >= Duration::from_millis(50) { 216 | last_status = Instant::now(); 217 | control.set_status((fi.path.clone(), ds.summary())); 218 | } 219 | } 220 | 221 | if fi.physical_size < fi.logical_size { 222 | ds.push(FileKind::Compressed, fi); 223 | } else if fi.logical_size <= 4096 224 | || metadata.file_attributes() 225 | & (FILE_ATTRIBUTE_READONLY 226 | | FILE_ATTRIBUTE_SYSTEM 227 | | FILE_ATTRIBUTE_TEMPORARY 228 | | FILE_ATTRIBUTE_COMPRESSED) 229 | != 0 230 | || incompressible.contains(entry.path()) 231 | || excludes.is_match(entry.path()) 232 | { 233 | ds.push(FileKind::Skipped, fi); 234 | } else { 235 | ds.push(FileKind::Compressible, fi); 236 | } 237 | } 238 | 239 | Ok(ds) 240 | } 241 | } 242 | 243 | #[test] 244 | fn it_walks() { 245 | use crate::background::BackgroundHandle; 246 | use crate::config::Config; 247 | 248 | let gs = Config::default().globset().unwrap(); 249 | let scanner = FolderScan::new("C:\\Games", gs); 250 | 251 | let task = BackgroundHandle::spawn(scanner); 252 | 253 | let deadline = Instant::now() + Duration::from_millis(2000); 254 | 255 | loop { 256 | let ret = task.wait_timeout(Duration::from_millis(100)); 257 | 258 | if ret.is_some() { 259 | println!("Scanned: {:?}", ret); 260 | break; 261 | } else { 262 | println!("Status: {:?}", task.status()); 263 | } 264 | 265 | if Instant::now() > deadline { 266 | task.cancel(); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | use std::sync::Arc; 4 | 5 | use crossbeam_channel::{bounded, Receiver}; 6 | use dirs_sys::known_folder; 7 | use serde_derive::{Deserialize, Serialize}; 8 | use web_view::*; 9 | use winapi::um::knownfolders; 10 | 11 | use crate::backend::Backend; 12 | use crate::config::Config; 13 | use crate::folder::FolderSummary; 14 | use crate::persistence::{self, config}; 15 | 16 | // messages received from the GUI 17 | #[derive(Deserialize, Debug, Clone)] 18 | #[serde(tag = "type")] 19 | pub enum GuiRequest { 20 | OpenUrl { 21 | url: String, 22 | }, 23 | SaveConfig { 24 | decimal: bool, 25 | compression: String, 26 | excludes: String, 27 | }, 28 | ResetConfig, 29 | ChooseFolder, 30 | Compress, 31 | Decompress, 32 | Pause, 33 | Resume, 34 | Analyse, 35 | Stop, 36 | Quit, 37 | } 38 | 39 | // messages to send to the GUI 40 | #[derive(Serialize)] 41 | #[serde(tag = "type")] 42 | pub enum GuiResponse { 43 | Version { 44 | date: String, 45 | version: String, 46 | }, 47 | Config { 48 | decimal: bool, 49 | compression: String, 50 | excludes: String, 51 | }, 52 | Folder { 53 | path: PathBuf, 54 | }, 55 | Status { 56 | status: String, 57 | pct: Option, 58 | }, 59 | FolderSummary { 60 | info: FolderSummary, 61 | }, 62 | Paused, 63 | Resumed, 64 | Scanned, 65 | Stopped, 66 | Compacting, 67 | } 68 | 69 | pub struct GuiWrapper(Handle); 70 | 71 | impl GuiWrapper { 72 | pub fn new(handle: Handle) -> Self { 73 | let gui = Self(handle); 74 | gui.version(); 75 | gui.config(); 76 | gui 77 | } 78 | 79 | pub fn send(&self, msg: &GuiResponse) { 80 | let js = format!( 81 | "Response.dispatch(JSON.parse({}))", 82 | serde_json::to_string(msg) 83 | .and_then(|s| serde_json::to_string(&s)) 84 | .expect("serialize") 85 | ); 86 | self.0.dispatch(move |wv| wv.eval(&js)).ok(); // let errors bubble through via messages 87 | } 88 | 89 | pub fn version(&self) { 90 | let version = GuiResponse::Version { 91 | date: env!("VERGEN_BUILD_DATE").to_string(), 92 | version: format!("{}-{}", env!("CARGO_PKG_VERSION"), env!("VERGEN_SHA_SHORT")), 93 | }; 94 | self.send(&version); 95 | } 96 | 97 | pub fn config(&self) { 98 | let s = config().read().unwrap().current(); 99 | self.send(&GuiResponse::Config { 100 | decimal: s.decimal, 101 | compression: s.compression.to_string(), 102 | excludes: s.excludes.join("\n"), 103 | }); 104 | } 105 | 106 | pub fn summary(&self, info: FolderSummary) { 107 | self.send(&GuiResponse::FolderSummary { info }); 108 | } 109 | 110 | pub fn status>(&self, msg: S, val: Option) { 111 | self.send(&GuiResponse::Status { 112 | status: msg.as_ref().to_owned(), 113 | pct: val, 114 | }); 115 | } 116 | 117 | pub fn folder>(&self, path: P) { 118 | self.send(&GuiResponse::Folder { 119 | path: path.as_ref().to_path_buf(), 120 | }); 121 | } 122 | 123 | pub fn paused(&self) { 124 | self.send(&GuiResponse::Paused); 125 | } 126 | 127 | pub fn resumed(&self) { 128 | self.send(&GuiResponse::Resumed); 129 | } 130 | 131 | pub fn scanned(&self) { 132 | self.send(&GuiResponse::Scanned); 133 | } 134 | 135 | pub fn stopped(&self) { 136 | self.send(&GuiResponse::Stopped); 137 | } 138 | 139 | pub fn compacting(&self) { 140 | self.send(&GuiResponse::Compacting); 141 | } 142 | 143 | pub fn choose_folder(&self) -> Receiver> { 144 | let (tx, rx) = bounded::>(1); 145 | let _ = self.0.dispatch(move |_| { 146 | let folder = known_folder(&knownfolders::FOLDERID_ProgramFiles); 147 | let folder = folder.and_then(|path| path.to_str().map(str::to_string)).unwrap_or_default(); 148 | let params = wfd::DialogParams { 149 | options: wfd::FOS_PICKFOLDERS, 150 | title: "Select a directory", 151 | default_folder: &folder, 152 | ..Default::default() 153 | }; 154 | let _ = tx.send( 155 | wfd::open_dialog(params).map(|res| res.selected_file_path).ok() 156 | ); 157 | Ok(()) 158 | }); 159 | 160 | rx 161 | } 162 | } 163 | 164 | pub fn spawn_gui() { 165 | let running = Arc::new(AtomicBool::new(true)); 166 | let r = running.clone(); 167 | ctrlc::set_handler(move || { 168 | r.store(false, Ordering::SeqCst); 169 | }) 170 | .expect("Error setting Ctrl-C handler"); 171 | 172 | let html = format!( 173 | include_str!("ui/index.html"), 174 | style = include_str!("ui/style.css"), 175 | script = format!( 176 | "{}\n{}", 177 | include_str!("ui/cash.min.js"), 178 | include_str!("ui/app.js") 179 | ) 180 | ); 181 | 182 | let (from_gui, from_gui_rx) = bounded::(128); 183 | 184 | let mut webview = web_view::builder() 185 | .title("Compactor") 186 | .content(Content::Html(html)) 187 | .size(750, 430) 188 | .resizable(true) 189 | .debug(true) 190 | .user_data(()) 191 | .invoke_handler(move |mut webview, arg| { 192 | match serde_json::from_str::(arg) { 193 | Ok(GuiRequest::OpenUrl { url }) => { 194 | let _ = open::that(url); 195 | } 196 | Ok(GuiRequest::SaveConfig { 197 | decimal, 198 | compression, 199 | excludes, 200 | }) => { 201 | let s = Config { 202 | decimal, 203 | compression: compression.parse().unwrap_or_default(), 204 | excludes: excludes.split('\n').map(str::to_owned).collect(), 205 | }; 206 | 207 | if let Err(msg) = s.globset() { 208 | tinyfiledialogs::message_box_ok( 209 | "Settings Error", 210 | &msg, 211 | tinyfiledialogs::MessageBoxIcon::Error, 212 | ); 213 | } else { 214 | message_dispatch( 215 | &mut webview, 216 | &GuiResponse::Config { 217 | decimal: s.decimal, 218 | compression: s.compression.to_string(), 219 | excludes: s.excludes.join("\n"), 220 | }, 221 | ); 222 | let c = config(); 223 | let mut c = c.write().unwrap(); 224 | c.replace(s); 225 | if let Err(e) = c.save() { 226 | tinyfiledialogs::message_box_ok( 227 | "Settings Error", 228 | &format!("Error saving settings: {:?}", e), 229 | tinyfiledialogs::MessageBoxIcon::Error, 230 | ); 231 | } 232 | } 233 | } 234 | Ok(GuiRequest::ResetConfig) => { 235 | let s = Config::default(); 236 | 237 | message_dispatch( 238 | &mut webview, 239 | &GuiResponse::Config { 240 | decimal: s.decimal, 241 | compression: s.compression.to_string(), 242 | excludes: s.excludes.join("\n"), 243 | }, 244 | ); 245 | let c = config(); 246 | let mut c = c.write().unwrap(); 247 | c.replace(s); 248 | if let Err(e) = c.save() { 249 | tinyfiledialogs::message_box_ok( 250 | "Settings Error", 251 | &format!("Error saving settings: {:?}", e), 252 | tinyfiledialogs::MessageBoxIcon::Error, 253 | ); 254 | } 255 | } 256 | Ok(msg) => { 257 | from_gui.send(msg).expect("GUI message queue"); 258 | } 259 | Err(err) => { 260 | eprintln!("Unhandled message {:?}: {:?}", arg, err); 261 | } 262 | } 263 | 264 | Ok(()) 265 | }) 266 | .build() 267 | .expect("WebView"); 268 | 269 | persistence::init(); 270 | 271 | let gui = GuiWrapper::new(webview.handle()); 272 | let mut backend = Backend::new(gui, from_gui_rx); 273 | let bg = std::thread::spawn(move || { 274 | backend.run(); 275 | }); 276 | 277 | while running.load(Ordering::SeqCst) { 278 | match webview.step() { 279 | Some(Ok(_)) => (), 280 | Some(e) => { 281 | eprintln!("Error: {:?}", e); 282 | } 283 | None => { 284 | break; 285 | } 286 | } 287 | } 288 | 289 | webview.into_inner(); 290 | 291 | bg.join().expect("background thread"); 292 | } 293 | 294 | fn message_dispatch(wv: &mut web_view::WebView<'_, T>, msg: &GuiResponse) { 295 | let js = format!( 296 | "Response.dispatch({})", 297 | serde_json::to_string(msg).expect("serialize") 298 | ); 299 | 300 | wv.eval(&js).ok(); 301 | } 302 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), windows_subsystem = "windows")] 2 | #![cfg_attr(test, windows_subsystem = "console")] 3 | #![allow(non_snake_case)] 4 | 5 | mod backend; 6 | mod background; 7 | mod compact; 8 | mod compression; 9 | mod config; 10 | mod console; 11 | mod folder; 12 | mod gui; 13 | mod persistence; 14 | 15 | fn setup_panic() { 16 | std::panic::set_hook(Box::new(|e| { 17 | if !console::alloc() { 18 | // No point trying to print without a console... 19 | return; 20 | } 21 | 22 | println!( 23 | r#" 24 | Oh dear, {app} has crashed. Sorry :( 25 | 26 | You can report this on the website at {website}/issues 27 | 28 | Please try to include everything below the line, and give some hints as to what 29 | you were doing - like what folder you were running it on. 30 | 31 | ############################################################################# 32 | 33 | App: {app}, Version: {ver}, Build Date: {date}, Hash: {hash} 34 | "#, 35 | app = env!("CARGO_PKG_NAME"), 36 | website = env!("CARGO_PKG_HOMEPAGE"), 37 | ver = env!("VERGEN_SEMVER"), 38 | date = env!("VERGEN_BUILD_DATE").to_string(), 39 | hash = env!("VERGEN_SHA_SHORT") 40 | ); 41 | 42 | if let Some(s) = e.payload().downcast_ref::<&'static str>() { 43 | println!("panic: {}", s); 44 | } else { 45 | println!("panic: [mysteriously lacks a string representation]"); 46 | } 47 | 48 | println!("\nHit Enter to print the rest of the debug info."); 49 | 50 | let _ = std::io::stdin().read_line(&mut String::new()); 51 | 52 | let backtrace = backtrace::Backtrace::new(); 53 | 54 | println!("\n{:?}\n", backtrace); 55 | println!("Hit Enter to continue."); 56 | 57 | let _ = std::io::stdin().read_line(&mut String::new()); 58 | })); 59 | } 60 | 61 | fn main() { 62 | setup_panic(); 63 | console::attach(); 64 | let ret = std::panic::catch_unwind(gui::spawn_gui); 65 | console::free(); 66 | 67 | if ret.is_err() { 68 | std::process::exit(1); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/persistence.rs: -------------------------------------------------------------------------------- 1 | use directories::ProjectDirs; 2 | use hashfilter::HashFilter; 3 | use lazy_static::lazy_static; 4 | use std::sync::RwLock; 5 | 6 | use crate::config::ConfigFile; 7 | 8 | lazy_static! { 9 | static ref PATHDB: RwLock = RwLock::new(HashFilter::default()); 10 | static ref CONFIG: RwLock = RwLock::new(ConfigFile::default()); 11 | } 12 | 13 | pub fn init() { 14 | if let Some(dirs) = ProjectDirs::from("", "Freaky", "Compactor") { 15 | pathdb() 16 | .write() 17 | .unwrap() 18 | .set_backing(dirs.cache_dir().join("incompressible.dat")); 19 | *config().write().unwrap() = ConfigFile::new(dirs.config_dir().join("config.json")); 20 | } 21 | } 22 | 23 | pub fn config() -> &'static RwLock { 24 | &CONFIG 25 | } 26 | 27 | pub fn pathdb() -> &'static RwLock { 28 | &PATHDB 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/app.js: -------------------------------------------------------------------------------- 1 | /* jshint strict: true, esversion: 5, browser: true */ 2 | 3 | var Util = (function() { 4 | "use strict"; 5 | 6 | var powers = '_KMGTPEZY'; 7 | var monotime = function() { return Date.now(); }; 8 | 9 | if (window.performance && window.performance.now) 10 | monotime = function() { return window.performance.now(); }; 11 | 12 | return { 13 | debounce: function(callback, delay) { 14 | var timeout; 15 | var fn = function() { 16 | var context = this; 17 | var args = arguments; 18 | 19 | clearTimeout(timeout); 20 | timeout = setTimeout(function() { 21 | timeout = null; 22 | callback.apply(context, args); 23 | }, delay); 24 | }; 25 | fn.clear = function() { 26 | clearTimeout(timeout); 27 | timeout = null; 28 | }; 29 | 30 | return fn; 31 | }, 32 | 33 | throttle: function(callback, delay) { 34 | var timeout; 35 | var last; 36 | var fn = function() { 37 | var context = this; 38 | var args = arguments; 39 | var now = monotime(); 40 | 41 | if (last && now < last + delay) { 42 | clearTimeout(timeout); 43 | timeout = setTimeout(function() { 44 | timeout = null; 45 | last = now; 46 | callback.apply(context, args); 47 | }, delay); 48 | } else { 49 | last = now; 50 | callback.apply(context, args); 51 | } 52 | }; 53 | fn.clear = function() { 54 | clearTimeout(timeout); 55 | timeout = null; 56 | }; 57 | 58 | return fn; 59 | }, 60 | 61 | format_number: function(number, digits) { 62 | if (digits === undefined) digits = 2; 63 | return number.toLocaleString("en", {minimumFractionDigits: digits, maximumFractionDigits: digits}); 64 | }, 65 | 66 | bytes_to_human_dec: function(bytes) { 67 | for (var i = powers.length - 1; i > 0; i--) { 68 | var div = Math.pow(10, 3 * i); 69 | if (bytes >= div) { 70 | return Util.format_number(bytes / div, 2) + " " + powers[i] + 'B'; 71 | } 72 | } 73 | 74 | return Util.format_number(bytes, 0) + ' B'; 75 | }, 76 | 77 | bytes_to_human_bin: function(bytes) { 78 | for (var i = powers.length - 1; i > 0; i--) { 79 | var div = Math.pow(2, 10*i); 80 | if (bytes >= div) { 81 | return Util.format_number(bytes / div, 2) + " " + powers[i] + 'iB'; 82 | } 83 | } 84 | 85 | return Util.format_number(bytes, 0) + ' B'; 86 | }, 87 | 88 | human_to_bytes: function(human) { 89 | if (!human) return null; 90 | var num = parseFloat(human); 91 | 92 | var match = (/\s*([KMGTPEZY])(i)?([Bb])?\s*$/i).exec(human); 93 | if (match) { 94 | var pow = (match[2] == 'i') ? 1024 : 1000; 95 | // var mul = (match[3] == 'B') ? 8 : 1; 96 | 97 | num *= Math.pow(pow, powers.indexOf(match[1].toUpperCase())); 98 | } 99 | 100 | return num; 101 | }, 102 | 103 | number_to_human: function(num) { 104 | for (var i = powers.length - 1; i > 0; i--) { 105 | var div = Math.pow(10, 3*i); 106 | if (num >= div) { 107 | return Util.format_number(num / div, 2) + powers[i]; 108 | } 109 | } 110 | 111 | return num; 112 | }, 113 | 114 | human_to_number: function(human) { 115 | if (!human) return null; 116 | var num = parseFloat(human); 117 | 118 | var match = (/\s*([KMGTPEZY])\s*$/i).exec(human); 119 | 120 | if (match) { 121 | num *= Math.pow(1000, powers.indexOf(match[1].toUpperCase())); 122 | } 123 | 124 | return num; 125 | }, 126 | 127 | sformat: function() { 128 | var args = arguments; 129 | return args[0].replace(/\{(\d+)\}/g, function (m, n) { return args[parseInt(n, 10) + 1]; }); 130 | }, 131 | 132 | range: function(a, b, step) { 133 | if (!step) step = 1; 134 | var arr = []; 135 | for (var i = a; i < b; i += step) { 136 | arr.push(i); 137 | } 138 | return arr; 139 | } 140 | }; 141 | })(); 142 | 143 | Util.bytes_to_human = Util.bytes_to_human_bin; 144 | 145 | // Actions call back into Rust 146 | var Action = (function() { 147 | "use strict"; 148 | 149 | return { 150 | open_url: function(url) { 151 | external.invoke(JSON.stringify({ type: 'OpenUrl', url: url })); 152 | }, 153 | 154 | reset_config: function() { 155 | external.invoke(JSON.stringify({ type: 'ResetConfig' })); 156 | }, 157 | 158 | save_config: function(config) { 159 | config.type = 'SaveConfig'; 160 | external.invoke(JSON.stringify(config)); 161 | }, 162 | 163 | choose_folder: function() { 164 | external.invoke(JSON.stringify({ type: 'ChooseFolder' })); 165 | }, 166 | 167 | compress: function() { 168 | external.invoke(JSON.stringify({ type: 'Compress' })); 169 | }, 170 | 171 | decompress: function() { 172 | external.invoke(JSON.stringify({ type: 'Decompress' })); 173 | }, 174 | 175 | pause: function() { 176 | external.invoke(JSON.stringify({ type: 'Pause' })); 177 | }, 178 | 179 | resume: function() { 180 | external.invoke(JSON.stringify({ type: 'Resume' })); 181 | }, 182 | 183 | analyse: function() { 184 | external.invoke(JSON.stringify({ type: 'Analyse' })); 185 | }, 186 | 187 | stop: function() { 188 | external.invoke(JSON.stringify({ type: 'Stop' })); 189 | }, 190 | 191 | quit: function() { 192 | external.invoke(JSON.stringify({ type: 'Quit' })); 193 | } 194 | }; 195 | })(); 196 | 197 | // Responses come from Rust 198 | var Response = (function() { 199 | "use strict"; 200 | 201 | return { 202 | dispatch: function(msg) { 203 | switch(msg.type) { 204 | case "Config": 205 | Gui.set_decimal(msg.decimal); 206 | Gui.set_compression(msg.compression); 207 | Gui.set_excludes(msg.excludes); 208 | break; 209 | 210 | case "Folder": 211 | Gui.set_folder(msg.path); 212 | break; 213 | 214 | case "Version": 215 | Gui.version(msg.date, msg.version); 216 | break; 217 | 218 | case "Status": 219 | Gui.set_status(msg.status, msg.pct); 220 | break; 221 | 222 | case "Paused": 223 | case "Resumed": 224 | case "Stopped": 225 | case "Scanned": 226 | case "Compacting": 227 | Gui[msg.type.toLowerCase()](); 228 | break; 229 | 230 | case "FolderSummary": 231 | Gui.set_folder_summary(msg.info); 232 | break; 233 | } 234 | } 235 | }; 236 | })(); 237 | 238 | // Anything poking the GUI lives here 239 | var Gui = (function() { 240 | "use strict"; 241 | 242 | return { 243 | boot: function() { 244 | $("a[href]").on("click", function(e) { 245 | e.preventDefault(); 246 | Action.open_url($(this).attr("href")); 247 | return false; 248 | }); 249 | 250 | $("#Button_Save").on("click", function() { 251 | Action.save_config({ 252 | decimal: $("#SI_Units").val() == "D", 253 | compression: $("#Compression_Mode").val(), 254 | excludes: $("#Excludes").val() 255 | }); 256 | }); 257 | 258 | $("#Button_Reset").on("click", function() { 259 | Action.reset_config(); 260 | }); 261 | }, 262 | 263 | page: function(page) { 264 | $("nav button").removeClass("active"); 265 | $("#Button_Page_" + page).addClass("active"); 266 | $("section.page").hide(); 267 | $("#" + page).show(); 268 | }, 269 | 270 | version: function(date, version) { 271 | $(".compile-date").text(date); 272 | $(".version").text(version); 273 | }, 274 | 275 | set_decimal: function(dec) { 276 | var field = $("#SI_Units"); 277 | if (dec) { 278 | field.val("D"); 279 | Util.bytes_to_human = Util.bytes_to_human_dec; 280 | } else { 281 | field.val("I"); 282 | Util.bytes_to_human = Util.bytes_to_human_bin; 283 | } 284 | }, 285 | 286 | set_compression: function(compression) { 287 | $("#Compression_Mode").val(compression); 288 | }, 289 | 290 | set_excludes: function(excludes) { 291 | $("#Excludes").val(excludes); 292 | }, 293 | 294 | set_folder: function(folder) { 295 | var bits = folder.split(/:\\|\\/).map(function(x) { return document.createTextNode(x); }); 296 | var end = bits.pop(); 297 | 298 | var button = $("#Button_Folder"); 299 | button.empty(); 300 | bits.forEach(function(bit) { 301 | button.append(bit); 302 | button.append($("")); 303 | }); 304 | button.append(end); 305 | 306 | Gui.scanning(); 307 | }, 308 | 309 | set_status: function(status, pct) { 310 | $("#Activity_Text").text(status); 311 | if (pct != null) { 312 | $("#Activity_Progress").val(pct); 313 | } else { 314 | $("#Activity_Progress").removeAttr("value"); 315 | } 316 | }, 317 | 318 | scanning: function() { 319 | Gui.reset_folder_summary(); 320 | $("#Activity").show(); 321 | $("#Analysis").show(); 322 | $("#Button_Pause").show(); 323 | $("#Button_Resume").hide(); 324 | $("#Button_Stop").show(); 325 | $("#Button_Analyse").hide(); 326 | $("#Button_Compress").hide(); 327 | $("#Button_Decompress").hide(); 328 | $("#Command").show(); 329 | }, 330 | 331 | compacting: function() { 332 | $("#Button_Pause").show(); 333 | $("#Button_Resume").hide(); 334 | $("#Button_Stop").show(); 335 | $("#Button_Analyse").hide(); 336 | $("#Button_Compress").hide(); 337 | $("#Button_Decompress").hide(); 338 | }, 339 | 340 | paused: function() { 341 | $("#Button_Pause").hide(); 342 | $("#Button_Resume").show(); 343 | }, 344 | 345 | resumed: function() { 346 | $("#Button_Pause").show(); 347 | $("#Button_Resume").hide(); 348 | }, 349 | 350 | stopped: function() { 351 | Gui.scanned(); 352 | }, 353 | 354 | scanned: function() { 355 | $("#Button_Pause").hide(); 356 | $("#Button_Resume").hide(); 357 | $("#Button_Stop").hide(); 358 | $("#Button_Analyse").show(); 359 | 360 | if ($("#File_Count_Compressible").text() != "0") { 361 | $("#Button_Compress").show(); 362 | } else { 363 | $("#Button_Compress").hide(); 364 | } 365 | 366 | if ($("#File_Count_Compressed").text() != "0") { 367 | $("#Button_Decompress").show(); 368 | } else { 369 | $("#Button_Decompress").hide(); 370 | } 371 | }, 372 | 373 | reset_folder_summary: function() { 374 | Gui.set_folder_summary({ 375 | logical_size: 0, 376 | physical_size: 0, 377 | compressed: {count: 0, logical_size: 0, physical_size: 0}, 378 | compressible: {count: 0, logical_size: 0, physical_size: 0}, 379 | skipped: {count: 0, logical_size: 0, physical_size: 0} 380 | }); 381 | }, 382 | 383 | set_folder_summary: function(data) { 384 | $("#Size_Logical").text(Util.bytes_to_human(data.logical_size)); 385 | $("#Size_Physical").text(Util.bytes_to_human(data.physical_size)); 386 | 387 | if (data.logical_size > 0) { 388 | var ratio = (data.physical_size / data.logical_size); 389 | $("#Compress_Ratio").text(Util.format_number(ratio, 2)); 390 | } else { 391 | $("#Compress_Ratio").text("1.00"); 392 | } 393 | 394 | if (data.logical_size > 0) { 395 | var total = data.logical_size; 396 | $("#Compressed_Size").text(Util.bytes_to_human(data.compressed.physical_size)); 397 | $("#Compressible_Size").text(Util.bytes_to_human(data.compressible.physical_size)); 398 | $("#Skipped_Size").text(Util.bytes_to_human(data.skipped.physical_size)); 399 | document.getElementById("Breakdown_Compressed").style.width = "" + 100 * (data.compressed.physical_size / total).toFixed(2) + "%"; 400 | document.getElementById("Breakdown_Compressible").style.width = "" + 100 * (data.compressible.physical_size / total).toFixed(2) + "%"; 401 | document.getElementById("Breakdown_Skipped").style.width = "" + 100 * (data.skipped.physical_size / total).toFixed(2) + "%"; 402 | } 403 | 404 | $("#Space_Saved").text(Util.bytes_to_human(data.compressed.logical_size - data.compressed.physical_size)); 405 | 406 | $("#File_Count_Compressed").text(Util.format_number(data.compressed.count, 0)); 407 | $("#File_Count_Compressible").text(Util.format_number(data.compressible.count, 0)); 408 | $("#File_Count_Skipped").text(Util.format_number(data.skipped.count, 0)); 409 | }, 410 | 411 | analysis_complete: function() { 412 | $("#Activity").hide(); 413 | $("#Analysis").show(); 414 | 415 | } 416 | }; 417 | })(); 418 | 419 | $(document).ready(Gui.boot); 420 | -------------------------------------------------------------------------------- /src/ui/cash.min.js: -------------------------------------------------------------------------------- 1 | /* MIT https://github.com/kenwheeler/cash */ 2 | (function(){ 3 | 'use strict';var e=document,f=window,k=e.createElement("div"),l=Array.prototype,m=l.filter,n=l.indexOf,aa=l.map,q=l.push,r=l.reverse,u=l.slice,v=l.some,ba=l.splice,ca=/^#[\w-]*$/,da=/^\.[\w-]*$/,ea=/<.+>/,fa=/^\w+$/;function w(a,b){void 0===b&&(b=e);return b!==e&&1!==b.nodeType&&9!==b.nodeType?[]:da.test(a)?b.getElementsByClassName(a.slice(1)):fa.test(a)?b.getElementsByTagName(a):b.querySelectorAll(a)} 4 | var x=function(){function a(a,c){void 0===c&&(c=e);if(a){if(a instanceof x)return a;var b=a;if(y(a)){if(b=c instanceof x?c[0]:c,b=ca.test(a)?b.getElementById(a.slice(1)):ea.test(a)?A(a):w(a,b),!b)return}else if(B(a))return this.ready(a);if(b.nodeType||b===f)b=[b];this.length=b.length;a=0;for(c=this.length;aa?a+this.length:a]};x.prototype.eq=function(a){return C(this.get(a))};x.prototype.first=function(){return this.eq(0)};x.prototype.last=function(){return this.eq(-1)};x.prototype.map=function(a){return C(aa.call(this,function(b,c){return a.call(b,c,b)}))};x.prototype.slice=function(){return C(u.apply(this,arguments))};var ha=/-([a-z])/g; 6 | function ia(a,b){return b.toUpperCase()}function D(a){return a.replace(ha,ia)}C.camelCase=D;function E(a,b){for(var c=0,d=a.length;cc?0:1;darguments.length?this[0]&&this[0][a]:this.each(function(c,g){g[a]=b});for(var c in a)this.prop(c,a[c]);return this}};function K(a){return y(a)?function(b,c){return H(c,a)}:B(a)?a:a instanceof x?function(b,c){return a.is(c)}:function(b,c){return c===a}}x.prototype.filter=function(a){if(!a)return C();var b=K(a);return C(m.call(this,function(a,d){return b.call(a,d,a)}))}; 9 | function L(a,b){return b&&a.length?a.filter(b):a}var ka=/\S+/g;function M(a){return y(a)?a.match(ka)||[]:[]}x.prototype.hasClass=function(a){return a&&v.call(this,function(b){return b.classList.contains(a)})};x.prototype.removeAttr=function(a){var b=M(a);return b.length?this.each(function(a,d){E(b,function(a,b){d.removeAttribute(b)})}):this}; 10 | x.prototype.attr=function(a,b){if(a){if(y(a)){if(2>arguments.length){if(!this[0])return;var c=this[0].getAttribute(a);return null===c?void 0:c}return null===b?this.removeAttr(a):this.each(function(c,g){g.setAttribute(a,b)})}for(c in a)this.attr(c,a[c]);return this}};x.prototype.toggleClass=function(a,b){var c=M(a),d=void 0!==b;return c.length?this.each(function(a,h){E(c,function(a,c){d?b?h.classList.add(c):h.classList.remove(c):h.classList.toggle(c)})}):this}; 11 | x.prototype.addClass=function(a){return this.toggleClass(a,!0)};x.prototype.removeClass=function(a){return arguments.length?this.toggleClass(a,!1):this.attr("class","")};function N(a){return 1arguments.length)return this[0]&&O(this[0],a,c);if(!a)return this;b=pa(a,b,c);return this.each(function(d,h){1===h.nodeType&&(c?h.style.setProperty(a,b):h.style[a]=b)})}for(var d in a)this.css(d,a[d]);return this};var qa=/^data-(.*)/;C.hasData=function(a){return"__cashData"in a};function S(a){return a.__cashData=a.__cashData||{}} 14 | function ra(a,b){var c=S(a);if(b){if(!(b in c)&&(a=a.dataset?a.dataset[b]||a.dataset[D(b)]:C(a).attr("data-"+b),void 0!==a)){try{a=JSON.parse(a)}catch(d){}c[b]=a}return c[b]}return c}x.prototype.data=function(a,b){var c=this;if(!a){if(!this[0])return;E(this[0].attributes,function(a,b){(a=b.name.match(qa))&&c.data(a[1])});return ra(this[0])}if(y(a))return void 0===b?this[0]&&ra(this[0],a):this.each(function(c,d){S(d)[a]=b});for(var d in a)this.data(d,a[d]);return this}; 15 | x.prototype.removeData=function(a){return this.each(function(b,c){void 0===a?delete c.__cashData:delete S(c)[a]})};function sa(a,b){return P(a,"border"+(b?"Left":"Top")+"Width")+P(a,"padding"+(b?"Left":"Top"))+P(a,"padding"+(b?"Right":"Bottom"))+P(a,"border"+(b?"Right":"Bottom")+"Width")}E(["Width","Height"],function(a,b){x.prototype["inner"+b]=function(){if(this[0])return this[0]===f?f["inner"+b]:this[0]["client"+b]}}); 16 | E(["width","height"],function(a,b){x.prototype[b]=function(c){if(!this[0])return void 0===c?void 0:this;if(!arguments.length)return this[0]===f?this[0][D("outer-"+b)]:this[0].getBoundingClientRect()[b]-sa(this[0],!a);var d=parseInt(c,10);return this.each(function(c,h){1===h.nodeType&&(c=O(h,"boxSizing"),h.style[b]=pa(b,d+("border-box"===c?sa(h,!a):0)))})}}); 17 | E(["Width","Height"],function(a,b){x.prototype["outer"+b]=function(c){if(this[0])return this[0]===f?f["outer"+b]:this[0]["offset"+b]+(c?P(this[0],"margin"+(a?"Top":"Left"))+P(this[0],"margin"+(a?"Bottom":"Right")):0)}});var T={}; 18 | x.prototype.toggle=function(a){return this.each(function(b,c){if(a=void 0!==a?a:"none"===O(c,"display")){if(c.style.display="","none"===O(c,"display")){b=c.style;c=c.tagName;if(T[c])c=T[c];else{var d=e.createElement(c);e.body.appendChild(d);var g=O(d,"display");e.body.removeChild(d);c=T[c]="none"!==g?g:"block"}b.display=c}}else c.style.display="none"})};x.prototype.hide=function(){return this.toggle(!1)};x.prototype.show=function(){return this.toggle(!0)}; 19 | function ta(a,b){return!b||!v.call(b,function(b){return 0>a.indexOf(b)})}var U={focus:"focusin",blur:"focusout"},ua={mouseenter:"mouseover",mouseleave:"mouseout"},va=/^(?:mouse|pointer|contextmenu|drag|drop|click|dblclick)/i;function wa(a,b,c,d,g){g.guid=g.guid||G++;var h=a.__cashEvents=a.__cashEvents||{};h[b]=h[b]||[];h[b].push([c,d,g]);a.addEventListener(b,g)}function V(a){a=a.split(".");return[a[0],a.slice(1).sort()]} 20 | function W(a,b,c,d,g){var h=a.__cashEvents=a.__cashEvents||{};if(b)h[b]&&(h[b]=h[b].filter(function(h){var p=h[0],za=h[1];h=h[2];if(g&&h.guid!==g.guid||!ta(p,c)||d&&d!==za)return!0;a.removeEventListener(b,h)}));else{for(b in h)W(a,b,c,d,g);delete a.__cashEvents}}x.prototype.off=function(a,b,c){var d=this;void 0===a?this.each(function(a,b){return W(b)}):(B(b)&&(c=b,b=""),E(M(a),function(a,h){a=V(ua[h]||U[h]||h);var g=a[0],z=a[1];d.each(function(a,d){return W(d,g,z,b,c)})}));return this}; 21 | x.prototype.on=function(a,b,c,d){var g=this;if(!y(a)){for(var h in a)this.on(h,b,a[h]);return this}B(b)&&(c=b,b="");E(M(a),function(a,h){a=V(ua[h]||U[h]||h);var p=a[0],z=a[1];g.each(function(a,h){a=function Aa(a){if(!a.namespace||ta(z,a.namespace.split("."))){var g=h;if(b){for(var t=a.target;!H(t,b);){if(t===h)return;t=t.parentNode;if(!t)return}g=t;a.__delegate=!0}a.__delegate&&Object.defineProperty(a,"currentTarget",{configurable:!0,get:function(){return g}});t=c.call(g,a,a.data);d&&W(h,p,z,b,Aa); 22 | !1===t&&(a.preventDefault(),a.stopPropagation())}};a.guid=c.guid=c.guid||G++;wa(h,p,z,b,a)})});return this};x.prototype.one=function(a,b,c){return this.on(a,b,c,!0)};x.prototype.ready=function(a){function b(){return a(C)}"loading"!==e.readyState?setTimeout(b):e.addEventListener("DOMContentLoaded",b);return this}; 23 | x.prototype.trigger=function(a,b){var c=a;if(y(a)){var d=V(a);a=d[0];d=d[1];var g=va.test(a)?"MouseEvents":"HTMLEvents";c=e.createEvent(g);c.initEvent(a,!0,!0);c.namespace=d.join(".")}c.data=b;var h=c.type in U;return this.each(function(a,b){if(h&&B(b[c.type]))b[c.type]();else b.dispatchEvent(c)})};function xa(a){return a.multiple?I(m.call(a.options,function(a){return a.selected&&!a.disabled&&!a.parentNode.disabled}),"value"):a.value||""}var ya=/%20/g,Ba=/file|reset|submit|button|image/i,Ca=/radio|checkbox/i; 24 | x.prototype.serialize=function(){var a="";this.each(function(b,c){E(c.elements||[c],function(b,c){c.disabled||!c.name||"FIELDSET"===c.tagName||Ba.test(c.type)||Ca.test(c.type)&&!c.checked||(b=xa(c),void 0!==b&&(b=J(b)?b:[b],E(b,function(b,d){b=a;d="&"+encodeURIComponent(c.name)+"="+encodeURIComponent(d).replace(ya,"+");a=b+d})))})});return a.substr(1)}; 25 | x.prototype.val=function(a){return void 0===a?this[0]&&xa(this[0]):this.each(function(b,c){if("SELECT"===c.tagName){var d=J(a)?a:null===a?[]:[a];E(c.options,function(a,b){b.selected=0<=d.indexOf(b.value)})}else c.value=null===a?"":a})};x.prototype.clone=function(){return this.map(function(a,b){return b.cloneNode(!0)})};x.prototype.detach=function(){return this.each(function(a,b){b.parentNode&&b.parentNode.removeChild(b)})};var Da=/^\s*<(\w+)[^>]*>/,Ea=/^\s*<(\w+)\s*\/?>(?:<\/\1>)?\s*$/,X; 26 | function A(a){if(!X){var b=e.createElement("table"),c=e.createElement("tr");X={"*":k,tr:e.createElement("tbody"),td:c,th:c,thead:b,tbody:b,tfoot:b}}if(!y(a))return[];if(Ea.test(a))return[e.createElement(RegExp.$1)];b=Da.test(a)&&RegExp.$1;b=X[b]||X["*"];b.innerHTML=a;return C(b.childNodes).detach().get()}C.parseHTML=A;x.prototype.empty=function(){var a=this[0];if(a)for(;a.firstChild;)a.removeChild(a.firstChild);return this}; 27 | x.prototype.html=function(a){return void 0===a?this[0]&&this[0].innerHTML:this.each(function(b,c){c.innerHTML=a})};x.prototype.remove=function(){return this.detach().off()};x.prototype.text=function(a){return void 0===a?this[0]?this[0].textContent:"":this.each(function(b,c){c.textContent=a})};x.prototype.unwrap=function(){this.parent().each(function(a,b){a=C(b);a.replaceWith(a.children())});return this};var Fa=e.documentElement; 28 | x.prototype.offset=function(){var a=this[0];if(a)return a=a.getBoundingClientRect(),{top:a.top+f.pageYOffset-Fa.clientTop,left:a.left+f.pageXOffset-Fa.clientLeft}};x.prototype.offsetParent=function(){return C(this[0]&&this[0].offsetParent)};x.prototype.position=function(){var a=this[0];if(a)return{left:a.offsetLeft,top:a.offsetTop}};x.prototype.children=function(a){var b=[];this.each(function(a,d){q.apply(b,d.children)});return L(C(N(b)),a)}; 29 | x.prototype.contents=function(){var a=[];this.each(function(b,c){q.apply(a,"IFRAME"===c.tagName?[c.contentDocument]:c.childNodes)});return C(N(a))};x.prototype.find=function(a){for(var b=[],c=0,d=this.length;c\s*$/g; 30 | function Y(a){a=C(a);a.filter("script").add(a.find("script")).each(function(a,c){!c.src&&Ga.test(c.type)&&c.ownerDocument.documentElement.contains(c)&&eval(c.textContent.replace(Ha,""))})}function Z(a,b,c){E(a,function(a,g){E(b,function(b,d){b=a?d.cloneNode(!0):d;c?g.insertBefore(b,c&&g.firstChild):g.appendChild(b);Y(b)})})}x.prototype.append=function(){var a=this;E(arguments,function(b,c){Z(a,C(c))});return this};x.prototype.appendTo=function(a){Z(C(a),this);return this}; 31 | x.prototype.insertAfter=function(a){var b=this;C(a).each(function(a,d){var c=d.parentNode;c&&b.each(function(b,g){b=a?g.cloneNode(!0):g;c.insertBefore(b,d.nextSibling);Y(b)})});return this};x.prototype.after=function(){var a=this;E(r.apply(arguments),function(b,c){r.apply(C(c).slice()).insertAfter(a)});return this};x.prototype.insertBefore=function(a){var b=this;C(a).each(function(a,d){var c=d.parentNode;c&&b.each(function(b,g){b=a?g.cloneNode(!0):g;c.insertBefore(b,d);Y(b)})});return this}; 32 | x.prototype.before=function(){var a=this;E(arguments,function(b,c){C(c).insertBefore(a)});return this};x.prototype.prepend=function(){var a=this;E(arguments,function(b,c){Z(a,C(c),!0)});return this};x.prototype.prependTo=function(a){Z(C(a),r.apply(this.slice()),!0);return this};x.prototype.replaceWith=function(a){return this.before(a).remove()};x.prototype.replaceAll=function(a){C(a).replaceWith(this);return this}; 33 | x.prototype.wrapAll=function(a){if(this[0]){a=C(a);this.first().before(a);for(a=a[0];a.children.length;)a=a.firstElementChild;this.appendTo(a)}return this};x.prototype.wrap=function(a){return this.each(function(b,c){var d=C(a)[0];C(c).wrapAll(b?d.cloneNode(!0):d)})};x.prototype.wrapInner=function(a){return this.each(function(b,c){b=C(c);c=b.contents();c.length?c.wrapAll(a):b.append(a)})}; 34 | x.prototype.has=function(a){var b=y(a)?function(b,d){return!!w(a,d).length}:function(b,d){return d.contains(a)};return this.filter(b)};x.prototype.is=function(a){if(!a||!this[0])return!1;var b=K(a),c=!1;this.each(function(a,g){c=b.call(g,a,g);return!c});return c};x.prototype.next=function(a,b){return L(C(N(I(this,"nextElementSibling",b))),a)};x.prototype.nextAll=function(a){return this.next(a,!0)}; 35 | x.prototype.not=function(a){if(!a||!this[0])return this;var b=K(a);return this.filter(function(a,d){return!b.call(d,a,d)})};x.prototype.parent=function(a){return L(C(N(I(this,"parentNode"))),a)};x.prototype.index=function(a){var b=a?C(a)[0]:this[0];a=a?this:C(b).parent().children();return n.call(a,b)};x.prototype.closest=function(a){if(!a||!this[0])return C();var b=this.filter(a);return b.length?b:this.parent().closest(a)}; 36 | x.prototype.parents=function(a){return L(C(N(I(this,"parentElement",!0))),a)};x.prototype.prev=function(a,b){return L(C(N(I(this,"previousElementSibling",b))),a)};x.prototype.prevAll=function(a){return this.prev(a,!0)};x.prototype.siblings=function(a){var b=this[0];return L(this.parent().children().filter(function(a,d){return d!==b}),a)};"undefined"!==typeof exports?module.exports=C:f.cash=f.$=C; 37 | })(); -------------------------------------------------------------------------------- /src/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Compactor

10 |
11 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 | 37 | 38 | 48 | 49 | 64 |
65 | 66 | 92 | 93 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/ui/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue",Helvetica,Arial,serif; 3 | color: #eee; 4 | background-color: rgb(61, 61, 61); 5 | margin: 0; padding: 0; 6 | } 7 | 8 | header { 9 | display: flex; 10 | border-bottom: 1px solid black; 11 | background-color: rgb(51, 51, 51); 12 | } 13 | 14 | header .push { 15 | margin-left: auto; 16 | } 17 | 18 | progress, textarea { 19 | margin: 6px; 20 | } 21 | 22 | h1 { 23 | text-transform: uppercase; 24 | font-family: Impact, Oswald, Tahoma, sans-serif; 25 | font-weight: lighter; 26 | margin: 1px; 27 | padding: 4px; 28 | background-color: #333; 29 | } 30 | 31 | h1 span.side { 32 | font-size: larger; 33 | font-weight: normal; 34 | color: #666; 35 | } 36 | 37 | h1 span.sml { 38 | font-size: smaller; 39 | } 40 | 41 | #Button_Folder span { 42 | color: #888; 43 | padding-left: 8px; 44 | padding-right: 8px; 45 | } 46 | 47 | #Button_Folder { 48 | width: 80%; 49 | } 50 | 51 | input, textarea, select { 52 | border: 1px solid black; 53 | padding: 4px; 54 | margin: 4px; 55 | font-size: larger; 56 | } 57 | 58 | a, a:visited { 59 | color: #229af7; 60 | text-decoration: none; 61 | } 62 | 63 | a:active, a:hover { 64 | color: #44abf9; 65 | text-decoration: underline; 66 | } 67 | 68 | section button.stop { 69 | background-color: rgb(219, 40, 40); 70 | } 71 | 72 | section button.pause { 73 | background-color: rgb(118, 118, 118); 74 | } 75 | 76 | section button.analyse { 77 | background-color: rgb(118, 118, 118); 78 | } 79 | 80 | section button.compress { 81 | background-color: rgb(33, 186, 69); 82 | } 83 | 84 | section button.decompress { 85 | background-color: rgb(33, 133, 208); 86 | } 87 | 88 | progress { 89 | height: 20px; 90 | width: 90%; 91 | } 92 | 93 | section { 94 | margin: 16px; 95 | display: block; 96 | } 97 | 98 | div.ctr { 99 | display: flex; 100 | justify-content: center; 101 | } 102 | 103 | textarea { 104 | width: 95%; 105 | height: 200px; 106 | } 107 | 108 | #Start { 109 | align-items: center; 110 | } 111 | 112 | #Activity_Text { 113 | line-height: 1.2em; 114 | height: 1.2em; 115 | overflow: hidden; 116 | white-space: nowrap; 117 | } 118 | 119 | #Activity_Progress { 120 | width: 95%; 121 | } 122 | 123 | #Analysis .saved .box, #Breakdown_Saved { 124 | background-color: white; 125 | } 126 | 127 | #Analysis .compressed .box, #Breakdown_Compressed { 128 | background-color: green; 129 | } 130 | 131 | #Analysis .compressible .box, #Breakdown_Compressible { 132 | background-color: #0079d6; 133 | } 134 | 135 | #Analysis .excluded .box, #Breakdown_Skipped { 136 | background-color: orange; 137 | } 138 | 139 | #Analysis .box { 140 | border: 1px solid black; 141 | } 142 | 143 | #Analysis div div { 144 | padding: 2px; 145 | } 146 | 147 | #File_Count_Breakdown { 148 | display:flex; 149 | width: 95%; 150 | font-size: smaller; 151 | border: 1px solid black; 152 | border-radius: 2px; 153 | margin: 8px auto; 154 | overflow: hidden; 155 | } 156 | 157 | #File_Count_Breakdown div { 158 | overflow: hidden; 159 | height: 18px; 160 | } 161 | 162 | #Breakdown_Compressed, #Breakdown_Compressible, #Breakdown_Skipped { 163 | width: 0; 164 | } 165 | 166 | #Breakdown_Saved { 167 | flex: auto; 168 | } 169 | 170 | nav button { 171 | display: inline-block; 172 | background-color: transparent; 173 | color: #fff; 174 | padding: 6px; 175 | border-width: 0; 176 | cursor: pointer; 177 | } 178 | 179 | nav button.active { 180 | background-color: #229af7; 181 | } 182 | 183 | section button { 184 | color: #fff; 185 | background-color: #0079d6; 186 | border-color: transparent; 187 | padding: 6px; 188 | border-radius: 4px; 189 | border-width: 1px; 190 | margin: 4px; 191 | cursor: pointer; 192 | } 193 | 194 | section button:hover, section button:active { 195 | background-color: #2289e6; 196 | } 197 | 198 | ul { 199 | list-style-type: none; 200 | display: block; 201 | margin: 0; 202 | padding: 0; 203 | width: 100%; 204 | } 205 | 206 | li { 207 | display: inline-block; 208 | } 209 | --------------------------------------------------------------------------------