refactor: improve code to more idiomatic

- Use lib.rs for library mode and used with log tests
- Improve enum for tasks on files with file_type.rs
- Fix main and directory_processing
- Add Errors
- Adjust loggint.rs with OneLock and Atomic to control with parallel tests
- Add test.rs for unitary tests
This commit is contained in:
Jesús Pérex 2025-05-27 00:58:59 +01:00
parent 0fb88cd3b7
commit 98a9649bf2
8 changed files with 869 additions and 270 deletions

405
Cargo.lock generated
View File

@ -26,6 +26,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@ -76,12 +85,29 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
@ -123,6 +149,21 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags 1.3.2",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.38" version = "4.5.38"
@ -142,7 +183,7 @@ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim", "strsim 0.11.1",
] ]
[[package]] [[package]]
@ -151,10 +192,10 @@ version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [ dependencies = [
"heck", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
] ]
[[package]] [[package]]
@ -180,9 +221,11 @@ name = "dir-odt-to-pdf"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap 4.5.38",
"env_logger", "env_logger",
"log", "log",
"serial_test",
"structopt",
"tempfile", "tempfile",
"thiserror", "thiserror",
"which", "which",
@ -239,6 +282,83 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.3" version = "0.3.3"
@ -251,12 +371,30 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.63" version = "0.1.63"
@ -308,7 +446,7 @@ checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
] ]
[[package]] [[package]]
@ -321,6 +459,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.172"
@ -333,6 +477,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.27"
@ -360,6 +514,41 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.11.0" version = "1.11.0"
@ -375,6 +564,30 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -399,6 +612,15 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.1",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -434,7 +656,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -447,6 +669,27 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "scc"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4"
dependencies = [
"sdd",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdd"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@ -464,7 +707,32 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
]
[[package]]
name = "serial_test"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
dependencies = [
"futures",
"log",
"once_cell",
"parking_lot",
"scc",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
] ]
[[package]] [[package]]
@ -473,12 +741,68 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "structopt"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [
"clap 2.34.0",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [
"heck 0.3.3",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.101" version = "2.0.101"
@ -503,6 +827,15 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
@ -520,7 +853,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
] ]
[[package]] [[package]]
@ -529,12 +862,36 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.2+wasi-0.2.4"
@ -566,7 +923,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -588,7 +945,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -614,6 +971,28 @@ dependencies = [
"winsafe", "winsafe",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@ -635,7 +1014,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
] ]
[[package]] [[package]]
@ -646,7 +1025,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.101",
] ]
[[package]] [[package]]
@ -758,5 +1137,5 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.1",
] ]

View File

@ -13,3 +13,8 @@ env_logger = "0.11.8"
thiserror = "2.0.12" thiserror = "2.0.12"
tempfile = "3.8" tempfile = "3.8"
chrono = "0.4" chrono = "0.4"
structopt = "0.3"
[dev-dependencies]
serial_test = "3.2.0"
tempfile = "3.8"

View File

