├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── images ├── cluttered_folder.png ├── dryrun.png ├── organized_folder.png ├── organized_folder_tree.png └── original_folder_tree.png └── src ├── cli.rs ├── compressed_file_processor.rs ├── config.rs ├── document_processor.rs ├── file_processor.rs ├── generic_processor.rs ├── image_processor.rs ├── main.rs ├── metadata.rs ├── organizer.rs ├── processing_mode.rs ├── traits ├── mod.rs └── processor.rs ├── video_processor.rs └── virtual_directory.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /organized 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle" 36 | version = "1.0.6" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 39 | 40 | [[package]] 41 | name = "anstyle-parse" 42 | version = "0.2.3" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 45 | dependencies = [ 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle-query" 51 | version = "1.0.2" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 54 | dependencies = [ 55 | "windows-sys", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-wincon" 60 | version = "3.0.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 63 | dependencies = [ 64 | "anstyle", 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "autocfg" 70 | version = "1.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "1.3.2" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 79 | 80 | [[package]] 81 | name = "bitflags" 82 | version = "2.4.2" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 85 | 86 | [[package]] 87 | name = "bumpalo" 88 | version = "3.14.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 91 | 92 | [[package]] 93 | name = "cc" 94 | version = "1.0.83" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 97 | dependencies = [ 98 | "libc", 99 | ] 100 | 101 | [[package]] 102 | name = "cfg-if" 103 | version = "1.0.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 106 | 107 | [[package]] 108 | name = "chrono" 109 | version = "0.4.33" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" 112 | dependencies = [ 113 | "android-tzdata", 114 | "iana-time-zone", 115 | "js-sys", 116 | "num-traits", 117 | "wasm-bindgen", 118 | "windows-targets", 119 | ] 120 | 121 | [[package]] 122 | name = "clap" 123 | version = "4.5.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" 126 | dependencies = [ 127 | "clap_builder", 128 | ] 129 | 130 | [[package]] 131 | name = "clap_builder" 132 | version = "4.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" 135 | dependencies = [ 136 | "anstream", 137 | "anstyle", 138 | "clap_lex", 139 | "strsim", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_lex" 144 | version = "0.7.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 147 | 148 | [[package]] 149 | name = "colorchoice" 150 | version = "1.0.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 153 | 154 | [[package]] 155 | name = "core-foundation-sys" 156 | version = "0.8.6" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 159 | 160 | [[package]] 161 | name = "crossbeam-deque" 162 | version = "0.8.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 165 | dependencies = [ 166 | "crossbeam-epoch", 167 | "crossbeam-utils", 168 | ] 169 | 170 | [[package]] 171 | name = "crossbeam-epoch" 172 | version = "0.9.18" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 175 | dependencies = [ 176 | "crossbeam-utils", 177 | ] 178 | 179 | [[package]] 180 | name = "crossbeam-utils" 181 | version = "0.8.19" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 184 | 185 | [[package]] 186 | name = "deranged" 187 | version = "0.3.11" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 190 | dependencies = [ 191 | "powerfmt", 192 | ] 193 | 194 | [[package]] 195 | name = "either" 196 | version = "1.10.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 199 | 200 | [[package]] 201 | name = "equivalent" 202 | version = "1.0.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 205 | 206 | [[package]] 207 | name = "errno" 208 | version = "0.3.8" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 211 | dependencies = [ 212 | "libc", 213 | "windows-sys", 214 | ] 215 | 216 | [[package]] 217 | name = "fastrand" 218 | version = "2.0.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 221 | 222 | [[package]] 223 | name = "filetime" 224 | version = "0.2.23" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" 227 | dependencies = [ 228 | "cfg-if", 229 | "libc", 230 | "redox_syscall", 231 | "windows-sys", 232 | ] 233 | 234 | [[package]] 235 | name = "hashbrown" 236 | version = "0.14.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 239 | 240 | [[package]] 241 | name = "iana-time-zone" 242 | version = "0.1.60" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 245 | dependencies = [ 246 | "android_system_properties", 247 | "core-foundation-sys", 248 | "iana-time-zone-haiku", 249 | "js-sys", 250 | "wasm-bindgen", 251 | "windows-core", 252 | ] 253 | 254 | [[package]] 255 | name = "iana-time-zone-haiku" 256 | version = "0.1.2" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 259 | dependencies = [ 260 | "cc", 261 | ] 262 | 263 | [[package]] 264 | name = "indexmap" 265 | version = "2.2.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" 268 | dependencies = [ 269 | "equivalent", 270 | "hashbrown", 271 | ] 272 | 273 | [[package]] 274 | name = "itoa" 275 | version = "1.0.10" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 278 | 279 | [[package]] 280 | name = "js-sys" 281 | version = "0.3.68" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" 284 | dependencies = [ 285 | "wasm-bindgen", 286 | ] 287 | 288 | [[package]] 289 | name = "kamadak-exif" 290 | version = "0.5.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" 293 | dependencies = [ 294 | "mutate_once", 295 | ] 296 | 297 | [[package]] 298 | name = "libc" 299 | version = "0.2.153" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 302 | 303 | [[package]] 304 | name = "linux-raw-sys" 305 | version = "0.4.13" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 308 | 309 | [[package]] 310 | name = "log" 311 | version = "0.4.20" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 314 | 315 | [[package]] 316 | name = "memchr" 317 | version = "2.7.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 320 | 321 | [[package]] 322 | name = "mime" 323 | version = "0.3.17" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 326 | 327 | [[package]] 328 | name = "mime_guess" 329 | version = "2.0.4" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 332 | dependencies = [ 333 | "mime", 334 | "unicase", 335 | ] 336 | 337 | [[package]] 338 | name = "mutate_once" 339 | version = "0.1.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" 342 | 343 | [[package]] 344 | name = "num-conv" 345 | version = "0.1.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 348 | 349 | [[package]] 350 | name = "num-traits" 351 | version = "0.2.18" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 354 | dependencies = [ 355 | "autocfg", 356 | ] 357 | 358 | [[package]] 359 | name = "num_threads" 360 | version = "0.1.6" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 363 | dependencies = [ 364 | "libc", 365 | ] 366 | 367 | [[package]] 368 | name = "once_cell" 369 | version = "1.19.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 372 | 373 | [[package]] 374 | name = "plexisort" 375 | version = "0.1.0" 376 | dependencies = [ 377 | "chrono", 378 | "clap", 379 | "filetime", 380 | "kamadak-exif", 381 | "log", 382 | "mime", 383 | "mime_guess", 384 | "rayon", 385 | "serde", 386 | "serde_json", 387 | "simplelog", 388 | "tempfile", 389 | "toml", 390 | "walkdir", 391 | ] 392 | 393 | [[package]] 394 | name = "powerfmt" 395 | version = "0.2.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 398 | 399 | [[package]] 400 | name = "proc-macro2" 401 | version = "1.0.78" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 404 | dependencies = [ 405 | "unicode-ident", 406 | ] 407 | 408 | [[package]] 409 | name = "quote" 410 | version = "1.0.35" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 413 | dependencies = [ 414 | "proc-macro2", 415 | ] 416 | 417 | [[package]] 418 | name = "rayon" 419 | version = "1.8.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" 422 | dependencies = [ 423 | "either", 424 | "rayon-core", 425 | ] 426 | 427 | [[package]] 428 | name = "rayon-core" 429 | version = "1.12.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 432 | dependencies = [ 433 | "crossbeam-deque", 434 | "crossbeam-utils", 435 | ] 436 | 437 | [[package]] 438 | name = "redox_syscall" 439 | version = "0.4.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 442 | dependencies = [ 443 | "bitflags 1.3.2", 444 | ] 445 | 446 | [[package]] 447 | name = "rustix" 448 | version = "0.38.31" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 451 | dependencies = [ 452 | "bitflags 2.4.2", 453 | "errno", 454 | "libc", 455 | "linux-raw-sys", 456 | "windows-sys", 457 | ] 458 | 459 | [[package]] 460 | name = "ryu" 461 | version = "1.0.16" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 464 | 465 | [[package]] 466 | name = "same-file" 467 | version = "1.0.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 470 | dependencies = [ 471 | "winapi-util", 472 | ] 473 | 474 | [[package]] 475 | name = "serde" 476 | version = "1.0.196" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 479 | dependencies = [ 480 | "serde_derive", 481 | ] 482 | 483 | [[package]] 484 | name = "serde_derive" 485 | version = "1.0.196" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 488 | dependencies = [ 489 | "proc-macro2", 490 | "quote", 491 | "syn", 492 | ] 493 | 494 | [[package]] 495 | name = "serde_json" 496 | version = "1.0.113" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" 499 | dependencies = [ 500 | "itoa", 501 | "ryu", 502 | "serde", 503 | ] 504 | 505 | [[package]] 506 | name = "serde_spanned" 507 | version = "0.6.5" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 510 | dependencies = [ 511 | "serde", 512 | ] 513 | 514 | [[package]] 515 | name = "simplelog" 516 | version = "0.12.1" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" 519 | dependencies = [ 520 | "log", 521 | "termcolor", 522 | "time", 523 | ] 524 | 525 | [[package]] 526 | name = "strsim" 527 | version = "0.11.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 530 | 531 | [[package]] 532 | name = "syn" 533 | version = "2.0.48" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 536 | dependencies = [ 537 | "proc-macro2", 538 | "quote", 539 | "unicode-ident", 540 | ] 541 | 542 | [[package]] 543 | name = "tempfile" 544 | version = "3.10.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" 547 | dependencies = [ 548 | "cfg-if", 549 | "fastrand", 550 | "rustix", 551 | "windows-sys", 552 | ] 553 | 554 | [[package]] 555 | name = "termcolor" 556 | version = "1.1.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 559 | dependencies = [ 560 | "winapi-util", 561 | ] 562 | 563 | [[package]] 564 | name = "time" 565 | version = "0.3.34" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" 568 | dependencies = [ 569 | "deranged", 570 | "itoa", 571 | "libc", 572 | "num-conv", 573 | "num_threads", 574 | "powerfmt", 575 | "serde", 576 | "time-core", 577 | "time-macros", 578 | ] 579 | 580 | [[package]] 581 | name = "time-core" 582 | version = "0.1.2" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 585 | 586 | [[package]] 587 | name = "time-macros" 588 | version = "0.2.17" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" 591 | dependencies = [ 592 | "num-conv", 593 | "time-core", 594 | ] 595 | 596 | [[package]] 597 | name = "toml" 598 | version = "0.8.10" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 601 | dependencies = [ 602 | "serde", 603 | "serde_spanned", 604 | "toml_datetime", 605 | "toml_edit", 606 | ] 607 | 608 | [[package]] 609 | name = "toml_datetime" 610 | version = "0.6.5" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 613 | dependencies = [ 614 | "serde", 615 | ] 616 | 617 | [[package]] 618 | name = "toml_edit" 619 | version = "0.22.4" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" 622 | dependencies = [ 623 | "indexmap", 624 | "serde", 625 | "serde_spanned", 626 | "toml_datetime", 627 | "winnow", 628 | ] 629 | 630 | [[package]] 631 | name = "unicase" 632 | version = "2.7.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 635 | dependencies = [ 636 | "version_check", 637 | ] 638 | 639 | [[package]] 640 | name = "unicode-ident" 641 | version = "1.0.12" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 644 | 645 | [[package]] 646 | name = "utf8parse" 647 | version = "0.2.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 650 | 651 | [[package]] 652 | name = "version_check" 653 | version = "0.9.4" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 656 | 657 | [[package]] 658 | name = "walkdir" 659 | version = "2.4.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 662 | dependencies = [ 663 | "same-file", 664 | "winapi-util", 665 | ] 666 | 667 | [[package]] 668 | name = "wasm-bindgen" 669 | version = "0.2.91" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" 672 | dependencies = [ 673 | "cfg-if", 674 | "wasm-bindgen-macro", 675 | ] 676 | 677 | [[package]] 678 | name = "wasm-bindgen-backend" 679 | version = "0.2.91" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" 682 | dependencies = [ 683 | "bumpalo", 684 | "log", 685 | "once_cell", 686 | "proc-macro2", 687 | "quote", 688 | "syn", 689 | "wasm-bindgen-shared", 690 | ] 691 | 692 | [[package]] 693 | name = "wasm-bindgen-macro" 694 | version = "0.2.91" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" 697 | dependencies = [ 698 | "quote", 699 | "wasm-bindgen-macro-support", 700 | ] 701 | 702 | [[package]] 703 | name = "wasm-bindgen-macro-support" 704 | version = "0.2.91" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" 707 | dependencies = [ 708 | "proc-macro2", 709 | "quote", 710 | "syn", 711 | "wasm-bindgen-backend", 712 | "wasm-bindgen-shared", 713 | ] 714 | 715 | [[package]] 716 | name = "wasm-bindgen-shared" 717 | version = "0.2.91" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" 720 | 721 | [[package]] 722 | name = "winapi" 723 | version = "0.3.9" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 726 | dependencies = [ 727 | "winapi-i686-pc-windows-gnu", 728 | "winapi-x86_64-pc-windows-gnu", 729 | ] 730 | 731 | [[package]] 732 | name = "winapi-i686-pc-windows-gnu" 733 | version = "0.4.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 736 | 737 | [[package]] 738 | name = "winapi-util" 739 | version = "0.1.6" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 742 | dependencies = [ 743 | "winapi", 744 | ] 745 | 746 | [[package]] 747 | name = "winapi-x86_64-pc-windows-gnu" 748 | version = "0.4.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 751 | 752 | [[package]] 753 | name = "windows-core" 754 | version = "0.52.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 757 | dependencies = [ 758 | "windows-targets", 759 | ] 760 | 761 | [[package]] 762 | name = "windows-sys" 763 | version = "0.52.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 766 | dependencies = [ 767 | "windows-targets", 768 | ] 769 | 770 | [[package]] 771 | name = "windows-targets" 772 | version = "0.52.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 775 | dependencies = [ 776 | "windows_aarch64_gnullvm", 777 | "windows_aarch64_msvc", 778 | "windows_i686_gnu", 779 | "windows_i686_msvc", 780 | "windows_x86_64_gnu", 781 | "windows_x86_64_gnullvm", 782 | "windows_x86_64_msvc", 783 | ] 784 | 785 | [[package]] 786 | name = "windows_aarch64_gnullvm" 787 | version = "0.52.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 790 | 791 | [[package]] 792 | name = "windows_aarch64_msvc" 793 | version = "0.52.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 796 | 797 | [[package]] 798 | name = "windows_i686_gnu" 799 | version = "0.52.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 802 | 803 | [[package]] 804 | name = "windows_i686_msvc" 805 | version = "0.52.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 808 | 809 | [[package]] 810 | name = "windows_x86_64_gnu" 811 | version = "0.52.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 814 | 815 | [[package]] 816 | name = "windows_x86_64_gnullvm" 817 | version = "0.52.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 820 | 821 | [[package]] 822 | name = "windows_x86_64_msvc" 823 | version = "0.52.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 826 | 827 | [[package]] 828 | name = "winnow" 829 | version = "0.5.39" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" 832 | dependencies = [ 833 | "memchr", 834 | ] 835 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plexisort" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Richard Chukwu "] 6 | description = "Plexisort is a command-line tool designed to organize your files based on metadata. It allows for flexible source and destination directory settings, supports dry-run operations for safe previews of potential changes, and even offers an undo functionality for reversing the last set of file movements." 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | rayon = "1.5.1" 12 | clap = "4.5.0" 13 | walkdir = "2.3" 14 | kamadak-exif = "0.5.5" 15 | chrono = "0.4" 16 | log = "0.4" 17 | simplelog = "0.12.0" 18 | toml = "0.8.10" 19 | mime_guess = "2.0.4" 20 | mime = "0.3" 21 | serde_json="1.0.59" 22 | serde= {version = "1.0.117", features = ["derive"]} 23 | filetime = "0.2" 24 | 25 | 26 | [dev-dependencies] 27 | tempfile = "3.2.0" 28 | 29 | [features] 30 | test_env = [] 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2024 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Plexisort 3 | 4 | Plexisort is a command-line tool designed to organize your files based on metadata. It allows for flexible source and destination directory settings, supports dry-run operations for safe previews of potential changes, and even offers an undo functionality for reversing the last set of file movements. 5 | 6 | ## Motivation 7 | 8 | The **Plexisort** project began out of necessity and curiosity. Like many, I was grappling with a cluttered Downloads folder, consisting of files accumulated over time. This clutter made it difficult to locate important documents. I also had quite a lot of images and wanted ot organize them by date. 9 | 10 | At about the same time, I started to learn Rust, and I was eager to put my new found power to the test. **Plexisort** emerged as the perfect project to address both my personal need for a cleaner digital workspace and my professional desire to become better at Rust. 11 | 12 | In sharing Plexisort, I hope to not only provide a solution to a common problem but also inspire others to embark on their own learning journeys. Whether it's tackling digital clutter or learning a new programming language, the essence of Plexisort is about embracing challenges and turning them into opportunities for growth. 13 | 14 | 15 | ## Version 16 | 0.1.0 17 | 18 | ## Features 19 | - **Custom Config File**: Use a custom configuration (toml) file to specify operational parameters. 20 | - **Source Directory**: Set one or more source directories for the organization process. 21 | - **Destination Directory**: Define a specific destination directory for organized files. 22 | - **Dry Run**: Execute the tool in a mode that shows what would be done without making any changes. 23 | - **Undo**: Revert the last set of changes made by the tool. 24 | 25 | ## How to Use 26 | 1. **Installation**: Ensure you have Rust installed on your system. Clone this repository and build the project using `cargo build --release`. 27 | 2. **Running**: Execute the tool with `cargo run -- [OPTIONS]`. The following options are available: 28 | - `-c, --config `: Sets a custom config file. 29 | - `--source `: Sets the source directory(s). Multiple directories can be specified. 30 | - `--destination `: Sets the destination directory. 31 | - `--dry-run`: Runs the organizer without making any changes. 32 | - `--undo`: Reverts the last set of file movements. 33 | 34 | ## Building the Configuration 35 | If not using a configuration file, the tool requires at least the source and destination directories to be specified through command-line options. 36 | 37 | ## Config File Example 38 | You can use a `config.toml` file like this for the configuration option: 39 | 40 | ```toml 41 | source_directories = ["path/to/cluttered_data"] 42 | destination = "organized_data/organized" 43 | ``` 44 | 45 | ## Example Command using the config.toml file 46 | ```bash 47 | cargo run -- --config config.toml 48 | ``` 49 | 50 | 51 | ## Example Command using source and destination paths 52 | ```bash 53 | cargo run -- --source /path/to/source --destination /path/to/destination 54 | ``` 55 | 56 | This will organize files from `/path/to/source` to `/path/to/destination` based on their metadata. 57 | 58 | Visual Examples: 59 | 60 | ## Dry run mode creates a preview of the organized folder without making any changes 61 | ```bash 62 | cargo run -- --config config.toml --dry-run=true 63 | ``` 64 | ![Dry run mode](images/dryrun.png) 65 | 66 | ## Folder Layout Before and After Running Plexisort 67 | 68 | ### Folder Structure Comparison 69 |

70 | Original folder structure 71 | Final folder structure after running Plexisort 72 |

73 |
74 |

Left: Original folder structure.

75 |

Right: Final folder structure after running Plexisort.

76 |
77 | 78 | ### Detailed Folder View Comparison 79 |

80 | Cluttered folder 81 | Organized folder after Plexisort 82 |

83 |
84 |

Left: Cluttered folder before Plexisort.

85 |

Right: Organized folder after running Plexisort.

86 |
87 | 88 | 89 | 90 | ## Using the --undo=true flag reverses the entire operation to the orignal state 91 | ```bash 92 | cargo run -- --config config.toml --undo=true 93 | ``` 94 | 95 | ## Logging 96 | Plexisort provides informative logging during its operation, indicating the progress and actions taken or to be taken in dry-run mode. 97 | 98 | ## Limitations 99 | Plexisort moves uncategorized files into the Other_Files directory. 100 | 101 | ## Contributions 102 | Contributions are welcome! Please feel free to submit pull requests or create issues for bugs and feature requests. 103 | 104 | ## License 105 | This project is open-source and available under the MIT License. 106 | -------------------------------------------------------------------------------- /images/cluttered_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinex/plexisort/c354fb792105ed500b1e84dc47e887d889c425f7/images/cluttered_folder.png -------------------------------------------------------------------------------- /images/dryrun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinex/plexisort/c354fb792105ed500b1e84dc47e887d889c425f7/images/dryrun.png -------------------------------------------------------------------------------- /images/organized_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinex/plexisort/c354fb792105ed500b1e84dc47e887d889c425f7/images/organized_folder.png -------------------------------------------------------------------------------- /images/organized_folder_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinex/plexisort/c354fb792105ed500b1e84dc47e887d889c425f7/images/organized_folder_tree.png -------------------------------------------------------------------------------- /images/original_folder_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinex/plexisort/c354fb792105ed500b1e84dc47e887d889c425f7/images/original_folder_tree.png -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, Command, ArgAction}; 2 | 3 | pub fn build_cli() -> Command { 4 | Command::new("Plexisort") 5 | .version("1.0") 6 | .author("Richard Chukwu ") 7 | .about("Organizes files by metadata") 8 | .arg(Arg::new("config") 9 | .short('c') 10 | .long("config") 11 | .value_name("FILE") 12 | .help("Sets a custom config file") 13 | .action(ArgAction::Set) 14 | .num_args(1)) 15 | .arg(Arg::new("source") 16 | .long("source") 17 | .value_name("SOURCE_DIR") 18 | .help("Sets the source directory(s)") 19 | .action(ArgAction::Append) 20 | .num_args(1..)) 21 | .arg(Arg::new("destination") 22 | .long("destination") 23 | .value_name("DEST_DIR") 24 | .help("Sets the destination directory") 25 | .action(ArgAction::Set) 26 | .num_args(1)) 27 | .arg(Arg::new("dry-run") 28 | .long("dry-run") 29 | .help("Runs the organizer without making any changes") 30 | .action(ArgAction::Set)) 31 | .arg(Arg::new("undo") 32 | .long("undo") 33 | .help("Reverts the last set of file movements") 34 | .action(ArgAction::Set)) 35 | } 36 | -------------------------------------------------------------------------------- /src/compressed_file_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::organizer::organize_file; 2 | use crate::processing_mode::ProcessingMode; 3 | use crate::traits::processor::Processor; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use log::{debug, error}; 7 | 8 | 9 | pub struct CompressedFileProcessor; 10 | 11 | impl Processor for CompressedFileProcessor { 12 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode) { 13 | // Determine the destination directory for compressed files 14 | let destination_dir = destination.join(self.get_destination_subfolder(path)); 15 | 16 | 17 | // Correctly specify the destination path for the file 18 | let destination_path = destination_dir.join(path.file_name().unwrap()); 19 | 20 | match mode { 21 | ProcessingMode::DryRun(virtual_directory) => { 22 | // In DryRun mode, just simulate the action 23 | debug!("Would move {} to {}", path.display(), destination_path.display()); 24 | let path_parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 25 | virtual_directory.add_path(&path_parts); 26 | }, 27 | ProcessingMode::Live => { 28 | // In Live mode, actually create the directory and move the file 29 | // Ensure the directory structure exists 30 | if let Err(e) = fs::create_dir_all(&destination_dir) { 31 | error!("Error creating destination directory: {}", e); 32 | return; 33 | } 34 | if let Err(e) = organize_file(path, &destination_path, mode) { 35 | error!("Failed to organize file: {}", e); 36 | } else { 37 | debug!("Successfully moved file from {} to {}", path.display(), destination_path.display()); 38 | } 39 | } 40 | } 41 | } 42 | 43 | fn get_destination_subfolder(&self, _path: &PathBuf) -> PathBuf { 44 | PathBuf::from("Compressed_Files") 45 | } 46 | } 47 | 48 | 49 | 50 | 51 | #[cfg(test)] 52 | mod compressed_file_processor_tests { 53 | use super::*; 54 | use std::fs::{self, File}; 55 | use tempfile::tempdir; 56 | 57 | #[test] 58 | fn test_compressed_file_processor_live() { 59 | let temp_dir = tempdir().unwrap(); 60 | let source_dir = temp_dir.path().join("source"); 61 | let destination_dir = temp_dir.path().join("destination"); 62 | fs::create_dir_all(&source_dir).unwrap(); 63 | let compressed_file_path = source_dir.join("archive.zip"); 64 | File::create(&compressed_file_path).unwrap(); 65 | 66 | let processor = CompressedFileProcessor {}; 67 | let mut mode = ProcessingMode::Live; 68 | 69 | processor.process(&compressed_file_path, &destination_dir, &mut mode); 70 | 71 | let expected_destination = destination_dir.join("Compressed_Files").join("archive.zip"); 72 | assert!(expected_destination.exists(), "Compressed file was not moved to the correct destination in Live mode."); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use log; 2 | use serde::Deserialize; 3 | use std::fs; 4 | use std::path::Path; 5 | use std::error::Error; 6 | 7 | #[derive(Deserialize, Debug)] 8 | pub struct Config { 9 | pub source_directories: Vec, // List of source directories as strings 10 | pub destination: String, // Single destination directory as string 11 | } 12 | 13 | impl Config { 14 | pub fn from_file(file_path: &str) -> Result> { 15 | log::info!("Attempting to load config from: {}", file_path); 16 | let contents = fs::read_to_string(file_path)?; 17 | let config: Config = toml::from_str(&contents)?; 18 | 19 | config.validate()?; 20 | 21 | Ok(config) 22 | } 23 | 24 | pub fn validate(&self) -> Result<(), Box> { 25 | if self.source_directories.is_empty() { 26 | return Err("At least one source directory must be specified.".into()); 27 | } 28 | 29 | for dir in &self.source_directories { 30 | if !Path::new(dir).exists() { 31 | return Err(format!("Source directory does not exist: {}", dir).into()); 32 | } 33 | } 34 | 35 | if !Path::new(&self.destination).exists() { 36 | log::warn!("Destination directory does not exist and will be created: {}", self.destination); 37 | } 38 | 39 | Ok(()) 40 | } 41 | } 42 | 43 | impl Default for Config { 44 | fn default() -> Self { 45 | Config { 46 | source_directories: vec![], // No default source directories 47 | destination: String::new(), // An empty string as the default destination 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/document_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::organizer::organize_file; 2 | use crate::processing_mode::ProcessingMode; 3 | use crate::traits::processor::Processor; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use log::{debug, error}; 7 | 8 | 9 | 10 | pub struct DocumentProcessor; 11 | 12 | impl Processor for DocumentProcessor { 13 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode) { 14 | let file_extension = path.extension() 15 | .unwrap_or_default() 16 | .to_str() 17 | .unwrap_or("") 18 | .to_lowercase(); 19 | 20 | let destination_dir = self.determine_destination_dir(&file_extension, destination); 21 | let destination_path = destination_dir.join(path.file_name().unwrap()); 22 | 23 | match mode { 24 | ProcessingMode::DryRun(virtual_directory) => { 25 | // In DryRun mode, just simulate the action 26 | debug!("Would move {} to {}", path.display(), destination_path.display()); 27 | let path_parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 28 | virtual_directory.add_path(&path_parts); 29 | 30 | 31 | }, 32 | ProcessingMode::Live => { 33 | // In Live mode, actually create the directory and move the file 34 | if let Err(e) = fs::create_dir_all(&destination_dir) { 35 | error!("Error creating destination directory: {}", e); 36 | return; 37 | } 38 | if let Err(e) = organize_file(path, &destination_path, mode) { 39 | error!("Failed to organize file: {}", e); 40 | } else { 41 | debug!("Successfully moved file from {} to {}", path.display(), destination_path.display()); 42 | } 43 | } 44 | } 45 | } 46 | 47 | fn get_destination_subfolder(&self, _path: &PathBuf) -> PathBuf { 48 | PathBuf::new() 49 | } 50 | } 51 | 52 | impl DocumentProcessor { 53 | /// Determine the destination directory based on the file extension. 54 | fn determine_destination_dir(&self, extension: &str, base_dest: &PathBuf) -> PathBuf { 55 | let subfolder = match extension { 56 | "doc" | "docx" => "Word_Documents", 57 | "xls" | "xlsx" => "Excel_Spreadsheets", 58 | "ppt" | "pptx" => "PowerPoint_Presentations", 59 | "csv" => "CSV_Files", 60 | "json" | "yaml" | "yml" => "Config_Files", 61 | "pdf" => "PDFs", 62 | "html" => "Web_Pages", 63 | "txt" => "Text_Files", 64 | _ => "Uncategorized_Documents", 65 | }; 66 | base_dest.join("Documents").join(subfolder) 67 | } 68 | } 69 | 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | use std::fs::{self, File}; 75 | use std::io::Write; 76 | use tempfile::tempdir; 77 | 78 | #[test] 79 | fn test_document_processor_live() { 80 | let temp_dir = tempdir().unwrap(); 81 | let source_dir = temp_dir.path().join("source"); 82 | let destination_dir = temp_dir.path().join("destination"); 83 | fs::create_dir_all(&source_dir).unwrap(); 84 | let document_file_path = source_dir.join("test_document.txt"); 85 | let mut file = File::create(&document_file_path).unwrap(); 86 | writeln!(file, "Test content").unwrap(); 87 | 88 | let processor = DocumentProcessor {}; 89 | let mut mode = ProcessingMode::Live; 90 | 91 | processor.process(&document_file_path, &destination_dir, &mut mode); 92 | 93 | // Update the expected destination to include "Documents" 94 | let expected_destination = destination_dir.join("Documents").join("Text_Files").join("test_document.txt"); 95 | assert!(expected_destination.exists(), "Document was not moved to the correct destination in Live mode."); 96 | } 97 | 98 | 99 | #[test] 100 | fn test_document_processor_logic() { 101 | let processor = DocumentProcessor {}; 102 | let temp_dir = tempdir().unwrap(); 103 | let source_dir = temp_dir.path().join("source"); 104 | let destination_dir = temp_dir.path().join("destination"); 105 | fs::create_dir_all(&source_dir).unwrap(); 106 | let document_file_path = source_dir.join("test_document.txt"); 107 | let mut file = File::create(&document_file_path).unwrap(); 108 | writeln!(file, "Test content").unwrap(); 109 | 110 | let mut mode = ProcessingMode::Live; 111 | 112 | processor.process(&document_file_path, &destination_dir, &mut mode); 113 | 114 | // Update the expected destination to include "Documents" 115 | let expected_destination = destination_dir.join("Documents").join("Text_Files").join("test_document.txt"); 116 | 117 | assert!(expected_destination.exists(), "Document was not moved to the correct destination in Live mode."); 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /src/file_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::traits::ProcessorFactory; 2 | 3 | use crate::processing_mode::ProcessingMode; 4 | 5 | use std::path::{Path, PathBuf}; 6 | use walkdir::WalkDir; 7 | 8 | 9 | 10 | pub fn process_directory( 11 | directory: &Path, 12 | base_dest: &PathBuf, 13 | mode: &mut ProcessingMode, 14 | factory: &dyn ProcessorFactory 15 | ) { 16 | let paths: Vec = WalkDir::new(directory) 17 | .into_iter() 18 | .filter_map(Result::ok) 19 | .filter(|e| e.file_type().is_file()) 20 | .map(|e| e.into_path()) 21 | .collect(); 22 | 23 | paths.iter().for_each(|path| { 24 | let processor = factory.create_processor(path); // Use the factory 25 | processor.process(path, base_dest, mode); 26 | }); 27 | 28 | // Debugging or DryRun mode output 29 | if let ProcessingMode::DryRun(virtual_dir) = mode { 30 | println!("Dry run: Preview of directory structure"); 31 | virtual_dir.print_tree(); 32 | } 33 | } 34 | 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | 39 | use crate::traits::{ProcessorFactory, TestProcessorFactory}; 40 | use crate::virtual_directory::VirtualDirectory; 41 | 42 | 43 | use std::fs::{self, File}; 44 | use std::io::Write; 45 | use std::sync::Mutex; 46 | use tempfile::tempdir; 47 | 48 | 49 | #[test] 50 | fn test_virtual_directory_contains_file() { 51 | let temp_dir = tempdir().unwrap(); 52 | 53 | // Ensure the source directory is created before creating a file within it 54 | let source_dir = temp_dir.path().join("source"); 55 | fs::create_dir_all(&source_dir).unwrap(); // Use create_dir_all to ensure the entire path is created 56 | 57 | let sample_file_path = source_dir.join("sample.txt"); 58 | let mut file = File::create(&sample_file_path).unwrap(); 59 | writeln!(file, "Hello, world!").unwrap(); 60 | 61 | let mut virtual_dir = VirtualDirectory::default(); 62 | 63 | let dest_dir = "dest"; 64 | let file_name = "sample.txt"; 65 | 66 | // Convert the path to Vec for the virtual directory 67 | let path_components = vec![dest_dir.to_string(), file_name.to_string()]; 68 | virtual_dir.add_path(&path_components); 69 | 70 | // Ensure the file is now "contained" within the virtual directory 71 | assert!(virtual_dir.contains_file(&path_components)); 72 | } 73 | 74 | 75 | #[test] 76 | fn test_processor_selection() { 77 | let factory = TestProcessorFactory { 78 | last_processor_type: Mutex::new(None), 79 | }; 80 | let temp_dir = tempdir().unwrap(); 81 | let source_dir = temp_dir.path().join("source"); 82 | fs::create_dir_all(&source_dir).unwrap(); 83 | 84 | // Create a mock file for each processor type you want to test 85 | let document_file = source_dir.join("test.docx"); 86 | let image_file = source_dir.join("test.png"); 87 | // Add other file types as needed 88 | 89 | // Document Processor test 90 | factory.create_processor(&document_file); 91 | assert_eq!(*factory.last_processor_type.lock().unwrap(), Some("DocumentProcessor".to_string()), "DocumentProcessor was not selected for .docx files."); 92 | 93 | // Image Processor test 94 | factory.create_processor(&image_file); 95 | assert_eq!(*factory.last_processor_type.lock().unwrap(), Some("ImageProcessor".to_string()), "ImageProcessor was not selected for .png files."); 96 | 97 | // Add tests for other processor types as needed 98 | } 99 | } 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/generic_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::organizer::organize_file; 2 | use crate::processing_mode::ProcessingMode; 3 | use crate::traits::processor::Processor; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use log::{debug, error}; 7 | 8 | 9 | pub struct GenericProcessor; 10 | 11 | impl Processor for GenericProcessor { 12 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode) { 13 | // Define the destination directory based on the subfolder and ensure it exists 14 | let destination_dir = destination.join(self.get_destination_subfolder(path)); 15 | 16 | // Specify the destination path for the file, correctly appending the filename 17 | let destination_path = destination_dir.join(path.file_name().unwrap()); 18 | 19 | match mode { 20 | ProcessingMode::DryRun(virtual_directory) => { 21 | // In DryRun mode, simulate the action without making changes 22 | debug!("Would move {} to {}", path.display(), destination_path.display()); 23 | let path_parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 24 | virtual_directory.add_path(&path_parts); 25 | }, 26 | ProcessingMode::Live => { 27 | // In Live mode, actually create the directory and move the file 28 | // Ensure the directory structure exists 29 | if let Err(e) = fs::create_dir_all(&destination_dir) { 30 | error!("Error creating destination directory: {}", e); 31 | return; 32 | } 33 | if let Err(e) = organize_file(path, &destination_path, mode) { 34 | error!("Failed to organize file: {}", e); 35 | } else { 36 | debug!("Successfully moved file from {} to {}", path.display(), destination_path.display()); 37 | } 38 | } 39 | } 40 | } 41 | 42 | fn get_destination_subfolder(&self, _path: &PathBuf) -> PathBuf { 43 | // Adjust the returned subfolder name as needed 44 | PathBuf::from("Other_Files") 45 | } 46 | } 47 | 48 | 49 | #[cfg(test)] 50 | mod generic_processor_tests { 51 | use super::*; 52 | use std::fs::{self, File}; 53 | use std::io::Write; 54 | use tempfile::tempdir; 55 | 56 | #[test] 57 | fn test_generic_processor_live() { 58 | let temp_dir = tempdir().unwrap(); 59 | let source_dir = temp_dir.path().join("source"); 60 | let destination_dir = temp_dir.path().join("destination"); 61 | fs::create_dir_all(&source_dir).unwrap(); 62 | // Create a generic file; the type or content doesn't matter for this processor 63 | let generic_file_path = source_dir.join("generic_file.txt"); 64 | let mut file = File::create(&generic_file_path).unwrap(); 65 | writeln!(file, "Generic file content").unwrap(); 66 | 67 | let processor = GenericProcessor {}; 68 | let mut mode = ProcessingMode::Live; 69 | 70 | processor.process(&generic_file_path, &destination_dir, &mut mode); 71 | 72 | // The expected destination is within the "Other_Files" directory 73 | let expected_destination = destination_dir.join("Other_Files").join("generic_file.txt"); 74 | assert!(expected_destination.exists(), "Generic file was not moved to the correct destination in Live mode."); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/image_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::extract_date_from_image; 2 | use crate::organizer::organize_file; 3 | use crate::processing_mode::ProcessingMode; 4 | use crate::traits::processor::Processor; 5 | use chrono::prelude::*; 6 | use std::{fs, io}; 7 | use std::path::PathBuf; 8 | use std::time::SystemTime; 9 | 10 | pub struct ImageProcessor; 11 | 12 | impl Processor for ImageProcessor { 13 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode) { 14 | let date_based_dir = self.get_destination_subfolder(path); 15 | let full_destination_dir = destination.join(&date_based_dir); 16 | if let Err(e) = move_image(path, &full_destination_dir, mode) { 17 | println!("Error moving image: {}", e); 18 | } 19 | } 20 | 21 | fn get_destination_subfolder(&self, path: &PathBuf) -> PathBuf { 22 | let date_based_subfolder = if let Some(date_str) = extract_date_from_image(path) { 23 | format_date_to_path(&date_str) 24 | } else { 25 | match fs::metadata(path).and_then(|metadata| metadata.modified()) { 26 | Ok(modified) => { 27 | let datetime = system_time_to_date_time(modified); 28 | format!("{}/{}", datetime.year(), format!("{:02} - {}", datetime.month(), datetime.format("%B"))) 29 | }, 30 | Err(_) => String::from("Unknown"), 31 | } 32 | }; 33 | // Prepend "Photos" directory to the date-based subfolder 34 | PathBuf::from("Images").join(date_based_subfolder) 35 | } 36 | 37 | } 38 | 39 | fn move_image(path: &PathBuf, destination_dir: &PathBuf, mode: &mut ProcessingMode) -> Result<(), io::Error> { 40 | let destination_path = destination_dir.join(path.file_name().unwrap()); 41 | 42 | match mode { 43 | ProcessingMode::DryRun(virtual_dir) => { 44 | let path_parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 45 | virtual_dir.add_path(&path_parts); 46 | Ok(()) 47 | }, 48 | ProcessingMode::Live => { 49 | fs::create_dir_all(destination_dir)?; 50 | organize_file(path, &destination_path, mode) 51 | } 52 | } 53 | } 54 | 55 | fn format_date_to_path(date_str: &str) -> String { 56 | // Assuming date_str is in "YYYY:MM:DD HH:MM:SS" format 57 | let parts: Vec<&str> = date_str 58 | .split_whitespace() 59 | .next() 60 | .unwrap() 61 | .split(':') 62 | .collect(); 63 | let year = parts.get(0).unwrap_or(&"UnknownYear"); 64 | let month_num = parts.get(1).unwrap_or(&"00"); 65 | let month = match &**month_num { 66 | "01" => "01 - January", 67 | "02" => "02 - February", 68 | "03" => "03 - March", 69 | "04" => "04 - April", 70 | "05" => "05 - May", 71 | "06" => "06 - June", 72 | "07" => "07 - July", 73 | "08" => "08 - August", 74 | "09" => "09 - September", 75 | "10" => "10 - October", 76 | "11" => "11 - November", 77 | "12" => "12 - December", 78 | _ => "UnknownMonth", 79 | }; 80 | format!("{}/{}", year, month) 81 | } 82 | 83 | // Utility function to convert SystemTime to DateTime 84 | fn system_time_to_date_time(local_time: SystemTime) -> DateTime { 85 | let datetime: DateTime = local_time.into(); 86 | datetime.with_timezone(&Local) 87 | } 88 | 89 | #[cfg(test)] 90 | mod image_processor_tests { 91 | use super::*; 92 | use std::fs::{self, File}; 93 | use std::io::Write; 94 | use tempfile::tempdir; 95 | use chrono::{Local, Datelike}; 96 | 97 | #[test] 98 | fn test_image_processor_live() { 99 | let temp_dir = tempdir().unwrap(); 100 | let source_dir = temp_dir.path().join("source"); 101 | let destination_dir = temp_dir.path().join("destination"); 102 | fs::create_dir_all(&source_dir).unwrap(); 103 | let image_file_path = source_dir.join("photo1.png"); 104 | let mut file = File::create(&image_file_path).unwrap(); 105 | writeln!(file, "Dummy image content").unwrap(); 106 | 107 | // Simulate an ImageProcessor instance and its processing 108 | let processor = ImageProcessor {}; 109 | let mut mode = ProcessingMode::Live; 110 | 111 | processor.process(&image_file_path, &destination_dir, &mut mode); 112 | 113 | // Dynamically determine the current year and month for the expected path 114 | let now = Local::now(); 115 | let expected_date_dir = format!("{}/{} - {}", now.year(), format!("{:02}", now.month()), now.format("%B")); 116 | let expected_destination = destination_dir.join("Images").join(expected_date_dir).join("photo1.png"); 117 | 118 | assert!(expected_destination.exists(), "Image was not moved to the correct destination in Live mode."); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod document_processor; 3 | mod file_processor; // Ensure this module is correctly defined and accessible 4 | mod image_processor; 5 | mod video_processor; 6 | mod metadata; 7 | mod organizer; 8 | mod processing_mode; 9 | mod virtual_directory; 10 | mod compressed_file_processor; 11 | mod generic_processor; 12 | mod cli; 13 | mod traits; 14 | 15 | use config::Config; 16 | use file_processor::process_directory; 17 | use log::LevelFilter; 18 | use processing_mode::ProcessingMode; 19 | 20 | use simplelog::SimpleLogger; 21 | use virtual_directory::VirtualDirectory; 22 | use std::path::{Path, PathBuf}; 23 | 24 | use std::{fs, process}; 25 | 26 | use organizer::undo_last_actions; 27 | use crate::organizer::{clear_undo_log, print_current_structure}; 28 | use crate::traits::DefaultProcessorFactory; 29 | 30 | fn main() { 31 | init_logging(); 32 | log::info!("Application starting up"); 33 | let matches = cli::build_cli().get_matches(); 34 | 35 | if let Err(e) = run_app(&matches) { 36 | // This is where the error gets logged, providing a single, clear error message. 37 | log::error!("Application error: {}", e); 38 | process::exit(1); 39 | } else { 40 | log::info!("Folder structure organized successfully."); 41 | } 42 | } 43 | 44 | 45 | fn init_logging() { 46 | SimpleLogger::init(LevelFilter::Warn, simplelog::Config::default()).expect("Failed to initialize logging"); 47 | } 48 | 49 | fn run_app(matches: &clap::ArgMatches) -> Result<(), Box> { 50 | let config = load_or_build_config(matches)?; 51 | let mut mode = determine_processing_mode(matches.contains_id("dry-run")); 52 | 53 | // Create an instance of the default processor factory 54 | let factory = DefaultProcessorFactory; 55 | 56 | println!("Original Directory Structure:"); 57 | for source_directory in &config.source_directories { 58 | println!("\nDirectory: {}", source_directory); 59 | let path = Path::new(source_directory); 60 | print_current_structure(path, ""); 61 | } 62 | 63 | check_source_directories(&config)?; 64 | 65 | // Now pass the factory when processing directories 66 | for source_directory in &config.source_directories { 67 | let source_path = PathBuf::from(source_directory); 68 | let dest_path = PathBuf::from(&config.destination); 69 | println!("Processing '{}'", source_path.display()); 70 | process_directory(&source_path, &dest_path, &mut mode, &factory); // Adjusted to include factory 71 | } 72 | 73 | handle_undo(matches)?; 74 | 75 | Ok(()) 76 | } 77 | 78 | 79 | fn check_source_directories(config: &Config) -> Result<(), Box> { 80 | for source_directory in &config.source_directories { 81 | let source_path = Path::new(source_directory); 82 | if !source_path.exists() { 83 | // Removed log::error! and directly return Err 84 | return Err(format!("Error: Source directory '{}' does not exist.", source_directory).into()); 85 | } 86 | if !source_path.is_dir() { 87 | return Err(format!("Error: '{}' is not a directory.", source_directory).into()); 88 | } 89 | if let Err(e) = fs::read_dir(source_path) { 90 | return Err(format!("Error: No permission to read source directory '{}': {}", source_directory, e).into()); 91 | } 92 | } 93 | Ok(()) 94 | } 95 | 96 | 97 | fn handle_undo(matches: &clap::ArgMatches) -> Result<(), Box> { 98 | if matches.contains_id("undo") { 99 | match undo_last_actions() { 100 | Ok(_) => { 101 | println!("Undo actions completed successfully."); 102 | clear_undo_log().map_err(|e| format!("Failed to clear undo log: {}", e))?; 103 | } 104 | Err(e) => { 105 | return Err(format!("Error undoing actions: {}", e).into()); 106 | } 107 | } 108 | } 109 | Ok(()) 110 | } 111 | 112 | // Load or build config based on CLI arguments or config file 113 | fn load_or_build_config(matches: &clap::ArgMatches) -> Result> { 114 | if let Some(config_path) = matches.get_one::("config") { 115 | Config::from_file(config_path) 116 | } else { 117 | build_config_from_cli_args(matches) 118 | } 119 | } 120 | 121 | // Build Config from CLI arguments 122 | fn build_config_from_cli_args(matches: &clap::ArgMatches) -> Result> { 123 | let source_directories: Vec = matches.get_many::("source") 124 | .unwrap_or_default() 125 | .map(|s| s.to_string()) 126 | .collect(); 127 | 128 | let destination = matches.get_one::("destination") 129 | .expect("Destination directory is required") 130 | .clone(); 131 | 132 | Ok(Config { 133 | source_directories, 134 | destination, 135 | }) 136 | } 137 | 138 | // Determine the processing mode based on CLI arguments 139 | fn determine_processing_mode(dry_run: bool) -> ProcessingMode { 140 | if dry_run { 141 | ProcessingMode::DryRun(VirtualDirectory::default()) 142 | } else { 143 | ProcessingMode::Live 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | use exif::{In, Reader, Tag}; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::path::Path; 5 | 6 | pub fn extract_date_from_image(path: &Path) -> Option { 7 | // Open the file at the given path 8 | let file = match File::open(path) { 9 | Ok(file) => file, 10 | Err(_) => return None, 11 | }; 12 | 13 | // Create a BufReader for the file 14 | let mut buf_reader = BufReader::new(file); 15 | 16 | // Create an EXIF reader and attempt to read the EXIF data from the BufReader 17 | let exif_reader = Reader::new().read_from_container(&mut buf_reader); 18 | 19 | match exif_reader { 20 | Ok(exif) => { 21 | // Attempt to get the 'DateTimeOriginal' field from the EXIF data 22 | let field = exif.get_field(Tag::DateTimeOriginal, In::PRIMARY); 23 | 24 | // If the field exists, return its display value as a String 25 | field.map(|field| field.display_value().to_string()) 26 | } 27 | Err(_) => None, // If there was an error reading the EXIF data, return None 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/organizer.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufRead, BufReader, Error, ErrorKind}; 3 | // organizer.rs 4 | use serde_json::Value; 5 | use std::collections::HashSet; 6 | use std::path::{Path, PathBuf}; 7 | use std::{fs, io}; 8 | use log::{debug, error, info, warn}; 9 | 10 | use crate::processing_mode::ProcessingMode; 11 | 12 | pub fn organize_file(source_path: &Path, destination_path: &Path, mode: &mut ProcessingMode) -> Result<(), io::Error> { 13 | match mode { 14 | ProcessingMode::DryRun(virtual_dir) => { 15 | let mut parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 16 | if let Some(file_name) = source_path.file_name().and_then(|n| n.to_str()) { 17 | parts.push(file_name.to_string()); 18 | } 19 | virtual_dir.add_path(&parts); 20 | Ok(()) 21 | } 22 | ProcessingMode::Live => { 23 | fs::rename(source_path, destination_path).map_err(|e| { 24 | println!("Failed to move file from {} to {}: {}", source_path.display(), destination_path.display(), e); 25 | e 26 | })?; 27 | debug!("Successfully moved file from {} to {}", source_path.display(), destination_path.display()); 28 | log_move_operation(source_path, destination_path).map_err(|log_err| { 29 | eprintln!("Failed to log the move operation: {}", log_err); 30 | log_err 31 | }) 32 | } 33 | } 34 | } 35 | 36 | #[cfg(not(feature = "test_env"))] 37 | fn log_move_operation(original_path: &Path, destination_path: &Path) -> std::io::Result<()> { 38 | use std::io::Write; 39 | 40 | use serde_json::json; 41 | 42 | let log_entry = json!({ 43 | "original_path": original_path.to_str(), 44 | "destination_path": destination_path.to_str(), 45 | }); 46 | 47 | // Validate JSON format 48 | if let Ok(log_str) = serde_json::to_string(&log_entry) { 49 | let mut log_file = fs::OpenOptions::new() 50 | .create(true) 51 | .append(true) 52 | .open("undo_log.jsonl")?; 53 | 54 | // Write the JSON string and manually append a newline 55 | if let Err(e) = write!(log_file, "{}\n", log_str) { 56 | return Err(e); // Handle write error 57 | } 58 | 59 | // Explicitly flush the buffer to ensure the newline is written 60 | log_file.flush()?; 61 | } else { 62 | eprintln!("Invalid JSON format for log entry"); 63 | return Err(io::Error::new( 64 | io::ErrorKind::InvalidData, 65 | "Invalid JSON format", 66 | )); 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | #[cfg(feature = "test_env")] 73 | fn log_move_operation(_original_path: &Path, _destination_path: &Path) -> std::io::Result<()> { 74 | Ok(()) 75 | } 76 | 77 | 78 | pub fn undo_last_actions() -> io::Result<()> { 79 | let log_path = Path::new("undo_log.jsonl"); 80 | let file = File::open(log_path)?; 81 | let reader = BufReader::new(file); 82 | 83 | let mut affected_dirs = HashSet::new(); 84 | 85 | // Process each line in the undo log 86 | for line in reader.lines() { 87 | let line = line?.trim().to_string(); 88 | if line.is_empty() { 89 | continue; 90 | } 91 | 92 | let action: Value = serde_json::from_str(&line) 93 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 94 | 95 | let original_path_str = action["original_path"].as_str().ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing 'original_path'"))?; 96 | let destination_path_str = action["destination_path"].as_str().ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing 'destination_path'"))?; 97 | 98 | let original_path = Path::new(original_path_str); 99 | let destination_path = Path::new(destination_path_str); 100 | 101 | if destination_path.exists() { 102 | if let Some(parent_dir) = original_path.parent() { 103 | fs::create_dir_all(parent_dir)?; 104 | } 105 | 106 | fs::rename(&destination_path, &original_path)?; 107 | debug!("Reversed move: {} -> {}", destination_path.display(), original_path.display()); 108 | 109 | let mut current_dir = destination_path.parent(); 110 | while let Some(dir) = current_dir { 111 | affected_dirs.insert(dir.to_path_buf()); 112 | current_dir = dir.parent(); 113 | } 114 | } else { 115 | return Err(Error::new(ErrorKind::NotFound, format!("Destination file does not exist: {}", destination_path.display()))); 116 | } 117 | } 118 | 119 | // Attempt to remove the log file before removing directories 120 | if log_path.exists() { 121 | fs::remove_file(log_path)?; 122 | println!("Undo log cleared."); 123 | } else { 124 | println!("Undo log file not found or already removed."); 125 | } 126 | 127 | // Now attempt to remove directories 128 | remove_directories(affected_dirs)?; 129 | 130 | Ok(()) 131 | } 132 | 133 | pub fn remove_directories(dirs: HashSet) -> io::Result<()> { 134 | let mut dirs_to_remove: Vec<_> = dirs.into_iter().collect(); 135 | dirs_to_remove.sort_by_key(|dir| dir.as_path().components().count()); 136 | dirs_to_remove.reverse(); 137 | 138 | for dir in dirs_to_remove { 139 | if dir.as_os_str().is_empty() { 140 | println!("Encountered an empty directory path, skipping."); 141 | continue; 142 | } 143 | 144 | println!("Checking if directory is empty: {}", dir.display()); 145 | if is_dir_empty(&dir)? { 146 | println!("Removing directory: {}", dir.display()); 147 | if let Err(e) = fs::remove_dir(&dir) { 148 | error!("Failed to remove directory {}: {}", dir.display(), e); 149 | } else { 150 | info!("Directory removed: {}", dir.display()); 151 | } 152 | } else { 153 | warn!("Directory not empty, skipping: {}", dir.display()); 154 | } 155 | } 156 | 157 | Ok(()) 158 | } 159 | 160 | 161 | fn is_dir_empty(dir: &Path) -> io::Result { 162 | let mut entries = fs::read_dir(dir)?; 163 | Ok(entries.next().is_none()) 164 | } 165 | 166 | 167 | pub fn clear_undo_log() -> std::io::Result<()> { 168 | let log_path = "undo_log.jsonl"; 169 | if Path::new(log_path).exists() { 170 | fs::remove_file(log_path)?; 171 | println!("Undo log cleared."); 172 | } else { 173 | println!("No undo log file found to clear."); 174 | } 175 | Ok(()) 176 | } 177 | 178 | 179 | 180 | pub fn print_current_structure(path: &Path, prefix: &str) { 181 | // Check if the path is a directory or a file 182 | if path.is_dir() { 183 | let entries = fs::read_dir(path).unwrap_or_else(|err| { 184 | panic!("Failed to read directory {}: {}", path.display(), err); 185 | }); 186 | 187 | let mut entries_vec: Vec = entries.filter_map(Result::ok).map(|e| e.path()).collect(); 188 | // Sort the entries for a consistent output 189 | entries_vec.sort(); 190 | 191 | for (i, entry) in entries_vec.iter().enumerate() { 192 | let file_name = entry.file_name().unwrap().to_str().unwrap(); 193 | let connector = if i == entries_vec.len() - 1 { "└── " } else { "├── " }; 194 | println!("{}{}{}", prefix, connector, file_name); 195 | 196 | // If the entry is a directory, recursively print its contents 197 | if entry.is_dir() { 198 | let new_prefix = if i == entries_vec.len() - 1 { 199 | format!("{} ", prefix) 200 | } else { 201 | format!("{}│ ", prefix) 202 | }; 203 | print_current_structure(entry, &new_prefix); 204 | } 205 | } 206 | } else { 207 | // If the path is a file, just print its name 208 | println!("{}── {}", prefix, path.file_name().unwrap().to_str().unwrap()); 209 | } 210 | } -------------------------------------------------------------------------------- /src/processing_mode.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::virtual_directory::VirtualDirectory; 4 | 5 | pub enum ProcessingMode { 6 | DryRun(VirtualDirectory), 7 | Live, 8 | } 9 | 10 | 11 | impl ProcessingMode { 12 | /// Checks if a file exists within the processing mode's context. For `DryRun`, it checks 13 | /// within the virtual directory. For `Live`, it always returns `false` as no virtual structure 14 | /// is inspected. 15 | /// 16 | /// # Parameters 17 | /// - `file_path`: The path of the file to check. 18 | /// - `dest_path`: The destination path to consider for the check. 19 | /// 20 | /// # Returns 21 | /// - `true` if the file exists within the virtual directory (`DryRun` mode). 22 | /// - `false` otherwise, or always in `Live` mode. 23 | /// 24 | /// # Examples 25 | /// Basic usage: 26 | /// ``` 27 | /// use std::path::{Path, PathBuf}; 28 | /// use crate::processing_mode::ProcessingMode; 29 | /// use crate::virtual_directory::VirtualDirectory; 30 | /// 31 | /// // Create a virtual directory and add a file path to it 32 | /// let mut virtual_dir = VirtualDirectory::default(); 33 | /// virtual_dir.add_file(PathBuf::from("dest/sample.txt")); 34 | /// 35 | /// // Initialize a ProcessingMode with the virtual directory 36 | /// let processing_mode = ProcessingMode::DryRun(virtual_dir); 37 | /// 38 | /// // Check if the file exists in the virtual directory 39 | /// let file_path = Path::new("sample.txt"); 40 | /// let dest_path = PathBuf::from("dest"); 41 | /// assert!(processing_mode.contains_file(file_path, &dest_path)); 42 | /// ``` 43 | /// 44 | /// Note: Replace `your_crate_name` with the actual name of your crate. 45 | #[allow(dead_code)] 46 | pub fn contains_file(&self, file_path: &Path, dest_path: &PathBuf) -> bool { 47 | match self { 48 | ProcessingMode::DryRun(virtual_dir) => { 49 | // Convert `file_path` and `dest_path` to a Vec representation. 50 | let mut path_components = Vec::new(); 51 | 52 | // Add the destination path components to the vector. 53 | if let Some(dest_str) = dest_path.to_str() { 54 | path_components.extend(dest_str.split('/').map(String::from)); 55 | } 56 | 57 | // Add the file name to the vector. 58 | if let Some(file_name) = file_path.file_name().and_then(|name| name.to_str()) { 59 | path_components.push(file_name.to_string()); 60 | } 61 | 62 | // Use the adapted vector to check if the file is contained within the virtual directory. 63 | virtual_dir.contains_file(&path_components) 64 | }, 65 | ProcessingMode::Live => false, // Live processing doesn't inspect a virtual structure. 66 | } 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use crate::processing_mode::ProcessingMode; 73 | use crate::virtual_directory::VirtualDirectory; 74 | use std::path::PathBuf; 75 | 76 | #[test] 77 | fn test_processing_mode_contains_file() { 78 | let mut virtual_dir = VirtualDirectory::default(); 79 | let path_components = vec!["dest".to_string(), "sample.txt".to_string()]; 80 | virtual_dir.add_path(&path_components); 81 | 82 | let processing_mode = ProcessingMode::DryRun(virtual_dir); 83 | 84 | // Assuming you have a method to convert a Vec back to a PathBuf for this check 85 | let file_path = PathBuf::from("sample.txt"); 86 | let dest_path = PathBuf::from("dest"); 87 | 88 | assert!(processing_mode.contains_file(&file_path, &dest_path)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Mutex}; 2 | 3 | use crate::{compressed_file_processor::CompressedFileProcessor, document_processor::DocumentProcessor, image_processor::ImageProcessor, generic_processor::GenericProcessor, video_processor::VideoProcessor}; 4 | 5 | use self::processor::Processor; 6 | use mime_guess::from_path; 7 | 8 | pub mod processor; 9 | 10 | 11 | pub trait ProcessorFactory { 12 | fn create_processor(&self, path: &PathBuf) -> Box; 13 | } 14 | 15 | 16 | pub struct DefaultProcessorFactory; 17 | 18 | impl ProcessorFactory for DefaultProcessorFactory { 19 | fn create_processor(&self, path: &PathBuf) -> Box { 20 | let mime_type = from_path(path).first_or_octet_stream(); 21 | let file_extension = path.extension().unwrap_or_default().to_str().unwrap_or("").to_lowercase(); 22 | 23 | match mime_type.type_() { 24 | mime::IMAGE => Box::new(ImageProcessor), 25 | mime::VIDEO => Box::new(VideoProcessor), 26 | mime::TEXT => Box::new(DocumentProcessor), 27 | mime::APPLICATION => match file_extension.as_str() { 28 | "pdf" | "doc" | "docx" | "ppt" | "pptx" | "xlsx" | "xls" | "json" | "yml" => Box::new(DocumentProcessor), 29 | "zip" | "tar" | "rar" | "7z" => Box::new(CompressedFileProcessor), 30 | _ => Box::new(GenericProcessor), 31 | }, 32 | _ => Box::new(GenericProcessor), 33 | } 34 | } 35 | } 36 | 37 | 38 | pub struct TestProcessorFactory { 39 | // Used in tests to check which processor was created last 40 | pub last_processor_type: Mutex>, 41 | } 42 | 43 | impl ProcessorFactory for TestProcessorFactory { 44 | fn create_processor(&self, path: &PathBuf) -> Box { 45 | let file_extension = path.extension().unwrap_or_default().to_str().unwrap_or("").to_lowercase(); 46 | let processor = match file_extension.as_str() { 47 | "jpg" | "png" => { 48 | self.last_processor_type.lock().unwrap().replace("ImageProcessor".to_string()); 49 | Box::new(ImageProcessor) as Box 50 | }, 51 | "docx" | "txt" => { 52 | self.last_processor_type.lock().unwrap().replace("DocumentProcessor".to_string()); 53 | Box::new(DocumentProcessor) as Box 54 | }, 55 | // Add other cases as necessary 56 | _ => { 57 | self.last_processor_type.lock().unwrap().replace("UnknownProcessor".to_string()); 58 | Box::new(GenericProcessor) as Box 59 | }, 60 | }; 61 | processor 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/traits/processor.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use crate::processing_mode::ProcessingMode; 3 | 4 | pub trait Processor { 5 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode); 6 | fn get_destination_subfolder(&self, path: &PathBuf) -> PathBuf; // New method 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/video_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::organizer::organize_file; 2 | use crate::processing_mode::ProcessingMode; 3 | use crate::traits::processor::Processor; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use log::{debug, error}; 7 | 8 | pub struct VideoProcessor; 9 | 10 | impl Processor for VideoProcessor { 11 | fn process(&self, path: &PathBuf, destination: &PathBuf, mode: &mut ProcessingMode) { 12 | // Determine the destination directory without including the filename 13 | let destination_dir = destination.join(self.get_destination_subfolder(path)); 14 | 15 | 16 | 17 | // Now the destination path includes the filename correctly 18 | let destination_path = destination_dir.join(path.file_name().unwrap()); 19 | 20 | match mode { 21 | ProcessingMode::DryRun(virtual_directory) => { 22 | // In DryRun mode, simulate the action 23 | debug!("Would move {} to {}", path.display(), destination_path.display()); 24 | let path_parts: Vec = destination_path.iter().map(|s| s.to_string_lossy().to_string()).collect(); 25 | virtual_directory.add_path(&path_parts); 26 | }, 27 | ProcessingMode::Live => { 28 | // In Live mode, actually create the directory and move the file 29 | // Ensure the directory structure exists 30 | if let Err(e) = fs::create_dir_all(&destination_dir) { 31 | error!("Error creating destination directory: {}", e); 32 | return; 33 | } 34 | if let Err(e) = organize_file(path, &destination_path, mode) { 35 | error!("Failed to organize file: {}", e); 36 | } else { 37 | debug!("Successfully moved file from {} to {}", path.display(), destination_path.display()); 38 | } 39 | } 40 | } 41 | } 42 | 43 | fn get_destination_subfolder(&self, _path: &PathBuf) -> PathBuf { 44 | PathBuf::from("Videos") 45 | } 46 | } 47 | 48 | 49 | #[cfg(test)] 50 | mod video_processor_tests { 51 | 52 | 53 | use super::*; 54 | use std::fs::{self, File}; 55 | use std::io::Write; 56 | use tempfile::tempdir; 57 | 58 | #[test] 59 | fn test_video_processor_live() { 60 | let processor = VideoProcessor {}; 61 | let temp_dir = tempdir().unwrap(); 62 | let source_dir = temp_dir.path().join("source"); 63 | let destination_dir = temp_dir.path().join("destination"); 64 | fs::create_dir_all(&source_dir).unwrap(); 65 | let video_file_path = source_dir.join("test_video.mp4"); 66 | let mut file = File::create(&video_file_path).unwrap(); 67 | writeln!(file, "Dummy video content").unwrap(); 68 | 69 | let mut mode = ProcessingMode::Live; 70 | 71 | processor.process(&video_file_path, &destination_dir, &mut mode); 72 | 73 | let expected_destination = destination_dir.join("Videos").join("test_video.mp4"); 74 | print!("{}", expected_destination.display()); 75 | 76 | assert!(expected_destination.exists(), "Document was not moved to the correct destination in Live mode."); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/virtual_directory.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Default, Debug)] 4 | pub struct VirtualDirectory { 5 | files: Vec, 6 | directories: HashMap, 7 | } 8 | 9 | impl VirtualDirectory { 10 | pub fn add_path(&mut self, parts: &[String]) { 11 | if parts.is_empty() { 12 | return; 13 | } 14 | 15 | let (first, rest) = parts.split_first().unwrap(); 16 | if rest.is_empty() { 17 | self.files.push(first.clone()); 18 | } else { 19 | self.directories 20 | .entry(first.clone()) 21 | .or_insert_with(VirtualDirectory::default) 22 | .add_path(rest); 23 | } 24 | } 25 | 26 | pub fn print(&self, prefix: &str) { 27 | let mut entries = self.directories.iter().collect::>(); 28 | entries.sort_by_key(|e| e.0); 29 | 30 | let total_entries = entries.len() + self.files.len(); 31 | let mut current_index = 0; // Keep track of the current index across both directories and files 32 | 33 | for (dir, sub_dir) in entries.iter() { 34 | let is_current_last = current_index == total_entries - 1; 35 | let connector = if is_current_last { 36 | "└── " 37 | } else { 38 | "├── " 39 | }; 40 | println!("{}{}{}", prefix, connector, dir); 41 | let new_prefix = if is_current_last { 42 | format!("{} ", prefix) 43 | } else { 44 | format!("{}│ ", prefix) 45 | }; 46 | sub_dir.print(&new_prefix); 47 | current_index += 1; // Increment the index after processing a directory 48 | } 49 | 50 | for (i, file) in self.files.iter().enumerate() { 51 | let is_current_last = current_index + i == total_entries - 1; 52 | let connector = if is_current_last { 53 | "└── " 54 | } else { 55 | "├── " 56 | }; 57 | println!("{}{}{}", prefix, connector, file); 58 | } 59 | } 60 | // Wrapper function to start the printing process without external parameters 61 | pub fn print_tree(&self) { 62 | self.print(""); 63 | } 64 | 65 | #[allow(dead_code)] 66 | pub fn contains_file(&self, path: &[String]) -> bool { 67 | if path.is_empty() { 68 | return false; 69 | } 70 | 71 | let (first, rest) = path.split_first().unwrap(); 72 | if rest.is_empty() { 73 | // We're at the last component, which should be a file. 74 | self.files.contains(first) 75 | } else { 76 | // We're looking at a directory; dive deeper. 77 | if let Some(sub_dir) = self.directories.get(first) { 78 | sub_dir.contains_file(rest) 79 | } else { 80 | false 81 | } 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use crate::virtual_directory::VirtualDirectory; 89 | 90 | #[test] 91 | fn test_add_path_to_virtual_directory() { 92 | let mut virtual_dir = VirtualDirectory::default(); 93 | let path_components = vec!["destination".to_string(), "Word_Documents".to_string(), "document.docx".to_string()]; 94 | virtual_dir.add_path(&path_components); 95 | 96 | // Debug print to verify structure after addition 97 | println!("{:#?}", virtual_dir); 98 | 99 | // Assertions to ensure the path was added correctly 100 | assert!(!virtual_dir.directories.is_empty(), "Directories should not be empty."); 101 | assert!(virtual_dir.contains_file(&path_components), "Path should exist in VirtualDirectory."); 102 | } 103 | 104 | #[test] 105 | fn direct_virtual_directory_test() { 106 | let mut virtual_dir = VirtualDirectory::default(); 107 | let path_components = vec!["destination".to_string(), "Word_Documents".to_string(), "document.docx".to_string()]; 108 | virtual_dir.add_path(&path_components); 109 | 110 | assert!(virtual_dir.contains_file(&path_components), 111 | "VirtualDirectory does not contain the expected path after direct addition."); 112 | } 113 | 114 | } 115 | --------------------------------------------------------------------------------