file-rotate-0.8.0/.cargo_vcs_info.json0000644000000001360000000000100132540ustar { "git": { "sha1": "8ee371fd0e50d281e87d8a755aaeaa2c824f86b1" }, "path_in_vcs": "" }file-rotate-0.8.0/.github/workflows/rust.yml000064400000000000000000000007751046102023000171720ustar 00000000000000name: Rust on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Check run: cargo check test: runs-on: '${{ matrix.os }}' strategy: matrix: include: - os: macos-latest - os: ubuntu-latest - os: windows-latest steps: - uses: actions/checkout@v3 - name: Test run: cargo test file-rotate-0.8.0/.gitignore000064400000000000000000000000311046102023000140260ustar 00000000000000Cargo.lock logs/ target/ file-rotate-0.8.0/Cargo.lock0000644000000461330000000000100112360ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ "iana-time-zone", "num-integer", "num-traits", "winapi", ] [[package]] name = "codespan-reporting" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", "unicode-width", ] [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "cxx" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" dependencies = [ "cc", "cxxbridge-flags", "cxxbridge-macro", "link-cplusplus", ] [[package]] name = "cxx-build" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" dependencies = [ "cc", "codespan-reporting", "once_cell", "proc-macro2", "quote", "scratch", "syn", ] [[package]] name = "cxxbridge-flags" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" [[package]] name = "cxxbridge-macro" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "log", "regex", ] [[package]] name = "errno" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ "errno-dragonfly", "libc", "winapi", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "file-rotate" version = "0.8.0" dependencies = [ "chrono", "filetime", "flate2", "quickcheck", "quickcheck_macros", "tempfile", ] [[package]] name = "filetime" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" dependencies = [ "cfg-if", "libc", "redox_syscall", "windows-sys 0.36.1", ] [[package]] name = "flate2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "getrandom" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "iana-time-zone" version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "winapi", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" dependencies = [ "cxx", "cxx-build", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" dependencies = [ "libc", "windows-sys 0.45.0", ] [[package]] name = "js-sys" version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.135" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" [[package]] name = "link-cplusplus" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" dependencies = [ "cc", ] [[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "miniz_oxide" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] [[package]] name = "quickcheck" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" dependencies = [ "env_logger", "log", "rand", "rand_core", ] [[package]] name = "quickcheck_macros" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", "rand_chacha", "rand_core", "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rustix" version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.45.0", ] [[package]] name = "scratch" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" [[package]] name = "syn" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.42.0", ] [[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "unicode-ident" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[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-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[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]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc 0.36.1", "windows_i686_gnu 0.36.1", "windows_i686_msvc 0.36.1", "windows_x86_64_gnu 0.36.1", "windows_x86_64_msvc 0.36.1", ] [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc 0.42.1", "windows_i686_gnu 0.42.1", "windows_i686_msvc 0.42.1", "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc 0.42.1", "windows_i686_gnu 0.42.1", "windows_i686_msvc 0.42.1", "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" file-rotate-0.8.0/Cargo.toml0000644000000026230000000000100112550ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "file-rotate" version = "0.8.0" authors = [ "Kevin Robert Stravers ", "Erlend Langseth <3rlendhl@gmail.com>", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Log rotation for files" homepage = "https://github.com/kstrafe/file-rotate" readme = "README.md" keywords = [ "log", "rotate", "logrotate", ] license = "MIT" repository = "https://github.com/kstrafe/file-rotate" [lib] name = "file_rotate" path = "src/lib.rs" [[example]] name = "rotate_by_date" path = "examples/rotate_by_date.rs" [dependencies.chrono] version = "0.4.20" features = ["clock"] default-features = false [dependencies.flate2] version = "1.0" [dev-dependencies.filetime] version = "0.2" [dev-dependencies.quickcheck] version = "0.9.2" [dev-dependencies.quickcheck_macros] version = "0.9.1" [dev-dependencies.tempfile] version = "3" file-rotate-0.8.0/Cargo.toml.orig000064400000000000000000000011011046102023000147240ustar 00000000000000[package] name = "file-rotate" version = "0.8.0" authors = ["Kevin Robert Stravers ", "Erlend Langseth <3rlendhl@gmail.com>"] edition = "2018" description = "Log rotation for files" homepage = "https://github.com/kstrafe/file-rotate" repository = "https://github.com/kstrafe/file-rotate" keywords= ["log", "rotate", "logrotate"] license = "MIT" [dependencies] chrono = { version = "0.4.20", default-features = false, features = ["clock"] } flate2 = "1.0" [dev-dependencies] filetime = "0.2" quickcheck = "0.9.2" quickcheck_macros = "0.9.1" tempfile = "3" file-rotate-0.8.0/LICENSE000064400000000000000000000020561046102023000130540ustar 00000000000000MIT License Copyright (c) 2020 BourgondAries Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. file-rotate-0.8.0/README.md000064400000000000000000000046071046102023000133320ustar 00000000000000# file-rotate Rotate files with configurable suffix. Look to the [docs](https://docs.rs/file-rotate/latest/file_rotate/index.html) for explanatory examples of all features, like: * Using count or timestamp as suffix * Age-based deletion of log files * Optional compression * Getting a list of log files Limitations / known issues: * `file-rotate` assumes that no other process or user moves files around in the logging directory, but we want to find a way to [support this](https://github.com/BourgondAries/file-rotate/issues/17) Following are some supplementary examples to get started. ## Basic example ```rust use file_rotate::{FileRotate, ContentLimit, compression::Compression, suffix::AppendCount}; use std::{fs, io::Write, path::PathBuf}; fn main() { let mut log = FileRotate::new("logs/log", AppendCount::new(2), ContentLimit::Lines(3), Compression::None, None); // Write a bunch of lines writeln!(log, "Line 1: Hello World!"); for idx in 2..=10 { writeln!(log, "Line {}", idx); } } ``` ``` $ ls logs log log.1 log.2 $ cat log.2 log.1 log Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 Line 10 ``` ## Example with timestamp suffixes ```rust let mut log = FileRotate::new( "logs/log", AppendTimestamp::default(FileLimit::MaxFiles(3)), ContentLimit::Lines(3), Compression::None, None, ); // Write a bunch of lines writeln!(log, "Line 1: Hello World!"); for idx in 2..=10 { std::thread::sleep(std::time::Duration::from_millis(200)); writeln!(log, "Line {}", idx); } ``` ``` $ ls logs log log.20210825T151133.1 log.20210825T151133 log.20210825T151134 $ cat logs/* Line 10 Line 1: Hello World! Line 2 Line 3 Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 ``` The timestamp format (including the extra trailing `.N`) works by default so that the lexical ordering of filenames equals the chronological ordering. So it almost works perfectly with `cat logs/*`, except that `log` is smaller (lexically "older") than all the rest. This can of course be fixed with a more complex script to assemble the logs. ## License This project is licensed under the [MIT license]. [MIT license]: https://github.com/BourgondAries/file-rotate/blob/master/LICENSE ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in file-rotate by you, shall be licensed as MIT, without any additional terms or conditions. file-rotate-0.8.0/examples/rotate_by_date.rs000064400000000000000000000012561046102023000172210ustar 00000000000000use file_rotate::{ compression::Compression, suffix::{AppendTimestamp, DateFrom, FileLimit}, ContentLimit, FileRotate, TimeFrequency, }; use std::io::Write; fn main() { let mut log = FileRotate::new( "logs/log", AppendTimestamp::with_format("%Y-%m-%d", FileLimit::MaxFiles(7), DateFrom::DateYesterday), ContentLimit::Time(TimeFrequency::Daily), Compression::None, None, ); // Write a bunch of lines writeln!(log, "Line 1: Hello World!").expect("write log"); for idx in 2..=10 { std::thread::sleep(std::time::Duration::from_millis(500)); writeln!(log, "Line {}", idx).expect("write log"); } } file-rotate-0.8.0/src/compression.rs000064400000000000000000000017341046102023000155470ustar 00000000000000//! Compression - configuration and implementation use flate2::write::GzEncoder; use std::{ fs::{self, File, OpenOptions}, io, path::{Path, PathBuf}, }; /// Compression mode - when to compress files. #[derive(Debug, Clone)] pub enum Compression { /// No compression None, /// Look for files to compress when rotating. /// First argument: How many files to keep uncompressed (excluding the original file) OnRotate(usize), } pub(crate) fn compress(path: &Path) -> io::Result<()> { let dest_path = PathBuf::from(format!("{}.gz", path.display())); let mut src_file = File::open(path)?; let dest_file = OpenOptions::new() .write(true) .create(true) .append(false) .open(&dest_path)?; assert!(path.exists()); assert!(dest_path.exists()); let mut encoder = GzEncoder::new(dest_file, flate2::Compression::default()); io::copy(&mut src_file, &mut encoder)?; fs::remove_file(path)?; Ok(()) } file-rotate-0.8.0/src/lib.rs000064400000000000000000000631231046102023000137540ustar 00000000000000//! Write output to a file and rotate the files when limits have been exceeded. //! //! Defines a simple [std::io::Write] object that you can plug into your writers as middleware. //! //! # Content limit # //! //! [ContentLimit] specifies at what point a log file has to be rotated. //! //! ## Rotating by Lines ## //! //! We can rotate log files with the amount of lines as a limit, by using [ContentLimit::Lines]. //! //! ``` //! use file_rotate::{FileRotate, ContentLimit, suffix::AppendCount, compression::Compression}; //! use std::{fs, io::Write}; //! //! // Create a new log writer. The first argument is anything resembling a path. The //! // basename is used for naming the log files. //! // //! // Here we choose to limit logs by 10 lines, and have at most 2 rotated log files. This //! // makes the total amount of log files 3, since the original file is present as well. //! //! # let directory = tempfile::TempDir::new().unwrap(); //! # let directory = directory.path(); //! let log_path = directory.join("my-log-file"); //! //! let mut log = FileRotate::new( //! log_path.clone(), //! AppendCount::new(2), //! ContentLimit::Lines(3), //! Compression::None, //! None, //! ); //! //! // Write a bunch of lines //! writeln!(log, "Line 1: Hello World!"); //! for idx in 2..11 { //! writeln!(log, "Line {}", idx); //! } //! //! assert_eq!("Line 10\n", fs::read_to_string(&log_path).unwrap()); //! //! assert_eq!("Line 4\nLine 5\nLine 6\n", fs::read_to_string(&directory.join("my-log-file.2")).unwrap()); //! assert_eq!("Line 7\nLine 8\nLine 9\n", fs::read_to_string(&directory.join("my-log-file.1")).unwrap()); //! ``` //! //! ## Rotating by Bytes ## //! //! Another method of rotation is by bytes instead of lines, with [ContentLimit::Bytes]. //! //! ``` //! use file_rotate::{FileRotate, ContentLimit, suffix::AppendCount, compression::Compression}; //! use std::{fs, io::Write}; //! //! # let directory = tempfile::TempDir::new().unwrap(); //! # let directory = directory.path(); //! let log_path = directory.join("my-log-file"); //! //! let mut log = FileRotate::new( //! "target/my-log-directory-bytes/my-log-file", //! AppendCount::new(2), //! ContentLimit::Bytes(5), //! Compression::None, //! None, //! ); //! //! writeln!(log, "Test file"); //! //! assert_eq!("Test ", fs::read_to_string(&log.log_paths()[0]).unwrap()); //! assert_eq!("file\n", fs::read_to_string("target/my-log-directory-bytes/my-log-file").unwrap()); //! //! fs::remove_dir_all("target/my-log-directory-bytes"); //! ``` //! //! # Rotation Method # //! //! Two rotation methods are provided, but any behaviour can be implemented with the [SuffixScheme] //! trait. //! //! ## Basic count ## //! //! With [AppendCount], when the limit is reached in the main log file, the file is moved with //! suffix `.1`, and subsequently numbered files are moved in a cascade. //! //! Here's an example with 1 byte limits: //! //! ``` //! use file_rotate::{FileRotate, ContentLimit, suffix::AppendCount, compression::Compression}; //! use std::{fs, io::Write}; //! //! # let directory = tempfile::TempDir::new().unwrap(); //! # let directory = directory.path(); //! let log_path = directory.join("my-log-file"); //! //! let mut log = FileRotate::new( //! log_path.clone(), //! AppendCount::new(3), //! ContentLimit::Bytes(1), //! Compression::None, //! None, //! ); //! //! write!(log, "A"); //! assert_eq!("A", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "B"); //! assert_eq!("A", fs::read_to_string(directory.join("my-log-file.1")).unwrap()); //! assert_eq!("B", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "C"); //! assert_eq!("A", fs::read_to_string(directory.join("my-log-file.2")).unwrap()); //! assert_eq!("B", fs::read_to_string(directory.join("my-log-file.1")).unwrap()); //! assert_eq!("C", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "D"); //! assert_eq!("A", fs::read_to_string(directory.join("my-log-file.3")).unwrap()); //! assert_eq!("B", fs::read_to_string(directory.join("my-log-file.2")).unwrap()); //! assert_eq!("C", fs::read_to_string(directory.join("my-log-file.1")).unwrap()); //! assert_eq!("D", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "E"); //! assert_eq!("B", fs::read_to_string(directory.join("my-log-file.3")).unwrap()); //! assert_eq!("C", fs::read_to_string(directory.join("my-log-file.2")).unwrap()); //! assert_eq!("D", fs::read_to_string(directory.join("my-log-file.1")).unwrap()); //! assert_eq!("E", fs::read_to_string(&log_path).unwrap()); //! ``` //! //! ## Timestamp suffix ## //! //! With [AppendTimestamp], when the limit is reached in the main log file, the file is moved with //! suffix equal to the current timestamp (with the specified or a default format). If the //! destination file name already exists, `.1` (and up) is appended. //! //! Note that this works somewhat different to `AppendCount` because of lexical ordering concerns: //! Higher numbers mean more recent logs, whereas `AppendCount` works in the opposite way. //! The reason for this is to keep the lexical ordering of log names consistent: Higher lexical value //! means more recent. //! This is of course all assuming that the format start with the year (or most significant //! component). //! //! With this suffix scheme, you can also decide whether to delete old files based on the age of //! their timestamp ([FileLimit::Age]), or just maximum number of files ([FileLimit::MaxFiles]). //! //! ``` //! use file_rotate::{FileRotate, ContentLimit, suffix::{AppendTimestamp, FileLimit}, //! compression::Compression}; //! use std::{fs, io::Write}; //! //! # let directory = tempfile::TempDir::new().unwrap(); //! # let directory = directory.path(); //! let log_path = directory.join("my-log-file"); //! //! let mut log = FileRotate::new( //! log_path.clone(), //! AppendTimestamp::default(FileLimit::MaxFiles(2)), //! ContentLimit::Bytes(1), //! Compression::None, //! None, //! ); //! //! write!(log, "A"); //! assert_eq!("A", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "B"); //! assert_eq!("A", fs::read_to_string(&log.log_paths()[0]).unwrap()); //! assert_eq!("B", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "C"); //! assert_eq!("A", fs::read_to_string(&log.log_paths()[0]).unwrap()); //! assert_eq!("B", fs::read_to_string(&log.log_paths()[1]).unwrap()); //! assert_eq!("C", fs::read_to_string(&log_path).unwrap()); //! //! write!(log, "D"); //! assert_eq!("B", fs::read_to_string(&log.log_paths()[0]).unwrap()); //! assert_eq!("C", fs::read_to_string(&log.log_paths()[1]).unwrap()); //! assert_eq!("D", fs::read_to_string(&log_path).unwrap()); //! ``` //! //! If you use timestamps as suffix, you can also configure files to be removed as they reach a //! certain age. For example: //! ```rust //! use file_rotate::suffix::{AppendTimestamp, FileLimit}; //! AppendTimestamp::default(FileLimit::Age(chrono::Duration::weeks(1))); //! ``` //! //! # Compression # //! //! Select a [Compression] mode to make the file rotater compress old files using flate2. //! Compressed files get an additional suffix `.gz` after the main suffix. //! //! ## Compression example ## //! If we run this: //! //! ```ignore //! use file_rotate::{compression::*, suffix::*, *}; //! use std::io::Write; //! //! let mut log = FileRotate::new( //! "./log", //! AppendTimestamp::default(FileLimit::MaxFiles(4)), //! ContentLimit::Bytes(1), //! Compression::OnRotate(2), //! None, //! ); //! //! for i in 0..6 { //! write!(log, "{}", i).unwrap(); //! std::thread::sleep(std::time::Duration::from_secs(1)); //! } //! ``` //! The following files will be created: //! ```ignore //! log log.20220112T112415.gz log.20220112T112416.gz log.20220112T112417 log.20220112T112418 //! ``` //! And we can assemble all the available log data with: //! ```ignore //! $ gunzip -c log.20220112T112415.gz ; gunzip -c log.20220112T112416.gz ; cat log.20220112T112417 log.20220112T112418 log //! 12345 //! ``` //! //! //! ## Get structured list of log files ## //! //! We can programmatically get the list of log files. //! The following code scans the current directory and recognizes log files based on their file name: //! //! ``` //! # use file_rotate::{suffix::*, *}; //! # use std::path::Path; //! println!( //! "{:#?}", //! AppendTimestamp::default(FileLimit::MaxFiles(4)).scan_suffixes(Path::new("./log")) //! ); //! ``` //! //! [SuffixScheme::scan_suffixes] also takes into account the possibility of the extra `.gz` suffix, and //! interprets it correctly as compression. The output: //! //! ```ignore //! { //! SuffixInfo { //! suffix: TimestampSuffix { //! timestamp: "20220112T112418", //! number: None, //! }, //! compressed: false, //! }, //! SuffixInfo { //! suffix: TimestampSuffix { //! timestamp: "20220112T112417", //! number: None, //! }, //! compressed: false, //! }, //! SuffixInfo { //! suffix: TimestampSuffix { //! timestamp: "20220112T112416", //! number: None, //! }, //! compressed: true, //! }, //! SuffixInfo { //! suffix: TimestampSuffix { //! timestamp: "20220112T112415", //! number: None, //! }, //! compressed: true, //! }, //! } //! ``` //! This information can be used by for example a program to assemble log history. //! //! # Filesystem Errors # //! //! If the directory containing the logs is deleted or somehow made inaccessible then the rotator //! will simply continue operating without fault. When a rotation occurs, it attempts to open a //! file in the directory. If it can, it will just continue logging. If it can't then the written //! data is sent to the void. #![deny( missing_docs, trivial_casts, trivial_numeric_casts, unsafe_code, unused_import_braces, unused_qualifications )] use chrono::prelude::*; use compression::*; use std::io::{BufRead, BufReader}; use std::{ cmp::Ordering, collections::BTreeSet, fs::{self, File, OpenOptions}, io::{self, Write}, path::{Path, PathBuf}, }; use suffix::*; pub mod compression; pub mod suffix; #[cfg(test)] mod tests; // --- /// At which frequency to rotate the file. #[derive(Clone, Copy, Debug)] pub enum TimeFrequency { /// Rotate every hour. Hourly, /// Rotate one time a day. Daily, /// Rotate ones a week. Weekly, /// Rotate every month. Monthly, /// Rotate yearly. Yearly, } /// When to move files: Condition on which a file is rotated. #[derive(Clone, Debug)] pub enum ContentLimit { /// Cut the log at the exact size in bytes. Bytes(usize), /// Cut the log file at line breaks. Lines(usize), /// Cut the log at time interval. Time(TimeFrequency), /// Cut the log file after surpassing size in bytes (but having written a complete buffer from a write call.) BytesSurpassed(usize), /// Don't do any rotation automatically None, } /// Used mostly internally. Info about suffix + compressed state. #[derive(Clone, Debug, Eq)] pub struct SuffixInfo { /// Suffix pub suffix: Repr, /// Whether there is a `.gz` suffix after the suffix pub compressed: bool, } impl PartialEq for SuffixInfo { fn eq(&self, other: &Self) -> bool { self.suffix == other.suffix } } impl SuffixInfo { /// Append this suffix (and eventual `.gz`) to a path pub fn to_path(&self, basepath: &Path) -> PathBuf { let path = self.suffix.to_path(basepath); if self.compressed { PathBuf::from(format!("{}.gz", path.display())) } else { path } } } impl Ord for SuffixInfo { fn cmp(&self, other: &Self) -> Ordering { self.suffix.cmp(&other.suffix) } } impl PartialOrd for SuffixInfo { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } /// The main writer used for rotating logs. #[derive(Debug)] pub struct FileRotate { basepath: PathBuf, file: Option, modified: Option>, content_limit: ContentLimit, count: usize, compression: Compression, suffix_scheme: S, /// The bool is whether or not there's a .gz suffix to the filename suffixes: BTreeSet>, open_options: Option, } impl FileRotate { /// Create a new [FileRotate]. /// /// The basename of the `path` is used to create new log files by appending an extension of the /// form `.N`, where N is `0..=max_files`. /// /// `content_limit` specifies the limits for rotating a file. /// /// `open_options`: If provided, you must set `.read(true).create(true).append(true)`! /// /// # Panics /// /// Panics if `bytes == 0` or `lines == 0`. pub fn new>( path: P, suffix_scheme: S, content_limit: ContentLimit, compression: Compression, open_options: Option, ) -> Self { match content_limit { ContentLimit::Bytes(bytes) => { assert!(bytes > 0); } ContentLimit::Lines(lines) => { assert!(lines > 0); } ContentLimit::Time(_) => {} ContentLimit::BytesSurpassed(bytes) => { assert!(bytes > 0); } ContentLimit::None => {} }; let basepath = path.as_ref().to_path_buf(); fs::create_dir_all(basepath.parent().unwrap()).expect("create dir"); let mut s = Self { file: None, modified: None, basepath, content_limit, count: 0, compression, suffixes: BTreeSet::new(), suffix_scheme, open_options, }; s.ensure_log_directory_exists(); s.scan_suffixes(); s } fn ensure_log_directory_exists(&mut self) { let path = self.basepath.parent().unwrap(); if !path.exists() { let _ = fs::create_dir_all(path).expect("create dir"); self.scan_suffixes(); } if !self.basepath.exists() || self.file.is_none() { // Open or create the file self.open_file(); match self.file { None => self.count = 0, Some(ref mut file) => { match self.content_limit { ContentLimit::Bytes(_) | ContentLimit::BytesSurpassed(_) => { // Update byte `count` if let Ok(metadata) = file.metadata() { self.count = metadata.len() as usize; } else { self.count = 0; } } ContentLimit::Lines(_) => { self.count = BufReader::new(file).lines().count(); } ContentLimit::Time(_) => { self.modified = mtime(file); } ContentLimit::None => {} } } } } } fn open_file(&mut self) { let open_options = self.open_options.clone().unwrap_or_else(|| { let mut o = OpenOptions::new(); o.read(true).create(true).append(true); o }); self.file = open_options.open(&self.basepath).ok(); } fn scan_suffixes(&mut self) { self.suffixes = self.suffix_scheme.scan_suffixes(&self.basepath); } /// Get paths of rotated log files (excluding the original/current log file), ordered from /// oldest to most recent pub fn log_paths(&mut self) -> Vec { self.suffixes .iter() .rev() .map(|suffix| suffix.to_path(&self.basepath)) .collect::>() } /// Recursive function that keeps moving files if there's any file name collision. /// If `suffix` is `None`, it moves from basepath to next suffix given by the SuffixScheme /// Assumption: Any collision in file name is due to an old log file. /// /// Returns the suffix of the new file (the last suffix after possible cascade of renames). fn move_file_with_suffix( &mut self, old_suffix_info: Option>, ) -> io::Result> { // NOTE: this newest_suffix is there only because AppendTimestamp specifically needs // it. Otherwise it might not be necessary to provide this to `rotate_file`. We could also // have passed the internal BTreeMap itself, but it would require to make SuffixInfo `pub`. let newest_suffix = self.suffixes.iter().next().map(|info| &info.suffix); let new_suffix = self.suffix_scheme.rotate_file( &self.basepath, newest_suffix, &old_suffix_info.clone().map(|i| i.suffix), )?; // The destination file/path eventual .gz suffix must match the source path let new_suffix_info = SuffixInfo { suffix: new_suffix, compressed: old_suffix_info .as_ref() .map(|x| x.compressed) .unwrap_or(false), }; let new_path = new_suffix_info.to_path(&self.basepath); // Whatever exists that would block a move to the new suffix let existing_suffix_info = self.suffixes.get(&new_suffix_info).cloned(); // Move destination file out of the way if it exists let newly_created_suffix = if let Some(existing_suffix_info) = existing_suffix_info { // We might move files in a way that the destination path doesn't equal the path that // was replaced. Due to possible `.gz`, a "conflicting" file doesn't mean that paths // are equal. self.suffixes.replace(new_suffix_info); // Recurse to move conflicting file. self.move_file_with_suffix(Some(existing_suffix_info))? } else { new_suffix_info }; let old_path = match old_suffix_info { Some(suffix) => suffix.to_path(&self.basepath), None => self.basepath.clone(), }; // Do the move assert!(old_path.exists()); assert!(!new_path.exists()); fs::rename(old_path, new_path)?; Ok(newly_created_suffix) } /// Trigger a log rotation manually. This is mostly intended for use with `ContentLimit::None` /// but will work with all content limits. pub fn rotate(&mut self) -> io::Result<()> { self.ensure_log_directory_exists(); let _ = self.file.take(); // This function will always create a new file. Returns suffix of that file let new_suffix_info = self.move_file_with_suffix(None)?; self.suffixes.insert(new_suffix_info); self.open_file(); self.count = 0; self.handle_old_files()?; Ok(()) } fn handle_old_files(&mut self) -> io::Result<()> { // Find the youngest suffix that is too old, and then remove all suffixes that are older or // equally old: let mut youngest_old = None; // Start from oldest suffix, stop when we find a suffix that is not too old let mut result = Ok(()); for (i, suffix) in self.suffixes.iter().enumerate().rev() { if self.suffix_scheme.too_old(&suffix.suffix, i) { result = result.and(fs::remove_file(suffix.to_path(&self.basepath))); youngest_old = Some((*suffix).clone()); } else { break; } } if let Some(youngest_old) = youngest_old { // Removes all the too old let _ = self.suffixes.split_off(&youngest_old); } // Compression if let Compression::OnRotate(max_file_n) = self.compression { let n = (self.suffixes.len() as i32 - max_file_n as i32).max(0) as usize; // The oldest N files should be compressed let suffixes_to_compress = self .suffixes .iter() .rev() .take(n) .filter(|info| !info.compressed) .cloned() .collect::>(); for info in suffixes_to_compress { // Do the compression let path = info.suffix.to_path(&self.basepath); compress(&path)?; self.suffixes.replace(SuffixInfo { compressed: true, ..info }); } } result } } impl Write for FileRotate { fn write(&mut self, mut buf: &[u8]) -> io::Result { let written = buf.len(); match self.content_limit { ContentLimit::Bytes(bytes) => { while self.count + buf.len() > bytes { let bytes_left = bytes.saturating_sub(self.count); if let Some(ref mut file) = self.file { file.write_all(&buf[..bytes_left])?; } self.rotate()?; buf = &buf[bytes_left..]; } self.count += buf.len(); if let Some(ref mut file) = self.file { file.write_all(buf)?; } } ContentLimit::Time(time) => { let local: DateTime = now(); if let Some(modified) = self.modified { match time { TimeFrequency::Hourly => { if local.hour() != modified.hour() || local.day() != modified.day() || local.month() != modified.month() || local.year() != modified.year() { self.rotate()?; } } TimeFrequency::Daily => { if local.date() > modified.date() { self.rotate()?; } } TimeFrequency::Weekly => { if local.iso_week().week() != modified.iso_week().week() || local.year() > modified.year() { self.rotate()?; } } TimeFrequency::Monthly => { if local.month() != modified.month() || local.year() != modified.year() { self.rotate()?; } } TimeFrequency::Yearly => { if local.year() > modified.year() { self.rotate()?; } } } } if let Some(ref mut file) = self.file { file.write_all(buf)?; self.modified = Some(local); } } ContentLimit::Lines(lines) => { while let Some((idx, _)) = buf.iter().enumerate().find(|(_, byte)| *byte == &b'\n') { if let Some(ref mut file) = self.file { file.write_all(&buf[..idx + 1])?; } self.count += 1; buf = &buf[idx + 1..]; if self.count >= lines { self.rotate()?; } } if let Some(ref mut file) = self.file { file.write_all(buf)?; } } ContentLimit::BytesSurpassed(bytes) => { if self.count > bytes { self.rotate()? } if let Some(ref mut file) = self.file { file.write_all(buf)?; } self.count += buf.len(); } ContentLimit::None => { if let Some(ref mut file) = self.file { file.write_all(buf)?; } } } Ok(written) } fn flush(&mut self) -> io::Result<()> { self.file .as_mut() .map(|file| file.flush()) .unwrap_or(Ok(())) } } /// Get modification time, in non test case. #[cfg(not(test))] fn mtime(file: &File) -> Option> { if let Ok(time) = file.metadata().and_then(|metadata| metadata.modified()) { return Some(time.into()); } None } /// Get modification time, in test case. #[cfg(test)] fn mtime(_: &File) -> Option> { Some(now()) } /// Get system time, in non test case. #[cfg(not(test))] fn now() -> DateTime { Local::now() } /// Get mocked system time, in test case. #[cfg(test)] pub mod mock_time { use super::*; use std::cell::RefCell; thread_local! { static MOCK_TIME: RefCell>> = RefCell::new(None); } /// Get current _mocked_ time pub fn now() -> DateTime { MOCK_TIME.with(|cell| cell.borrow().as_ref().cloned().unwrap_or_else(Local::now)) } /// Set mocked time pub fn set_mock_time(time: DateTime) { MOCK_TIME.with(|cell| *cell.borrow_mut() = Some(time)); } } #[cfg(test)] pub use mock_time::now; file-rotate-0.8.0/src/suffix.rs000064400000000000000000000364231046102023000145150ustar 00000000000000//! Suffix schemes determine the suffix of rotated files //! //! This behaviour is fully extensible through the [SuffixScheme] trait, and two behaviours are //! provided: [AppendCount] and [AppendTimestamp] //! use super::now; use crate::SuffixInfo; use chrono::{format::ParseErrorKind, offset::Local, Duration, NaiveDateTime}; use std::{ cmp::Ordering, collections::BTreeSet, io, path::{Path, PathBuf}, }; /// Representation of a suffix /// `Ord + PartialOrd`: sort by age of the suffix. Most recent first (smallest). pub trait Representation: Ord + ToString + Eq + Clone + std::fmt::Debug { /// Create path fn to_path(&self, basepath: &Path) -> PathBuf { PathBuf::from(format!("{}.{}", basepath.display(), self.to_string())) } } /// How to move files: How to rename, when to delete. pub trait SuffixScheme { /// The representation of suffixes that this suffix scheme uses. /// E.g. if the suffix is a number, you can use `usize`. type Repr: Representation; /// `file-rotate` calls this function when the file at `suffix` needs to be rotated, and moves the log file /// accordingly. Thus, this function should not move any files itself. /// /// If `suffix` is `None`, it means it's the main log file (with path equal to just `basepath`) /// that is being rotated. /// /// Returns the target suffix that the log file should be moved to. /// If the target suffix already exists, `rotate_file` is called again with `suffix` set to the /// target suffix. Thus it cascades files by default, and if this is not desired, it's up to /// `rotate_file` to return a suffix that does not already exist on disk. /// /// `newest_suffix` is provided just in case it's useful (depending on the particular suffix scheme, it's not always useful) fn rotate_file( &mut self, basepath: &Path, newest_suffix: Option<&Self::Repr>, suffix: &Option, ) -> io::Result; /// Parse suffix from string. fn parse(&self, suffix: &str) -> Option; /// Whether either the suffix or the chronological file number indicates that the file is old /// and should be deleted, depending of course on the file limit. /// `file_number` starts at 0 for the most recent suffix. fn too_old(&self, suffix: &Self::Repr, file_number: usize) -> bool; /// Find all files in the basepath.parent() directory that has path equal to basepath + a valid /// suffix. Return sorted collection - sorted from most recent to oldest based on the /// [Ord] implementation of `Self::Repr`. fn scan_suffixes(&self, basepath: &Path) -> BTreeSet> { let mut suffixes = BTreeSet::new(); let filename_prefix = basepath .file_name() .expect("basepath.file_name()") .to_string_lossy(); // We need the parent directory of the given basepath, but this should also work when the path // only has one segment. Thus we prepend the current working dir if the path is relative: let basepath = if basepath.is_relative() { let mut path = std::env::current_dir().unwrap(); path.push(basepath); path } else { basepath.to_path_buf() }; let parent = basepath.parent().unwrap(); let filenames = std::fs::read_dir(parent) .unwrap() .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().is_file()) .map(|entry| entry.file_name()); for filename in filenames { let filename = filename.to_string_lossy(); if !filename.starts_with(&*filename_prefix) { continue; } let (filename, compressed) = prepare_filename(&*filename); let suffix_str = filename.strip_prefix(&format!("{}.", filename_prefix)); if let Some(suffix) = suffix_str.and_then(|s| self.parse(s)) { suffixes.insert(SuffixInfo { suffix, compressed }); } } suffixes } } fn prepare_filename(path: &str) -> (&str, bool) { path.strip_suffix(".gz") .map(|x| (x, true)) .unwrap_or((path, false)) } /// Append a number when rotating the file. /// The greater the number, the older. The oldest files are deleted. pub struct AppendCount { max_files: usize, } impl AppendCount { /// New suffix scheme, deleting files when the number of rotated files (i.e. excluding the main /// file) exceeds `max_files`. /// For example, if `max_files` is 3, then the files `log`, `log.1`, `log.2`, `log.3` may exist /// but not `log.4`. In other words, `max_files` determines the largest possible suffix number. pub fn new(max_files: usize) -> Self { Self { max_files } } } impl Representation for usize {} impl SuffixScheme for AppendCount { type Repr = usize; fn rotate_file( &mut self, _basepath: &Path, _: Option<&usize>, suffix: &Option, ) -> io::Result { Ok(match suffix { Some(suffix) => suffix + 1, None => 1, }) } fn parse(&self, suffix: &str) -> Option { suffix.parse::().ok() } fn too_old(&self, _suffix: &usize, file_number: usize) -> bool { file_number >= self.max_files } } /// Add timestamp from: pub enum DateFrom { /// Date yesterday, to represent the timestamps within the log file. DateYesterday, /// Date from hour ago, useful with rotate hourly. DateHourAgo, /// Date from now. Now, } /// Append current timestamp as suffix when rotating files. /// If the timestamp already exists, an additional number is appended. /// /// Current limitations: /// - Neither `format` nor the base filename can include the character `"."`. /// - The `format` should ensure that the lexical and chronological orderings are the same pub struct AppendTimestamp { /// The format of the timestamp suffix pub format: &'static str, /// The file limit, e.g. when to delete an old file - by age (given by suffix) or by number of files pub file_limit: FileLimit, /// Add timestamp from DateFrom pub date_from: DateFrom, } impl AppendTimestamp { /// With format `"%Y%m%dT%H%M%S"` pub fn default(file_limit: FileLimit) -> Self { Self { format: "%Y%m%dT%H%M%S", file_limit, date_from: DateFrom::Now, } } /// Create new AppendTimestamp suffix scheme pub fn with_format(format: &'static str, file_limit: FileLimit, date_from: DateFrom) -> Self { Self { format, file_limit, date_from, } } } /// Structured representation of the suffixes of AppendTimestamp. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TimestampSuffix { /// The timestamp pub timestamp: String, /// Optional number suffix if two timestamp suffixes are the same pub number: Option, } impl Representation for TimestampSuffix {} impl Ord for TimestampSuffix { fn cmp(&self, other: &Self) -> Ordering { // Most recent = smallest (opposite as the timestamp Ord) // Smallest = most recent. Thus, biggest timestamp first. And then biggest number match other.timestamp.cmp(&self.timestamp) { Ordering::Equal => other.number.cmp(&self.number), unequal => unequal, } } } impl PartialOrd for TimestampSuffix { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl std::fmt::Display for TimestampSuffix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self.number { Some(n) => write!(f, "{}.{}", self.timestamp, n), None => write!(f, "{}", self.timestamp), } } } impl SuffixScheme for AppendTimestamp { type Repr = TimestampSuffix; fn rotate_file( &mut self, _basepath: &Path, newest_suffix: Option<&TimestampSuffix>, suffix: &Option, ) -> io::Result { assert!(suffix.is_none()); if suffix.is_none() { let mut now = now(); match self.date_from { DateFrom::DateYesterday => { now = now - Duration::days(1); } DateFrom::DateHourAgo => { now = now - Duration::hours(1); } _ => {} }; let fmt_now = now.format(self.format).to_string(); let number = if let Some(newest_suffix) = newest_suffix { if newest_suffix.timestamp == fmt_now { Some(newest_suffix.number.unwrap_or(0) + 1) } else { None } } else { None }; Ok(TimestampSuffix { timestamp: fmt_now, number, }) } else { // This rotation scheme dictates that only the main log file should ever be renamed. // In debug build the above assert will catch this. Err(io::Error::new( io::ErrorKind::InvalidData, "Critical error in file-rotate algorithm", )) } } fn parse(&self, suffix: &str) -> Option { let (timestamp_str, n) = if let Some(dot) = suffix.find('.') { if let Ok(n) = suffix[(dot + 1)..].parse::() { (&suffix[..dot], Some(n)) } else { return None; } } else { (suffix, None) }; let success = match NaiveDateTime::parse_from_str(timestamp_str, self.format) { Ok(_) => true, Err(e) => e.kind() == ParseErrorKind::NotEnough, }; if success { Some(TimestampSuffix { timestamp: timestamp_str.to_string(), number: n, }) } else { None } } fn too_old(&self, suffix: &TimestampSuffix, file_number: usize) -> bool { match self.file_limit { FileLimit::MaxFiles(max_files) => file_number >= max_files, FileLimit::Age(age) => { let old_timestamp = (Local::now() - age).format(self.format).to_string(); suffix.timestamp < old_timestamp } FileLimit::Unlimited => false, } } } /// How to determine whether a file should be deleted, in the case of [AppendTimestamp]. pub enum FileLimit { /// Delete the oldest files if number of files is too high MaxFiles(usize), /// Delete files whose age exceeds the `Duration` - age is determined by the suffix of the file Age(Duration), /// Never delete files Unlimited, } #[cfg(test)] mod test { use super::*; use std::fs::File; use tempfile::TempDir; #[test] fn timestamp_ordering() { assert!( TimestampSuffix { timestamp: "2021".to_string(), number: None } < TimestampSuffix { timestamp: "2020".to_string(), number: None } ); assert!( TimestampSuffix { timestamp: "2021".to_string(), number: Some(1) } < TimestampSuffix { timestamp: "2021".to_string(), number: None } ); } #[test] fn timestamp_scan_suffixes_base_paths() { let working_dir = TempDir::new().unwrap(); let working_dir = working_dir.path().join("dir"); let suffix_scheme = AppendTimestamp::default(FileLimit::Age(Duration::weeks(1))); // Test `scan_suffixes` for different possible paths given to it // (it used to have a bug taking e.g. "log".parent() --> panic) for relative_path in ["logs/log", "./log", "log", "../log", "../logs/log"] { std::fs::create_dir_all(&working_dir).unwrap(); println!("Testing relative path: {}", relative_path); let relative_path = Path::new(relative_path); let log_file = working_dir.join(relative_path); let log_dir = log_file.parent().unwrap(); // Ensure all directories needed exist std::fs::create_dir_all(log_dir).unwrap(); // We cd into working_dir std::env::set_current_dir(&working_dir).unwrap(); // Need to create the log file in order to canonicalize it and then get the parent File::create(working_dir.join(&relative_path)).unwrap(); let canonicalized = relative_path.canonicalize().unwrap(); let relative_dir = canonicalized.parent().unwrap(); File::create(relative_dir.join("log.20210911T121830")).unwrap(); File::create(relative_dir.join("log.20210911T121831.gz")).unwrap(); let paths = suffix_scheme.scan_suffixes(relative_path); assert_eq!(paths.len(), 2); // Reset CWD: necessary on Windows only - otherwise we get the error: // "The process cannot access the file because it is being used by another process." // (code 32) std::env::set_current_dir("/").unwrap(); // Cleanup std::fs::remove_dir_all(&working_dir).unwrap(); } } #[test] fn timestamp_scan_suffixes_formats() { struct TestCase { format: &'static str, suffixes: &'static [&'static str], incorrect_suffixes: &'static [&'static str], } let cases = [ TestCase { format: "%Y%m%dT%H%M%S", suffixes: &["20220201T101010", "20220202T101010"], incorrect_suffixes: &["20220201T1010", "20220201T999999", "2022-02-02"], }, TestCase { format: "%Y-%m-%d", suffixes: &["2022-02-01", "2022-02-02"], incorrect_suffixes: &[ "abc", "2022-99-99", "2022-05", "2022", "20220202", "2022-02-02T112233", ], }, ]; for (i, case) in cases.iter().enumerate() { println!("Case {}", i); let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("file"); for suffix in case.suffixes.iter().chain(case.incorrect_suffixes) { File::create(dir.join(format!("file.{}", suffix))).unwrap(); } let scheme = AppendTimestamp::with_format( case.format, FileLimit::MaxFiles(1), DateFrom::DateYesterday, ); // Scan for suffixes let suffixes_set = scheme.scan_suffixes(&log_path); // Collect these suffixes, and the expected suffixes, into Vec, and sort let mut suffixes = suffixes_set .into_iter() .map(|x| x.suffix.to_string()) .collect::>(); suffixes.sort_unstable(); let mut expected_suffixes = case.suffixes.to_vec(); expected_suffixes.sort_unstable(); assert_eq!(suffixes, case.suffixes); println!("Passed\n"); } } } file-rotate-0.8.0/src/tests.rs000064400000000000000000000451731046102023000143550ustar 00000000000000use super::{suffix::*, *}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use tempfile::TempDir; // Just useful to debug why test doesn't succeed #[allow(dead_code)] fn list(dir: &Path) { let files = fs::read_dir(dir) .unwrap() .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().is_file()) .map(|entry| (entry.file_name(), fs::read_to_string(entry.path()))) .collect::>(); println!("Files on disk:"); for (name, content) in files { println!("{:?}: {:?}", name, content); } } #[test] fn timestamp_max_files_rotation() { let tmp_dir = TempDir::new().unwrap(); let log_path = tmp_dir.path().join("log"); let mut log = FileRotate::new( &log_path, AppendTimestamp::default(FileLimit::MaxFiles(4)), ContentLimit::Lines(2), Compression::None, None, ); // Write 9 lines // This should result in 5 files in total (4 rotated files). The main file will have one line. write!(log, "a\nb\nc\nd\ne\nf\ng\nh\ni\n").unwrap(); let log_paths = log.log_paths(); assert_eq!(log_paths.len(), 4); // Log names should be sorted. Low (old timestamp) to high (more recent timestamp) let mut log_paths_sorted = log_paths.clone(); log_paths_sorted.sort(); assert_eq!(log_paths, log_paths_sorted); assert_eq!("a\nb\n", fs::read_to_string(&log_paths[0]).unwrap()); assert_eq!("c\nd\n", fs::read_to_string(&log_paths[1]).unwrap()); assert_eq!("e\nf\n", fs::read_to_string(&log_paths[2]).unwrap()); assert_eq!("g\nh\n", fs::read_to_string(&log_paths[3]).unwrap()); assert_eq!("i\n", fs::read_to_string(&log_path).unwrap()); // Write 4 more lines write!(log, "j\nk\nl\nm\n").unwrap(); let log_paths = log.log_paths(); assert_eq!(log_paths.len(), 4); let mut log_paths_sorted = log_paths.clone(); log_paths_sorted.sort(); assert_eq!(log_paths, log_paths_sorted); list(tmp_dir.path()); assert_eq!("e\nf\n", fs::read_to_string(&log_paths[0]).unwrap()); assert_eq!("g\nh\n", fs::read_to_string(&log_paths[1]).unwrap()); assert_eq!("i\nj\n", fs::read_to_string(&log_paths[2]).unwrap()); assert_eq!("k\nl\n", fs::read_to_string(&log_paths[3]).unwrap()); assert_eq!("m\n", fs::read_to_string(&log_path).unwrap()); } #[test] fn timestamp_max_age_deletion() { // In order not to have to sleep, and keep it deterministic, let's already create the log files and see how FileRotate // cleans up the old ones. let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("log"); // One recent file: let recent_file = Local::now().format("log.%Y%m%dT%H%M%S").to_string(); File::create(dir.join(&recent_file)).unwrap(); // Two very old files: File::create(dir.join("log.20200825T151133")).unwrap(); File::create(dir.join("log.20200825T151133.1")).unwrap(); let mut log = FileRotate::new( &*log_path.to_string_lossy(), AppendTimestamp::default(FileLimit::Age(chrono::Duration::weeks(1))), ContentLimit::Lines(1), Compression::None, None, ); writeln!(log, "trigger\nat\nleast\none\nrotation").unwrap(); let mut filenames = fs::read_dir(dir) .unwrap() .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().is_file()) .map(|entry| entry.file_name().to_string_lossy().into_owned()) .collect::>(); filenames.sort(); assert!(filenames.contains(&"log".to_string())); assert!(filenames.contains(&recent_file)); assert!(!filenames.contains(&"log.20200825T151133".to_string())); assert!(!filenames.contains(&"log.20200825T151133.1".to_string())); } #[test] fn count_max_files_rotation() { let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let mut log = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(4), ContentLimit::Lines(2), Compression::None, None, ); // Write 9 lines // This should result in 5 files in total (4 rotated files). The main file will have one line. write!(log, "a\nb\nc\nd\ne\nf\ng\nh\ni\n").unwrap(); // 9 lines let log_paths = vec![ parent.join("log.4"), parent.join("log.3"), parent.join("log.2"), parent.join("log.1"), ]; assert_eq!(log_paths, log.log_paths()); assert_eq!("a\nb\n", fs::read_to_string(&log_paths[0]).unwrap()); assert_eq!("c\nd\n", fs::read_to_string(&log_paths[1]).unwrap()); assert_eq!("e\nf\n", fs::read_to_string(&log_paths[2]).unwrap()); assert_eq!("g\nh\n", fs::read_to_string(&log_paths[3]).unwrap()); assert_eq!("i\n", fs::read_to_string(&log_path).unwrap()); // Write 4 more lines write!(log, "j\nk\nl\nm\n").unwrap(); list(parent); assert_eq!(log_paths, log.log_paths()); assert_eq!("e\nf\n", fs::read_to_string(&log_paths[0]).unwrap()); assert_eq!("g\nh\n", fs::read_to_string(&log_paths[1]).unwrap()); assert_eq!("i\nj\n", fs::read_to_string(&log_paths[2]).unwrap()); assert_eq!("k\nl\n", fs::read_to_string(&log_paths[3]).unwrap()); assert_eq!("m\n", fs::read_to_string(&log_path).unwrap()); } #[test] fn rotate_to_deleted_directory() { let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let mut log = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(4), ContentLimit::Lines(1), Compression::None, None, ); write!(log, "a\nb\n").unwrap(); assert_eq!("", fs::read_to_string(&log_path).unwrap()); assert_eq!("a\n", fs::read_to_string(&log.log_paths()[0]).unwrap()); let _ = fs::remove_dir_all(parent); // Will fail to write `"c"` writeln!(log, "c").unwrap(); log.flush().unwrap(); // But the next `write` will succeed writeln!(log, "d").unwrap(); assert_eq!("", fs::read_to_string(&log_path).unwrap()); assert_eq!("d\n", fs::read_to_string(&log.log_paths()[1]).unwrap()); } #[test] fn write_complete_record_until_bytes_surpassed() { let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("log"); let mut log = FileRotate::new( &log_path, AppendTimestamp::default(FileLimit::MaxFiles(100)), ContentLimit::BytesSurpassed(1), Compression::None, None, ); write!(log, "0123456789").unwrap(); log.flush().unwrap(); assert!(log_path.exists()); // shouldn't exist yet - because entire record was written in one shot assert!(log.log_paths().is_empty()); // This should create the second file write!(log, "0123456789").unwrap(); log.flush().unwrap(); assert!(&log.log_paths()[0].exists()); } #[test] fn compression_on_rotation() { let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let mut log = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::Lines(1), Compression::OnRotate(1), // Keep one file uncompressed None, ); writeln!(log, "A").unwrap(); writeln!(log, "B").unwrap(); writeln!(log, "C").unwrap(); list(tmp_dir.path()); let log_paths = log.log_paths(); assert_eq!( log_paths, vec![ parent.join("log.3.gz"), parent.join("log.2.gz"), parent.join("log.1"), ] ); assert_eq!("", fs::read_to_string(&log_path).unwrap()); fn compress(text: &str) -> Vec { let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); encoder.write_all(text.as_bytes()).unwrap(); encoder.finish().unwrap() } assert_eq!(compress("A\n"), fs::read(&log.log_paths()[0]).unwrap()); assert_eq!(compress("B\n"), fs::read(&log.log_paths()[1]).unwrap()); assert_eq!("C\n", fs::read_to_string(&log.log_paths()[2]).unwrap()); } #[test] fn no_truncate() { // Don't truncate log file if it already exists let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let file_rotate = || { FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::Lines(10000), Compression::None, None, ) }; writeln!(file_rotate(), "A").unwrap(); list(parent); writeln!(file_rotate(), "B").unwrap(); list(parent); assert_eq!("A\nB\n", fs::read_to_string(&log_path).unwrap()); } #[test] fn byte_count_recalculation() { // If there is already some content in the logging file, FileRotate should set its `count` // field to the size of the file, so that it rotates at the right time let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); fs::write(&log_path, b"a").unwrap(); let mut file_rotate = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::Bytes(2), Compression::None, None, ); write!(file_rotate, "bc").unwrap(); assert_eq!(file_rotate.log_paths().len(), 1); // The size of the rotated file should be 2 ('ab) let rotated_content = fs::read(&file_rotate.log_paths()[0]).unwrap(); assert_eq!(rotated_content, b"ab"); // The size of the main file should be 1 ('c') let main_content = fs::read(log_path).unwrap(); assert_eq!(main_content, b"c"); } #[test] fn line_count_recalculation() { // If there is already some content in the logging file, FileRotate should set its `count` // field to the line count of the file, so that it rotates at the right time let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); fs::write(&log_path, b"a\n").unwrap(); let mut file_rotate = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::Lines(2), Compression::None, None, ); // A single line existed before the new logger ('a') assert_eq!(file_rotate.count, 1); writeln!(file_rotate, "b").unwrap(); writeln!(file_rotate, "c").unwrap(); assert_eq!(file_rotate.log_paths().len(), 1); // The line count of the rotated file should be 2 ('a' & 'b') let mut lines = BufReader::new(File::open(&file_rotate.log_paths()[0]).unwrap()).lines(); assert_eq!(lines.next().unwrap().unwrap(), "a".to_string()); assert_eq!(lines.next().unwrap().unwrap(), "b".to_string()); // The line count of the main file should be 1 ('c') let mut lines = BufReader::new(File::open(&log_path).unwrap()).lines(); assert_eq!(lines.next().unwrap().unwrap(), "c".to_string()); } #[cfg(unix)] #[test] fn unix_file_permissions() { use std::os::unix::fs::OpenOptionsExt; let permissions = &[0o600, 0o644]; for permission in permissions { let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let mut options = OpenOptions::new(); options .read(true) .create(true) .append(true) .mode(*permission); let mut file_rotate = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::Lines(2), Compression::None, Some(options), ); // Trigger a rotation by writing three lines writeln!(file_rotate, "a").unwrap(); writeln!(file_rotate, "b").unwrap(); writeln!(file_rotate, "c").unwrap(); assert_eq!(file_rotate.log_paths().len(), 1); // The file created at initialization time should have the right permissions ... let metadata = fs::metadata(&log_path).unwrap(); assert_eq!(metadata.permissions().mode() & 0o777, *permission); // ... and also the one generated through a rotation let metadata = fs::metadata(&file_rotate.log_paths()[0]).unwrap(); assert_eq!(metadata.permissions().mode() & 0o777, *permission); } } #[test] fn manual_rotation() { // Check that manual rotation works as intented let tmp_dir = TempDir::new().unwrap(); let parent = tmp_dir.path(); let log_path = parent.join("log"); let mut log = FileRotate::new( &*log_path.to_string_lossy(), AppendCount::new(3), ContentLimit::None, Compression::None, None, ); writeln!(log, "A").unwrap(); log.rotate().unwrap(); list(parent); writeln!(log, "B").unwrap(); list(parent); dbg!(log.log_paths()); let logs = log.log_paths(); assert_eq!(logs.len(), 1); assert_eq!("A\n", fs::read_to_string(&logs[0]).unwrap()); assert_eq!("B\n", fs::read_to_string(&log_path).unwrap()); } #[quickcheck_macros::quickcheck] fn arbitrary_lines(count: usize) { let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("log"); let count = count.max(1); let mut log = FileRotate::new( &log_path, AppendTimestamp::default(FileLimit::MaxFiles(100)), ContentLimit::Lines(count), Compression::None, None, ); for _ in 0..count - 1 { writeln!(log).unwrap(); } log.flush().unwrap(); assert!(log.log_paths().is_empty()); writeln!(log).unwrap(); assert!(Path::new(&log.log_paths()[0]).exists()); } #[quickcheck_macros::quickcheck] fn arbitrary_bytes(count: usize) { let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("log"); let count = count.max(1); let mut log = FileRotate::new( &log_path, AppendTimestamp::default(FileLimit::MaxFiles(100)), ContentLimit::Bytes(count), Compression::None, None, ); for _ in 0..count { write!(log, "0").unwrap(); } log.flush().unwrap(); assert!(log.log_paths().is_empty()); write!(log, "1").unwrap(); assert!(&log.log_paths()[0].exists()); } #[test] fn rotate_by_time_frequency() { // Test time frequency by hours. test_time_frequency( "2022-05-03T06:00:12", "2022-05-03T06:59:00", "2022-05-03T07:01:00", "2022-05-03_06-01-00", TimeFrequency::Hourly, DateFrom::DateHourAgo, ); // Test time frequency by days. test_time_frequency( "2022-05-02T12:59:59", "2022-05-02T23:01:15", "2022-05-03T01:01:00", "2022-05-02_01-01-00", TimeFrequency::Daily, DateFrom::DateYesterday, ); // Test time frequency by weeks. test_time_frequency( "2022-05-02T12:34:02", "2022-05-06T11:30:00", "2022-05-09T13:01:00", "2022-05-08_13-01-00", TimeFrequency::Weekly, DateFrom::DateYesterday, ); // Test time frequency by months. test_time_frequency( "2022-03-01T11:50:01", "2022-03-30T15:30:10", "2022-04-02T05:03:50", "2022-04-02_05-03-50", TimeFrequency::Monthly, DateFrom::Now, ); // Test time frequency by year. test_time_frequency( "2021-08-31T12:34:02", "2021-12-15T15:20:00", "2022-09-02T13:01:00", "2022-09-01_13-01-00", TimeFrequency::Yearly, DateFrom::DateYesterday, ); } #[test] fn test_file_limit() { let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("file"); let old_file = dir.join("file.2022-02-01"); File::create(&old_file).unwrap(); let first = get_fake_date_time("2022-02-02T01:00:00"); let second = get_fake_date_time("2022-02-03T01:00:00"); let third = get_fake_date_time("2022-02-04T01:00:00"); let mut log = FileRotate::new( log_path, AppendTimestamp::with_format("%Y-%m-%d", FileLimit::MaxFiles(1), DateFrom::DateYesterday), ContentLimit::Time(TimeFrequency::Daily), Compression::None, None, ); mock_time::set_mock_time(first); writeln!(log, "1").unwrap(); mock_time::set_mock_time(second); writeln!(log, "2").unwrap(); mock_time::set_mock_time(third); writeln!(log, "3").unwrap(); assert_eq!(log.log_paths(), [dir.join("file.2022-02-03")]); assert!(!old_file.is_file()); } #[test] fn test_panic() { use std::io::Write; let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("file"); // write 9 bytes of data { let mut log = FileRotate::new( &log_path, AppendCount::new(2), ContentLimit::None, Compression::None, None, ); write!(log, "nineteen characters").unwrap(); } // set content limit to less than the existing file size let mut log = FileRotate::new( &log_path, AppendCount::new(2), ContentLimit::Bytes(8), Compression::None, None, ); write!(log, "0123").unwrap(); let log_paths = log.log_paths(); assert_eq!( "nineteen characters", fs::read_to_string(&log_paths[0]).unwrap() ); assert_eq!("0123", fs::read_to_string(&log_path).unwrap()); } fn get_fake_date_time(date_time: &str) -> DateTime { let date_obj = NaiveDateTime::parse_from_str(date_time, "%Y-%m-%dT%H:%M:%S"); Local.from_local_datetime(&date_obj.unwrap()).unwrap() } fn test_time_frequency( old_time: &str, second_old_time: &str, new_time: &str, test_suffix: &str, frequency: TimeFrequency, date_from: DateFrom, ) { let old_time = get_fake_date_time(old_time); let new_time = get_fake_date_time(new_time); let second_old_time = get_fake_date_time(second_old_time); let tmp_dir = TempDir::new().unwrap(); let dir = tmp_dir.path(); let log_path = dir.join("log"); mock_time::set_mock_time(old_time); let mut log = FileRotate::new( &log_path, AppendTimestamp::with_format("%Y-%m-%d_%H-%M-%S", FileLimit::MaxFiles(7), date_from), ContentLimit::Time(frequency), Compression::None, None, ); writeln!(log, "a").unwrap(); log.flush().unwrap(); filetime::set_file_mtime( log_path, filetime::FileTime::from_system_time(old_time.into()), ) .unwrap(); mock_time::set_mock_time(second_old_time); writeln!(log, "b").unwrap(); mock_time::set_mock_time(new_time); writeln!(log, "c").unwrap(); assert!(&log.log_paths()[0].exists()); assert_eq!( log.log_paths()[0] .display() .to_string() .split('.') .collect::>() .last(), Some(&test_suffix) ); }