@ -1,19 +1,23 @@
use crate::error::{ProcessError, Result}; use crate::error::{ProcessError, Result};
use crate::file_type::FileType;
// use crate::processed_path::ProcessedPath;
use crate::FILES_TO_CONVERT;
use crate::tools; use crate::tools;
use crate::{FILES_TO_CONVERT, FILES_TO_COPY, PATHS_TO_IGNORE};
use log::{debug, info, warn}; use log::{debug, info, warn};
use std::collections::HashSet; use std::{
use std::fs; collections::HashSet,
use std::path::{Path, PathBuf}; fs,
use std::time::UNIX_EPOCH; path::{Path, PathBuf},
time::SystemTime,
};
/// DirectoryProcessor handles the conversion of documents from a source directory /// DirectoryProcessor handles the conversion of documents from a source directory
/// to a target directory, managing file conversions, copies, and cleanup. /// to a target directory, managing file conversions, copies, and cleanup.
#[derive(Debug)] #[derive(Debug)]
pub struct DirectoryProcessor { pub struct DirectoryProcessor {
pub(crate) source_dir: PathBuf, pub source_dir: PathBuf,
pub(crate) target_dir: PathBuf, pub target_dir: PathBuf,
pub(crate) source_files: HashSet<PathBuf>, pub source_files: HashSet<PathBuf>,
} }
impl DirectoryProcessor { impl DirectoryProcessor {
@ -30,36 +34,38 @@ impl DirectoryProcessor {
} }
} }
/// Determines if a file needs to be copied or converted based on modification times. /// Determines if a file needs to be processed based on modification times.
pub(crate) fn needs_copy_or_conversion(source_path: &Path, dest_path: &Path) -> bool { pub fn needs_processing(source: &Path, target: &Path) -> bool {
if !dest_path.exists() { if !target.exists() {
return true; return true;
} }
let source_modified = fs::metadata(source_path) let source_time = fs::metadata(source)
.and_then(|m| m.modified()) .and_then(|m| m.modified())
.unwrap_or(UNIX_EPOCH); .unwrap_or_else(|_| SystemTime::now());
let dest_modified = fs::metadata(dest_path) let target_time = fs::metadata(target)
.and_then(|m| m.modified()) .and_then(|m| m.modified())
.unwrap_or(UNIX_EPOCH); .unwrap_or_else(|_| SystemTime::now());
source_modified > dest_modified source_time > target_time
} }
/// Collects all source files that need processing. /// Collects all source files that need processing.
fn get_source_files(&mut self, current_dir: &Path) -> Result<()> { fn collect_source_files(&mut self, dir: &Path) -> Result<()> {
let entries = fs::read_dir(current_dir).map_err(ProcessError::Io)?; for entry in fs::read_dir(dir).map_err(ProcessError::Io)?.flatten() {
for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
self.get_source_files(&path)?; self.collect_source_files(&path)?;
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { continue;
let ext = ext.to_lowercase(); }
if FILES_TO_CONVERT.contains(&ext.as_str()) || FILES_TO_COPY.contains(&ext.as_str()) {
if let Ok(rel_path) = path.strip_prefix(&self.source_dir) { let file_type = FileType::from_path(&path, &self.source_dir, &self.target_dir)?;
self.source_files.insert(rel_path.to_path_buf()); if file_type.should_process() {
if let Some(source) = file_type.source() {
if let Ok(relative) = source.strip_prefix(&self.source_dir) {
self.source_files.insert(relative.to_path_buf());
} }
} }
} }
@ -67,20 +73,72 @@ impl DirectoryProcessor {
Ok(()) Ok(())
} }
/// Cleans up the target directory by removing obsolete files and empty directories. /// Processes all files in the source directory.
fn clean_target_directory(&self, current_dir: &Path) -> Result<bool> { fn process_directory(&self, dir: &Path) -> Result<()> {
let entries = fs::read_dir(current_dir).map_err(ProcessError::Io)?; for entry in fs::read_dir(dir).map_err(ProcessError::Io)?.flatten() {
let mut is_empty = true;
for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
// Check if path should be ignored if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) { fs::create_dir_all(self.target_dir.join(path.strip_prefix(&self.source_dir)?))
if PATHS_TO_IGNORE.iter().any(|&ignore| name.contains(ignore)) { .map_err(ProcessError::Io)?;
is_empty = false; self.process_directory(&path)?;
continue; continue;
} }
let file_type = FileType::from_path(&path, &self.source_dir, &self.target_dir)?;
self.process_file(file_type)?;
}
Ok(())
}
/// Processes a single file based on its type.
fn process_file(&self, file_type: FileType) -> Result<()> {
match file_type {
FileType::Convert { source, target } => {
if Self::needs_processing(&source, &target) {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(ProcessError::Io)?;
}
tools::convert_file(&source, target.parent().unwrap_or(&self.target_dir))
.map_err(ProcessError::Processing)?;
info!(
"Converted: {} -> {}",
source.strip_prefix(&self.source_dir)?.display(),
target.strip_prefix(&self.target_dir)?.display()
);
}
}
FileType::Copy { source, target } => {
if Self::needs_processing(&source, &target) {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(ProcessError::Io)?;
}
fs::copy(&source, &target).map_err(ProcessError::Io)?;
info!(
"Copied: {} -> {}",
source.strip_prefix(&self.source_dir)?.display(),
target.strip_prefix(&self.target_dir)?.display()
);
}
}
_ => {}
}
Ok(())
}
/// Cleans up the target directory by removing obsolete files.
fn clean_target_directory(&self, dir: &Path) -> Result<bool> {
let mut is_empty = true;
for entry in fs::read_dir(dir).map_err(ProcessError::Io)?.flatten() {
let path = entry.path();
let file_type = FileType::from_path(&path, &self.target_dir, &self.target_dir)?;
if file_type.should_ignore() {
is_empty = false;
continue;
} }
if path.is_dir() { if path.is_dir() {
@ -101,35 +159,33 @@ impl DirectoryProcessor {
is_empty = false; is_empty = false;
} }
} }
} else { continue;
let rel_path = path.strip_prefix(&self.target_dir).map_err(ProcessError::StripPrefix)?; }
let should_exist = self.should_file_exist(rel_path);
if !should_exist { let relative = path.strip_prefix(&self.target_dir)?;
if !self.should_file_exist(relative) {
if let Err(e) = fs::remove_file(&path) { if let Err(e) = fs::remove_file(&path) {
warn!("Could not remove file {}: {}", path.display(), e); warn!("Could not remove file {}: {}", relative.display(), e);
} else { } else {
debug!("Removed obsolete file: {}", path.display()); debug!("Removed obsolete file: {}", relative.display());
} }
} else { } else {
is_empty = false; is_empty = false;
} }
} }
}
Ok(is_empty) Ok(is_empty)
} }
/// Determines if a file in the target directory should exist based on source files. /// Determines if a file in the target directory should exist.
fn should_file_exist(&self, rel_path: &Path) -> bool { fn should_file_exist(&self, rel_path: &Path) -> bool {
if let Some(ext) = rel_path.extension().and_then(|e| e.to_str()) { if let Some(ext) = rel_path.extension().and_then(|e| e.to_str()) {
if ext == "pdf" { if ext == "pdf" {
// For PDF files, check if any corresponding source file exists // Check if any corresponding source file exists
FILES_TO_CONVERT FILES_TO_CONVERT
.iter() .iter()
.any(|&ext| self.source_files.contains(&rel_path.with_extension(ext))) .any(|&ext| self.source_files.contains(&rel_path.with_extension(ext)))
} else { } else {
// For other files, check if they exist in source
self.source_files.contains(rel_path) self.source_files.contains(rel_path)
} }
} else { } else {
@ -137,89 +193,18 @@ impl DirectoryProcessor {
} }
} }
/// Processes a single directory, converting or copying files as needed. /// Process the entire directory structure.
fn process_directory(&self, current_source: &Path, current_target: &Path) -> Result<()> {
fs::create_dir_all(current_target).map_err(ProcessError::Io)?;
let entries = fs::read_dir(current_source).map_err(ProcessError::Io)?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let relative_path = path
.strip_prefix(&self.source_dir)
.map_err(ProcessError::StripPrefix)?;
let dest_subdir = self.target_dir.join(relative_path);
self.process_directory(&path, &dest_subdir)?;
} else {
self.process_file(&path, current_source, current_target)?;
}
}
Ok(())
}
/// Processes a single file, either converting it to PDF or copying it.
fn process_file(&self, path: &Path, current_source: &Path, current_target: &Path) -> Result<()> {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let relative_path = path
.strip_prefix(current_source)
.map_err(ProcessError::StripPrefix)?;
let ext = ext.to_lowercase();
if FILES_TO_CONVERT.contains(&ext.as_str()) {
self.convert_file(path, relative_path, current_target)?;
} else if FILES_TO_COPY.contains(&ext.as_str()) {
self.copy_file(path, relative_path, current_target)?;
}
}
Ok(())
}
/// Converts a file to PDF format.
fn convert_file(&self, path: &Path, relative_path: &Path, current_target: &Path) -> Result<()> {
let pdf_path = current_target.join(relative_path.with_extension("pdf"));
if Self::needs_copy_or_conversion(path, &pdf_path) {
tools::convert_file(path, pdf_path.parent().unwrap_or(current_target))
.map_err(|e| ProcessError::Processing(e))?;
info!(
"Converted: {} -> {}",
path.strip_prefix(&self.source_dir).unwrap().display(),
pdf_path.strip_prefix(&self.target_dir).unwrap().display()
);
}
Ok(())
}
/// Copies a file to the target directory.
fn copy_file(&self, path: &Path, relative_path: &Path, current_target: &Path) -> Result<()> {
let dest_path = current_target.join(relative_path);
if Self::needs_copy_or_conversion(path, &dest_path) {
fs::copy(path, &dest_path).map_err(ProcessError::Io)?;
info!(
"Copied file: {} -> {}",
path.strip_prefix(&self.source_dir).unwrap().display(),
dest_path.strip_prefix(&self.target_dir).unwrap().display()
);
}
Ok(())
}
/// Processes all files in the source directory, converting or copying them as needed,
/// and then cleans up the target directory.
pub fn process(&mut self) -> Result<()> { pub fn process(&mut self) -> Result<()> {
debug!("Collecting source files"); // Clone paths before mutable borrow to avoid borrowing conflicts
self.get_source_files(&self.source_dir.to_owned())?; let source_dir = self.source_dir.to_owned();
let target_dir = self.target_dir.to_owned();
debug!("Starting directory processing"); // Now we can use the mutable borrow for collect_source_files
self.process_directory(&self.source_dir.to_owned(), &self.target_dir.to_owned())?; self.collect_source_files(&source_dir)?;
debug!("Cleaning target directory"); // And use the cloned paths for the remaining operations
self.clean_target_directory(&self.target_dir.to_owned())?; self.process_directory(&source_dir)?;
self.clean_target_directory(&target_dir)?;
info!("Directory processing completed successfully");
Ok(()) Ok(())
} }
} }

View File

@ -11,6 +11,24 @@ pub enum ProcessError {
#[error("Directory processing error: {0}")] #[error("Directory processing error: {0}")]
Processing(String), Processing(String),
#[error("Logging error: {0}")]
Log(#[from] LogError),
}
#[derive(Error, Debug)]
pub enum LogError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Logger initialization error: {0}")]
Init(String),
#[error("Logger already initialized")]
AlreadyInitialized,
#[error("Lock poisoned: {0}")]
LockPoisoned(String),
} }
pub type Result<T> = std::result::Result<T, ProcessError>; pub type Result<T> = std::result::Result<T, ProcessError>;

70
src/file_type.rs Normal file
View File

@ -0,0 +1,70 @@
use crate::error::{ProcessError, Result};
use crate::{FILES_TO_CONVERT, FILES_TO_COPY, PATHS_TO_IGNORE};
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum FileType {
Convert { source: PathBuf, target: PathBuf },
Copy { source: PathBuf, target: PathBuf },
Ignore,
Other,
}
impl FileType {
/// Determines the type of a file and its processing requirements
pub fn from_path(path: &Path, source_base: &Path, target_base: &Path) -> Result<Self> {
// First check if it should be ignored
if Self::is_ignored(path) {
return Ok(FileType::Ignore);
}
let relative = path
.strip_prefix(source_base)
.map_err(ProcessError::StripPrefix)?;
let source = path.to_path_buf();
let target = target_base.join(relative);
// Then check file extension
match path
.extension()
.and_then(|e| e.to_str())
.map(|ext| ext.to_lowercase())
{
Some(ext) if FILES_TO_CONVERT.contains(&ext.as_str()) => Ok(FileType::Convert {
source,
target: target.with_extension("pdf"),
}),
Some(ext) if FILES_TO_COPY.contains(&ext.as_str()) => {
Ok(FileType::Copy { source, target })
}
_ => Ok(FileType::Other),
}
}
/// Check if a path should be ignored
fn is_ignored(path: &Path) -> bool {
path.file_name()
.and_then(|n| n.to_str())
.map(|name| PATHS_TO_IGNORE.iter().any(|&ignore| name.contains(ignore)))
.unwrap_or(false)
}
/// Get the source path if this is a processable file type
pub fn source(&self) -> Option<&Path> {
match self {
FileType::Convert { source, .. } | FileType::Copy { source, .. } => Some(source),
_ => None,
}
}
/// Returns true if this file type should be processed
pub fn should_process(&self) -> bool {
matches!(self, FileType::Convert { .. } | FileType::Copy { .. })
}
/// Returns true if this file type should be ignored
pub fn should_ignore(&self) -> bool {
matches!(self, FileType::Ignore)
}
}

48
src/lib.rs Normal file
View File

@ -0,0 +1,48 @@
pub mod directory_processor;
pub mod error;
pub mod file_type;
pub mod logging;
pub mod tools;
// Constants for file processing
pub const FILES_TO_CONVERT: [&str; 3] = ["odt", "doc", "docx"];
pub const FILES_TO_COPY: [&str; 9] = [
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "avif", "txt",
];
pub const PATHS_TO_IGNORE: [&str; 5] = [
".DS_Store",
".syncthing",
".sync-conflict-",
".stfolder",
".stversions",
];
// Re-export commonly used items
pub use crate::logging::LogConfig;
pub use log::{debug, error, info, warn};
/// Macro for logging with file and line information
#[macro_export]
macro_rules! log_detail {
($level:expr, $($arg:tt)+) => {
log::log!(
$level,
"[{}:{}] {}",
file!(),
line!(),
format_args!($($arg)+)
);
};
}
/// Macro for performance logging with timing information
#[macro_export]
macro_rules! log_timed {
($level:expr, $desc:expr, $body:expr) => {{
let start = std::time::Instant::now();
let result = $body;
let duration = start.elapsed();
log::log!($level, "{} completed in {:.2?}", $desc, duration);
result
}};
}

View File

@ -1,118 +1,214 @@
use env_logger::{Builder, Target}; use crate::error::{LogError, Result};
use log::LevelFilter; use chrono::Local;
use log::{LevelFilter, Log, Metadata, Record};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::{Arc, Mutex};
/// Custom writer that writes either to stderr or to a file /// Configuration for logging setup
pub(crate) struct LogWriter { #[derive(Debug, Clone, PartialEq)]
pub struct LogConfig {
pub log_file: Option<PathBuf>,
pub log_level: LevelFilter,
pub append_log: bool,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
log_file: None,
log_level: LevelFilter::Info,
append_log: false,
}
}
}
/// Custom writer that can write to either a file or stderr
#[derive(Debug)]
pub struct LogWriter {
file: Option<std::fs::File>, file: Option<std::fs::File>,
} }
impl LogWriter { impl LogWriter {
fn new(file: Option<std::fs::File>) -> Self { pub fn new(file: Option<std::fs::File>) -> Self {
Self { file } Self { file }
} }
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
match &mut self.file {
Some(file) => file.write_all(buf),
None => io::stderr().write_all(buf),
}
}
pub fn flush(&mut self) -> io::Result<()> {
match &mut self.file {
Some(file) => file.flush(),
None => io::stderr().flush(),
}
}
} }
impl Write for LogWriter { impl Write for LogWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match &mut self.file { self.write_all(buf)?;
Some(file) => file.write_all(buf)?, // Write to file if specified
None => io::stderr().write_all(buf)?, // Otherwise write to stderr
}
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
match &mut self.file { self.flush()
Some(file) => file.flush()?,
None => io::stderr().flush()?,
}
Ok(())
} }
} }
pub struct LogConfig { #[derive(Debug)]
pub log_file: Option<PathBuf>, struct SimpleLogger {
pub log_level: String, writer: Arc<Mutex<LogWriter>>,
pub append_log: bool, level: Arc<Mutex<LevelFilter>>,
} }
/// Initialize logging to either stderr or file (if specified) impl SimpleLogger {
pub fn init_logging(config: &LogConfig) -> Result<(), Box<dyn std::error::Error>> { fn new(writer: Arc<Mutex<LogWriter>>, level: Arc<Mutex<LevelFilter>>) -> Self {
let mut builder = Builder::from_default_env(); Self { writer, level }
}
// Set log level from command line argument fn get_level(&self) -> Result<LevelFilter> {
let level = match config.log_level.to_lowercase().as_str() { self.level
"error" => LevelFilter::Error, .lock()
"warn" => LevelFilter::Warn, .map_err(|e| LogError::LockPoisoned(e.to_string()).into())
"info" => LevelFilter::Info, .map(|guard| *guard)
"debug" => LevelFilter::Debug, }
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
builder.filter_level(level);
// Format with timestamps, module path, and line numbers for debug/trace fn write_log(&self, message: &str) -> Result<()> {
builder.format(move |buf, record| { self.writer
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); .lock()
if level >= LevelFilter::Debug { .map_err(|e| LogError::LockPoisoned(e.to_string()).into())
writeln!( .and_then(|mut writer| {
buf, writer
"{} [{}] [{}:{}] - {}", .write_all(message.as_bytes())
timestamp, .and_then(|_| writer.flush())
record.level(), .map_err(|e| LogError::Io(e).into())
record.module_path().unwrap_or("unknown"), })
record.line().unwrap_or(0), }
record.args() }
)
} else { impl Log for SimpleLogger {
writeln!( fn enabled(&self, metadata: &Metadata) -> bool {
buf, self.get_level()
"{} [{}] - {}", .map(|level| metadata.level() <= level)
timestamp, .unwrap_or(false)
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let message = format!(
"{} [{:<5}] - {}\n",
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(), record.level(),
record.args() record.args()
) );
}
});
// Set up the writer for either file or console output if let Err(e) = self.write_log(&message) {
eprintln!("Failed to write log message: {}", e);
}
}
fn flush(&self) {
if let Err(e) = self
.writer
.lock()
.map_err(|e| LogError::LockPoisoned(e.to_string()))
.and_then(|mut w| w.flush().map_err(LogError::Io))
{
eprintln!("Failed to flush logger: {}", e);
}
}
}
static LOGGER: OnceLock<Arc<SimpleLogger>> = OnceLock::new();
static LEVEL: OnceLock<Arc<Mutex<LevelFilter>>> = OnceLock::new();
/// Initialize logging with enhanced features
///
/// # Errors
///
/// Returns an error if:
/// - Failed to create log directory
/// - Failed to open log file
/// - Failed to write initial log header
/// - Failed to set global logger
/// - Logger is already initialized
pub fn init_logging(config: LogConfig) -> Result<()> {
let log_file = if let Some(log_path) = &config.log_file { let log_file = if let Some(log_path) = &config.log_file {
// Create parent directory if it doesn't exist
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent).map_err(LogError::Io)?;
}
let file = OpenOptions::new() let file = OpenOptions::new()
.create(true) .create(true)
.write(true) .write(true)
.append(config.append_log) .append(config.append_log)
.truncate(!config.append_log) .truncate(!config.append_log)
.open(log_path)?; .open(log_path)
.map_err(LogError::Io)?;
// Write header to log file if not appending // Write header for new log files
if !config.append_log { if !config.append_log {
writeln!( writeln!(
&file, &file,
"=== Log started at {} ===", "=== Log started at {} ===",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S") Local::now().format("%Y-%m-%d %H:%M:%S")
)?; )
.map_err(LogError::Io)?;
} }
Some(file) Some(file)
} else { } else {
None None
}; };
// Create and set the writer let level = LEVEL
let writer = LogWriter::new(log_file); .get_or_init(|| Arc::new(Mutex::new(config.log_level)))
builder.target(Target::Pipe(Box::new(writer))); .clone();
builder.init(); {
let mut lvl = level
.lock()
.map_err(|e| LogError::LockPoisoned(e.to_string()))?;
*lvl = config.log_level;
}
// Log initial message with configuration info let writer = Arc::new(Mutex::new(LogWriter::new(log_file)));
let logger = Arc::new(SimpleLogger::new(writer.clone(), level.clone()));
// Try to set the global logger
if LOGGER.get().is_some() {
return Err(LogError::AlreadyInitialized.into());
}
// Set the logger and store it
log::set_boxed_logger(Box::new(SimpleLogger::new(writer, level.clone())))
.map_err(|e| LogError::Init(e.to_string()))?;
// Store our logger instance
if LOGGER.set(logger).is_err() {
return Err(LogError::AlreadyInitialized.into());
}
log::set_max_level(config.log_level);
// Log initial configuration
log::info!( log::info!(
"Logging initialized (level: {}, output: {})", "Logging initialized (level: {}, output: {})",
config.log_level, config.log_level,
config.log_file config
.log_file
.as_ref() .as_ref()
.map(|p| format!("file: {}", p.display())) .map(|p| p.display().to_string())
.unwrap_or_else(|| "console".to_string()) .unwrap_or_else(|| "console".to_string())
); );

View File

@ -1,77 +1,75 @@
use clap::Parser;
use std::path::PathBuf;
use log::{info, debug, error};
mod directory_processor;
mod tools;
mod error;
mod logging;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use directory_processor::DirectoryProcessor; use dir_odt_to_pdf::{
use logging::{LogConfig, init_logging}; directory_processor::DirectoryProcessor,
error::Result,
info, log_detail, log_timed,
logging::{LogConfig, init_logging},
};
use log::LevelFilter;
use std::path::PathBuf;
use structopt::StructOpt;
pub const FILES_TO_COPY: [&str; 10] = [ #[derive(Debug, StructOpt)]
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "avif", "txt", "md", #[structopt(
]; name = "dir-odt-to-pdf",
pub const FILES_TO_CONVERT: [&str; 3] = ["odt", "doc", "docx"]; about = "Convert ODT files to PDF in a directory"
pub const PATHS_TO_IGNORE: [&str; 5] = [".DS_Store", ".syncthing", ".sync-conflict-", ".stfolder", ".stversions"];
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "Convert source directory with document files (odt/doc/docx) to target path with pdf files with changes verification"
)] )]
struct Args { struct Opt {
#[arg(help = "Source directory with .odt, .doc, or .docx files")] /// Source directory containing ODT files
source: PathBuf, #[structopt(parse(from_os_str))]
source_dir: PathBuf,
#[arg(help = "Target directory for PDFs converted files")] /// Target directory for PDF files
dest: PathBuf, #[structopt(parse(from_os_str))]
target_dir: PathBuf,
#[arg(long, help = "Log file path (optional)")] /// Log file path (optional)
#[structopt(parse(from_os_str), long)]
log_file: Option<PathBuf>, log_file: Option<PathBuf>,
#[arg( /// Log level (trace, debug, info, warn, error)
long, #[structopt(long, default_value = "info")]
help = "Log level (error, warn, info, debug, trace)", log_level: LevelFilter,
default_value = "info"
)]
log_level: String,
#[arg( /// Append to existing log file instead of overwriting
long, #[structopt(long)]
help = "Append to log file instead of overwriting",
default_value_t = false
)]
append_log: bool, append_log: bool,
} }
fn main() { fn main() -> Result<()> {
let args = Args::parse(); let args = Opt::from_args();
// Initialize logging
let log_config = LogConfig { let log_config = LogConfig {
log_file: args.log_file.to_owned(), log_file: args.log_file,
log_level: args.log_level.to_owned(), log_level: args.log_level,
append_log: args.append_log.to_owned(), append_log: args.append_log,
}; };
if let Err(e) = init_logging(&log_config) { // Initialize logging system
eprintln!("Failed to initialize logging: {}", e); init_logging(log_config)?;
std::process::exit(1);
}
info!("Starting document conversion"); info!("Starting directory processing");
debug!("Source directory: {}", args.source.display()); log_detail!(
debug!("Target directory: {}", args.dest.display()); log::Level::Info,
"Source directory: {}",
args.source_dir.display()
);
log_detail!(
log::Level::Info,
"Target directory: {}",
args.target_dir.display()
);
let mut processor = DirectoryProcessor::new(args.source, args.dest); let mut processor = DirectoryProcessor::new(args.source_dir, args.target_dir);
if let Err(e) = processor.process() {
error!("Error processing directory: {}", e);
std::process::exit(1);
}
info!("Document conversion completed successfully"); // Process directory and measure time
log_timed!(log::Level::Info, "Directory processing", {
processor.process()?
});
info!("Processing completed successfully");
Ok(())
} }