czkawka_core-10.0.0/.cargo_vcs_info.json0000644000000001520000000000100135530ustar { "git": { "sha1": "73afe0f9bee9f5df4139476ee61faf13e869b6e3" }, "path_in_vcs": "czkawka_core" }czkawka_core-10.0.0/Cargo.lock0000644000003763260000000000100115510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "ab_glyph" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", ] [[package]] name = "ab_glyph_rasterizer" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adler32" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "aligned-vec" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" dependencies = [ "equator", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[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 = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anyhow" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arg_enum_proc_macro" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "arrayref" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "audio_checker" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20234f0225492ef6ee5b9258277720993b343c694873da45b3c46221816d8831" dependencies = [ "symphonia", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av-data" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fca67ba5d317924c02180c576157afd54babe48a76ebc66ce6d34bb8ba08308e" dependencies = [ "byte-slice-cast", "bytes", "num-derive", "num-rational", "num-traits", ] [[package]] name = "av1-grain" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" dependencies = [ "anyhow", "arrayvec", "log", "nom 7.1.3", "num-rational", "v_frame", ] [[package]] name = "avif-serialize" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "basic-toml" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ "serde", ] [[package]] name = "bincode" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ "serde", ] [[package]] name = "bit_field" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "bitreader" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "886559b1e163d56c765bc3a985febb4eee8009f625244511d8ee3c432e08c066" dependencies = [ "cfg-if", ] [[package]] name = "bitstream-io" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "bitstream-io" version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b348c85aeb6d0bb7dee47de5506b587d9c6c17856d1314eb4695ad751edc7231" dependencies = [ "core2", ] [[package]] name = "bitvec" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ "funty", "radium", "tap", "wyz", ] [[package]] name = "bk-tree" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8283fb8e64b873918f8bc527efa6aff34956296e48ea750a9c909cd47c01546" dependencies = [ "fnv", "triple_accel", ] [[package]] name = "blake3" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "brotli-decompressor" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "built" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytecount" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" dependencies = [ "libbz2-rs-sys", ] [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher", ] [[package]] name = "cc" version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cfb" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", "uuid", ] [[package]] name = "cfg-expr" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon 0.12.16", ] [[package]] name = "cfg-expr" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" dependencies = [ "smallvec", "target-lexicon 0.13.2", ] [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "clap" version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core2" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" dependencies = [ "memchr", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "itertools 0.13.0", "num-traits", "oorandom", "regex", "serde", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", "itertools 0.13.0", ] [[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "czkawka_core" version = "10.0.0" dependencies = [ "anyhow", "audio_checker", "bincode", "bitflags 2.9.2", "bk-tree", "blake3", "crc32fast", "criterion", "crossbeam-channel", "deunicode", "directories-next", "dunce", "fast_image_resize", "ffmpeg_cmdline_utils", "file-id", "file-rotate", "fun_time", "hamming-bitwise-fast", "handsome_logger", "humansize", "i18n-embed", "i18n-embed-fl", "image", "image_hasher", "infer", "itertools 0.14.0", "jxl-oxide", "libheif-rs", "libheif-sys", "libraw-rs", "lofty", "log", "log-panics", "lopdf", "mime_guess", "nom-exif", "once_cell", "os_info", "rawler", "rayon", "rust-embed", "rustc_version", "rusty-chromaprint", "serde", "serde_json", "state", "static_assertions", "symphonia", "tempfile", "trash", "vid_dup_finder_lib", "xxhash-rust", "zip", ] [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", "syn 1.0.109", ] [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", "quote", "syn 1.0.109", ] [[package]] name = "dary_heap" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dav1d" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80c3f80814db85397819d464bb553268992c393b4b3b5554b89c1655996d5926" dependencies = [ "av-data", "bitflags 2.9.2", "dav1d-sys", "static_assertions", ] [[package]] name = "dav1d-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c91aea6668645415331133ed6f8ddf0e7f40160cd97a12d59e68716a58704b" dependencies = [ "libc", "system-deps 7.0.5", ] [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "derive_arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "deunicode" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "directories-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "document-features" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" dependencies = [ "litrs", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecb" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" dependencies = [ "cipher", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "enum-utils" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed327f716d0d351d86c9fd3398d20ee39ad8f681873cc081da2ca1c10fed398a" dependencies = [ "enum-utils-from-str", "failure", "proc-macro2", "quote", "serde_derive_internals", "syn 1.0.109", ] [[package]] name = "enum-utils-from-str" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49be08bad6e4ca87b2b8e74146987d4e5cb3b7512efa50ef505b51a22227ee1" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "enumn" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "equator" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" dependencies = [ "equator-macro", ] [[package]] name = "equator-macro" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "exr" version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", ] [[package]] name = "extended" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" [[package]] name = "failure" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" dependencies = [ "backtrace", ] [[package]] name = "fallible_collections" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" dependencies = [ "hashbrown 0.13.2", ] [[package]] name = "fast_image_resize" version = "5.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d372ab3252d8f162d858d675a3d88a8c33ba24a6238837c50c8851911c7e89cd" dependencies = [ "bytemuck", "cfg-if", "document-features", "image", "num-traits", "thiserror 1.0.69", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "ffmpeg_cmdline_utils" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30cbcb92e5f36bda100292a8bf8989631f3b6c4e4b71454ca803a9b837f63441" dependencies = [ "image", "serde", "serde_json", "thiserror 2.0.15", "winapi", ] [[package]] name = "ffmpeg_gst_wrapper" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2e75be881230e5808200de02c435cfee05b5e0b978ce50cdbcf6527e8d13de" dependencies = [ "cfg-if", "ffmpeg_cmdline_utils", "image", "serde", "thiserror 2.0.15", "url", ] [[package]] name = "file-id" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "file-rotate" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e8e2fa049328a1f3295991407a88585805d126dfaadf74b9fe8c194c730aafc" dependencies = [ "chrono", "flate2", ] [[package]] name = "find-crate" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" dependencies = [ "toml 0.5.11", ] [[package]] name = "flate2" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] [[package]] name = "fluent" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" dependencies = [ "fluent-bundle", "unic-langid", ] [[package]] name = "fluent-bundle" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" dependencies = [ "fluent-langneg", "fluent-syntax", "intl-memoizer", "intl_pluralrules", "rustc-hash", "self_cell", "smallvec", "unic-langid", ] [[package]] name = "fluent-langneg" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" dependencies = [ "unic-langid", ] [[package]] name = "fluent-syntax" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", "thiserror 2.0.15", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "four-cc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d73a076bdabd78c2f9045dba1b90664a655fa8372581c238596e1eb3a5e1b7" [[package]] name = "fun_time" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee194d43605ea83cca7af42af5f9001fab1a8e2220cb8a012e21dda6167fdb0" dependencies = [ "fun_time_derive", "log", ] [[package]] name = "fun_time_derive" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71555fd2db00938d82d29d8fa62a2ae80aed2c162c328d775f79e98d9212f013" dependencies = [ "darling", "log", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[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-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[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-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generator" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" dependencies = [ "cc", "libc", "log", "rustversion", "windows 0.48.0", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "geo-types" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75a4dcd69d35b2c87a7c83bce9af69fd65c9d68d3833a0ded568983928f3fc99" dependencies = [ "approx", "num-traits", "serde", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gif" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hamming-bitwise-fast" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06d16627a786f2f40f9079bd54a3c7987df493d421f2a6fecca7dc0886ebc7b9" [[package]] name = "handsome_logger" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70a23d1869ef63d7d0f1a24213014eff2cf26494d0e4976d510426d249dbbd05" dependencies = [ "log", "termcolor", "time", "tz-rs", ] [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "humansize" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" dependencies = [ "libm", ] [[package]] name = "i18n-config" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" dependencies = [ "basic-toml", "log", "serde", "serde_derive", "thiserror 1.0.69", "unic-langid", ] [[package]] name = "i18n-embed" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" dependencies = [ "arc-swap", "fluent", "fluent-langneg", "fluent-syntax", "i18n-embed-impl", "intl-memoizer", "log", "parking_lot", "rust-embed", "sys-locale", "thiserror 1.0.69", "unic-langid", "walkdir", ] [[package]] name = "i18n-embed-fl" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e598ed73b67db92f61e04672e599eef2991a262a40e1666735b8a86d2e7e9f30" dependencies = [ "find-crate", "fluent", "fluent-syntax", "i18n-config", "i18n-embed", "proc-macro-error2", "proc-macro2", "quote", "strsim 0.11.1", "syn 2.0.106", "unic-langid", ] [[package]] name = "i18n-embed-impl" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" dependencies = [ "find-crate", "i18n-config", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core 0.61.2", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "image" version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", "color_quant", "dav1d", "exr", "gif", "image-webp", "mp4parse", "num-traits", "png", "qoi", "ravif", "rayon", "rgb", "tiff", "zune-core", "zune-jpeg", ] [[package]] name = "image-webp" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" dependencies = [ "byteorder-lite", "quick-error", ] [[package]] name = "image_hasher" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c191dc6138f559a0177b8413eaf2a37784d8e63c697e247aa3740930f1c9364" dependencies = [ "base64", "fast_image_resize", "image", "rustdct", "serde", "transpose", ] [[package]] name = "imageproc" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d" dependencies = [ "ab_glyph", "approx", "getrandom 0.2.16", "image", "itertools 0.12.1", "nalgebra", "num", "rand 0.8.5", "rand_distr", "rayon", ] [[package]] name = "imgref" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.5", ] [[package]] name = "infer" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ "cfb", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "interpolate_name" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "intl-memoizer" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ "type-map", "unic-langid", ] [[package]] name = "intl_pluralrules" version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" dependencies = [ "unic-langid", ] [[package]] name = "io-uring" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags 2.9.2", "cfg-if", "libc", ] [[package]] name = "iso6709parse" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5090db9c6a716d1f4eeb729957e889e9c28156061c825cbccd44950cf0f3c66" dependencies = [ "geo-types", "nom 7.1.3", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", "windows-sys 0.59.0", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "jiff-tzdb" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ "getrandom 0.3.3", "libc", ] [[package]] name = "jpeg-decoder" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "jxl-bitstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda699770a7f4ea38f8eb21d91b545eb6448be28e540acc7ce84498bcead4903" dependencies = [ "tracing", ] [[package]] name = "jxl-coding" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd972bcd125e776f1eb241ac50e39f956095a1c2770c64736c968f8946bd9a3c" dependencies = [ "jxl-bitstream", "tracing", ] [[package]] name = "jxl-color" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f316b1358c1711755b3ee8e8cb5c4a1dad12e796233088a7a513440782de80b2" dependencies = [ "jxl-bitstream", "jxl-coding", "jxl-grid", "jxl-image", "jxl-oxide-common", "jxl-threadpool", "tracing", ] [[package]] name = "jxl-frame" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d967c6fd669c7c01060b5022d8835fa82fd46b06ffc98b549f17600a097c2b3" dependencies = [ "jxl-bitstream", "jxl-coding", "jxl-grid", "jxl-image", "jxl-modular", "jxl-oxide-common", "jxl-threadpool", "jxl-vardct", "tracing", ] [[package]] name = "jxl-grid" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0e0ef92d5d60e76bf41098e57e985f523185e08fad54268da448637feca6989" dependencies = [ "tracing", ] [[package]] name = "jxl-image" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f752d62577c702a94dbbce4045caf08cb58639e8a4d56464b40ecf33ffe565" dependencies = [ "jxl-bitstream", "jxl-grid", "jxl-oxide-common", "tracing", ] [[package]] name = "jxl-jbr" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35d032bcec660647828527ff42c6f5776d2fd44b8357f9f6d9ac6dc07218e46" dependencies = [ "brotli-decompressor", "jxl-bitstream", "jxl-frame", "jxl-grid", "jxl-image", "jxl-modular", "jxl-oxide-common", "jxl-threadpool", "jxl-vardct", "tracing", ] [[package]] name = "jxl-modular" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da758b2f989aafd9eeb39489fe43d7be5a3a0d2ad61cf1bad705eb6990a6053c" dependencies = [ "jxl-bitstream", "jxl-coding", "jxl-grid", "jxl-oxide-common", "jxl-threadpool", "tracing", ] [[package]] name = "jxl-oxide" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa635162d7d53c650ae9e429a4e354ac1d63f0d3b1bdd1991b400c22cd301a6d" dependencies = [ "brotli-decompressor", "bytemuck", "image", "jxl-bitstream", "jxl-color", "jxl-frame", "jxl-grid", "jxl-image", "jxl-jbr", "jxl-oxide-common", "jxl-render", "jxl-threadpool", "tracing", ] [[package]] name = "jxl-oxide-common" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62394c5021b3a9e7e0dbb2d639d555d019090c9946c39f6d3b09d390db4157b" dependencies = [ "jxl-bitstream", ] [[package]] name = "jxl-render" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa0c3100918bd3c41bb0f8ce1f4f1664e48f3032ff8eeab0d6a2cfc3276f462d" dependencies = [ "bytemuck", "jxl-bitstream", "jxl-coding", "jxl-color", "jxl-frame", "jxl-grid", "jxl-image", "jxl-modular", "jxl-oxide-common", "jxl-threadpool", "jxl-vardct", "tracing", ] [[package]] name = "jxl-threadpool" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f15eb830aa77a7f21148d72e153562a26bfe570139bd4922eab1908dd499d3" dependencies = [ "rayon", "rayon-core", "tracing", ] [[package]] name = "jxl-vardct" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce72a18c6d3a47172ab6c479be2bdb56f22066b5d7092663f03b4490820b4511" dependencies = [ "jxl-bitstream", "jxl-coding", "jxl-grid", "jxl-modular", "jxl-oxide-common", "jxl-threadpool", "tracing", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lebe" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libbz2-rs-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libflate" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" dependencies = [ "adler32", "core2", "crc32fast", "dary_heap", "libflate_lz77", ] [[package]] name = "libflate_lz77" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" dependencies = [ "core2", "hashbrown 0.14.5", "rle-decode-fast", ] [[package]] name = "libfuzzer-sys" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", ] [[package]] name = "libheif-rs" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d09b0d2d69da084eeeda9534662bc6b6096fbce3f307149750c0e572ad0ccd" dependencies = [ "enumn", "four-cc", "libheif-sys", ] [[package]] name = "libheif-sys" version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af8b7a4151ae10f6d2e8684f7172c43f09c0258c84190fd9704422588ceec63" dependencies = [ "libc", ] [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libraw-rs" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ec60aab878560c299c6e70a0c6dc2278a2159ac6fe09650917266b8985387f" dependencies = [ "libraw-rs-sys", ] [[package]] name = "libraw-rs-sys" version = "0.0.4+libraw-0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba094a3b8b04cc42fdeafaff06f81d3b13a7d01cc7a8eae55b943dae1b65c3cc" dependencies = [ "cc", "libc", ] [[package]] name = "libredox" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags 2.9.2", "libc", ] [[package]] name = "libz-rs-sys" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "lofty" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca260c51a9c71f823fbfd2e6fbc8eb2ee09834b98c00763d877ca8bfa85cde3e" dependencies = [ "byteorder", "data-encoding", "flate2", "lofty_attr", "log", "ogg_pager", "paste", ] [[package]] name = "lofty_attr" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "log-panics" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" dependencies = [ "backtrace", "log", ] [[package]] name = "loom" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", "generator", "scoped-tls", "serde", "serde_json", "tracing", "tracing-subscriber", ] [[package]] name = "loop9" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ "imgref", ] [[package]] name = "lopdf" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "674a3504c1224247e00762afb90690991b673c461f6779565e055e91926a49da" dependencies = [ "aes", "bitflags 2.9.2", "cbc", "chrono", "ecb", "encoding_rs", "flate2", "getrandom 0.3.3", "indexmap", "itoa", "jiff", "log", "md-5", "nom 8.0.0", "nom_locate", "rand 0.9.2", "rangemap", "rayon", "sha2", "stringprep", "thiserror 2.0.15", "time", "weezl", ] [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "matrixmultiply" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ "autocfg", "rawpointer", ] [[package]] name = "maybe-rayon" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", "rayon", ] [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", "digest", ] [[package]] name = "md5" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] name = "mp4parse" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570" dependencies = [ "bitreader", "byteorder", "fallible_collections", "log", "num-traits", "static_assertions", ] [[package]] name = "multiversion" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201" dependencies = [ "multiversion-macros", "target-features", ] [[package]] name = "multiversion-macros" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", "target-features", ] [[package]] name = "nalgebra" version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" dependencies = [ "approx", "matrixmultiply", "num-complex", "num-rational", "num-traits", "simba", "typenum", ] [[package]] name = "ndarray" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "matrixmultiply", "num-complex", "num-integer", "num-traits", "portable-atomic", "portable-atomic-util", "rawpointer", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nom" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", ] [[package]] name = "nom-exif" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a6703c263bdeb67ea61c7a7605ebfd42996c49cbf8558724b88fd67804f35d" dependencies = [ "bytes", "chrono", "iso6709parse", "nom 7.1.3", "regex", "thiserror 2.0.15", "tracing", ] [[package]] name = "nom_locate" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", "memchr", "nom 8.0.0", ] [[package]] name = "noop_proc_macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-derive" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "num_enum" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", "rustversion", ] [[package]] name = "num_enum_derive" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "objc2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.2", "objc2", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "ogg_pager" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e034c10fb5c1c012c1b327b85df89fb0ef98ae66ec28af30f0d1eed804a40c19" dependencies = [ "byteorder", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "os_info" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" dependencies = [ "log", "plist", "windows-sys 0.52.0", ] [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ "ttf-parser", ] [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[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]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64", "indexmap", "quick-xml", "serde", "time", ] [[package]] name = "png" version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "primal-check" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" dependencies = [ "num-integer", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro-error-attr2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "proc-macro-error2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", "syn 2.0.106", ] [[package]] name = "qoi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" dependencies = [ "bytemuck", ] [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" dependencies = [ "memchr", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.3", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", ] [[package]] name = "rand_distr" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", "rand 0.8.5", ] [[package]] name = "rangemap" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" [[package]] name = "rav1e" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" dependencies = [ "arbitrary", "arg_enum_proc_macro", "arrayvec", "av1-grain", "bitstream-io 2.6.0", "built", "cfg-if", "interpolate_name", "itertools 0.12.1", "libc", "libfuzzer-sys", "log", "maybe-rayon", "new_debug_unreachable", "noop_proc_macro", "num-derive", "num-traits", "once_cell", "paste", "profiling", "rand 0.8.5", "rand_chacha 0.3.1", "simd_helpers", "system-deps 6.2.2", "thiserror 1.0.69", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error", "rav1e", "rayon", "rgb", ] [[package]] name = "rawler" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ee1ec76f71a9485dd706323ec3eea78a641ce7a2497b19a196b5e31d94f8bf2" dependencies = [ "async-trait", "backtrace", "bitstream-io 4.5.0", "byteorder", "chrono", "enumn", "futures", "glob", "hex", "image", "itertools 0.14.0", "lazy_static", "libc", "libflate", "log", "md5", "memmap2", "multiversion", "num_enum", "rayon", "rustc_version", "serde", "serde_json", "thiserror 2.0.15", "tokio", "toml 0.8.23", "uuid", "weezl", ] [[package]] name = "rawpointer" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "realfft" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" dependencies = [ "rustfft", ] [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags 2.9.2", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" [[package]] name = "rle-decode-fast" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rubato" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" dependencies = [ "num-complex", "num-integer", "num-traits", "realfft", ] [[package]] name = "rust-embed" version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ "rust-embed-impl", "rust-embed-utils", "walkdir", ] [[package]] name = "rust-embed-impl" version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", "syn 2.0.106", "walkdir", ] [[package]] name = "rust-embed-utils" version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ "sha2", "walkdir", ] [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustdct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b61555105d6a9bf98797c063c362a1d24ed8ab0431655e38f1cf51e52089551" dependencies = [ "rustfft", ] [[package]] name = "rustfft" version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4" dependencies = [ "num-complex", "num-integer", "num-traits", "primal-check", "strength_reduce", "transpose", ] [[package]] name = "rustix" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags 2.9.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.60.2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-chromaprint" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e4234523e38d9c12201955f8216e1a60313e64c5077f4e1cf49b0db77bd7e8" dependencies = [ "rubato", "rustfft", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe_arch" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" dependencies = [ "bytemuck", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "serde_derive_internals" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "serde_json" version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "simba" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" dependencies = [ "approx", "num-complex", "num-traits", "paste", "wide", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simd_helpers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ "quote", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "state" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ "loom", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strength_reduce" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ "unicode-bidi", "unicode-normalization", "unicode-properties", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", "symphonia-bundle-flac", "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-adpcm", "symphonia-codec-alac", "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", "symphonia-format-caf", "symphonia-format-isomp4", "symphonia-format-mkv", "symphonia-format-ogg", "symphonia-format-riff", "symphonia-metadata", ] [[package]] name = "symphonia-bundle-flac" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" dependencies = [ "log", "symphonia-core", "symphonia-metadata", "symphonia-utils-xiph", ] [[package]] name = "symphonia-bundle-mp3" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" dependencies = [ "lazy_static", "log", "symphonia-core", "symphonia-metadata", ] [[package]] name = "symphonia-codec-aac" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" dependencies = [ "lazy_static", "log", "symphonia-core", ] [[package]] name = "symphonia-codec-adpcm" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" dependencies = [ "log", "symphonia-core", ] [[package]] name = "symphonia-codec-alac" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" dependencies = [ "log", "symphonia-core", ] [[package]] name = "symphonia-codec-pcm" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" dependencies = [ "log", "symphonia-core", ] [[package]] name = "symphonia-codec-vorbis" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" dependencies = [ "log", "symphonia-core", "symphonia-utils-xiph", ] [[package]] name = "symphonia-core" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", "bitflags 1.3.2", "bytemuck", "lazy_static", "log", ] [[package]] name = "symphonia-format-caf" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" dependencies = [ "log", "symphonia-core", "symphonia-metadata", ] [[package]] name = "symphonia-format-isomp4" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" dependencies = [ "encoding_rs", "log", "symphonia-core", "symphonia-metadata", "symphonia-utils-xiph", ] [[package]] name = "symphonia-format-mkv" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" dependencies = [ "lazy_static", "log", "symphonia-core", "symphonia-metadata", "symphonia-utils-xiph", ] [[package]] name = "symphonia-format-ogg" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" dependencies = [ "log", "symphonia-core", "symphonia-metadata", "symphonia-utils-xiph", ] [[package]] name = "symphonia-format-riff" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" dependencies = [ "extended", "log", "symphonia-core", "symphonia-metadata", ] [[package]] name = "symphonia-metadata" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" dependencies = [ "encoding_rs", "lazy_static", "log", "symphonia-core", ] [[package]] name = "symphonia-utils-xiph" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" dependencies = [ "symphonia-core", "symphonia-metadata", ] [[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]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "sys-locale" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" dependencies = [ "libc", ] [[package]] name = "system-deps" version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr 0.15.8", "heck", "pkg-config", "toml 0.8.23", "version-compare", ] [[package]] name = "system-deps" version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ "cfg-expr 0.20.2", "heck", "pkg-config", "toml 0.8.23", "version-compare", ] [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-features" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ "thiserror-impl 2.0.15", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "thiserror-impl" version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tiff" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", "socket2", "tokio-macros", "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", "winnow", ] [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "transpose" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" dependencies = [ "num-integer", "strength_reduce", ] [[package]] name = "trash" version = "5.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65a334451012017a39758aa85a30827c13ac684245bf6b08249483c063f64ff3" dependencies = [ "chrono", "libc", "log", "objc2", "objc2-foundation", "once_cell", "percent-encoding", "scopeguard", "urlencoding", "windows 0.56.0", ] [[package]] name = "triple_accel" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c" [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" [[package]] name = "type-map" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ "rustc-hash", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "tz-rs" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" [[package]] name = "unic-langid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" dependencies = [ "unic-langid-impl", ] [[package]] name = "unic-langid-impl" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ "serde", "tinystr", ] [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", "serde", "wasm-bindgen", ] [[package]] name = "v_frame" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" dependencies = [ "aligned-vec", "num-traits", "wasm-bindgen", ] [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-compare" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vid_dup_finder_common" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b601345173cab95df37a54b3351a77f85a9d11429487310b6a2e49ac37bc1942" dependencies = [ "fast_image_resize", "image", "imageproc", "itertools 0.14.0", "rand 0.9.2", "winapi", ] [[package]] name = "vid_dup_finder_lib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2bcf6135b99bca822ea095fc485066bf8c0f8788f575d6808a12619ba721b38" dependencies = [ "bitvec", "cfg-if", "enum-utils", "ffmpeg_gst_wrapper", "image", "itertools 0.14.0", "ndarray", "rand 0.9.2", "rustdct", "serde", "thiserror 2.0.15", "vid_dup_finder_common", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "weezl" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wide" version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" dependencies = [ "bytemuck", "safe_arch", ] [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[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" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ "windows-core 0.56.0", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ "windows-implement 0.56.0", "windows-interface 0.56.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", "windows-link", "windows-result 0.3.4", "windows-strings", ] [[package]] name = "windows-implement" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-interface" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.3", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.2", ] [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "zip" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b" dependencies = [ "aes", "arbitrary", "bzip2", "constant_time_eq", "crc32fast", "flate2", "getrandom 0.3.3", "hmac", "indexmap", "memchr", "pbkdf2", "sha1", "time", "zeroize", "zopfli", ] [[package]] name = "zlib-rs" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" [[package]] name = "zopfli" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] [[package]] name = "zune-core" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-inflate" version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] [[package]] name = "zune-jpeg" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" dependencies = [ "zune-core", ] czkawka_core-10.0.0/Cargo.toml0000644000000101650000000000100115560ustar # 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 = "2024" rust-version = "1.85.0" name = "czkawka_core" version = "10.0.0" authors = ["Rafał Mikrut "] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Core of Czkawka app" homepage = "https://github.com/qarmin/czkawka" readme = "README.md" license = "MIT" repository = "https://github.com/qarmin/czkawka" [features] default = [] heif = [ "dep:libheif-rs", "dep:libheif-sys", ] libavif = [ "image/avif-native", "image/avif", ] libraw = ["dep:libraw-rs"] [lib] name = "czkawka_core" path = "src/lib.rs" [[bench]] name = "hash_calculation_benchmark" path = "benches/hash_calculation_benchmark.rs" harness = false [dependencies.anyhow] version = "1.0.89" [dependencies.audio_checker] version = "0.1" [dependencies.bincode] version = "<2.0" [dependencies.bitflags] version = "2.6" [dependencies.bk-tree] version = "0.5" [dependencies.blake3] version = "1.5" [dependencies.crc32fast] version = "1.4" [dependencies.crossbeam-channel] version = "0.5" [dependencies.deunicode] version = "1.6.2" [dependencies.directories-next] version = "2.0" [dependencies.dunce] version = "1.0.5" [dependencies.fast_image_resize] version = "=5.1.4" [dependencies.ffmpeg_cmdline_utils] version = "0.4" [dependencies.file-rotate] version = "0.8.0" [dependencies.fun_time] version = "0.3" features = ["log"] [dependencies.hamming-bitwise-fast] version = "1.0" [dependencies.handsome_logger] version = "0.9" [dependencies.humansize] version = "2.1" [dependencies.i18n-embed] version = "0.16" features = [ "fluent-system", "desktop-requester", ] [dependencies.i18n-embed-fl] version = "0.10" [dependencies.image] version = "0.25" features = [ "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp", "rayon", ] default-features = false [dependencies.image_hasher] version = "3.0" features = ["fast_resize_unstable"] [dependencies.infer] version = "0.19" [dependencies.itertools] version = "0.14" [dependencies.jxl-oxide] version = "0.12.0" features = ["image"] [dependencies.libheif-rs] version = "=0.18.0" optional = true [dependencies.libheif-sys] version = "=1.14.2" optional = true [dependencies.libraw-rs] version = "0.0.4" optional = true [dependencies.lofty] version = "0.22" [dependencies.log] version = "0.4.22" [dependencies.log-panics] version = "2.1.0" features = ["with-backtrace"] [dependencies.lopdf] version = "0.37.0" [dependencies.mime_guess] version = "2.0" [dependencies.nom-exif] version = "2.1.0" [dependencies.once_cell] version = "1.20" [dependencies.os_info] version = "3" default-features = false [dependencies.rawler] version = "0.7.0" [dependencies.rayon] version = "1.10" [dependencies.rust-embed] version = "8.5" features = ["debug-embed"] [dependencies.rusty-chromaprint] version = "0.3" [dependencies.serde] version = "1.0" [dependencies.serde_json] version = "1.0" [dependencies.state] version = "0.6" [dependencies.static_assertions] version = "1.1.0" [dependencies.symphonia] version = "0.5" features = ["all"] [dependencies.tempfile] version = "3.13" [dependencies.trash] version = "5.1" [dependencies.vid_dup_finder_lib] version = "0.4" [dependencies.xxhash-rust] version = "0.8" features = ["xxh3"] [dependencies.zip] version = "4.0" features = [ "aes-crypto", "bzip2", "deflate", "time", ] default-features = false [dev-dependencies.criterion] version = "0.7" features = [] default-features = false [build-dependencies.rustc_version] version = "0.4" [target."cfg(windows)".dependencies.file-id] version = "0.2.2" czkawka_core-10.0.0/Cargo.toml.orig000064400000000000000000000062431046102023000152410ustar 00000000000000[package] name = "czkawka_core" version = "10.0.0" authors = ["Rafał Mikrut "] edition = "2024" rust-version = "1.85.0" description = "Core of Czkawka app" license = "MIT" homepage = "https://github.com/qarmin/czkawka" repository = "https://github.com/qarmin/czkawka" build = "build.rs" [dependencies] humansize = "2.1" rayon = "1.10" crossbeam-channel = "0.5" # For saving/loading config files to specific directories directories-next = "2.0" # Needed by similar images image_hasher = { version = "3.0", features = ["fast_resize_unstable"] } bk-tree = "0.5" image = { version = "0.25", default-features = false, features = ["bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp", "rayon"] } hamming-bitwise-fast = "1.0" # Needed by same music bitflags = "2.6" lofty = "0.22" # Needed by broken files zip = { version = "4.0", features = ["aes-crypto", "bzip2", "deflate", "time"], default-features = false } audio_checker = "0.1" lopdf = "0.37.0" # Needed by audio similarity feature rusty-chromaprint = "0.3" symphonia = { version = "0.5", features = ["all"] } # Hashes for duplicate files blake3 = "1.5" crc32fast = "1.4" xxhash-rust = { version = "0.8", features = ["xxh3"] } tempfile = "3.13" # Video Duplicates vid_dup_finder_lib = "0.4" ffmpeg_cmdline_utils = "0.4" # Saving/Loading Cache serde = "1.0" # TODO - looks that newer bincode requires to add for all structs specific derives, but not all structs are in my control # Probably the only external struct that is used in cache is `VideoHash` from `vid_dup_finder_lib` bincode = "<2.0" serde_json = "1.0" # Language i18n-embed = { version = "0.16", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.10" rust-embed = { version = "8.5", features = ["debug-embed"] } once_cell = "1.20" # Raw image files rawler = "0.7.0" libraw-rs = { version = "0.0.4", optional = true } jxl-oxide = { version = "0.12.0", features = ["image"] } # Checking for invalid extensions mime_guess = "2.0" infer = "0.19" # Heif/Heic libheif-rs = { version = "=0.18.0", optional = true } # Do not upgrade now, since Ubuntu 22.04 not works with newer version libheif-sys = { version = "=1.14.2", optional = true } # 1.14.3 brake compilation on Ubuntu 22.04, so pin it to this version anyhow = { version = "1.0.89" } nom-exif = "2.1.0" state = "0.6" trash = "5.1" dunce = "1.0.5" os_info = { version = "3", default-features = false } log = "0.4.22" handsome_logger = "0.9" fun_time = { version = "0.3", features = ["log"] } itertools = "0.14" static_assertions = "1.1.0" file-rotate = "0.8.0" log-panics = { version = "2.1.0", features = ["with-backtrace"] } deunicode = "1.6.2" fast_image_resize = "=5.1.4" # TODO, greater versions uses unstable features, that were stabilized after 1.85.0 [target.'cfg(windows)'.dependencies] file-id = "0.2.2" [build-dependencies] rustc_version = "0.4" [dev-dependencies] criterion = { version = "0.7", default-features = false, features = [] } [[bench]] name = "hash_calculation_benchmark" harness = false [features] default = [] heif = ["dep:libheif-rs", "dep:libheif-sys"] libraw = ["dep:libraw-rs"] libavif = ["image/avif-native", "image/avif"] czkawka_core-10.0.0/LICENSE_MIT000064400000000000000000000020621046102023000140630ustar 00000000000000MIT License Copyright (c) 2020-2025 Rafał Mikrut 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.czkawka_core-10.0.0/README.md000064400000000000000000000000761046102023000136270ustar 00000000000000# Czkawka Core Core of Czkawka GUI/CLI and Krokiet projects. czkawka_core-10.0.0/benches/hash_calculation_benchmark.rs000064400000000000000000000046331046102023000216430ustar 00000000000000use std::env::temp_dir; use std::fs::File; use std::hint::black_box; use std::io::Write; use std::path::PathBuf; use std::sync::Arc; use criterion::{Criterion, criterion_group, criterion_main}; use czkawka_core::common::model::HashType; use czkawka_core::tools::duplicate::{DuplicateEntry, hash_calculation}; fn setup_test_file(size: u64) -> PathBuf { let mut path = temp_dir(); path.push("test_file"); let mut file = File::create(&path).expect("Failed to create test file"); file.write_all(&vec![0u8; size as usize]).expect("Failed to write to test file"); path } fn get_file_entry(size: u64) -> DuplicateEntry { let path = setup_test_file(size); DuplicateEntry { path, modified_date: 0, size, hash: String::new(), } } fn benchmark_hash_calculation_vec(c: &mut Criterion) { let file_entry = get_file_entry(FILE_SIZE); let function_name = format!("hash_calculation_vec_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}"); c.bench_function(&function_name, |b| { b.iter(|| { let mut buffer = vec![0u8; BUFFER_SIZE]; hash_calculation( black_box(&mut buffer), black_box(&file_entry), black_box(HashType::Blake3), &Arc::default(), &Arc::default(), ) .expect("Failed to calculate hash"); }); }); } fn benchmark_hash_calculation_arr(c: &mut Criterion) { let file_entry = get_file_entry(FILE_SIZE); let function_name = format!("hash_calculation_arr_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}"); c.bench_function(&function_name, |b| { b.iter(|| { let mut buffer = [0u8; BUFFER_SIZE]; hash_calculation( black_box(&mut buffer), black_box(&file_entry), black_box(HashType::Blake3), &Arc::default(), &Arc::default(), ) .expect("Failed to calculate hash"); }); }); } criterion_group!(benches, benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {16 * 1024}>, benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {1024 * 1024}>, benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {16 * 1024}>, benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {1024 * 1024}>, ); criterion_main!(benches); czkawka_core-10.0.0/build.rs000064400000000000000000000014531046102023000140150ustar 00000000000000fn main() { let rust_version = match rustc_version::version_meta() { Ok(meta) => { let rust_v = meta.semver.to_string(); let rust_date = meta.commit_date.unwrap_or_default(); format!("{rust_v} ({rust_date})") } Err(_) => "".to_string(), }; println!("cargo:rustc-env=RUST_VERSION_INTERNAL={rust_version}"); if let Ok(encoded) = std::env::var("CARGO_ENCODED_RUSTFLAGS") { println!("cargo:rustc-env=UUSED_RUSTFLAGS={encoded}"); } let using_cranelift = std::env::var("CARGO_PROFILE_RELEASE_CODEGEN_UNITS") == Ok("1".to_string()) || std::env::var("CARGO_PROFILE_DEV_CODEGEN_BACKEND") == Ok("cranelift".to_string()); if using_cranelift { println!("cargo:rustc-env=USING_CRANELIFT=1"); } } czkawka_core-10.0.0/data/com.github.qarmin.czkawka.desktop000064400000000000000000000013141046102023000216270ustar 00000000000000[Desktop Entry] Type=Application Terminal=false Exec=czkawka_gui Name=Czkawka Name[it]=Singhiozzo Comment=Multi functional app to clean OS which allow to find duplicates, empty folders, similar files etc. Comment[it]=Programma multifunzionale per pulire il sistema, che permette di trovare file duplicati, cartelle vuote, file simili, ecc... Comment[zh_CN]=可用于清理文件副本、空文件夹、相似文件等的系统清理工具 Comment[zh_TW]=可用於清理重複檔案、空資料夾、相似檔案等的系統清理工具 Icon=com.github.qarmin.czkawka Categories=System;FileTools Keywords=Hiccup;duplicate;same;similar;cleaner;copy;copies;compare;files; StartupWMClass=czkawka_gui TryExec=czkawka_gui czkawka_core-10.0.0/data/com.github.qarmin.czkawka.metainfo.xml000064400000000000000000000031151046102023000225600ustar 00000000000000 com.github.qarmin.czkawka Czkawka Multi functional app to find duplicates, empty folders, similar images, broken files etc. CC0-1.0 MIT

Czkawka is simple, fast and easy to use app to remove unnecessary files from your computer.

com.github.qarmin.czkawka.desktop https://user-images.githubusercontent.com/41945903/147875238-7f82fa27-c6dd-47e7-87ed-e253fb2cbc3e.png https://user-images.githubusercontent.com/41945903/147875239-bcf9776c-885d-45ac-ba82-5a426d8e1647.png https://user-images.githubusercontent.com/41945903/147875243-e654e683-37f7-46fa-8321-119a4c5775e7.png Rafał Mikrut Rafał Mikrut https://github.com/qarmin/czkawka https://github.com/qarmin/czkawka/issues https://github.com/sponsors/qarmin https://crowdin.com/project/czkawka
czkawka_core-10.0.0/data/icons/com.github.qarmin.czkawka-symbolic.svg000064400000000000000000000120161046102023000237100ustar 00000000000000 czkawka_core-10.0.0/data/icons/com.github.qarmin.czkawka.Devel.svg000064400000000000000000000421761046102023000231410ustar 00000000000000 czkawka_core-10.0.0/data/icons/com.github.qarmin.czkawka.svg000064400000000000000000000240101046102023000220660ustar 00000000000000 czkawka_core-10.0.0/i18n/ar/czkawka_core.ftl000064400000000000000000000051721046102023000167050ustar 00000000000000# Core core_similarity_original = الأصل core_similarity_very_high = عالية جدا core_similarity_high = مرتفع core_similarity_medium = متوسط core_similarity_small = صغير core_similarity_very_small = صغير جدا core_similarity_minimal = الحد الأدنى core_cannot_open_dir = لا يمكن فتح dir { $dir }، السبب { $reason } core_cannot_read_entry_dir = لا يمكن قراءة الإدخال في dir { $dir }، السبب { $reason } core_cannot_read_metadata_dir = لا يمكن قراءة البيانات الوصفية في dir { $dir }، السبب { $reason } core_file_modified_before_epoch = يبدو أن الملف { $name } قد تم تعديله قبل يونكس Epoch core_folder_modified_before_epoch = يبدو أن المجلد { $name } قد تم تعديله قبل يونكس Epoch core_file_no_modification_date = غير قادر على الحصول على تاريخ التعديل من الملف { $name }، السبب { $reason } core_folder_no_modification_date = غير قادر على الحصول على تاريخ التعديل من المجلد { $name }، السبب { $reason } core_missing_no_chosen_included_directory = يجب توفير دليل واحد على الأقل core_directory_must_exists = الأدلة: يجب أن يكون مسار المجلد المتوفر موجودا، تجاهل { $path } core_directory_must_be_directory = الأدلة: المسار المقدم يجب أن يشير إلى الدليل، تجاهل { $path } core_included_directory_zero_valid_directories = خطأ في الدليل المضمن: لا يوجد حتى مسار واحد صحيح للإدراج المطلوب core_excluded_directory_pointless_slash = الأدلة: استبعاد / لا معنى له، لأنه يعني أنه لن يتم مسح أي ملفات core_directory_overlap = الأدلة: جميع الدلائل للبحث عن التداخل مع الدلائل المستبعدة core_directory_unable_to_get_device_id = الأدلة: غير قادر على الحصول على معرف الجهاز من المجلد { $path } core_ffmpeg_not_found = لا يمكن العثور على التثبيت المناسب لـ FFmpeg، هذا برنامج خارجي، تحتاج إلى تثبيته يدويا. core_ffmpeg_not_found_windows = تأكد من أن ffmpeg.exe و ffprobe.exe متاحان في PATH أو يتم وضعهما مباشرة لنفس المجلد حيث التطبيق قابل للتنفيذ core_invalid_symlink_infinite_recursion = التكرار اللامتناهي core_invalid_symlink_non_existent_destination = ملف الوجهة غير موجود czkawka_core-10.0.0/i18n/bg/czkawka_core.ftl000064400000000000000000000062461046102023000166760ustar 00000000000000# Core core_similarity_original = Оригинален core_similarity_very_high = Много висок core_similarity_high = Висок core_similarity_medium = Среден core_similarity_small = Малък core_similarity_very_small = Много малък core_similarity_minimal = Минимален core_cannot_open_dir = Не може да се отвори папка { $dir }, причината е { $reason } core_cannot_read_entry_dir = Не може да се прочете папка { $dir }, причината е { $reason } core_cannot_read_metadata_dir = Не могат да се прочетат мета-данните в папка { $dir }, причината е { $reason } core_file_modified_before_epoch = Файлът { $name } изглежда да е променен преди Unix Epoc core_folder_modified_before_epoch = Папка { $name } изглежда да е променена преди Unix Epoc core_file_no_modification_date = Невъзможно е да се получи променената дата от файл { $name }, причината е { $reason } core_folder_no_modification_date = Невъзможно е да се извлече променената дата от файл { $name }, причината е { $reason } core_missing_no_chosen_included_directory = Трябва да се предостави поне една директория core_directory_must_exists = Директории: Предоставеният път до папката трябва да съществува, като се игнорира { $path } core_directory_must_be_directory = Директории: Предоставеният път трябва да сочи към директорията, като не се взема под внимание { $path } core_included_directory_zero_valid_directories = Включена директория ГРЕШКА: Не е намерен дори един правилен път към включената директория, която се изисква core_excluded_directory_pointless_slash = Директории: Изключването на / е безсмислено, защото означава, че няма да бъдат сканирани никакви файлове core_directory_overlap = Директории: Всички директории за търсене се припокриват с изключените директории core_directory_unable_to_get_device_id = Директории: Невъзможно е да се получи идентификатор на устройството от папка { $path } core_ffmpeg_not_found = Cannot find proper installation of FFmpeg, this is external program, that you need to install manually. core_ffmpeg_not_found_windows = Уверете се, че ffmpeg.exe и ffprobe.exe са налични в PATH или са поставени директно в същата папка, където е изпълнимото приложение core_invalid_symlink_infinite_recursion = Безкрайна рекурсия core_invalid_symlink_non_existent_destination = Несъществуващ дестинационен файл czkawka_core-10.0.0/i18n/cs/czkawka_core.ftl000064400000000000000000000042641046102023000167110ustar 00000000000000# Core core_similarity_original = Originál core_similarity_very_high = Velmi vysoká core_similarity_high = Vysoká core_similarity_medium = Střední core_similarity_small = Malá core_similarity_very_small = Velmi malá core_similarity_minimal = Minimální core_cannot_open_dir = Nelze otevřít adresář { $dir }, důvod { $reason } core_cannot_read_entry_dir = Nelze načíst záznam v adresáři { $dir }, důvod { $reason } core_cannot_read_metadata_dir = Nelze načíst metadata v adresáři { $dir }, důvod { $reason } core_file_modified_before_epoch = Soubor { $name } se zdá být upraven před unixovým Epochem (1.1.1970) core_folder_modified_before_epoch = Složka { $name } se zdá být upravena před unixovým Epochem (1.1.1970) core_file_no_modification_date = Nelze získat datum úpravy ze souboru { $name }, důvod { $reason } core_folder_no_modification_date = Nelze získat datum úpravy ze složky { $name }, důvod { $reason } core_missing_no_chosen_included_directory = Musí být uveden alespoň jeden adresář core_directory_must_exists = Adresáře: Poskytnutá cesta ke složce musí existovat, ignoruji { $path } core_directory_must_be_directory = Adresáře: Poskytnutá cesta musí směřovat do adresáře, ignoruji { $path } core_included_directory_zero_valid_directories = CHYBA zahrnutí adresáře: Nenalezena ani jedna správná cesta k zahrnutí, která je vyžadována core_excluded_directory_pointless_slash = Adresáře: Vyloučení / je bezúčelné, protože to znamená, že žádné soubory nebudou naskenovány core_directory_overlap = Adresáře: Všechny adresáře pro vyhledávání se překrývají s vyloučením adresářů core_directory_unable_to_get_device_id = Adresáře: Nelze získat ID zařízení ze složky { $path } core_ffmpeg_not_found = Nelze najít správnou instalaci FFmpeg, toto je externí program, který musíte nainstalovat ručně. core_ffmpeg_not_found_windows = Ujistěte se, že ffmpeg.exe a ffprobe.exe jsou k dispozici v PATH nebo jsou umístěny přímo do stejné složky, kde lze spustit aplikaci core_invalid_symlink_infinite_recursion = Nekonečná rekurze core_invalid_symlink_non_existent_destination = Neexistující cílový soubor czkawka_core-10.0.0/i18n/de/czkawka_core.ftl000064400000000000000000000044571046102023000167000ustar 00000000000000# Core core_similarity_original = Original core_similarity_very_high = Sehr Hoch core_similarity_high = Hoch core_similarity_medium = Mittel core_similarity_small = Klein core_similarity_very_small = Sehr klein core_similarity_minimal = Minimal core_cannot_open_dir = Verzeichnis { $dir } kann nicht geöffnet werden, Grund { $reason } core_cannot_read_entry_dir = Kann Eintrag in Verzeichnis { $dir } nicht lesen, Grund { $reason } core_cannot_read_metadata_dir = Metadaten können in Verzeichnis { $dir } nicht gelesen werden, Grund { $reason } core_file_modified_before_epoch = Datei { $name } scheint vor dieser Unix-Epoche geändert worden zu sein core_folder_modified_before_epoch = Ordner { $name } scheint vor dieser Unix-Epoche geändert worden zu sein core_file_no_modification_date = Konnte das Änderungsdatum von Datei { $name } nicht abrufen, Grund { $reason } core_folder_no_modification_date = Konnte das Änderungsdatum aus dem Ordner { $name } nicht abrufen, Grund { $reason } core_missing_no_chosen_included_directory = Mindestens ein Verzeichnis muss angegeben werden core_directory_must_exists = Verzeichnisse: Der angegebene Ordnerpfad muss existieren, { $path } wird ignoriert core_directory_must_be_directory = Verzeichnisse: Der angegebene Pfad muss auf das Verzeichnis zeigen, { $path } wird ignoriert core_included_directory_zero_valid_directories = Einbezogenes Verzeichnis-FEHLER: Kein korrekter Pfad gefunden, welcher einbezogen werden soll, was erforderlich ist core_excluded_directory_pointless_slash = Verzeichnisse: / auszuschließen ist sinnlos, weil somit keine Dateien gescannt werden core_directory_overlap = Verzeichnisse: Alle zu durchsuchende Verzeichnisse überlappen mit den ausgeschlossenen Verzeichnissen core_directory_unable_to_get_device_id = Verzeichnisse: Geräte-ID kann nicht aus dem Ordner { $path } geholt werden core_ffmpeg_not_found = Es kann keine richtige Installation von FFmpeg gefunden werden, dies ist ein externes Programm, das Sie manuell installieren müssen. core_ffmpeg_not_found_windows = Stellen Sie sicher, dass ffmpeg.exe und ffprobe.exe in PATH verfügbar sind oder direkt in den gleichen Ordner gelegt werden, in dem die App ausführbar ist core_invalid_symlink_infinite_recursion = Endlose Rekursion core_invalid_symlink_non_existent_destination = Nicht existierende Zieldatei czkawka_core-10.0.0/i18n/el/czkawka_core.ftl000064400000000000000000000063141046102023000167020ustar 00000000000000# Core core_similarity_original = Αρχικό core_similarity_very_high = Πολύ Υψηλή core_similarity_high = Υψηλή core_similarity_medium = Μεσαίο core_similarity_small = Μικρό core_similarity_very_small = Πολύ Μικρό core_similarity_minimal = Ελάχιστα core_cannot_open_dir = Αδυναμία ανοίγματος dir { $dir }, λόγος { $reason } core_cannot_read_entry_dir = Αδυναμία ανάγνωσης καταχώρησης στον κατάλογο { $dir }, λόγος { $reason } core_cannot_read_metadata_dir = Αδύνατη η ανάγνωση μεταδεδομένων στον κατάλογο { $dir }, λόγος { $reason } core_file_modified_before_epoch = Το { $name } φαίνεται να τροποποιείται πριν το Unix Epoch core_folder_modified_before_epoch = Ο φάκελος { $name } φαίνεται να τροποποιείται πριν το Unix Epoch core_file_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το αρχείο { $name }, λόγος { $reason } core_folder_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το φάκελο { $name }, λόγος { $reason } core_missing_no_chosen_included_directory = Πρέπει να παρέχεται τουλάχιστον ένας κατάλογος core_directory_must_exists = Κατάλογοι: Η παρεχόμενη διαδρομή φακέλου πρέπει να υπάρχει, αγνοώντας { $path } core_directory_must_be_directory = Κατάλογοι: Παρέχεται διαδρομή πρέπει να δείχνει στον κατάλογο, αγνοώντας { $path } core_included_directory_zero_valid_directories = Συμπεριλαμβανόμενος κατάλογος ΣΦΑΛΜΑ: Δεν βρέθηκε ούτε μια σωστή διαδρομή για να συμπεριληφθεί η οποία απαιτείται core_excluded_directory_pointless_slash = Κατάλογοι: Εξαιρούνται / είναι άσκοπες, επειδή σημαίνει ότι δεν θα σαρωθούν αρχεία core_directory_overlap = Κατάλογοι: Όλοι οι κατάλογοι για αναζήτηση επικαλύψεων με αποκλεισμένους καταλόγους core_directory_unable_to_get_device_id = Κατάλογοι: Αδυναμία λήψης id συσκευής από το φάκελο { $path } core_ffmpeg_not_found = Αδυναμία εύρεσης σωστής εγκατάστασης του FFmpeg, αυτό είναι ένα εξωτερικό πρόγραμμα, το οποίο θα πρέπει να εγκαταστήσετε χειροκίνητα. core_ffmpeg_not_found_windows = Να είστε βέβαιος ότι ffmpeg.exe και ffprobe.exe είναι διαθέσιμα σε PATH ή τίθενται απευθείας στον ίδιο φάκελο όπου είναι εκτελέσιμο app core_invalid_symlink_infinite_recursion = Άπειρη αναδρομή core_invalid_symlink_non_existent_destination = Αρχείο ανύπαρκτου προορισμού czkawka_core-10.0.0/i18n/en/czkawka_core.ftl000064400000000000000000000037571046102023000167140ustar 00000000000000# Core core_similarity_original = Original core_similarity_very_high = Very High core_similarity_high = High core_similarity_medium = Medium core_similarity_small = Small core_similarity_very_small = Very Small core_similarity_minimal = Minimal core_cannot_open_dir = Cannot open dir {$dir}, reason {$reason} core_cannot_read_entry_dir = Cannot read entry in dir {$dir}, reason {$reason} core_cannot_read_metadata_dir = Cannot read metadata in dir {$dir}, reason {$reason} core_file_modified_before_epoch = File {$name} seems to be modified before Unix Epoch core_folder_modified_before_epoch = Folder {$name} seems to be modified before Unix Epoch core_file_no_modification_date = Unable to get modification date from file {$name}, reason {$reason} core_folder_no_modification_date = Unable to get modification date from folder {$name}, reason {$reason} core_missing_no_chosen_included_directory = At least one directory must be provided core_directory_must_exists = Directories: Provided folder path must exist, ignoring { $path } core_directory_must_be_directory = Directories: Provided path must point at the directory, ignoring { $path } core_included_directory_zero_valid_directories = Included Directory ERROR: Not found even one correct path to included which is required core_excluded_directory_pointless_slash = Directories: Excluding / is pointless, because it means that no files will be scanned core_directory_overlap = Directories: All directories to search overlaps with excluded directories core_directory_unable_to_get_device_id = Directories: Unable to get device id from folder { $path } core_ffmpeg_not_found = Cannot find proper installation of FFmpeg, this is external program, that you need to install manually. core_ffmpeg_not_found_windows = Be sure that ffmpeg.exe and ffprobe.exe are available in PATH or are put directly to same folder where is app executable core_invalid_symlink_infinite_recursion = Infinite recursion core_invalid_symlink_non_existent_destination = Non-existent destination fileczkawka_core-10.0.0/i18n/es-ES/czkawka_core.ftl000064400000000000000000000043111046102023000172110ustar 00000000000000# Core core_similarity_original = Original core_similarity_very_high = Muy alta core_similarity_high = Alta core_similarity_medium = Medio core_similarity_small = Pequeño core_similarity_very_small = Muy pequeño core_similarity_minimal = Mínimo core_cannot_open_dir = No se puede abrir el directorio { $dir }, razón { $reason } core_cannot_read_entry_dir = No se puede leer la entrada en directorio { $dir }, razón { $reason } core_cannot_read_metadata_dir = No se pueden leer metadatos en el directorio { $dir }, razón { $reason } core_file_modified_before_epoch = El archivo { $name } parece ser modificado antes de Unix Epoch core_folder_modified_before_epoch = La carpeta { $name } parece ser modificada antes del Epoch Unix core_file_no_modification_date = No se puede obtener la fecha de modificación del archivo { $name }, razón { $reason } core_folder_no_modification_date = No se puede obtener la fecha de modificación de la carpeta { $name }, razón { $reason } core_missing_no_chosen_included_directory = Debe proporcionarse al menos un directorio core_directory_must_exists = Directorios: La ruta de la carpeta debe salir, ignorando { $path } core_directory_must_be_directory = Directorios: La ruta proporcionada debe apuntar al directorio, ignorando { $path } core_included_directory_zero_valid_directories = ERROR del directorio incluido: No se ha encontrado ni una ruta correcta a incluida que es necesaria core_excluded_directory_pointless_slash = Directorios: Excluyendo / es inútil, ya que no se analizarán archivos core_directory_overlap = Directorios: Todos los directorios para buscar superposiciones con directorios excluidos core_directory_unable_to_get_device_id = Directorios: No se puede obtener el id del dispositivo de la carpeta { $path } core_ffmpeg_not_found = No se puede encontrar la instalación correcta de FFmpeg, este es un programa externo, que necesita instalar manualmente. core_ffmpeg_not_found_windows = Asegúrese de que ffmpeg.exe y ffprobe.exe están disponibles en PATH o se colocan directamente en la misma carpeta donde es ejecutable la aplicación core_invalid_symlink_infinite_recursion = Recursión infinita core_invalid_symlink_non_existent_destination = Archivo de destino inexistente czkawka_core-10.0.0/i18n/fr/czkawka_core.ftl000064400000000000000000000045031046102023000167070ustar 00000000000000# Core core_similarity_original = Originale core_similarity_very_high = Très haute core_similarity_high = Haute core_similarity_medium = Moyenne core_similarity_small = Basse core_similarity_very_small = Très basse core_similarity_minimal = Minimale core_cannot_open_dir = Impossible d’ouvrir le répertoire { $dir }. Raison : { $reason } core_cannot_read_entry_dir = Impossible de lire l'entrée dans le répertoire { $dir }. Raison : { $reason } core_cannot_read_metadata_dir = Impossible de lire les métadonnées dans le répertoire { $dir }. Raison  : { $reason } core_file_modified_before_epoch = Le fichier { $name } semble avoir été modifié avant l'epoch Unix core_folder_modified_before_epoch = Le dossier { $name } semble avoir été modifié avant l'epoch Unix core_file_no_modification_date = Impossible d'obtenir la date de modification du fichier { $name }. Raison  : { $reason } core_folder_no_modification_date = Impossible d'obtenir la date de modification du dossier { $name }. Raison : { $reason } core_missing_no_chosen_included_directory = Au moins un répertoire doit être fourni core_directory_must_exists = Répertoires : le chemin du dossier fourni doit exister. { $path } est ignoré core_directory_must_be_directory = Répertoires : le chemin fourni doit pointer vers le répertoire, { $path } est ignoré core_included_directory_zero_valid_directories = ERREUR de répertoire inclus : aucun chemin correct n'a été trouvé alors qu'au moins un est nécessaire core_excluded_directory_pointless_slash = Répertoires: exclure « / » est inutile car cela signifie qu'aucun fichier ne sera scanné core_directory_overlap = Répertoires : tous les répertoires dans lesquels rechercher des chevauchements avec des répertoires exclus core_directory_unable_to_get_device_id = Répertoires : impossible d'obtenir l'ID de l'appareil depuis le dossier { $path } core_ffmpeg_not_found = Impossible de trouver une installation correcte de FFmpeg, c'est un programme externe, que vous devez installer manuellement. core_ffmpeg_not_found_windows = Assurez-vous que ffmpeg.exe et ffprobe.exe sont disponibles dans PATH ou sont présents dans le même dossier que l'exécutable de l'application core_invalid_symlink_infinite_recursion = Récursion infinie core_invalid_symlink_non_existent_destination = Fichier de destination inexistant czkawka_core-10.0.0/i18n/it/czkawka_core.ftl000064400000000000000000000044031046102023000167130ustar 00000000000000# Core core_similarity_original = Originali core_similarity_very_high = Altissima core_similarity_high = Alta core_similarity_medium = Media core_similarity_small = Piccola core_similarity_very_small = Piccolissima core_similarity_minimal = Minima core_cannot_open_dir = Impossibile aprire cartella { $dir }, motivo { $reason } core_cannot_read_entry_dir = Impossibile leggere elemento nella cartella { $dir }, ragione { $reason } core_cannot_read_metadata_dir = Impossibile leggere metadati nella cartella { $dir }, ragione { $reason } core_file_modified_before_epoch = Il file { $name } sembra essere stato modificato prima dell'Epoca Unix core_folder_modified_before_epoch = La cartella { $name } sembra essere stato modificata prima dell'Epoca Unix core_file_no_modification_date = Impossibile recuperare data di modifica dal file { $name }, ragione { $reason } core_folder_no_modification_date = Impossibile recuperare data di modifica dalla cartella { $name }, ragione { $reason } core_missing_no_chosen_included_directory = Almeno una directory deve essere fornita core_directory_must_exists = Directories: Il percorso della cartella fornito deve uscire, ignorando { $path } core_directory_must_be_directory = Directories: Il percorso fornito deve puntare alla directory, ignorando { $path } core_included_directory_zero_valid_directories = ERRORE Directory incluso: Non trovato nemmeno un percorso corretto incluso che è richiesto core_excluded_directory_pointless_slash = Cartelle: Escludere / è inutile, perché significa che nessun file verrà scansionato core_directory_overlap = Directories: Tutte le directory per cercare sovrapposizioni con directory escluse core_directory_unable_to_get_device_id = Directory: non è possibile ottenere l'id del dispositivo dalla cartella { $path } core_ffmpeg_not_found = Impossibile trovare la corretta installazione di FFmpeg, questo è un programma esterno, che è necessario installare manualmente. core_ffmpeg_not_found_windows = Quando si utilizza Windows essere sicuri che ffmpeg.exe e ffprobe.exe sono disponibili in PATH o sono messi direttamente nella stessa cartella dove è eseguibile l'applicazione core_invalid_symlink_infinite_recursion = Ricorsione infinita core_invalid_symlink_non_existent_destination = File di destinazione inesistente czkawka_core-10.0.0/i18n/ja/czkawka_core.ftl000064400000000000000000000050611046102023000166720ustar 00000000000000# Core core_similarity_original = 新規に作成 core_similarity_very_high = 非常に高い core_similarity_high = 高い core_similarity_medium = ミディアム core_similarity_small = 小 core_similarity_very_small = 非常に小さい core_similarity_minimal = 最小 core_cannot_open_dir = ディレクトリを開くことができません { $dir }、理由 { $reason } core_cannot_read_entry_dir = Dir { $dir } でエントリを読み込めません、理由 { $reason } core_cannot_read_metadata_dir = Dir { $dir } でメタデータを読み込めません、理由 { $reason } core_file_modified_before_epoch = ファイル { $name } は Unix Epoch より前に変更されているようです core_folder_modified_before_epoch = フォルダ { $name } は、Unix Epoch の前に変更されているようです core_file_no_modification_date = ファイル { $name } から変更日を取得できません、理由 { $reason } core_folder_no_modification_date = フォルダ { $name } から変更日を取得できません、理由 { $reason } core_missing_no_chosen_included_directory = 少なくとも 1 つのディレクトリを指定する必要があります。 core_directory_must_exists = ディレクトリ: 指定されたフォルダパスは、 { $path } を無視して終了する必要があります core_directory_must_be_directory = ディレクトリ: 指定されたパスはディレクトリを指す必要があります。 { $path } を無視します core_included_directory_zero_valid_directories = 含まれるディレクトリエラー: 必須の正しいパスが1つも見つかりません core_excluded_directory_pointless_slash = ディレクトリ: ファイルがスキャンされないことを意味するため、除外/無意味です core_directory_overlap = ディレクトリ: 除外されたディレクトリとオーバーラップを検索するすべてのディレクトリ core_directory_unable_to_get_device_id = ディレクトリ: フォルダ { $path } からデバイス ID を取得できません core_ffmpeg_not_found = FFmpegの適切なインストールが見つかりません。これは外部プログラムです。手動でインストールする必要があります。 core_ffmpeg_not_found_windows = ffmpeg.exeとffprobe.exeがPATHで使用できることを確認するか、アプリ実行ファイルのある同じフォルダに直接配置してください。 core_invalid_symlink_infinite_recursion = 無限再帰性 core_invalid_symlink_non_existent_destination = 保存先ファイルが存在しません czkawka_core-10.0.0/i18n/ko/czkawka_core.ftl000064400000000000000000000045551046102023000167200ustar 00000000000000# Core core_similarity_original = 원본 core_similarity_very_high = 매우 높음 core_similarity_high = 높음 core_similarity_medium = 보통 core_similarity_small = 낮음 core_similarity_very_small = 매우 낮음 core_similarity_minimal = 최소 core_cannot_open_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason } core_cannot_read_entry_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason } core_cannot_read_metadata_dir = { $dir } 디렉터리의 메타데이터를 열 수 없습니다. 이유: { $reason } core_file_modified_before_epoch = { $name } 파일이 Unix 시간 이전에 수정된 것 같습니다. core_folder_modified_before_epoch = { $name } 폴더가 Unix 시간 이전에 수정된 것 같습니다. core_file_no_modification_date = { $name } 파일의 수정된 시각을 읽을 수 없습니다. 이유: { $reason } core_folder_no_modification_date = { $name } 폴더의 수정된 시각을 읽을 수 없습니다. 이유: { $reason } core_missing_no_chosen_included_directory = 적어도 1개 이상의 디렉터리가 주어져야 합니다. core_directory_must_exists = 디렉터리: 주어진 폴더 경로는 반드시 존재해야 합니다. "{ $path }"는 무시됩니다. core_directory_must_be_directory = 디렉터리: 주어진 경로는 디렉터리를 가리켜야 합니다. "{ $path }"는 무시됩니다. core_included_directory_zero_valid_directories = 검색 대상 디렉터리 오류: 적어도 1개 이상의 유효한 경로가 주어져야 합니다. 유효한 경로가 하나도 없습니다. core_excluded_directory_pointless_slash = 디렉터리: "/"를 제외하는 것은 아무런 파일도 스캔하지 않는다는 것이므로, 의미가 없습니다. core_directory_overlap = 디렉터리: 모든 주어진 경로가 검색 제외 경로와 겹칩니다. core_directory_unable_to_get_device_id = 디렉터리: { $path }의 장치 ID를 가져올 수 없습니다. core_ffmpeg_not_found = Cannot find proper installation of FFmpeg, this is external program, that you need to install manually. core_ffmpeg_not_found_windows = ffmpeg.exe와 ffprobe.exe가 시스템 변수 PATH에서 사용 가능하거나, 이 프로그램의 경로와 같은 곳에 위치하는지 확인하세요. core_invalid_symlink_infinite_recursion = 무한 재귀 core_invalid_symlink_non_existent_destination = 목표 파일이 없음 czkawka_core-10.0.0/i18n/nl/czkawka_core.ftl000064400000000000000000000040071046102023000167100ustar 00000000000000# Core core_similarity_original = Origineel core_similarity_very_high = Zeer hoog core_similarity_high = hoog core_similarity_medium = Middelgroot core_similarity_small = Klein core_similarity_very_small = Zeer Klein core_similarity_minimal = Minimaal core_cannot_open_dir = Kan dir { $dir }niet openen, reden { $reason } core_cannot_read_entry_dir = Kan invoer niet lezen in map { $dir }, reden { $reason } core_cannot_read_metadata_dir = Kan metadata niet lezen in map { $dir }, reden { $reason } core_file_modified_before_epoch = Het bestand { $name } lijkt aangepast te zijn voor Unix Epoch core_folder_modified_before_epoch = Map { $name } lijkt aangepast te zijn voor Unix Epoch core_file_no_modification_date = Niet in staat om de datum van bestand { $name }te krijgen, reden { $reason } core_folder_no_modification_date = Niet in staat om wijzigingsdatum van map { $name }te krijgen, reden { $reason } core_missing_no_chosen_included_directory = Ten minste één map moet worden opgegeven core_directory_must_exists = Maps: Opgegeven mappad moet bestaan, afwijzend { $path } core_directory_must_be_directory = Directories: Het opgegeven pad moet naar de map wijzen, { $path } wordt genegeerd core_included_directory_zero_valid_directories = Inclusief map FOUT: Er is niet één juist pad gevonden naar de map die vereist is core_excluded_directory_pointless_slash = Maps: Uitsluiten/is zinloos, omdat er geen bestanden worden gescand core_directory_overlap = Maps: alle mappen om overlappingen te zoeken met uitgesloten mappen core_directory_unable_to_get_device_id = Maps: Kan apparaat-id niet ophalen uit map { $path } core_ffmpeg_not_found = Kan de juiste installatie van FFmpeg, dit is een extern programma dat u handmatig moet installeren. core_ffmpeg_not_found_windows = Zorg ervoor dat ffmpeg.exe en ffprobe.exe beschikbaar zijn in PATH of direct in dezelfde map geplaatst zijn waar de app uitvoerbaar is core_invalid_symlink_infinite_recursion = Oneindige recursie core_invalid_symlink_non_existent_destination = Niet-bestaand doelbestand czkawka_core-10.0.0/i18n/no/czkawka_core.ftl000064400000000000000000000040051046102023000167110ustar 00000000000000# Core core_similarity_original = Opprinnelig core_similarity_very_high = Veldig høy core_similarity_high = Høy core_similarity_medium = Middels core_similarity_small = Liten core_similarity_very_small = Veldig liten core_similarity_minimal = Minimal core_cannot_open_dir = Kan ikke åpne dir { $dir }, årsak { $reason } core_cannot_read_entry_dir = Kan ikke lese oppføringen i dir { $dir }, årsak { $reason } core_cannot_read_metadata_dir = Kan ikke lese metadata i dir { $dir }, årsak { $reason } core_file_modified_before_epoch = Filen { $name } ser ut til å bli endret før Unix Epoch core_folder_modified_before_epoch = Mappen { $name } ser ut til å bli endret før Unix Epoch core_file_no_modification_date = Klarte ikke å hente endringsdato fra filen { $name }. Årsak { $reason } core_folder_no_modification_date = Klarte ikke å hente endringsdato fra mappen { $name }. Årsak { $reason } core_missing_no_chosen_included_directory = Minst en katalog må angis core_directory_must_exists = Kataloger: Angitt sti for mappe må eksistere. Ignorerer { $path } core_directory_must_be_directory = Kataloger: Angitt sti må peke på mappen. Ignorerer { $path } core_included_directory_zero_valid_directories = Feil med inkludert katalog: Fant ikke én eneste sti til den inkluderte mappen, noe som er påkrevd core_excluded_directory_pointless_slash = Kataloger: Ekskludere / er poengløst, fordi det betyr at ingen filer vil bli skannet core_directory_overlap = Kataloger: Alle kataloger å søke overlapper med ekskluderte mapper core_directory_unable_to_get_device_id = Mapper: Kan ikke hente enhets id fra mappen { $path } core_ffmpeg_not_found = Finner ikke riktig installasjon av FFmpeg, dette er et eksternt program som du må installere manuelt. core_ffmpeg_not_found_windows = Pass på at ffmpeg.exe og ffprobe.exe er tilgjengelig i PATH eller plasseres direkte i samme mappe som appen kan kjøres core_invalid_symlink_infinite_recursion = Uendelig rekursjon core_invalid_symlink_non_existent_destination = Ikke-eksisterende målfil czkawka_core-10.0.0/i18n/pl/czkawka_core.ftl000064400000000000000000000043031046102023000167110ustar 00000000000000# Core core_similarity_original = Oryginalny core_similarity_very_high = Bardzo Duże core_similarity_high = Duże core_similarity_medium = Średnie core_similarity_small = Małe core_similarity_very_small = Bardzo Małe core_similarity_minimal = Minimalne core_cannot_open_dir = Nie można otworzyć folderu { $dir }, powód { $reason } core_cannot_read_entry_dir = Nie można odczytać danych z folderu { $dir }, powód { $reason } core_cannot_read_metadata_dir = Nie można odczytać metadanych folderu { $dir }, powód { $reason } core_file_modified_before_epoch = Plik { $name } ma datę modyfikacji sprzed epoki unixa core_folder_modified_before_epoch = Folder { $name } ma datę modyfikacji sprzed epoki unixa core_file_no_modification_date = Nie udało się pobrać daty modyfikacji z pliku { $name }, powód { $reason } core_folder_no_modification_date = Nie udało się pobrać daty modyfikacji z folderu { $name }, powód { $reason } core_missing_no_chosen_included_directory = Należy podać co najmniej jeden katalog core_directory_must_exists = Katalogi: Podana ścieżka do folderu musi istnieć, ignorowanie { $path } core_directory_must_be_directory = Katalogi: Podana ścieżka musi wskazywać na katalog, ignorowanie { $path } core_included_directory_zero_valid_directories = Błąd katalogów do przeszukiwania: Nie znaleziono nawet jednej poprawnej ścieżki do przeszukania core_excluded_directory_pointless_slash = Katalogi: Wykluczanie folderu / jest bezcelowe, ponieważ oznacza to, że żadne pliki nie zostaną sprawdzone core_directory_overlap = Katalogi: Wszystkie katalogi do wyszukiwania pokrywają się z wykluczonymi core_directory_unable_to_get_device_id = Katalogi: Nie można uzyskać identyfikatora urządzenia z folderu { $path } core_ffmpeg_not_found = Nie można znaleźć poprawnej instalacji FFmpeg, jest to zewnętrzny program, który musisz zainstalować ręcznie. core_ffmpeg_not_found_windows = Upewnij się, że ffmpeg.exe i ffprobe.exe są dostępne w PATH lub są umieszczone bezpośrednio w tym samym folderze, w którym aplikacja jest uruchamiana. core_invalid_symlink_infinite_recursion = Nieskończona rekurencja core_invalid_symlink_non_existent_destination = Nieistniejący docelowy plik czkawka_core-10.0.0/i18n/pt-BR/czkawka_core.ftl000064400000000000000000000044321046102023000172250ustar 00000000000000# Core core_similarity_original = Original core_similarity_very_high = Muito alto core_similarity_high = alta core_similarity_medium = Média core_similarity_small = Pequeno core_similarity_very_small = Muito Pequeno core_similarity_minimal = Mínimo core_cannot_open_dir = Não é possível abrir o dir { $dir }, razão { $reason } core_cannot_read_entry_dir = Não é possível ler a entrada no diretório { $dir }, razão { $reason } core_cannot_read_metadata_dir = Não é possível ler os metadados no diretório { $dir }, razão { $reason } core_file_modified_before_epoch = O arquivo { $name } parece ser modificado antes do Epoch Unix core_folder_modified_before_epoch = Pasta { $name } parece ser modificada antes do Epoch Unix core_file_no_modification_date = Não é possível obter a data de modificação do arquivo { $name }, motivo { $reason } core_folder_no_modification_date = Não é possível obter a data de modificação da pasta { $name }, motivo { $reason } core_missing_no_chosen_included_directory = Pelo menos um diretório deve ser fornecido core_directory_must_exists = Diretórios: Caminho da pasta fornecida deve existir, ignorando { $path } core_directory_must_be_directory = Directorias: Caminho fornecido deve apontar para o diretório, ignorando { $path } core_included_directory_zero_valid_directories = ERRO do Diretório incluído: Não foi encontrado nenhum caminho correto que é necessário incluir core_excluded_directory_pointless_slash = Directorias: Excluir / não faz sentido, porque significa que nenhum arquivo será escaneado core_directory_overlap = Diretórios: Todos os diretórios para pesquisar sobreposições com diretórios excluídos core_directory_unable_to_get_device_id = Directorias: Não foi possível obter o dispositivo de ajuda da pasta { $path } core_ffmpeg_not_found = Não foi possível encontrar a instalação adequada do FFmpeg, este é um programa externo, que você precisa instalar manualmente. core_ffmpeg_not_found_windows = Certifique-se de que o ffmpeg.exe e ffprobe.exe estão disponíveis no PATH ou são colocados diretamente na mesma pasta onde o aplicativo é executável core_invalid_symlink_infinite_recursion = Ocorreu um erro de execução na recursão infinita core_invalid_symlink_non_existent_destination = O arquivo de destino não existe czkawka_core-10.0.0/i18n/pt-PT/czkawka_core.ftl000064400000000000000000000043701046102023000172460ustar 00000000000000# Core core_similarity_original = Original core_similarity_very_high = Muito alto core_similarity_high = Alto core_similarity_medium = Média core_similarity_small = Pequeno core_similarity_very_small = Muito Pequeno core_similarity_minimal = Mínimo core_cannot_open_dir = Não é possível abrir o diretório { $dir }, razão { $reason } core_cannot_read_entry_dir = Não é possível ler a entrada no diretório { $dir }, razão { $reason } core_cannot_read_metadata_dir = Não é possível ler os metadados no diretório { $dir }, razão { $reason } core_file_modified_before_epoch = Arquivo { $name } parece ser modificado antes do Epoch Unix core_folder_modified_before_epoch = A pasta { $name } parece ser modificada antes do Epoch Unix core_file_no_modification_date = Não foi possível obter a data de modificação do arquivo { $name }, motivo { $reason } core_folder_no_modification_date = Não foi possível obter a data de modificação da pasta { $name }, motivo { $reason } core_missing_no_chosen_included_directory = Pelo menos um diretório deve ser fornecido core_directory_must_exists = Directórios: Caminho da pasta fornecida deve sair, ignorando { $path } core_directory_must_be_directory = Diretórios: Caminho fornecido deve apontar para o diretório, ignorando { $path } core_included_directory_zero_valid_directories = ERRO do Diretório incluído: Não foi encontrado nenhum caminho correto que é necessário incluir core_excluded_directory_pointless_slash = Directorias: Excluir / não faz sentido, porque significa que nenhum arquivo será escaneado core_directory_overlap = Diretórios: Todos os diretórios para pesquisar sobreposições com diretórios excluídos core_directory_unable_to_get_device_id = Directorias: Não foi possível obter o dispositivo id da pasta { $path } core_ffmpeg_not_found = Não foi possível encontrar a instalação adequada do FFmpeg, este é um programa externo, que você precisa instalar manualmente. core_ffmpeg_not_found_windows = Certifique-se de que o ffmpeg.exe e ffprobe.exe estão disponíveis no PATH ou são colocados diretamente na mesma pasta onde o aplicativo é executável core_invalid_symlink_infinite_recursion = Recursão infinita core_invalid_symlink_non_existent_destination = Arquivo de destino não existe czkawka_core-10.0.0/i18n/ro/czkawka_core.ftl000064400000000000000000000042631046102023000167230ustar 00000000000000# Core core_similarity_original = Originală core_similarity_very_high = Foarte Mare core_similarity_high = Ridicat core_similarity_medium = Medie core_similarity_small = Mică core_similarity_very_small = Foarte mic core_similarity_minimal = Minimă core_cannot_open_dir = Nu se poate deschide dir { $dir }, motiv { $reason } core_cannot_read_entry_dir = Nu se poate citi intrarea în dir { $dir }, motivul { $reason } core_cannot_read_metadata_dir = Metadatele nu pot fi citite în dir { $dir }, motivul { $reason } core_file_modified_before_epoch = Fișierul { $name } pare să fie modificat înainte de Epoch Unix core_folder_modified_before_epoch = Dosarul { $name } pare să fie modificat înainte de Epoc Unix core_file_no_modification_date = Imposibil de obținut data modificării din fișierul { $name }, motivul { $reason } core_folder_no_modification_date = Imposibil de obținut data modificării din dosarul { $name }, motivul { $reason } core_missing_no_chosen_included_directory = Trebuie furnizat cel puțin un director core_directory_must_exists = Directoare: Calea dosarului furnizat trebuie să existe, ignorând { $path } core_directory_must_be_directory = Directoare: Calea specificată trebuie să indice în director, ignorând { $path } core_included_directory_zero_valid_directories = EROARE din Director inclusă: Nici măcar o cale corectă de inclus, care este necesară core_excluded_directory_pointless_slash = Directoare: Excludere / este inutilă, deoarece înseamnă că niciun fișier nu va fi scanat core_directory_overlap = Directoare: Toate directoarele pentru a căuta suprapuneri cu directoarele excluse core_directory_unable_to_get_device_id = Directoare: Imposibil de obținut ID-ul dispozitivului din folderul { $path } core_ffmpeg_not_found = Nu se poate găsi instalarea corectă a FFmpeg, acesta este un program extern, pe care trebuie să-l instalați manual. core_ffmpeg_not_found_windows = Asigurați-vă că ffmpeg.exe și ffprobe.exe sunt disponibile în PATH sau sunt puse direct în același folder unde este executabilă aplicația core_invalid_symlink_infinite_recursion = Recepţie infinită core_invalid_symlink_non_existent_destination = Fișier destinație inexistent czkawka_core-10.0.0/i18n/ru/czkawka_core.ftl000064400000000000000000000063331046102023000167310ustar 00000000000000# Core core_similarity_original = Оригинальное core_similarity_very_high = Очень высокое core_similarity_high = Высокое core_similarity_medium = Среднее core_similarity_small = Низкое core_similarity_very_small = Очень низкое core_similarity_minimal = Минимальное core_cannot_open_dir = Невозможно открыть каталог { $dir }, причина: { $reason } core_cannot_read_entry_dir = Невозможно прочитать запись в директории { $dir }, причина: { $reason } core_cannot_read_metadata_dir = Невозможно прочитать метаданные в директории { $dir }, причина: { $reason } core_file_modified_before_epoch = Файл { $name }, кажется, изменён до начала эпохи Unix core_folder_modified_before_epoch = Папка { $name }, кажется, изменена до начала эпохи Unix core_file_no_modification_date = Не удаётся получить дату изменения из файла { $name }, причина: { $reason } core_folder_no_modification_date = Не удаётся получить дату изменения из папки { $name }, причина: { $reason } core_missing_no_chosen_included_directory = Должен быть указан хотя бы один каталог core_directory_must_exists = Директории: Указанный путь к папке должен существовать, будет проигнорирован{ $path } core_directory_must_be_directory = Директории: Указанный путь должен указывать на директорию, будет проигнорирован { $path } core_included_directory_zero_valid_directories = Включённый каталог, ОШИБКА: Не найдено ни одного корректного пути для включения в список поиска — обязательно добавить хотя бы один core_excluded_directory_pointless_slash = Директории: Исключение корневой папки «/» бессмысленно, потому что в таком случае ни один файл не будет просканирован core_directory_overlap = Каталоги: Все директории для поиска также присутствуют в списке исключённых каталогов core_directory_unable_to_get_device_id = Каталоги: Не удалось получить идентификатор устройства из папки { $path } core_ffmpeg_not_found = Не удается найти подходящую установку FFmpeg, это внешняя программа, которую необходимо установить вручную. core_ffmpeg_not_found_windows = Убедитесь, что ffmpeg.exe и ffprobe.exe доступны в PATH или находятся в той же папке, где это исполняемый файл core_invalid_symlink_infinite_recursion = Бесконечная рекурсия core_invalid_symlink_non_existent_destination = Не найден конечный файл czkawka_core-10.0.0/i18n/sv-SE/czkawka_core.ftl000064400000000000000000000041151046102023000172340ustar 00000000000000# Core core_similarity_original = Ursprunglig core_similarity_very_high = Mycket Hög core_similarity_high = Hög core_similarity_medium = Mellan core_similarity_small = Litet core_similarity_very_small = Väldigt Liten core_similarity_minimal = Minimalt core_cannot_open_dir = Kan inte öppna dir { $dir }anledning { $reason } core_cannot_read_entry_dir = Kan inte läsa post i dir { $dir }, anledning { $reason } core_cannot_read_metadata_dir = Kan inte läsa metadata i dir { $dir }, anledning { $reason } core_file_modified_before_epoch = Filen { $name } verkar ändras innan Unix Epoch core_folder_modified_before_epoch = Mappen { $name } verkar ändras innan Unix Epoch core_file_no_modification_date = Det går inte att hämta ändringsdatum från filen { $name }, anledning { $reason } core_folder_no_modification_date = Det går inte att hämta ändringsdatum från mappen { $name }, anledning { $reason } core_missing_no_chosen_included_directory = Minst en katalog måste tillhandahållas core_directory_must_exists = Kataloger: Tillhandahållen mappsökväg måste finnas, ignorerar { $path } core_directory_must_be_directory = Kataloger: Tillhandahållen sökväg måste peka på katalogen, ignorerar { $path } core_included_directory_zero_valid_directories = Inkluderad katalog FEL: Hittas inte ens en korrekt sökväg till inkluderad som krävs core_excluded_directory_pointless_slash = Kataloger: Exklusive / är meningslös, eftersom det innebär att inga filer kommer att skannas core_directory_overlap = Kataloger: Alla kataloger att söka överlappar med uteslutna kataloger core_directory_unable_to_get_device_id = Kataloger: Det går inte att hämta enhets-id från mappen { $path } core_ffmpeg_not_found = Kan inte hitta rätt installation av FFmpeg, detta är externt program, som du behöver installera manuellt. core_ffmpeg_not_found_windows = Se till att ffmpeg.exe och ffprobe.exe är tillgängliga i PATH eller sätts direkt till samma mapp där är app körbar core_invalid_symlink_infinite_recursion = Oändlig recursion core_invalid_symlink_non_existent_destination = Icke-existerande målfil czkawka_core-10.0.0/i18n/tr/czkawka_core.ftl000064400000000000000000000044161046102023000167300ustar 00000000000000# Core core_similarity_original = Asıl core_similarity_very_high = Çok Yüksek core_similarity_high = Yüksek core_similarity_medium = Orta core_similarity_small = Düşük core_similarity_very_small = Çok Düşük core_similarity_minimal = Aşırı Düşük core_cannot_open_dir = { $dir } dizini açılamıyor, nedeni: { $reason } core_cannot_read_entry_dir = { $dir } dizinindeki girdi okunamıyor, nedeni: { $reason } core_cannot_read_metadata_dir = { $dir } dizinindeki metaveri okunamıyor, nedei: { $reason } core_file_modified_before_epoch = { $name } dosyası Unix Epoch'tan önce değiştirilmiş gibi görünüyor. core_folder_modified_before_epoch = { $name } klasörü Unix Epoch'tan önce değiştirilmiş gibi görünüyor. core_file_no_modification_date = { $name } dosyasının değişiklik tarihine erişilemiyor, nedeni: { $reason } core_folder_no_modification_date = { $name } klasörünün değişiklik tarihine erişilemiyor, nedeni: { $reason } core_missing_no_chosen_included_directory = "Aranacak Dizinler" listesinde en az bir dizin yer almalıdır. core_directory_must_exists = Dizinler: Girilen klasör yolu var olmalı, { $path } yok sayıldı. core_directory_must_be_directory = Dizinler: Girilen yol bir dizini göstermelidir, { $path } yok sayıldı. core_included_directory_zero_valid_directories = "Aranacak Dizinler" listesinde HATA: Tarama yapılması için gerekli olan tek bir doğru yol bile bulunamadı. core_excluded_directory_pointless_slash = Dizinler: "/" kök dizinini hariç tutmak anlamsızdır, çünkü bu hiçbir dosyanın taranmayacağı anlamına gelir. core_directory_overlap = Dizinler: Aranacak tüm dizinler, hariç tutulan dizinlerle çakışıyor. core_directory_unable_to_get_device_id = Dizinler: { $path } klasörünün aygıt kimliği bilgisine erişilemiyor. core_ffmpeg_not_found = Cannot find proper installation of FFmpeg, this is external program, that you need to install manually. core_ffmpeg_not_found_windows = "ffmpeg(.exe)" ve "ffprobe(.exe)" uygulamalarının PATH dizininde ya da uygulamanın doğrudan yürütüldüğü dizinde yer aldığından ve 'yürütülebilir' olarak işaretlendiğinden emin olun. core_invalid_symlink_infinite_recursion = Sonsuz özyineleme core_invalid_symlink_non_existent_destination = Var olmayan hedef dosya czkawka_core-10.0.0/i18n/uk/czkawka_core.ftl000064400000000000000000000061701046102023000167210ustar 00000000000000# Core core_similarity_original = Оригінал core_similarity_very_high = Дуже висока core_similarity_high = Висока core_similarity_medium = Середня core_similarity_small = Низька core_similarity_very_small = Дуже низька core_similarity_minimal = Мінімальна core_cannot_open_dir = Не вдалося відкрити каталог { $dir }, причина: { $reason } core_cannot_read_entry_dir = Не вдалося прочитати запис в каталозі { $dir }, причина: { $reason } core_cannot_read_metadata_dir = Не вдалося прочитати метадані в каталозі { $dir }, причина: { $reason } core_file_modified_before_epoch = Файл { $name }, здається, змінено до початку епохи Unix core_folder_modified_before_epoch = Папка { $name }, здається, змінена до початку епохи Unix core_file_no_modification_date = Не вдалося отримати дату модифікації з файлу { $name }, причина: { $reason } core_folder_no_modification_date = Не вдалося отримати дату модифікації з каталогу { $name }, причина: { $reason } core_missing_no_chosen_included_directory = Необхідно вказати принаймні один каталог core_directory_must_exists = Директорії: Вказаний шлях до папки має існувати, буде проігнорован { $path } core_directory_must_be_directory = Директорії: Вказаний шлях повинен вказувати на директорію, буде проігнорован { $path } core_included_directory_zero_valid_directories = Включений каталог, ПОМИЛКА: Не знайдено жодного коректного шляху для включення до списку пошуку — обов'язково додати хоча б один core_excluded_directory_pointless_slash = Директорії: Виключення кореневого каталогу «/» не має сенсу, тому що в такому разі жоден файл не буде просканований core_directory_overlap = Каталоги: Усі директорії для пошуку також присутні у списку виключених каталогів core_directory_unable_to_get_device_id = Каталоги: Не вдалося отримати ідентифікатор пристрою з папки { $path } core_ffmpeg_not_found = Не вдається встановити FFmpeg, це зовнішня програма, яку потрібно встановити вручну. core_ffmpeg_not_found_windows = Будьте впевнені, що ffmpeg.exe і ffprobe.exe доступні в PATH або прямо в тій же папці, де є виконуваний додаток core_invalid_symlink_infinite_recursion = Нескінченна рекурсія core_invalid_symlink_non_existent_destination = Неіснуючий файл призначення czkawka_core-10.0.0/i18n/zh-CN/czkawka_core.ftl000064400000000000000000000036451046102023000172250ustar 00000000000000# Core core_similarity_original = 原版 core_similarity_very_high = 非常高 core_similarity_high = 高 core_similarity_medium = 中 core_similarity_small = 小的 core_similarity_very_small = 非常小 core_similarity_minimal = 最小化 core_cannot_open_dir = 无法打开目录 { $dir },因为 { $reason } core_cannot_read_entry_dir = 无法在目录 { $dir } 中读取条目,因为 { $reason } core_cannot_read_metadata_dir = 无法读取目录 { $dir } 中的元数据,因为 { $reason } core_file_modified_before_epoch = 文件 { $name } 似乎在 Unix Epoch 之前被修改过 core_folder_modified_before_epoch = 文件夹 { $name } 似乎在 Unix Epoch 之前被修改过 core_file_no_modification_date = 无法从文件 { $name } 获取修改日期,因为 { $reason } core_folder_no_modification_date = 无法从文件夹 { $name } 获取修改日期,因为 { $reason } core_missing_no_chosen_included_directory = 必须至少提供一个目录 core_directory_must_exists = 目录:提供的文件夹路径必须退出,忽略 { $path } core_directory_must_be_directory = 目录:提供的路径必须指向目录,忽略 { $path } core_included_directory_zero_valid_directories = 包括目录错误:即使找不到一个需要包含的正确路径 core_excluded_directory_pointless_slash = 目录:不包括 / 无意义,因为它意味着没有文件将被扫描 core_directory_overlap = 目录:所有要搜索与排除目录重叠的目录 core_directory_unable_to_get_device_id = 目录:无法从文件夹 { $path } 获取设备 id core_ffmpeg_not_found = 找不到正确安装FFmpeg的程序,这是您需要手动安装的外部程序。 core_ffmpeg_not_found_windows = 请确保 ffmpeg.exe 和 ffprobe.exe 在 PATH 中可用,或者直接放入应用可执行文件的同一文件夹中 core_invalid_symlink_infinite_recursion = 无限递归性 core_invalid_symlink_non_existent_destination = 目标文件不存在 czkawka_core-10.0.0/i18n/zh-TW/czkawka_core.ftl000064400000000000000000000037311046102023000172530ustar 00000000000000# Core core_similarity_original = 原始 core_similarity_very_high = 極高 core_similarity_high = 高 core_similarity_medium = 中等 core_similarity_small = 小 core_similarity_very_small = 非常小 core_similarity_minimal = 最小 core_cannot_open_dir = 無法開啟目錄 { $dir },原因是 { $reason } core_cannot_read_entry_dir = 無法讀取目錄 { $dir } 中的項目,原因是 { $reason } core_cannot_read_metadata_dir = 無法讀取目錄 { $dir } 的中繼資料,原因是 { $reason } core_file_modified_before_epoch = 檔案 { $name } 似乎在 Unix Epoch 之前就已被修改 core_folder_modified_before_epoch = 資料夾 { $name } 似乎在 Unix Epoch 之前就已被修改 core_file_no_modification_date = 無法取得檔案 { $name } 的修改日期,原因是 { $reason } core_folder_no_modification_date = 無法取得資料夾 { $name } 的修改日期,原因是 { $reason } core_missing_no_chosen_included_directory = 必須至少選擇一個目錄 core_directory_must_exists = 目錄:所提供的資料夾路徑必須存在,已忽略 { $path } core_directory_must_be_directory = 目錄:所提供的路徑必須為目錄,已忽略 { $path } core_included_directory_zero_valid_directories = 包含目錄錯誤:未找到任何有效的包含路徑 core_excluded_directory_pointless_slash = 目錄:排除 / 是無意義的,因為這意味著不會有任何檔案被掃描 core_directory_overlap = 目錄:所有搜尋目錄與排除目錄均有重疊 core_directory_unable_to_get_device_id = 目錄:無法從資料夾 { $path } 取得裝置 ID core_ffmpeg_not_found = Cannot find proper installation of FFmpeg, this is external program, that you need to install manually. core_ffmpeg_not_found_windows = 請確保 ffmpeg.exe 和 ffprobe.exe 在 PATH 中可用,或者直接將它們放在與可執行應用程式的同一個資料夾中 core_invalid_symlink_infinite_recursion = 無限遞迴 core_invalid_symlink_non_existent_destination = 目標檔案不存在 czkawka_core-10.0.0/i18n.toml000064400000000000000000000007111046102023000140200ustar 00000000000000# (Required) The language identifier of the language used in the # source code for gettext system, and the primary fallback language # (for which all strings must be present) when using the fluent # system. fallback_language = "en" # Use the fluent localization system. [fluent] # (Required) The path to the assets directory. # The paths inside the assets directory should be structured like so: # `assets_dir/{language}/{domain}.ftl` assets_dir = "i18n" czkawka_core-10.0.0/src/common/cache.rs000064400000000000000000000242111046102023000160350ustar 00000000000000#![allow(clippy::useless_let_if_seq)] use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io::{BufReader, BufWriter}; use std::path::Path; use bincode::Options; use fun_time::fun_time; use humansize::{BINARY, format_size}; use log::{debug, error}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use crate::common::config_cache_path::open_cache_folder; use crate::common::traits::ResultEntry; use crate::helpers::messages::Messages; pub(crate) const CACHE_VERSION: &str = "100"; pub(crate) const CACHE_DUPLICATE_VERSION: &str = "100"; pub(crate) const CACHE_IMAGE_VERSION: &str = "100"; pub(crate) const CACHE_VIDEO_VERSION: &str = "100"; const MEMORY_LIMIT: u64 = 8 * 1024 * 1024 * 1024; fn get_cache_size(file_name: &Path) -> String { fs::metadata(file_name).map_or_else(|_| "".to_string(), |metadata| format_size(metadata.len(), BINARY)) } #[fun_time(message = "save_cache_to_file_generalized", level = "debug")] pub fn save_cache_to_file_generalized(cache_file_name: &str, hashmap: &BTreeMap, save_also_as_json: bool, minimum_file_size: u64) -> Messages where T: Serialize + ResultEntry + Sized + Send + Sync, { let mut text_messages = Messages::new(); if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(cache_file_name, true, save_also_as_json, &mut text_messages.warnings) { let hashmap_to_save = hashmap.values().filter(|t| t.get_size() >= minimum_file_size).collect::>(); { let writer = BufWriter::new(file_handler.expect("Cannot fail, because for saving, this always exists")); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); if let Err(e) = options.serialize_into(writer, &hashmap_to_save) { text_messages.warnings.push(format!("Cannot write data to cache file {cache_file:?}, reason {e}")); debug!("Failed to save cache to file {cache_file:?} - {e}"); return text_messages; } debug!("Saved cache to binary file {cache_file:?} with size {}", get_cache_size(&cache_file)); } if save_also_as_json { if let Some(file_handler_json) = file_handler_json { let writer = BufWriter::new(file_handler_json); if let Err(e) = serde_json::to_writer(writer, &hashmap_to_save) { text_messages.warnings.push(format!("Cannot write data to cache file {cache_file_json:?}, reason {e}")); debug!("Failed to save cache to file {cache_file_json:?} - {e}"); return text_messages; } debug!("Saved cache to json file {cache_file_json:?} with size {}", get_cache_size(&cache_file_json)); } } text_messages.messages.push(format!("Properly saved to file {} cache entries.", hashmap.len())); debug!("Properly saved to file {} cache entries.", hashmap.len()); } else { debug!("Failed to save cache to file {cache_file_name} because not exists"); } text_messages } pub(crate) fn extract_loaded_cache( loaded_hash_map: &BTreeMap, files_to_check: BTreeMap, records_already_cached: &mut BTreeMap, non_cached_files_to_check: &mut BTreeMap, ) where T: Clone, { for (name, file_entry) in files_to_check { if let Some(cached_file_entry) = loaded_hash_map.get(&name) { records_already_cached.insert(name, cached_file_entry.clone()); } else { non_cached_files_to_check.insert(name, file_entry); } } } #[fun_time(message = "load_cache_from_file_generalized_by_path", level = "debug")] pub fn load_cache_from_file_generalized_by_path(cache_file_name: &str, delete_outdated_cache: bool, used_files: &BTreeMap) -> (Messages, Option>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, { let check_file = |file_entry: &T| { let file_entry_path_str = file_entry.get_path().to_string_lossy(); if let Some(used_file) = used_files.get(file_entry_path_str.as_ref()) { if file_entry.get_size() != used_file.get_size() { return false; } if file_entry.get_modified_date() != used_file.get_modified_date() { return false; } } true }; let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file); let Some(vec_loaded_entries) = vec_loaded_cache else { return (text_messages, None); }; debug!("Converting cache Vec into BTreeMap"); let map_loaded_entries: BTreeMap = vec_loaded_entries .into_iter() .map(|file_entry| (file_entry.get_path().to_string_lossy().into_owned(), file_entry)) .collect(); debug!("Converted cache Vec into BTreeMap"); (text_messages, Some(map_loaded_entries)) } #[fun_time(message = "load_cache_from_file_generalized_by_size", level = "debug")] pub fn load_cache_from_file_generalized_by_size( cache_file_name: &str, delete_outdated_cache: bool, cache_not_converted: &BTreeMap>, ) -> (Messages, Option>>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, { debug!("Converting cache BtreeMap> into HashMap"); let used_files: HashMap = cache_not_converted .iter() .flat_map(|(size, vec)| { vec.iter() .map(move |file_entry| (file_entry.get_path().to_string_lossy().into_owned(), (*size, file_entry.get_modified_date()))) }) .collect(); debug!("Converted cache BtreeMap> into HashMap"); let check_file = |file_entry: &T| { let file_entry_path_str = file_entry.get_path().to_string_lossy(); if let Some((size, modification_date)) = used_files.get(file_entry_path_str.as_ref()) { if file_entry.get_size() != *size { return false; } if file_entry.get_modified_date() != *modification_date { return false; } } true }; let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file); let Some(vec_loaded_entries) = vec_loaded_cache else { return (text_messages, None); }; debug!("Converting cache Vec into BTreeMap>"); let mut map_loaded_entries: BTreeMap> = Default::default(); for file_entry in vec_loaded_entries { map_loaded_entries.entry(file_entry.get_size()).or_default().push(file_entry); } debug!("Converted cache Vec into BTreeMap>"); (text_messages, Some(map_loaded_entries)) } #[fun_time(message = "load_cache_from_file_generalized", level = "debug")] fn load_cache_from_file_generalized(cache_file_name: &str, delete_outdated_cache: bool, check_func: F) -> (Messages, Option>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, F: Fn(&T) -> bool + Send + Sync, { let mut text_messages = Messages::new(); if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(cache_file_name, false, true, &mut text_messages.warnings) { let cache_full_name; let mut vec_loaded_entries: Vec; if let Some(file_handler) = file_handler { cache_full_name = cache_file.clone(); let reader = BufReader::new(file_handler); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); vec_loaded_entries = match options.deserialize_from(reader) { Ok(t) => t, Err(e) => { text_messages.warnings.push(format!("Failed to load data from cache file {cache_file:?}, reason {e}")); error!("Failed to load cache from file {cache_file:?} - {e}"); return (text_messages, None); } }; } else { cache_full_name = cache_file_json.clone(); let reader = BufReader::new(file_handler_json.expect("This cannot fail, because if file_handler is None, then this cannot be None")); vec_loaded_entries = match serde_json::from_reader(reader) { Ok(t) => t, Err(e) => { text_messages .warnings .push(format!("Failed to load data from json cache file {cache_file_json:?}, reason {e}")); debug!("Failed to load cache from file {cache_file:?} - {e}"); return (text_messages, None); } }; } debug!("Starting removing outdated cache entries (removing non existent files from cache - {delete_outdated_cache})"); let initial_number_of_entries = vec_loaded_entries.len(); vec_loaded_entries = vec_loaded_entries .into_par_iter() .filter(|file_entry| { if !check_func(file_entry) { return false; } if delete_outdated_cache && !file_entry.get_path().exists() { return false; } true }) .collect(); debug!( "Completed removing outdated cache entries, removed {} out of all {} entries", initial_number_of_entries - vec_loaded_entries.len(), initial_number_of_entries ); text_messages.messages.push(format!("Properly loaded {} cache entries.", vec_loaded_entries.len())); debug!( "Loaded cache from file {cache_file_name} (or json alternative) - {} results - size {}", vec_loaded_entries.len(), get_cache_size(&cache_full_name) ); return (text_messages, Some(vec_loaded_entries)); } debug!("Failed to load cache from file {cache_file_name} because not exists"); (text_messages, None) } czkawka_core-10.0.0/src/common/config_cache_path.rs000064400000000000000000000144631046102023000204060ustar 00000000000000use std::fs::{File, OpenOptions}; use std::path::PathBuf; use std::{env, fs}; use directories_next::ProjectDirs; use log::{info, warn}; use once_cell::sync::OnceCell; static CONFIG_CACHE_PATH: OnceCell> = OnceCell::new(); #[derive(Debug, Clone)] pub struct ConfigCachePath { pub config_folder: PathBuf, pub cache_folder: PathBuf, } pub fn get_config_cache_path() -> Option { CONFIG_CACHE_PATH.get().expect("Cannot fail if set_config_cache_path was called before").clone() } pub fn set_config_cache_path(cache_name: &'static str, config_name: &'static str) -> (Vec, Vec) { // By default, such folders are used: // Lin: /home/username/.config/czkawka // Win: C:\Users\Username\AppData\Roaming\Qarmin\Czkawka\config // Mac: /Users/Username/Library/Application Support/pl.Qarmin.Czkawka let mut infos = vec![]; let mut warnings = vec![]; let config_folder_env = env::var("CZKAWKA_CONFIG_PATH").unwrap_or_default().trim().to_string(); let cache_folder_env = env::var("CZKAWKA_CACHE_PATH").unwrap_or_default().trim().to_string(); let default_cache_folder = ProjectDirs::from("pl", "Qarmin", cache_name).map(|proj_dirs| proj_dirs.cache_dir().to_path_buf()); let default_config_folder = ProjectDirs::from("pl", "Qarmin", config_name).map(|proj_dirs| proj_dirs.config_dir().to_path_buf()); let mut resolve_folder = |env_var: &str, default_folder: Option, name: &'static str| { let default_folder_str = default_folder.as_ref().map_or("".to_string(), |t| t.to_string_lossy().to_string()); if env_var.is_empty() { default_folder } else { let folder_path = PathBuf::from(env_var); let _ = fs::create_dir_all(&folder_path); if !folder_path.exists() { warnings.push(format!( "{name} folder \"{}\" does not exist, using default folder \"{}\"", folder_path.to_string_lossy(), default_folder_str )); return default_folder; }; if !folder_path.is_dir() { warnings.push(format!( "{name} folder \"{}\" is not a directory, using default folder \"{}\"", folder_path.to_string_lossy(), default_folder_str )); return default_folder; } match dunce::canonicalize(folder_path) { Ok(t) => Some(t), Err(_e) => { warnings.push(format!( "Cannot canonicalize {} folder \"{}\", using default folder \"{}\"", name.to_ascii_lowercase(), env_var, default_folder_str )); default_folder } } } }; let config_folder = resolve_folder(&config_folder_env, default_config_folder, "Config"); let cache_folder = resolve_folder(&cache_folder_env, default_cache_folder, "Cache"); let config_cache_path = if let (Some(config_folder), Some(cache_folder)) = (config_folder, cache_folder) { infos.push(format!( "Config folder set to \"{}\" and cache folder set to \"{}\"", config_folder.to_string_lossy(), cache_folder.to_string_lossy() )); if !config_folder.exists() { if let Err(e) = fs::create_dir_all(&config_folder) { warnings.push(format!("Cannot create config folder \"{}\", reason {e}", config_folder.to_string_lossy())); } } if !cache_folder.exists() { if let Err(e) = fs::create_dir_all(&cache_folder) { warnings.push(format!("Cannot create cache folder \"{}\", reason {e}", cache_folder.to_string_lossy())); } } Some(ConfigCachePath { config_folder, cache_folder }) } else { warnings.push("Cannot set config/cache path - config and cache will not be used.".to_string()); None }; CONFIG_CACHE_PATH.set(config_cache_path).expect("Cannot set config/cache path twice"); (infos, warnings) } pub(crate) fn open_cache_folder( cache_file_name: &str, save_to_cache: bool, use_json: bool, warnings: &mut Vec, ) -> Option<((Option, PathBuf), (Option, PathBuf))> { let cache_dir = get_config_cache_path()?.cache_folder; let cache_file = cache_dir.join(cache_file_name); let cache_file_json = cache_dir.join(cache_file_name.replace(".bin", ".json")); let mut file_handler_default = None; let mut file_handler_json = None; if save_to_cache { file_handler_default = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) { Ok(t) => t, Err(e) => { warnings.push(format!("Cannot create or open cache file \"{}\", reason {e}", cache_file.to_string_lossy())); return None; } }); if use_json { file_handler_json = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file_json) { Ok(t) => t, Err(e) => { warnings.push(format!("Cannot create or open cache file \"{}\", reason {e}", cache_file_json.to_string_lossy())); return None; } }); } } else { if let Ok(t) = OpenOptions::new().read(true).open(&cache_file) { file_handler_default = Some(t); } else { if use_json { file_handler_json = Some(OpenOptions::new().read(true).open(&cache_file_json).ok()?); } else { // messages.push(format!("Cannot find or open cache file {cache_file:?}")); // No error or warning return None; } } }; Some(((file_handler_default, cache_file), (file_handler_json, cache_file_json))) } // When initializing logger or settings config/cache folders, logger is not yet initialized, // so we need to delay them until logger is initialized pub fn print_infos_and_warnings(infos: Vec, warnings: Vec) { for info in infos { info!("{info}"); } for warning in warnings { warn!("{warning}"); } } czkawka_core-10.0.0/src/common/consts.rs000064400000000000000000000042731046102023000163110ustar 00000000000000pub const DEFAULT_THREAD_SIZE: usize = 8 * 1024 * 1024; // 8 MB pub const DEFAULT_WORKER_THREAD_SIZE: usize = 4 * 1024 * 1024; // 4 MB pub const TEMP_HARDLINK_FILE: &str = "rzeczek.rxrxrxl"; pub const RAW_IMAGE_EXTENSIONS: &[&str] = &[ "ari", "cr3", "cr2", "crw", "erf", "raf", "3fr", "kdc", "dcs", "dcr", "iiq", "mos", "mef", "mrw", "nef", "nrw", "orf", "rw2", "pef", "srw", "arw", "srf", "sr2", ]; pub const JXL_IMAGE_EXTENSIONS: &[&str] = &["jxl"]; #[cfg(feature = "libavif")] pub const IMAGE_RS_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "bmp", "tiff", "tif", "tga", "ff", "jif", "jfi", "webp", "gif", "ico", "exr", "qoi", "avif", ]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "bmp", "tiff", "tif", "tga", "ff", "jif", "jfi", "webp", "gif", "ico", "exr", "qoi"]; #[cfg(feature = "libavif")] pub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "bmp", "webp", "exr", "qoi", "avif"]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "bmp", "webp", "exr", "qoi"]; #[cfg(feature = "libavif")] pub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "gif", "bmp", "ico", "jfif", "jpe", "pnz", "dib", "webp", "exr", "avif", ]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "gif", "bmp", "ico", "jfif", "jpe", "pnz", "dib", "webp", "exr", ]; pub const HEIC_EXTENSIONS: &[&str] = &["heif", "heifs", "heic", "heics", "avci", "avcs"]; pub const ZIP_FILES_EXTENSIONS: &[&str] = &["zip", "jar"]; pub const PDF_FILES_EXTENSIONS: &[&str] = &["pdf"]; pub const AUDIO_FILES_EXTENSIONS: &[&str] = &[ "mp3", "flac", "wav", "ogg", "m4a", "aac", "aiff", "pcm", "aif", "aiff", "aifc", "m3a", "mp2", "mp4a", "mp2a", "mpga", "wave", "weba", "wma", "oga", ]; pub const VIDEO_FILES_EXTENSIONS: &[&str] = &[ "mp4", "mpv", "flv", "mp4a", "webm", "mpg", "mp2", "mpeg", "m4p", "m4v", "avi", "wmv", "qt", "mov", "swf", "mkv", ]; czkawka_core-10.0.0/src/common/dir_traversal.rs000064400000000000000000000575751046102023000176560ustar 00000000000000use std::collections::BTreeMap; use std::fs; use std::fs::{DirEntry, FileType, Metadata}; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::UNIX_EPOCH; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::directories::Directories; use crate::common::extensions::Extensions; use crate::common::items::ExcludedItems; use crate::common::model::{CheckingMethod, FileEntry, ToolType}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::CommonToolData; use crate::flc; #[derive(Copy, Clone, Eq, PartialEq)] pub enum Collect { InvalidSymlinks, Files, } #[derive(Eq, PartialEq, Copy, Clone, Debug)] enum EntryType { File, Dir, Symlink, Other, } pub struct DirTraversalBuilder<'b, F> { group_by: Option, root_dirs: Vec, stop_flag: Option>, progress_sender: Option<&'b Sender>, minimal_file_size: Option, maximal_file_size: Option, checking_method: CheckingMethod, collect: Collect, recursive_search: bool, directories: Option, excluded_items: Option, extensions: Option, tool_type: ToolType, } pub struct DirTraversal<'b, F> { group_by: F, root_dirs: Vec, stop_flag: Arc, progress_sender: Option<&'b Sender>, recursive_search: bool, directories: Directories, excluded_items: ExcludedItems, extensions: Extensions, minimal_file_size: u64, maximal_file_size: u64, checking_method: CheckingMethod, tool_type: ToolType, collect: Collect, } impl Default for DirTraversalBuilder<'_, ()> { fn default() -> Self { Self::new() } } impl DirTraversalBuilder<'_, ()> { pub fn new() -> Self { DirTraversalBuilder { group_by: None, root_dirs: vec![], stop_flag: None, progress_sender: None, checking_method: CheckingMethod::None, minimal_file_size: None, maximal_file_size: None, collect: Collect::Files, recursive_search: false, directories: None, extensions: None, excluded_items: None, tool_type: ToolType::None, } } } impl<'b, F> DirTraversalBuilder<'b, F> { pub(crate) fn common_data(mut self, common_tool_data: &CommonToolData) -> Self { self.root_dirs = common_tool_data.directories.included_directories.clone(); self.extensions = Some(common_tool_data.extensions.clone()); self.excluded_items = Some(common_tool_data.excluded_items.clone()); self.recursive_search = common_tool_data.recursive_search; self.minimal_file_size = Some(common_tool_data.minimal_file_size); self.maximal_file_size = Some(common_tool_data.maximal_file_size); self.tool_type = common_tool_data.tool_type; self.directories = Some(common_tool_data.directories.clone()); self } pub(crate) fn stop_flag(mut self, stop_flag: &Arc) -> Self { self.stop_flag = Some(stop_flag.clone()); self } pub(crate) fn progress_sender(mut self, progress_sender: Option<&'b Sender>) -> Self { self.progress_sender = progress_sender; self } pub(crate) fn checking_method(mut self, checking_method: CheckingMethod) -> Self { self.checking_method = checking_method; self } pub(crate) fn minimal_file_size(mut self, minimal_file_size: u64) -> Self { self.minimal_file_size = Some(minimal_file_size); self } pub(crate) fn maximal_file_size(mut self, maximal_file_size: u64) -> Self { self.maximal_file_size = Some(maximal_file_size); self } pub(crate) fn collect(mut self, collect: Collect) -> Self { self.collect = collect; self } pub(crate) fn group_by(self, group_by: G) -> DirTraversalBuilder<'b, G> where G: Fn(&FileEntry) -> T, { DirTraversalBuilder { group_by: Some(group_by), root_dirs: self.root_dirs, stop_flag: self.stop_flag, progress_sender: self.progress_sender, directories: self.directories, extensions: self.extensions, excluded_items: self.excluded_items, recursive_search: self.recursive_search, maximal_file_size: self.maximal_file_size, minimal_file_size: self.minimal_file_size, collect: self.collect, checking_method: self.checking_method, tool_type: self.tool_type, } } pub(crate) fn build(self) -> DirTraversal<'b, F> { DirTraversal { group_by: self.group_by.expect("could not build"), root_dirs: self.root_dirs, stop_flag: self.stop_flag.expect("Stop flag must be always initialized"), progress_sender: self.progress_sender, checking_method: self.checking_method, minimal_file_size: self.minimal_file_size.unwrap_or(0), maximal_file_size: self.maximal_file_size.unwrap_or(u64::MAX), collect: self.collect, directories: self.directories.expect("could not build"), excluded_items: self.excluded_items.expect("could not build"), extensions: self.extensions.unwrap_or_default(), recursive_search: self.recursive_search, tool_type: self.tool_type, } } } pub enum DirTraversalResult { SuccessFiles { warnings: Vec, grouped_file_entries: BTreeMap>, }, Stopped, } fn entry_type(file_type: FileType) -> EntryType { if file_type.is_dir() { EntryType::Dir } else if file_type.is_symlink() { EntryType::Symlink } else if file_type.is_file() { EntryType::File } else { EntryType::Other } } impl DirTraversal<'_, F> where F: Fn(&FileEntry) -> T, T: Ord + PartialOrd, { #[fun_time(message = "run(collecting files/dirs)", level = "debug")] pub(crate) fn run(self) -> DirTraversalResult { assert_ne!(self.tool_type, ToolType::None, "Tool type cannot be None"); let mut all_warnings = vec![]; let mut grouped_file_entries: BTreeMap> = BTreeMap::new(); // Add root folders for finding let mut folders_to_check: Vec = self.root_dirs.clone(); let progress_handler = prepare_thread_handler_common(self.progress_sender, CurrentStage::CollectingFiles, 0, (self.tool_type, self.checking_method), 0); let DirTraversal { collect, directories, excluded_items, extensions, recursive_search, minimal_file_size, maximal_file_size, stop_flag, .. } = self; while !folders_to_check.is_empty() { if check_if_stop_received(&stop_flag) { progress_handler.join_thread(); return DirTraversalResult::Stopped; } let segments: Vec<_> = folders_to_check .into_par_iter() .with_max_len(2) // Avoiding checking too many folders in batch .map(|current_folder| { let mut dir_result = vec![]; let mut warnings = vec![]; let mut fe_result = vec![]; let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return Some((dir_result, warnings, fe_result)); }; let mut counter = 0; // Check every sub folder/file/link etc. for entry in read_dir { if check_if_stop_received(&stop_flag) { return None; } let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, ¤t_folder) else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue }; match (entry_type(file_type), collect) { (EntryType::Dir, Collect::Files | Collect::InvalidSymlinks) => { process_dir_in_file_symlink_mode(recursive_search, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items); } (EntryType::File, Collect::Files) => { counter += 1; process_file_in_file_mode( entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items, minimal_file_size, maximal_file_size, ); } (EntryType::File, Collect::InvalidSymlinks) => { counter += 1; } (EntryType::Symlink, Collect::InvalidSymlinks) => { counter += 1; process_symlink_in_symlink_mode(entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items); } (EntryType::Symlink, Collect::Files) | (EntryType::Other, _) => { // nothing to do } } } if counter > 0 { // Increase counter in batch, because usually it may be slow to add multiple times atomic value progress_handler.increase_items(counter); } Some((dir_result, warnings, fe_result)) }) .while_some() .collect(); let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, mut fe_result) in segments { folders_to_check.extend(segment); all_warnings.extend(warnings); fe_result.sort_by_cached_key(|fe| fe.path.to_string_lossy().to_string()); for fe in fe_result { let key = (self.group_by)(&fe); grouped_file_entries.entry(key).or_default().push(fe); } } } progress_handler.join_thread(); debug!("Collected {} files", grouped_file_entries.values().map(Vec::len).sum::()); match collect { Collect::Files | Collect::InvalidSymlinks => DirTraversalResult::SuccessFiles { grouped_file_entries, warnings: all_warnings, }, } } } fn process_file_in_file_mode( entry_data: &DirEntry, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, directories: &Directories, excluded_items: &ExcludedItems, minimal_file_size: u64, maximal_file_size: u64, ) { if !extensions.check_if_entry_have_valid_extension(entry_data) { return; } let current_file_name = entry_data.path(); if excluded_items.is_excluded(¤t_file_name) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(¤t_file_name) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } let Some(metadata) = common_get_metadata_dir(entry_data, warnings, ¤t_file_name) else { return; }; if (minimal_file_size..=maximal_file_size).contains(&metadata.len()) { // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), path: current_file_name, }; fe_result.push(fe); } } fn process_dir_in_file_symlink_mode( recursive_search: bool, entry_data: &DirEntry, directories: &Directories, dir_result: &mut Vec, warnings: &mut Vec, excluded_items: &ExcludedItems, ) { if !recursive_search { return; } let dir_path = entry_data.path(); if directories.is_excluded(&dir_path) { return; } if excluded_items.is_excluded(&dir_path) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&dir_path) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } dir_result.push(dir_path); } fn process_symlink_in_symlink_mode( entry_data: &DirEntry, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, directories: &Directories, excluded_items: &ExcludedItems, ) { if !extensions.check_if_entry_have_valid_extension(entry_data) { return; } let current_file_name = entry_data.path(); if excluded_items.is_excluded(¤t_file_name) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(¤t_file_name) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } let Some(metadata) = common_get_metadata_dir(entry_data, warnings, ¤t_file_name) else { return; }; // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), path: current_file_name, }; fe_result.push(fe); } pub(crate) fn common_read_dir(current_folder: &Path, warnings: &mut Vec) -> Option>> { match fs::read_dir(current_folder) { Ok(t) => Some(t.collect()), Err(e) => { warnings.push(flc!("core_cannot_open_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string())); None } } } pub(crate) fn common_get_entry_data<'a>(entry: &'a Result, warnings: &mut Vec, current_folder: &Path) -> Option<&'a DirEntry> { let entry_data = match entry { Ok(t) => t, Err(e) => { warnings.push(flc!( "core_cannot_read_entry_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string() )); return None; } }; Some(entry_data) } pub(crate) fn common_get_metadata_dir(entry_data: &DirEntry, warnings: &mut Vec, current_folder: &Path) -> Option { let metadata: Metadata = match entry_data.metadata() { Ok(t) => t, Err(e) => { warnings.push(flc!( "core_cannot_read_metadata_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string() )); return None; } }; Some(metadata) } pub(crate) fn get_modified_time(metadata: &Metadata, warnings: &mut Vec, current_file_name: &Path, is_folder: bool) -> u64 { match metadata.modified() { Ok(t) => match t.duration_since(UNIX_EPOCH) { Ok(d) => d.as_secs(), Err(_inspected) => { if is_folder { warnings.push(flc!("core_folder_modified_before_epoch", name = current_file_name.to_string_lossy().to_string())); } else { warnings.push(flc!("core_file_modified_before_epoch", name = current_file_name.to_string_lossy().to_string())); } 0 } }, Err(e) => { if is_folder { warnings.push(flc!( "core_folder_no_modification_date", name = current_file_name.to_string_lossy().to_string(), reason = e.to_string() )); } else { warnings.push(flc!( "core_file_no_modification_date", name = current_file_name.to_string_lossy().to_string(), reason = e.to_string() )); } 0 } } } #[cfg(target_family = "windows")] pub(crate) fn inode(_fe: &FileEntry) -> Option { None } #[cfg(target_family = "unix")] pub(crate) fn inode(fe: &FileEntry) -> Option { if let Ok(meta) = fs::metadata(&fe.path) { Some(meta.ino()) } else { None } } pub(crate) fn take_1_per_inode((k, mut v): (Option, Vec)) -> Vec { if k.is_some() { v.drain(1..); } v } #[cfg(test)] mod tests { use std::collections::HashSet; use std::fs::File; use std::io::prelude::*; use std::time::{Duration, SystemTime}; use std::{fs, io}; use once_cell::sync::Lazy; use tempfile::TempDir; use super::*; use crate::common::tool_data::*; impl CommonData for CommonToolData { type Info = (); type Parameters = (); fn get_information(&self) -> Self::Info {} fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { self } fn get_cd_mut(&mut self) -> &mut CommonToolData { self } fn found_any_broken_files(&self) -> bool { false } } static NOW: Lazy = Lazy::new(|| SystemTime::UNIX_EPOCH + Duration::new(100, 0)); const CONTENT: &[u8; 1] = b"a"; fn create_files(dir: &TempDir) -> io::Result<(PathBuf, PathBuf, PathBuf)> { let (src, hard, other) = (dir.path().join("a"), dir.path().join("b"), dir.path().join("c")); let mut file = File::create(&src)?; file.write_all(CONTENT)?; fs::hard_link(&src, &hard)?; file.set_modified(*NOW)?; let mut file = File::create(&other)?; file.write_all(CONTENT)?; file.set_modified(*NOW)?; Ok((src, hard, other)) } #[test] fn test_traversal() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, hard, other) = create_files(&dir)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail calculating duration since epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_directory([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: HashSet<_> = grouped_file_entries.into_values().flatten().collect(); assert_eq!( HashSet::from([ FileEntry { path: src, size: 1, modified_date: secs, }, FileEntry { path: hard, size: 1, modified_date: secs, }, FileEntry { path: other, size: 1, modified_date: secs, }, ]), actual ); } DirTraversalResult::Stopped => { panic!("Expect SuccessFiles."); } }; Ok(()) } #[cfg(target_family = "unix")] #[test] fn test_traversal_group_by_inode() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, _, other) = create_files(&dir)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail calculating duration since epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_directory([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(inode) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: HashSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect(); assert_eq!( HashSet::from([ FileEntry { path: src, size: 1, modified_date: secs, }, FileEntry { path: other, size: 1, modified_date: secs, }, ]), actual ); } DirTraversalResult::Stopped => { panic!("Expect SuccessFiles."); } }; Ok(()) } #[cfg(target_family = "windows")] #[test] fn test_traversal_group_by_inode() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, hard, other) = create_files(&dir)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail duration from epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_directory([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(inode) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: HashSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect(); assert_eq!( HashSet::from([ FileEntry { path: src, size: 1, modified_date: secs, }, FileEntry { path: hard, size: 1, modified_date: secs, }, FileEntry { path: other, size: 1, modified_date: secs, }, ]), actual ); } _ => { panic!("Expect SuccessFiles."); } }; Ok(()) } } czkawka_core-10.0.0/src/common/directories.rs000064400000000000000000000264421046102023000173160ustar 00000000000000use std::path::{Path, PathBuf}; #[cfg(target_family = "unix")] use std::{fs, os::unix::fs::MetadataExt}; use crate::common::normalize_windows_path; use crate::common::traits::ResultEntry; use crate::flc; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct Directories { pub excluded_directories: Vec, pub included_directories: Vec, pub reference_directories: Vec, pub exclude_other_filesystems: Option, #[cfg(target_family = "unix")] pub included_dev_ids: Vec, } impl Directories { pub fn new() -> Self { Default::default() } pub(crate) fn set_reference_directory(&mut self, reference_directory: &[PathBuf]) -> Messages { let mut messages: Messages = Messages::new(); self.reference_directories = reference_directory .iter() .filter_map(|directory| { let (dir, msg) = Self::canonicalize_and_clear_path(directory, false); messages.extend_with_another_messages(msg); dir }) .collect::>(); messages } pub(crate) fn set_included_directory(&mut self, included_directory: Vec) -> Messages { let mut messages: Messages = Messages::new(); if included_directory.is_empty() { messages.errors.push(flc!("core_missing_no_chosen_included_directory")); return messages; } let directories: Vec = included_directory; let mut checked_directories: Vec = Vec::new(); for directory in directories { let (dir, msg) = Self::canonicalize_and_clear_path(&directory, false); messages.extend_with_another_messages(msg); if let Some(dir) = dir { checked_directories.push(dir); } } if checked_directories.is_empty() { messages.warnings.push(flc!("core_included_directory_zero_valid_directories")); return messages; } self.included_directories = checked_directories; messages } pub(crate) fn set_excluded_directory(&mut self, excluded_directory: Vec) -> Messages { let mut messages: Messages = Messages::new(); if excluded_directory.is_empty() { return messages; } let directories: Vec = excluded_directory; let mut checked_directories: Vec = Vec::new(); for directory in directories { let directory_as_string = directory.to_string_lossy(); if directory_as_string == "/" { messages.errors.push(flc!("core_excluded_directory_pointless_slash")); break; } let (dir, msg) = Self::canonicalize_and_clear_path(&directory, true); messages.extend_with_another_messages(msg); if let Some(dir) = dir { checked_directories.push(dir); } } self.excluded_directories = checked_directories; messages } fn canonicalize_and_clear_path(directory: &Path, is_excluded: bool) -> (Option, Messages) { let mut messages = Messages::new(); let mut directory = directory.to_path_buf(); if !directory.exists() { if !is_excluded { messages.warnings.push(flc!("core_directory_must_exists", path = directory.to_string_lossy().to_string())); } return (None, messages); } if !directory.is_dir() { messages .warnings .push(flc!("core_directory_must_be_directory", path = directory.to_string_lossy().to_string())); return (None, messages); } if let Ok(dir) = dunce::canonicalize(&directory) { directory = dir; } (Some(directory), messages) } #[cfg(target_family = "unix")] pub(crate) fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) { self.exclude_other_filesystems = Some(exclude_other_filesystems); } pub(crate) fn optimize_directories(&mut self, recursive_search: bool) -> Messages { let mut messages: Messages = Messages::new(); let mut optimized_included: Vec = Vec::new(); let mut optimized_excluded: Vec = Vec::new(); if cfg!(target_family = "windows") { self.included_directories = self.included_directories.iter().map(normalize_windows_path).collect(); self.excluded_directories = self.excluded_directories.iter().map(normalize_windows_path).collect(); self.reference_directories = self.reference_directories.iter().map(normalize_windows_path).collect(); } // Remove duplicated entries like: "/", "/" self.excluded_directories.sort_unstable(); self.included_directories.sort_unstable(); self.reference_directories.sort_unstable(); self.excluded_directories.dedup(); self.included_directories.dedup(); self.reference_directories.dedup(); // Optimize for duplicated included directories - "/", "/home". "/home/Pulpit" to "/" // Do not use when not using recursive search or using if recursive_search && !self.exclude_other_filesystems.unwrap_or(false) { // This is only point which can't be done when recursive search is disabled. let mut is_inside: bool; for ed_checked in &self.excluded_directories { is_inside = false; for ed_help in &self.excluded_directories { if ed_checked == ed_help { // We checking same element continue; } if ed_checked.starts_with(ed_help) { is_inside = true; break; } } if !is_inside { optimized_excluded.push(ed_checked.clone()); } } for id_checked in &self.included_directories { is_inside = false; for id_help in &self.included_directories { if id_checked == id_help { // We checking same element continue; } if id_checked.starts_with(id_help) { is_inside = true; break; } } if !is_inside { optimized_included.push(id_checked.clone()); } } self.included_directories = optimized_included; optimized_included = Vec::new(); self.excluded_directories = optimized_excluded; optimized_excluded = Vec::new(); } // Remove included directories which are inside any excluded directory for id in &self.included_directories { let mut is_inside: bool = false; for ed in &self.excluded_directories { if id.starts_with(ed) { is_inside = true; break; } } if !is_inside { optimized_included.push(id.clone()); } } self.included_directories = optimized_included; optimized_included = Vec::new(); // Remove non existed directories for id in &self.included_directories { let path = Path::new(id); if path.exists() { optimized_included.push(id.clone()); } } for ed in &self.excluded_directories { let path = Path::new(ed); if path.exists() { optimized_excluded.push(ed.clone()); } } self.included_directories = optimized_included; self.excluded_directories = optimized_excluded; optimized_excluded = Vec::new(); // Excluded paths must are inside included path, because for ed in &self.excluded_directories { let mut is_inside: bool = false; for id in &self.included_directories { if ed.starts_with(id) { is_inside = true; break; } } if is_inside { optimized_excluded.push(ed.clone()); } } self.excluded_directories = optimized_excluded; // Selecting Reference folders { let mut ref_folders = Vec::new(); for folder in &self.reference_directories { if self.included_directories.iter().any(|e| folder.starts_with(e)) { ref_folders.push(folder.clone()); } } self.reference_directories = ref_folders; } if self.included_directories.is_empty() { messages.errors.push(flc!("core_directory_overlap")); return messages; } // Not needed, but better is to have sorted everything self.excluded_directories.sort_unstable(); self.included_directories.sort_unstable(); // Get device IDs for included directories #[cfg(target_family = "unix")] if self.exclude_other_filesystems() { for d in &self.included_directories { match fs::metadata(d) { Ok(m) => self.included_dev_ids.push(m.dev()), Err(_) => messages.errors.push(flc!("core_directory_unable_to_get_device_id", path = d.to_string_lossy().to_string())), } } } messages } pub(crate) fn is_in_referenced_directory(&self, path: &Path) -> bool { self.reference_directories.iter().any(|e| path.starts_with(e)) } pub(crate) fn is_excluded(&self, path: &Path) -> bool { #[cfg(target_family = "windows")] let path = normalize_windows_path(path); // We're assuming that `excluded_directories` are already normalized self.excluded_directories.iter().any(|p| p.as_path() == path) } #[cfg(target_family = "unix")] pub(crate) fn exclude_other_filesystems(&self) -> bool { self.exclude_other_filesystems.unwrap_or(false) } #[cfg(target_family = "unix")] pub(crate) fn is_on_other_filesystems(&self, path: impl AsRef) -> Result { let path = path.as_ref(); match fs::metadata(path) { Ok(m) => Ok(!self.included_dev_ids.iter().any(|&id| id == m.dev())), Err(_) => Err(flc!("core_directory_unable_to_get_device_id", path = path.to_string_lossy().to_string())), } } pub(crate) fn filter_reference_folders(&self, entries_to_check: Vec>) -> Vec<(T, Vec)> where T: ResultEntry, { entries_to_check .into_iter() .filter_map(|vec_file_entry| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry.into_iter().partition(|e| self.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>() } } czkawka_core-10.0.0/src/common/extensions.rs000064400000000000000000000123101046102023000171660ustar 00000000000000use std::collections::HashSet; use std::fs::DirEntry; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct Extensions { allowed_extensions_hashset: HashSet, excluded_extensions_hashset: HashSet, } impl Extensions { pub fn new() -> Self { Default::default() } pub(crate) fn filter_extensions(mut file_extensions: String) -> (HashSet, Messages) { let mut messages = Messages::new(); let mut extensions_hashset = HashSet::new(); if file_extensions.trim().is_empty() { return (Default::default(), messages); } file_extensions = file_extensions.replace("IMAGE", "jpg,kra,gif,png,bmp,tiff,hdr,svg"); file_extensions = file_extensions.replace("VIDEO", "mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp"); file_extensions = file_extensions.replace("MUSIC", "mp3,flac,ogg,tta,wma,webm"); file_extensions = file_extensions.replace("TEXT", "txt,doc,docx,odt,rtf"); let extensions: Vec = file_extensions.split(',').map(str::trim).map(String::from).collect(); for mut extension in extensions { if extension.is_empty() || extension.replace(['.', ' '], "").trim().is_empty() { continue; } if extension.starts_with('.') { extension = extension.chars().skip(1).collect::(); } if extension.contains('.') { messages.warnings.push(format!("{extension} is not valid extension because contains dot inside")); continue; } if extension.contains(' ') { messages.warnings.push(format!("{extension} is not valid extension because contains empty space inside")); continue; } extensions_hashset.insert(extension); } (extensions_hashset, messages) } /// List of allowed extensions, only files with this extensions will be checking if are duplicates /// After, extensions cannot contain any dot, commas etc. pub(crate) fn set_allowed_extensions(&mut self, allowed_extensions: String) -> Messages { let (extensions, messages) = Self::filter_extensions(allowed_extensions); self.allowed_extensions_hashset = extensions; messages } pub(crate) fn set_excluded_extensions(&mut self, excluded_extensions: String) -> Messages { let (extensions, messages) = Self::filter_extensions(excluded_extensions); self.excluded_extensions_hashset = extensions; messages } #[allow(clippy::string_slice)] // Valid, because we address go to dot, which is known ascii character pub(crate) fn check_if_entry_have_valid_extension(&self, entry_data: &DirEntry) -> bool { if self.allowed_extensions_hashset.is_empty() && self.excluded_extensions_hashset.is_empty() { return true; } // Using entry_data.path().extension() is a lot of slower, even 5 times let file_name = entry_data.file_name(); let Some(file_name_str) = file_name.to_str() else { return false }; let Some(extension_idx) = file_name_str.rfind('.') else { return false }; let extension = &file_name_str[extension_idx + 1..]; if !self.allowed_extensions_hashset.is_empty() { if extension.chars().all(|c| c.is_ascii_lowercase()) { self.allowed_extensions_hashset.contains(extension) } else { self.allowed_extensions_hashset.contains(&extension.to_lowercase()) } } else { if extension.chars().all(|c| c.is_ascii_lowercase()) { !self.excluded_extensions_hashset.contains(extension) } else { !self.excluded_extensions_hashset.contains(&extension.to_lowercase()) } } } pub(crate) fn set_any_extensions(&self) -> bool { !self.allowed_extensions_hashset.is_empty() } fn extend_allowed_extensions(&mut self, file_extensions: &[&str]) { for extension in file_extensions { let extension_without_dot = extension.trim_start_matches('.'); self.allowed_extensions_hashset.insert(extension_without_dot.to_string()); } } // E.g. when using similar videos, user can provide extensions like "mp4,flv", but if user provide "mp4,jpg" then // it will be only "mp4" because "jpg" is not valid extension for videos fn union_allowed_extensions(&mut self, file_extensions: &[&str]) { let mut new_extensions = HashSet::new(); for extension in file_extensions { let extension_without_dot = extension.trim_start_matches('.'); if !self.allowed_extensions_hashset.contains(extension_without_dot) { new_extensions.insert(extension_without_dot.to_string()); } } self.allowed_extensions_hashset = new_extensions; } pub(crate) fn set_and_validate_allowed_extensions(&mut self, file_extensions: &[&str]) { if self.allowed_extensions_hashset.is_empty() { self.extend_allowed_extensions(file_extensions); } else { self.union_allowed_extensions(file_extensions); } } } czkawka_core-10.0.0/src/common/image.rs000064400000000000000000000213071046102023000160570ustar 00000000000000#![allow(unused_imports)] // I don't want to fight with unused(heif) imports in this file, so simply ignore it to avoid too much complexity use std::fs::File; use std::path::Path; use std::sync::{Arc, atomic}; use std::thread::{JoinHandle, sleep}; use std::time::{Duration, Instant, SystemTime}; use std::{fs, panic, thread}; use anyhow::anyhow; use fun_time::fun_time; use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; use jxl_oxide::image::BitDepth; use jxl_oxide::integration::JxlDecoder; use jxl_oxide::{JxlImage, PixelFormat}; #[cfg(feature = "heif")] use libheif_rs::{ColorSpace, HeifContext, RgbChroma}; #[cfg(feature = "libraw")] use libraw::Processor; use log::{LevelFilter, Record, debug, error, info, trace, warn}; use nom_exif::{ExifIter, ExifTag, MediaParser, MediaSource}; use rawler::RawLoader; use rawler::decoders::RawDecodeParams; use rawler::imgop::develop::RawDevelop; use rawler::rawsource::RawSource; use symphonia::core::conv::IntoSample; use crate::common; use crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_EXTENSIONS, IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, JXL_IMAGE_EXTENSIONS, RAW_IMAGE_EXTENSIONS}; use crate::common::create_crash_message; use crate::helpers::debug_timer::Timer; // #[cfg(feature = "heif")] // use libheif_rs::LibHeif; pub(crate) fn get_jxl_image(path: &str) -> anyhow::Result { let file = File::open(path)?; let decoder = JxlDecoder::new(file)?; let image = DynamicImage::from_decoder(decoder)?; Ok(image) } pub fn get_dynamic_image_from_path(path: &str) -> Result { let path_lower = Path::new(path).extension().unwrap_or_default().to_string_lossy().to_lowercase(); trace!("decoding file {path}"); let res = panic::catch_unwind(|| { if HEIC_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) { #[cfg(feature = "heif")] { get_dynamic_image_from_heic(path).map_err(|e| format!("Cannot open heic file \"{path}\": {e}")) } #[cfg(not(feature = "heif"))] { image::open(path).map_err(|e| format!("Cannot open image file \"{path}\": {e}")) } } else if JXL_IMAGE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) { get_jxl_image(path).map_err(|e| format!("Cannot open jxl image file \"{path}\": {e}")) } else if RAW_IMAGE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) { get_raw_image(path).map_err(|e| format!("Cannot open raw image file \"{path}\": {e}")) } else { image::open(path).map_err(|e| format!("Cannot open image file \"{path}\": {e}")) } }); if let Ok(res) = res { match res { Ok(t) => { let rotation = get_rotation_from_exif(path).unwrap_or(None); match rotation { Some(ExifOrientation::Normal) | None => Ok(t), Some(ExifOrientation::MirrorHorizontal) => Ok(t.fliph()), Some(ExifOrientation::Rotate180) => Ok(t.rotate180()), Some(ExifOrientation::MirrorVertical) => Ok(t.flipv()), Some(ExifOrientation::MirrorHorizontalAndRotate270CW) => Ok(t.fliph().rotate270()), Some(ExifOrientation::Rotate90CW) => Ok(t.rotate90()), Some(ExifOrientation::MirrorHorizontalAndRotate90CW) => Ok(t.fliph().rotate90()), Some(ExifOrientation::Rotate270CW) => Ok(t.rotate270()), } } Err(e) => Err(format!("Cannot open image file \"{path}\": {e}")), } } else { let message = create_crash_message("Image-rs or libraw-rs or jxl-oxide", path, "https://github.com/image-rs/image/issues"); error!("{message}"); Err(message) } } #[cfg(feature = "heif")] pub(crate) fn get_dynamic_image_from_heic(path: &str) -> anyhow::Result { // let libheif = LibHeif::new(); let im = HeifContext::read_from_file(path)?; let handle = im.primary_image_handle()?; // let image = libheif.decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None)?; // Enable when using libheif 0.19 let image = handle.decode(ColorSpace::Rgb(RgbChroma::Rgb), None)?; let width = image.width(); let height = image.height(); let planes = image.planes(); let interleaved_plane = planes.interleaved.ok_or_else(|| anyhow::anyhow!("Failed to get interleaved plane"))?; ImageBuffer::from_raw(width, height, interleaved_plane.data.to_owned()) .map(DynamicImage::ImageRgb8) .ok_or_else(|| anyhow::anyhow!("Failed to create image buffer")) } #[cfg(feature = "libraw")] pub(crate) fn get_raw_image(path: impl AsRef) -> anyhow::Result { let buf = fs::read(path.as_ref())?; let processor = Processor::new(); let processed = processor.process_8bit(&buf)?; let width = processed.width(); let height = processed.height(); let data = processed.to_vec(); let data_len = data.len(); let buffer = ImageBuffer::from_raw(width, height, data).ok_or(anyhow::anyhow!(format!( "Cannot create ImageBuffer from raw image with width: {width} and height: {height} and data length: {data_len}", )))?; Ok(DynamicImage::ImageRgb8(buffer)) } #[cfg(not(feature = "libraw"))] pub(crate) fn get_raw_image(path: impl AsRef + std::fmt::Debug) -> Result { let mut timer = Timer::new("Rawler"); let raw_source = RawSource::new(path.as_ref()).map_err(|err| format!("Failed to create RawSource from path {path:?}: {err}"))?; timer.checkpoint("Created RawSource"); let decoder = rawler::get_decoder(&raw_source).map_err(|e| e.to_string())?; timer.checkpoint("Got decoder"); let raw_image = decoder.raw_image(&raw_source, &RawDecodeParams::default(), false).map_err(|e| e.to_string())?; timer.checkpoint("Decoded raw image"); let developer = RawDevelop::default(); let developed_image = developer.develop_intermediate(&raw_image).map_err(|e| e.to_string())?; timer.checkpoint("Developed raw image"); let dynamic_image = developed_image.to_dynamic_image().ok_or("Failed to convert image to DynamicImage".to_string())?; timer.checkpoint("Converted to DynamicImage"); let rgb_image = DynamicImage::from(dynamic_image.to_rgb8()); timer.checkpoint("Reconverted to RGB"); trace!("{}", timer.report("Everything", false)); Ok(rgb_image) } pub fn check_if_can_display_image(path: &str) -> bool { let Some(extension) = Path::new(path).extension() else { return false; }; let extension_str = extension.to_string_lossy().to_lowercase(); #[cfg(feature = "heif")] let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS, JXL_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat(); #[cfg(not(feature = "heif"))] let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS, JXL_IMAGE_EXTENSIONS].concat(); allowed_extensions.iter().any(|ext| extension_str.ends_with(ext)) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExifOrientation { Normal, MirrorHorizontal, Rotate180, MirrorVertical, MirrorHorizontalAndRotate270CW, Rotate90CW, MirrorHorizontalAndRotate90CW, Rotate270CW, } pub(crate) fn get_rotation_from_exif(path: &str) -> Result, nom_exif::Error> { let res = panic::catch_unwind(|| { let mut parser = MediaParser::new(); let ms = MediaSource::file_path(path)?; if !ms.has_exif() { return Ok(None); } let exif_iter: ExifIter = parser.parse(ms)?; for exif_entry in exif_iter { if exif_entry.tag() == Some(ExifTag::Orientation) { if let Some(value) = exif_entry.get_value() { return match value.to_string().as_str() { "1" => Ok(Some(ExifOrientation::Normal)), "2" => Ok(Some(ExifOrientation::MirrorHorizontal)), "3" => Ok(Some(ExifOrientation::Rotate180)), "4" => Ok(Some(ExifOrientation::MirrorVertical)), "5" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate270CW)), "6" => Ok(Some(ExifOrientation::Rotate90CW)), "7" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate90CW)), "8" => Ok(Some(ExifOrientation::Rotate270CW)), _ => Ok(None), }; } } } Ok(None) }); res.unwrap_or_else(|_| { let message = create_crash_message("nom-exif", path, "https://github.com/mindeng/nom-exif"); error!("{message}"); Err(nom_exif::Error::IOError(std::io::Error::other("Panic in get_rotation_from_exif"))) }) } czkawka_core-10.0.0/src/common/items.rs000064400000000000000000000076541046102023000161270ustar 00000000000000use std::path::Path; #[cfg(not(target_family = "unix"))] use crate::common::normalize_windows_path; use crate::common::regex_check; use crate::helpers::messages::Messages; #[cfg(target_family = "unix")] pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["/proc", "/dev", "/sys", "/snap"]; #[cfg(not(target_family = "unix"))] pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["C:\\Windows"]; #[cfg(target_family = "unix")] pub const DEFAULT_EXCLUDED_ITEMS: &str = "*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,*/snap/*,/home/*/.cache/*"; #[cfg(not(target_family = "unix"))] pub const DEFAULT_EXCLUDED_ITEMS: &str = "*\\.git\\*,*\\node_modules\\*,*\\lost+found\\*,*:\\windows\\*,*:\\$RECYCLE.BIN\\*,*:\\$SysReset\\*,*:\\System Volume Information\\*,*:\\OneDriveTemp\\*,*:\\hiberfil.sys,*:\\pagefile.sys,*:\\swapfile.sys"; #[derive(Debug, Clone, Default)] pub struct ExcludedItems { expressions: Vec, connected_expressions: Vec, } #[derive(Debug, Clone, Default)] pub struct SingleExcludedItem { pub expression: String, pub expression_splits: Vec, pub unique_extensions_splits: Vec, } impl ExcludedItems { pub fn new() -> Self { Default::default() } pub fn new_from(excluded_items: Vec) -> Self { let mut s = Self::new(); s.set_excluded_items(excluded_items); s } pub(crate) fn set_excluded_items(&mut self, excluded_items: Vec) -> Messages { let mut warnings: Vec = Vec::new(); if excluded_items.is_empty() { return Messages::new(); } let expressions: Vec = excluded_items; let mut checked_expressions: Vec = Vec::new(); for expression in expressions { let expression: String = expression.trim().to_string(); if expression.is_empty() { continue; } #[cfg(target_family = "windows")] let expression = expression.replace("/", "\\"); if expression == "DEFAULT" { checked_expressions.push(DEFAULT_EXCLUDED_ITEMS.to_string()); continue; } if !expression.contains('*') { warnings.push("Excluded Items Warning: Wildcard * is required in expression, ignoring ".to_string() + expression.as_str()); continue; } checked_expressions.push(expression); } for checked_expression in &checked_expressions { let item = new_excluded_item(checked_expression); self.expressions.push(item.expression.clone()); self.connected_expressions.push(item); } Messages { messages: vec![], warnings, errors: vec![], } } pub(crate) fn get_excluded_items(&self) -> &Vec { &self.expressions } pub(crate) fn is_excluded(&self, path: &Path) -> bool { if self.connected_expressions.is_empty() { return false; } #[cfg(target_family = "windows")] let path = normalize_windows_path(path); let path_str = path.to_string_lossy(); for expression in &self.connected_expressions { if regex_check(expression, &path_str) { return true; } } false } } pub fn new_excluded_item(expression: &str) -> SingleExcludedItem { let expression = expression.trim().to_string(); let expression_splits: Vec = expression.split('*').filter_map(|e| if e.is_empty() { None } else { Some(e.to_string()) }).collect(); let mut unique_extensions_splits = expression_splits.clone(); unique_extensions_splits.sort(); unique_extensions_splits.dedup(); unique_extensions_splits.sort_by_key(|b| std::cmp::Reverse(b.len())); SingleExcludedItem { expression, expression_splits, unique_extensions_splits, } } czkawka_core-10.0.0/src/common/logger.rs000064400000000000000000000135501046102023000162550ustar 00000000000000use std::env; use file_rotate::compression::Compression; use file_rotate::suffix::{AppendTimestamp, FileLimit}; use file_rotate::{ContentLimit, FileRotate}; use handsome_logger::{ColorChoice, CombinedLogger, ConfigBuilder, FormatText, SharedLogger, TermLogger, TerminalMode, TimeFormat, WriteLogger}; use log::{LevelFilter, Record, info, warn}; use crate::CZKAWKA_VERSION; use crate::common::config_cache_path::get_config_cache_path; use crate::common::get_all_available_threads; #[allow(clippy::print_stdout)] pub fn setup_logger(disabled_terminal_printing: bool, app_name: &str, filtering_messages_func: fn(&Record) -> bool) { log_panics::init(); let terminal_log_level = if disabled_terminal_printing && ![Ok("1"), Ok("true")].contains(&env::var("ENABLE_TERMINAL_LOGS_IN_CLI").as_deref()) { LevelFilter::Off } else { LevelFilter::Info }; let file_log_level = LevelFilter::Debug; let term_config = ConfigBuilder::default() .set_level(terminal_log_level) .set_message_filtering(Some(filtering_messages_func)) .build(); let file_config = ConfigBuilder::default() .set_level(file_log_level) .set_write_once(true) .set_message_filtering(Some(filtering_messages_func)) .set_time_format(TimeFormat::DateTimeWithMicro, None) .set_format_text(FormatText::DefaultWithThreadFile.get(), None) .build(); let combined_logger = (|| { let Some(config_cache_path) = get_config_cache_path() else { // println!("No config cache path configured, using default config folder"); return None; }; let cache_logs_path = config_cache_path.cache_folder.join(format!("{app_name}.log")); let write_rotater = FileRotate::new( &cache_logs_path, AppendTimestamp::default(FileLimit::MaxFiles(3)), ContentLimit::BytesSurpassed(100 * 1024 * 1024), Compression::None, None, ); let combined_logs: Vec> = if [Ok("1"), Ok("true")].contains(&env::var("DISABLE_FILE_LOGGING").as_deref()) { vec![TermLogger::new_from_config(term_config.clone())] } else { vec![TermLogger::new_from_config(term_config.clone()), WriteLogger::new(file_config, write_rotater)] }; CombinedLogger::init(combined_logs).ok().inspect(|()| { info!("Logging to file \"{}\" and terminal", cache_logs_path.to_string_lossy()); }) })(); if combined_logger.is_none() { TermLogger::init(term_config, TerminalMode::Mixed, ColorChoice::Always).expect("Cannot initialize logger"); info!("Logging to terminal only, file logging is disabled"); } } pub fn filtering_messages(record: &Record) -> bool { if let Some(module_path) = record.module_path() { // Printing not supported modules // if !["krokiet", "czkawka", "log_panics", "smithay_client_toolkit", "sctk_adwaita"] // .iter() // .any(|t| module_path.starts_with(t)) // { // println!("{:?}", module_path); // return true; // } else { // return false; // } ["krokiet", "czkawka", "log_panics"].iter().any(|t| module_path.starts_with(t)) } else { true } } #[allow(clippy::vec_init_then_push)] #[allow(unused_mut)] pub fn print_version_mode(app: &str) { let rust_version = env!("RUST_VERSION_INTERNAL"); let debug_release = if cfg!(debug_assertions) { "debug" } else { "release" }; let processors = get_all_available_threads(); let info = os_info::get(); let mut features: Vec<&str> = vec![]; #[cfg(feature = "heif")] features.push("heif"); #[cfg(feature = "libavif")] features.push("libavif"); #[cfg(feature = "libraw")] features.push("libraw"); let mut app_cpu_version = "Baseline"; let mut os_cpu_version = "Baseline"; if cfg!(target_feature = "sse2") { app_cpu_version = "x86-64-v1 (SSE2)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("sse2") { os_cpu_version = "x86-64-v1 (SSE2)"; } if cfg!(target_feature = "popcnt") { app_cpu_version = "x86-64-v2 (SSE4.2 + POPCNT)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("popcnt") { os_cpu_version = "x86-64-v2 (SSE4.2 + POPCNT)"; } if cfg!(target_feature = "avx2") { app_cpu_version = "x86-64-v3 (AVX2)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("avx2") { os_cpu_version = "x86-64-v3 (AVX2)"; } if cfg!(target_feature = "avx512f") { app_cpu_version = "x86-64-v4 (AVX-512)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("avx512f") { os_cpu_version = "x86-64-v4 (AVX-512)"; } // TODO - probably needs to add arm and other architectures, need help, because I don't have access to them info!( "{app} version: {CZKAWKA_VERSION}, {debug_release} mode, rust {rust_version}, os {} {} ({} {}), {processors} cpu/threads, features({}): [{}], app cpu version: {}, os cpu version: {}", info.os_type(), info.version(), env::consts::ARCH, info.bitness(), features.len(), features.join(", "), app_cpu_version, os_cpu_version, ); if cfg!(debug_assertions) { warn!("You are running debug version of app which is a lot of slower than release version."); } if option_env!("USING_CRANELIFT").is_some() { warn!("You are running app with cranelift which is intended only for fast compilation, not runtime performance."); } if cfg!(panic = "abort") { warn!("You are running app compiled with panic='abort', which may cause panics when processing untrusted data."); } } czkawka_core-10.0.0/src/common/mod.rs000064400000000000000000000333631046102023000155610ustar 00000000000000pub mod cache; pub mod config_cache_path; pub mod consts; pub mod dir_traversal; pub mod directories; pub mod extensions; pub mod image; pub mod items; pub mod logger; pub mod model; pub mod progress_data; pub mod progress_stop_handler; pub mod tool_data; pub mod traits; use std::cmp::Ordering; use std::ffi::OsString; use std::io::Error; use std::path::{Path, PathBuf}; use std::{fs, io, thread}; use items::SingleExcludedItem; use log::debug; use crate::common::consts::{DEFAULT_WORKER_THREAD_SIZE, TEMP_HARDLINK_FILE}; static NUMBER_OF_THREADS: state::InitCell = state::InitCell::new(); static ALL_AVAILABLE_THREADS: state::InitCell = state::InitCell::new(); pub fn get_number_of_threads() -> usize { let data = NUMBER_OF_THREADS.get(); if *data >= 1 { *data } else { get_all_available_threads() } } pub fn get_all_available_threads() -> usize { *ALL_AVAILABLE_THREADS.get_or_init(|| { let available_threads = thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1); ALL_AVAILABLE_THREADS.set(available_threads); available_threads }) } pub fn set_number_of_threads(thread_number: usize) { NUMBER_OF_THREADS.set(thread_number); let additional_message = if thread_number == 0 { format!( " (0 - means that all available threads will be used({}))", thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1) ) } else { "".to_string() }; debug!("Number of threads set to {thread_number}{additional_message}"); rayon::ThreadPoolBuilder::new() .num_threads(get_number_of_threads()) .stack_size(DEFAULT_WORKER_THREAD_SIZE) .build_global() .expect("Cannot set number of threads"); } pub fn check_if_folder_contains_only_empty_folders(path: impl AsRef) -> Result<(), String> { let path = path.as_ref(); if !path.is_dir() { return Err(format!("Trying to remove folder \"{}\" which is not a directory", path.to_string_lossy())); } let mut entries_to_check = Vec::new(); let Ok(initial_entry) = path.read_dir() else { return Err(format!("Cannot read directory \"{}\"", path.to_string_lossy())); }; for entry in initial_entry { if let Ok(entry) = entry { entries_to_check.push(entry); } else { return Err(format!("Cannot read entry from directory \"{}\"", path.to_string_lossy())); } } loop { let Some(entry) = entries_to_check.pop() else { break; }; let Some(file_type) = entry.file_type().ok() else { return Err(format!( "Folder contains file with unknown type \"{}\" inside \"{}\"", entry.path().to_string_lossy(), path.to_string_lossy() )); }; if !file_type.is_dir() { return Err(format!("Folder contains file \"{}\" inside \"{}\"", entry.path().to_string_lossy(), path.to_string_lossy())); } let Ok(internal_read_dir) = entry.path().read_dir() else { return Err(format!( "Cannot read directory \"{}\" inside \"{}\"", entry.path().to_string_lossy(), path.to_string_lossy() )); }; for internal_elements in internal_read_dir { if let Ok(internal_element) = internal_elements { entries_to_check.push(internal_element); } else { return Err(format!( "Cannot read entry from directory \"{}\" inside \"{}\"", entry.path().to_string_lossy(), path.to_string_lossy() )); } } } Ok(()) } pub fn remove_folder_if_contains_only_empty_folders(path: impl AsRef, remove_to_trash: bool) -> Result<(), String> { check_if_folder_contains_only_empty_folders(&path)?; let path = path.as_ref(); if remove_to_trash { trash::delete(path).map_err(|e| format!("Cannot move folder \"{}\" to trash, reason {e}", path.to_string_lossy())) } else { fs::remove_dir_all(path).map_err(|e| format!("Cannot remove directory \"{}\", reason {e}", path.to_string_lossy())) } } pub fn split_path(path: &Path) -> (String, String) { match (path.parent(), path.file_name()) { (Some(dir), Some(file)) => (dir.to_string_lossy().to_string(), file.to_string_lossy().into_owned()), (Some(dir), None) => (dir.to_string_lossy().to_string(), String::new()), (None, _) => (String::new(), String::new()), } } pub fn split_path_compare(path_a: &Path, path_b: &Path) -> Ordering { match path_a.parent().cmp(&path_b.parent()) { Ordering::Equal => path_a.file_name().cmp(&path_b.file_name()), other => other, } } pub(crate) fn create_crash_message(library_name: &str, file_path: &str, home_library_url: &str) -> String { format!( "{library_name} library crashed when opening \"{file_path}\", please check if this is fixed with the latest version of {library_name} and if it is not fixed, please report bug here - {home_library_url}" ) } #[allow(clippy::string_slice)] pub fn regex_check(expression_item: &SingleExcludedItem, directory_name: &str) -> bool { if expression_item.expression_splits.is_empty() { return true; } // Early checking if directory contains all parts needed by expression for split in &expression_item.unique_extensions_splits { if !directory_name.contains(split) { return false; } } // `git*` shouldn't be true for `/gitsfafasfs` if !expression_item.expression.starts_with('*') && directory_name .find(&expression_item.expression_splits[0]) .expect("Cannot fail, because split must exists in directory_name") > 0 { return false; } // `*home` shouldn't be true for `/homeowner` if !expression_item.expression.ends_with('*') && !directory_name.ends_with(expression_item.expression_splits.last().expect("Cannot fail, because at least one item is available")) { return false; } // At the end we check if parts between * are correctly positioned let mut last_split_point = directory_name.find(&expression_item.expression_splits[0]).expect("Cannot fail, because is checked earlier"); let mut current_index: usize = 0; let mut found_index: usize; for spl in &expression_item.expression_splits[1..] { found_index = match directory_name[current_index..].find(spl) { Some(t) => t, None => return false, }; current_index = last_split_point + spl.len(); last_split_point = found_index + current_index; } true } #[allow(clippy::string_slice)] // Is in char boundary pub fn normalize_windows_path(path_to_change: impl AsRef) -> PathBuf { let path = path_to_change.as_ref(); // Don't do anything, because network path may be case intensive if path.to_string_lossy().starts_with('\\') { return path.to_path_buf(); } match path.to_str() { Some(path) if path.is_char_boundary(1) => { let replaced = path.replace('/', "\\"); let mut new_path = OsString::new(); if replaced[1..].starts_with(':') { new_path.push(replaced[..1].to_ascii_uppercase()); new_path.push(replaced[1..].to_ascii_lowercase()); } else { new_path.push(replaced.to_ascii_lowercase()); } PathBuf::from(new_path) } _ => path.to_path_buf(), } } pub fn make_hard_link(src: &Path, dst: &Path) -> io::Result<()> { let dst_dir = dst.parent().ok_or_else(|| Error::other("No parent"))?; let temp = dst_dir.join(TEMP_HARDLINK_FILE); fs::rename(dst, temp.as_path())?; let result = fs::hard_link(src, dst); if result.is_err() { fs::rename(temp.as_path(), dst)?; } fs::remove_file(temp)?; result } #[cfg(test)] mod test { use std::fs::{File, Metadata, read_dir}; use std::io::Write; #[cfg(target_family = "windows")] use std::os::fs::MetadataExt; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::{fs, io}; use tempfile::tempdir; use crate::common::items::new_excluded_item; use crate::common::{make_hard_link, normalize_windows_path, regex_check, remove_folder_if_contains_only_empty_folders}; #[cfg(target_family = "unix")] fn assert_inode(before: &Metadata, after: &Metadata) { assert_eq!(before.ino(), after.ino()); } #[cfg(target_family = "windows")] fn assert_inode(_: &Metadata, _: &Metadata) {} #[test] fn test_make_hard_link() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; let metadata = fs::metadata(&src)?; File::create(&dst)?; make_hard_link(&src, &dst)?; assert_inode(&metadata, &fs::metadata(&dst)?); assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); assert_inode(&metadata, &fs::metadata(&src)?); assert_eq!(metadata.permissions(), fs::metadata(&src)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&src)?.modified()?); let mut actual = read_dir(&dir)?.flatten().map(|e| e.path()).collect::>(); actual.sort_unstable(); assert_eq!(vec![src, dst], actual); Ok(()) } #[test] fn test_make_hard_link_fails() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&dst)?; let metadata = fs::metadata(&dst)?; assert!(make_hard_link(&src, &dst).is_err()); assert_inode(&metadata, &fs::metadata(&dst)?); assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); assert_eq!(vec![dst], read_dir(&dir)?.flatten().map(|e| e.path()).collect::>()); Ok(()) } #[test] fn test_remove_folder_if_contains_only_empty_folders() { let dir = tempdir().expect("Cannot create temporary directory"); let sub_dir = dir.path().join("sub_dir"); fs::create_dir(&sub_dir).expect("Cannot create directory"); // Test with empty directory assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_ok()); assert!(!Path::new(&sub_dir).exists()); // Test with directory containing an empty directory fs::create_dir(&sub_dir).expect("Cannot create directory"); fs::create_dir(sub_dir.join("empty_sub_dir")).expect("Cannot create directory"); assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_ok()); assert!(!Path::new(&sub_dir).exists()); // Test with directory containing a file fs::create_dir(&sub_dir).expect("Cannot create directory"); let mut file = File::create(sub_dir.join("file.txt")).expect("Cannot create file"); writeln!(file, "Hello, world!").expect("Cannot write to file"); assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_err()); assert!(Path::new(&sub_dir).exists()); } #[test] fn test_regex() { assert!(regex_check(&new_excluded_item("*"), "/home/rafal")); assert!(regex_check(&new_excluded_item("*home*"), "/home/rafal")); assert!(regex_check(&new_excluded_item("*home"), "/home")); assert!(regex_check(&new_excluded_item("*home/"), "/home/")); assert!(regex_check(&new_excluded_item("*home/*"), "/home/")); assert!(regex_check(&new_excluded_item("*.git*"), "/home/.git")); assert!(regex_check(&new_excluded_item("*/home/rafal*rafal*rafal*rafal*"), "/home/rafal/rafalrafalrafal")); assert!(regex_check(&new_excluded_item("AAA"), "AAA")); assert!(regex_check(&new_excluded_item("AAA*"), "AAABDGG/QQPW*")); assert!(!regex_check(&new_excluded_item("*home"), "/home/")); assert!(!regex_check(&new_excluded_item("*home"), "/homefasfasfasfasf/")); assert!(!regex_check(&new_excluded_item("*home"), "/homefasfasfasfasf")); assert!(!regex_check(&new_excluded_item("rafal*afal*fal"), "rafal")); assert!(!regex_check(&new_excluded_item("rafal*a"), "rafal")); assert!(!regex_check(&new_excluded_item("AAAAAAAA****"), "/AAAAAAAAAAAAAAAAA")); assert!(!regex_check(&new_excluded_item("*.git/*"), "/home/.git")); assert!(!regex_check(&new_excluded_item("*home/*koc"), "/koc/home/")); assert!(!regex_check(&new_excluded_item("*home/"), "/home")); assert!(!regex_check(&new_excluded_item("*TTT"), "/GGG")); assert!(regex_check( &new_excluded_item("*/home/*/.local/share/containers"), "/var/home/roman/.local/share/containers" )); if cfg!(target_family = "windows") { assert!(regex_check(&new_excluded_item("*\\home"), "C:\\home")); assert!(regex_check(&new_excluded_item("*/home"), "C:\\home")); } } #[test] fn test_windows_path() { assert_eq!(PathBuf::from("C:\\path.txt"), normalize_windows_path("c:/PATH.tXt")); assert_eq!(PathBuf::from("H:\\reka\\weza\\roman.txt"), normalize_windows_path("h:/RekA/Weza\\roMan.Txt")); assert_eq!(PathBuf::from("T:\\a"), normalize_windows_path("T:\\A")); assert_eq!(PathBuf::from("\\\\aBBa"), normalize_windows_path("\\\\aBBa")); assert_eq!(PathBuf::from("a"), normalize_windows_path("a")); assert_eq!(PathBuf::from(""), normalize_windows_path("")); } } czkawka_core-10.0.0/src/common/model.rs000064400000000000000000000030461046102023000160750ustar 00000000000000use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use xxhash_rust::xxh3::Xxh3; use crate::common::traits::ResultEntry; use crate::tools::duplicate::MyHasher; #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub enum ToolType { Duplicate, EmptyFolders, EmptyFiles, InvalidSymlinks, BrokenFiles, BadExtensions, BigFile, SameMusic, SimilarImages, SimilarVideos, TemporaryFiles, #[default] None, } #[derive(PartialEq, Eq, Clone, Debug, Copy, Default, Deserialize, Serialize)] pub enum CheckingMethod { #[default] None, Name, SizeName, Size, Hash, AudioTags, AudioContent, } #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct FileEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, } impl ResultEntry for FileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(PartialEq, Eq, Clone, Debug, Copy, Default)] pub enum HashType { #[default] Blake3, Crc32, Xxh3, } impl HashType { pub(crate) fn hasher(self) -> Box { match self { Self::Blake3 => Box::new(blake3::Hasher::new()), Self::Crc32 => Box::new(crc32fast::Hasher::new()), Self::Xxh3 => Box::new(Xxh3::new()), } } } #[derive(Debug, PartialEq)] pub enum WorkContinueStatus { Continue, Stop, } czkawka_core-10.0.0/src/common/progress_data.rs000064400000000000000000000223571046102023000176400ustar 00000000000000use log::error; use crate::common::model::{CheckingMethod, ToolType}; // Empty files // 0 - Collecting files // Empty folders // 0 - Collecting folders // Big files // 0 - Collecting files // Same music // 0 - Collecting files // 1 - Loading cache // 2 - Checking tags // 3 - Saving cache // 4 - TAGS - Comparing tags // 4 - CONTENT - Loading cache // 5 - CONTENT - Calculating fingerprints // 6 - CONTENT - Saving cache // 7 - CONTENT - Comparing fingerprints // Similar images // 0 - Collecting files // 1 - Scanning images // 2 - Comparing hashes // Similar videos // 0 - Collecting files // 1 - Scanning videos // Temporary files // 0 - Collecting files // Invalid symlinks // 0 - Collecting files // Broken files // 0 - Collecting files // 1 - Scanning files // Bad extensions // 0 - Collecting files // 1 - Scanning files // Duplicates - Hash // 0 - Collecting files // 1 - Loading cache // 2 - Hash - first 1KB file // 3 - Saving cache // 4 - Loading cache // 5 - Hash - normal hash // 6 - Saving cache // Duplicates - Name or SizeName or Size // 0 - Collecting files // Deleting files // Renaming files #[derive(Debug, Clone, Copy)] pub struct ProgressData { pub sstage: CurrentStage, pub checking_method: CheckingMethod, pub current_stage_idx: u8, pub max_stage_idx: u8, pub entries_checked: usize, pub entries_to_check: usize, pub bytes_checked: u64, pub bytes_to_check: u64, pub tool_type: ToolType, } impl ProgressData { pub fn get_empty_state(current_stage: CurrentStage) -> Self { Self { sstage: current_stage, checking_method: CheckingMethod::None, current_stage_idx: 0, max_stage_idx: 0, entries_checked: 0, entries_to_check: 0, bytes_checked: 0, bytes_to_check: 0, tool_type: ToolType::None, } } } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum CurrentStage { DeletingFiles, RenamingFiles, MovingFiles, CollectingFiles, DuplicateCacheSaving, DuplicateCacheLoading, DuplicatePreHashCacheSaving, DuplicatePreHashCacheLoading, DuplicateScanningName, DuplicateScanningSizeName, DuplicateScanningSize, DuplicatePreHashing, DuplicateFullHashing, SameMusicCacheSavingTags, SameMusicCacheLoadingTags, SameMusicCacheSavingFingerprints, SameMusicCacheLoadingFingerprints, SameMusicReadingTags, SameMusicCalculatingFingerprints, SameMusicComparingTags, SameMusicComparingFingerprints, SimilarImagesCalculatingHashes, SimilarImagesComparingHashes, SimilarVideosCalculatingHashes, BrokenFilesChecking, BadExtensionsChecking, } impl ProgressData { pub(crate) fn validate(&self) { assert!( self.current_stage_idx <= self.max_stage_idx, "Current stage index: {}, max stage index: {}, stage {:?}", self.current_stage_idx, self.max_stage_idx, self.sstage ); assert_eq!( self.max_stage_idx, self.tool_type.get_max_stage(self.checking_method), "Max stage index: {}, tool type: {:?}, checking method: {:?}", self.max_stage_idx, self.tool_type, self.checking_method ); if self.sstage != CurrentStage::CollectingFiles { assert!( self.entries_checked <= self.entries_to_check, "Entries checked: {}, entries to check: {}, stage {:?}", self.entries_checked, self.entries_to_check, self.sstage ); } // This could be an assert, but it is possible that in duplicate finder, file that will // be checked, will increase the size of the file between collecting file to scan and // scanning it. So it is better to just log it if self.bytes_checked > self.bytes_to_check { error!("Bytes checked: {}, bytes to check: {}, stage {:?}", self.bytes_checked, self.bytes_to_check, self.sstage); }; let tool_type_checking_method: Option = match self.checking_method { CheckingMethod::AudioTags | CheckingMethod::AudioContent => Some(ToolType::SameMusic), CheckingMethod::Name | CheckingMethod::SizeName | CheckingMethod::Size | CheckingMethod::Hash => Some(ToolType::Duplicate), CheckingMethod::None => None, }; if let Some(tool_type) = tool_type_checking_method { assert_eq!(self.tool_type, tool_type, "Tool type: {:?}, checking method: {:?}", self.tool_type, self.checking_method); } let tool_type_current_stage: Option = match self.sstage { CurrentStage::CollectingFiles | CurrentStage::DeletingFiles | CurrentStage::RenamingFiles | CurrentStage::MovingFiles => None, CurrentStage::DuplicateCacheSaving | CurrentStage::DuplicateCacheLoading | CurrentStage::DuplicatePreHashCacheSaving | CurrentStage::DuplicatePreHashCacheLoading => { Some(ToolType::Duplicate) } CurrentStage::DuplicateScanningName | CurrentStage::DuplicateScanningSizeName | CurrentStage::DuplicateScanningSize | CurrentStage::DuplicatePreHashing | CurrentStage::DuplicateFullHashing => Some(ToolType::Duplicate), CurrentStage::SameMusicCacheLoadingTags | CurrentStage::SameMusicCacheSavingTags | CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::SameMusicComparingTags | CurrentStage::SameMusicReadingTags | CurrentStage::SameMusicComparingFingerprints | CurrentStage::SameMusicCalculatingFingerprints => Some(ToolType::SameMusic), CurrentStage::SimilarImagesCalculatingHashes | CurrentStage::SimilarImagesComparingHashes => Some(ToolType::SimilarImages), CurrentStage::SimilarVideosCalculatingHashes => Some(ToolType::SimilarVideos), CurrentStage::BrokenFilesChecking => Some(ToolType::BrokenFiles), CurrentStage::BadExtensionsChecking => Some(ToolType::BadExtensions), }; if let Some(tool_type) = tool_type_current_stage { assert_eq!(self.tool_type, tool_type, "Tool type: {:?}, stage {:?}", self.tool_type, self.sstage); } } } impl ToolType { pub(crate) fn get_max_stage(&self, checking_method: CheckingMethod) -> u8 { match *self { Self::Duplicate => 6, Self::EmptyFolders | Self::EmptyFiles | Self::InvalidSymlinks | Self::BigFile | Self::TemporaryFiles => 0, Self::BrokenFiles | Self::BadExtensions | Self::SimilarVideos => 1, Self::SimilarImages => 2, Self::None => unreachable!("ToolType::None is not allowed"), Self::SameMusic => match checking_method { CheckingMethod::AudioTags => 4, CheckingMethod::AudioContent => 7, _ => unreachable!("CheckingMethod {checking_method:?} in same music mode is not allowed"), }, } } } impl CurrentStage { pub fn is_special_non_tool_stage(&self) -> bool { matches!(self, Self::DeletingFiles | Self::RenamingFiles | Self::MovingFiles) } pub fn get_current_stage(&self) -> u8 { #[allow(clippy::match_same_arms)] // Now it is easier to read match self { Self::DeletingFiles => 0, Self::RenamingFiles => 0, Self::MovingFiles => 0, Self::CollectingFiles => 0, Self::DuplicateScanningName => 0, Self::DuplicateScanningSizeName => 0, Self::DuplicateScanningSize => 0, Self::DuplicatePreHashCacheLoading => 1, Self::DuplicatePreHashing => 2, Self::DuplicatePreHashCacheSaving => 3, Self::DuplicateCacheLoading => 4, Self::DuplicateFullHashing => 5, Self::DuplicateCacheSaving => 6, Self::SimilarImagesCalculatingHashes => 1, Self::SimilarImagesComparingHashes => 2, Self::SimilarVideosCalculatingHashes => 1, Self::BrokenFilesChecking => 1, Self::BadExtensionsChecking => 1, Self::SameMusicCacheLoadingTags => 1, Self::SameMusicReadingTags => 2, Self::SameMusicCacheSavingTags => 3, Self::SameMusicComparingTags => 4, Self::SameMusicCacheLoadingFingerprints => 4, Self::SameMusicCalculatingFingerprints => 5, Self::SameMusicCacheSavingFingerprints => 6, Self::SameMusicComparingFingerprints => 7, } } pub fn check_if_loading_saving_cache(&self) -> bool { self.check_if_saving_cache() || self.check_if_loading_cache() } pub fn check_if_loading_cache(&self) -> bool { matches!( self, Self::SameMusicCacheLoadingFingerprints | Self::SameMusicCacheLoadingTags | Self::DuplicateCacheLoading | Self::DuplicatePreHashCacheLoading ) } pub fn check_if_saving_cache(&self) -> bool { matches!( self, Self::SameMusicCacheSavingFingerprints | Self::SameMusicCacheSavingTags | Self::DuplicateCacheSaving | Self::DuplicatePreHashCacheSaving ) } } czkawka_core-10.0.0/src/common/progress_stop_handler.rs000064400000000000000000000113541046102023000214040ustar 00000000000000use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize}; use std::sync::{Arc, atomic}; use std::thread; use std::thread::{JoinHandle, sleep}; use std::time::{Duration, Instant}; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::{CheckingMethod, ToolType}; use crate::common::progress_data::{CurrentStage, ProgressData}; pub const LOOP_DURATION: u32 = 20; pub const SEND_PROGRESS_DATA_TIME_BETWEEN: u32 = 200; pub(crate) struct ProgressThreadHandler { progress_thread_handle: JoinHandle<()>, progress_thread_running: Arc, progress_status: ProgressStatus, } impl ProgressThreadHandler { pub fn new(progress_thread_handle: JoinHandle<()>, progress_thread_running: Arc, progress_status: ProgressStatus) -> Self { Self { progress_thread_handle, progress_thread_running, progress_status, } } pub fn join_thread(self) { self.progress_thread_running.store(false, atomic::Ordering::Relaxed); self.progress_thread_handle .join() .expect("Cannot join progress thread - quite fatal error, but I hope, that it will never happen :)"); } pub fn increase_items(&self, count: usize) { self.progress_status.items_counter.fetch_add(count, atomic::Ordering::Relaxed); } pub fn increase_size(&self, size: u64) { self.progress_status.size_counter.fetch_add(size, atomic::Ordering::Relaxed); } pub fn items_counter(&self) -> &Arc { &self.progress_status.items_counter } pub fn size_counter(&self) -> &Arc { &self.progress_status.size_counter } } #[derive(Clone)] pub(crate) struct ProgressStatus { items_counter: Arc, size_counter: Arc, } impl ProgressStatus { pub fn new() -> Self { Self { items_counter: Arc::new(AtomicUsize::new(0)), size_counter: Arc::new(AtomicU64::new(0)), } } } pub(crate) fn prepare_thread_handler_common( progress_sender: Option<&Sender>, sstage: CurrentStage, max_items: usize, test_type: (ToolType, CheckingMethod), max_size: u64, ) -> ProgressThreadHandler { let (tool_type, checking_method) = test_type; assert_ne!(tool_type, ToolType::None, "Cannot send progress data for ToolType::None"); let progress_status = ProgressStatus::new(); let progress_thread_running = Arc::new(AtomicBool::new(true)); let progress_thread_sender = if let Some(progress_sender) = progress_sender.cloned() { let progress_status = progress_status.clone(); let progress_thread_running = progress_thread_running.clone(); thread::spawn(move || { // Use earlier time, to send immediately first message let mut time_since_last_send = Instant::now().checked_sub(Duration::from_secs(10u64)).unwrap_or_else(Instant::now); loop { if time_since_last_send.elapsed().as_millis() > SEND_PROGRESS_DATA_TIME_BETWEEN as u128 { let progress_data = ProgressData { sstage, checking_method, current_stage_idx: sstage.get_current_stage(), max_stage_idx: tool_type.get_max_stage(checking_method), entries_checked: progress_status.items_counter.load(atomic::Ordering::Relaxed), entries_to_check: max_items, bytes_checked: progress_status.size_counter.load(atomic::Ordering::Relaxed), bytes_to_check: max_size, tool_type, }; progress_data.validate(); progress_sender.send(progress_data).expect("Cannot send progress data"); time_since_last_send = Instant::now(); } if !progress_thread_running.load(atomic::Ordering::Relaxed) { break; } sleep(Duration::from_millis(LOOP_DURATION as u64)); } }) } else { thread::spawn(|| {}) }; ProgressThreadHandler::new(progress_thread_sender, progress_thread_running, progress_status) } #[inline] pub(crate) fn check_if_stop_received(stop_flag: &Arc) -> bool { stop_flag.load(atomic::Ordering::Relaxed) } #[fun_time(message = "send_info_and_wait_for_ending_all_threads", level = "debug")] pub(crate) fn send_info_and_wait_for_ending_all_threads(progress_thread_run: &Arc, progress_thread_handle: JoinHandle<()>) { progress_thread_run.store(false, atomic::Ordering::Relaxed); progress_thread_handle.join().expect("Cannot join progress thread - quite fatal error, but happens rarely"); } czkawka_core-10.0.0/src/common/tool_data.rs000064400000000000000000000463061046102023000167510ustar 00000000000000use std::fs; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Duration; use crossbeam_channel::Sender; use humansize::{BINARY, format_size}; use log::info; use rayon::prelude::*; use crate::common::directories::Directories; use crate::common::extensions::Extensions; use crate::common::items::ExcludedItems; use crate::common::model::{CheckingMethod, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::remove_folder_if_contains_only_empty_folders; use crate::common::traits::ResultEntry; use crate::helpers::delayed_sender::DelayedSender; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct CommonToolData { pub(crate) tool_type: ToolType, pub(crate) text_messages: Messages, pub(crate) directories: Directories, pub(crate) extensions: Extensions, pub(crate) excluded_items: ExcludedItems, pub(crate) recursive_search: bool, pub(crate) delete_method: DeleteMethod, pub(crate) maximal_file_size: u64, pub(crate) minimal_file_size: u64, pub(crate) stopped_search: bool, pub(crate) use_cache: bool, pub(crate) delete_outdated_cache: bool, pub(crate) save_also_as_json: bool, pub(crate) use_reference_folders: bool, pub(crate) dry_run: bool, } #[derive(Debug, Clone, Default)] pub struct DeleteResult { deleted_files: usize, gained_bytes: u64, failed_to_delete_files: usize, errors: Vec, infos: Vec, } impl DeleteResult { pub(crate) fn add_to_messages(&self, messages: &mut Messages) { messages.errors.extend(self.errors.clone()); messages.messages.extend(self.infos.clone()); } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum DeleteItemType { DeletingFiles(Vec), DeletingFolders(Vec), HardlinkingFiles(Vec<(T, Vec)>), } impl DeleteItemType { fn calculate_size_to_delete(&self) -> u64 { match &self { Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.iter().map(|item| item.get_size()).sum(), Self::HardlinkingFiles(items) => items.iter().map(|(item, _)| item.get_size()).sum(), } } fn calculate_entries_to_delete(&self) -> usize { match &self { Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.len(), Self::HardlinkingFiles(items) => items.iter().map(|(_original, files)| files.len()).sum(), } } } #[derive(Eq, PartialEq, Clone, Debug, Copy, Default)] pub enum DeleteMethod { #[default] None, Delete, // Just delete items AllExceptNewest, AllExceptOldest, OneOldest, OneNewest, HardLink, AllExceptBiggest, AllExceptSmallest, OneBiggest, OneSmallest, } impl CommonToolData { pub fn new(tool_type: ToolType) -> Self { Self { tool_type, text_messages: Messages::new(), directories: Directories::new(), extensions: Extensions::new(), excluded_items: ExcludedItems::new(), recursive_search: true, delete_method: DeleteMethod::None, maximal_file_size: u64::MAX, minimal_file_size: 8192, stopped_search: false, use_cache: true, delete_outdated_cache: true, save_also_as_json: false, use_reference_folders: false, dry_run: false, } } } pub trait CommonData { type Info; type Parameters; fn get_information(&self) -> Self::Info; fn get_params(&self) -> Self::Parameters; fn get_cd(&self) -> &CommonToolData; fn get_cd_mut(&mut self) -> &mut CommonToolData; fn get_check_method(&self) -> CheckingMethod { CheckingMethod::None } fn get_test_type(&self) -> (ToolType, CheckingMethod) { (self.get_cd().tool_type, self.get_check_method()) } fn found_any_broken_files(&self) -> bool; fn get_tool_type(&self) -> ToolType { self.get_cd().tool_type } fn set_dry_run(&mut self, dry_run: bool) { self.get_cd_mut().dry_run = dry_run; } fn get_dry_run(&self) -> bool { self.get_cd().dry_run } fn set_use_cache(&mut self, use_cache: bool) { self.get_cd_mut().use_cache = use_cache; } fn get_use_cache(&self) -> bool { self.get_cd().use_cache } fn set_delete_outdated_cache(&mut self, delete_outdated_cache: bool) { self.get_cd_mut().delete_outdated_cache = delete_outdated_cache; } fn get_delete_outdated_cache(&self) -> bool { self.get_cd().delete_outdated_cache } fn get_stopped_search(&self) -> bool { self.get_cd().stopped_search } fn set_stopped_search(&mut self, stopped_search: bool) { self.get_cd_mut().stopped_search = stopped_search; } fn set_maximal_file_size(&mut self, maximal_file_size: u64) { self.get_cd_mut().maximal_file_size = match maximal_file_size { 0 => 1, t => t, }; } fn get_maximal_file_size(&self) -> u64 { self.get_cd().maximal_file_size } fn set_minimal_file_size(&mut self, minimal_file_size: u64) { self.get_cd_mut().minimal_file_size = match minimal_file_size { 0 => 1, t => t, }; } fn get_minimal_file_size(&self) -> u64 { self.get_cd().minimal_file_size } fn set_reference_directory(&mut self, reference_directory: Vec) { let messages = self.get_cd_mut().directories.set_reference_directory(&reference_directory); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } #[cfg(target_family = "unix")] fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) { self.get_cd_mut().directories.set_exclude_other_filesystems(exclude_other_filesystems); } #[cfg(not(target_family = "unix"))] fn set_exclude_other_filesystems(&mut self, _exclude_other_filesystems: bool) {} fn get_text_messages(&self) -> &Messages { &self.get_cd().text_messages } fn get_text_messages_mut(&mut self) -> &mut Messages { &mut self.get_cd_mut().text_messages } fn set_save_also_as_json(&mut self, save_also_as_json: bool) { self.get_cd_mut().save_also_as_json = save_also_as_json; } fn get_save_also_as_json(&self) -> bool { self.get_cd().save_also_as_json } fn set_recursive_search(&mut self, recursive_search: bool) { self.get_cd_mut().recursive_search = recursive_search; } fn get_recursive_search(&self) -> bool { self.get_cd().recursive_search } fn set_use_reference_folders(&mut self, use_reference_folders: bool) { self.get_cd_mut().use_reference_folders = use_reference_folders; } fn get_use_reference_folders(&self) -> bool { self.get_cd().use_reference_folders } fn set_delete_method(&mut self, delete_method: DeleteMethod) { self.get_cd_mut().delete_method = delete_method; } fn get_delete_method(&self) -> DeleteMethod { self.get_cd().delete_method } fn set_included_directory(&mut self, included_directory: Vec) { let messages = self.get_cd_mut().directories.set_included_directory(included_directory); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_directory(&mut self, excluded_directory: Vec) { let messages = self.get_cd_mut().directories.set_excluded_directory(excluded_directory); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_allowed_extensions(&mut self, allowed_extensions: String) { let messages = self.get_cd_mut().extensions.set_allowed_extensions(allowed_extensions); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_extensions(&mut self, excluded_extensions: String) { let messages = self.get_cd_mut().extensions.set_excluded_extensions(excluded_extensions); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_items(&mut self, excluded_items: Vec) { let messages = self.get_cd_mut().excluded_items.set_excluded_items(excluded_items); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn prepare_items(&mut self) { let recursive_search = self.get_cd().recursive_search; // Optimizes directories and removes recursive calls let messages = self.get_cd_mut().directories.optimize_directories(recursive_search); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn delete_simple_elements_and_add_to_messages( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, delete_item_type: DeleteItemType, ) -> WorkContinueStatus { let delete_results = self.delete_elements(stop_flag, progress_sender, delete_item_type); if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { WorkContinueStatus::Stop } else { delete_results.add_to_messages(self.get_text_messages_mut()); WorkContinueStatus::Continue } } fn delete_advanced_elements_and_add_to_messages( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, files_to_process: Vec>, ) -> WorkContinueStatus { let delete_method = self.get_cd().delete_method; let sorting_by_size = matches!( delete_method, DeleteMethod::AllExceptBiggest | DeleteMethod::AllExceptSmallest | DeleteMethod::OneBiggest | DeleteMethod::OneSmallest ); let sort_items = |mut input: Vec| -> Vec { input.sort_unstable_by_key(if sorting_by_size { ResultEntry::get_size } else { ResultEntry::get_modified_date }); input }; let delete_results = if delete_method == DeleteMethod::HardLink { let res = files_to_process .into_iter() .map(|values| { let mut all_values = sort_items(values); let original = all_values.remove(0); (original, all_values) }) .collect::>(); self.delete_elements(stop_flag, progress_sender, DeleteItemType::HardlinkingFiles(res)) } else { let res = files_to_process .into_iter() .flat_map(|values| { // TODO - probably a little too much cloning, so later could be this optimized let len = values.len(); let all_values = sort_items(values); match delete_method { DeleteMethod::Delete => &all_values, DeleteMethod::AllExceptNewest | DeleteMethod::AllExceptBiggest => &all_values[..(len - 1)], DeleteMethod::AllExceptOldest | DeleteMethod::AllExceptSmallest => &all_values[1..], DeleteMethod::OneOldest | DeleteMethod::OneSmallest => &all_values[..1], DeleteMethod::OneNewest | DeleteMethod::OneBiggest => &all_values[(len - 1)..], DeleteMethod::HardLink | DeleteMethod::None => unreachable!("HardLink and None should be handled before"), } .to_vec() }) .collect::>(); self.delete_elements(stop_flag, progress_sender, DeleteItemType::DeletingFiles(res)) }; if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { WorkContinueStatus::Stop } else { delete_results.add_to_messages(self.get_text_messages_mut()); WorkContinueStatus::Continue } } fn delete_elements( &self, stop_flag: &Arc, progress_sender: Option<&Sender>, delete_item_type: DeleteItemType, ) -> DeleteResult { let dry_run = self.get_cd().dry_run; let mut progress = ProgressData::get_empty_state(CurrentStage::DeletingFiles); progress.bytes_to_check = delete_item_type.calculate_size_to_delete(); progress.entries_to_check = delete_item_type.calculate_entries_to_delete(); let is_hardlinking = matches!(delete_item_type, DeleteItemType::HardlinkingFiles(_)); let msg_common = format!( "{} items, total size: {} bytes, dry_run: {dry_run}", progress.entries_to_check, format_size(progress.bytes_to_check, BINARY) ); if is_hardlinking { info!("Hardlinking {msg_common}"); } else { info!("Deleting {msg_common}"); } let delayed_sender = progress_sender.map(|e| DelayedSender::new(e.clone(), Duration::from_millis(200))); let bytes_processed = Arc::new(std::sync::atomic::AtomicU64::new(0)); let files_processed = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let res = match delete_item_type { DeleteItemType::DeletingFiles(ref items) | DeleteItemType::DeletingFolders(ref items) => items .into_par_iter() .map(|e| { if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { return None; } let mut progress_tmp = progress; progress_tmp.bytes_checked = bytes_processed.fetch_add(e.get_size(), std::sync::atomic::Ordering::Relaxed); progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if let Some(e) = delayed_sender.as_ref() { e.send(progress_tmp); } if dry_run { return Some(vec![(e, None)]); } let delete_res = if matches!(delete_item_type, DeleteItemType::DeletingFiles(_)) { fs::remove_file(e.get_path()).map_err(|err| format!("Failed to delete \"{}\": {err}", e.get_path().to_string_lossy())) } else { remove_folder_if_contains_only_empty_folders(e.get_path(), false) // TODO remove to trash should be an option }; match delete_res { Ok(()) => Some(vec![(e, None)]), Err(err) => Some(vec![(e, Some(err))]), } }) .while_some() .flatten() .collect::>(), DeleteItemType::HardlinkingFiles(ref items) => items .into_par_iter() .map(|(original, files)| { if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { return None; } let mut progress_tmp = progress; progress_tmp.bytes_checked = bytes_processed.fetch_add(files.iter().map(|e| e.get_size()).sum(), std::sync::atomic::Ordering::Relaxed); progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if let Some(e) = delayed_sender.as_ref() { e.send(progress_tmp); } if dry_run { return Some(files.iter().map(|e| (e, None)).collect::>()); } let res = files .iter() .map(|file| { let err = match fs::hard_link(original.get_path(), file.get_path()) { Ok(()) => None, Err(err) => Some(format!( "Failed to hardlink \"{}\" to \"{}\": {err}", original.get_path().to_string_lossy(), file.get_path().to_string_lossy() )), }; (file, err) }) .collect::>(); Some(res) }) .while_some() .flatten() .collect::>(), }; let mut delete_result = DeleteResult::default(); for (file_entry, delete_err) in res { if let Some(err) = delete_err { delete_result.errors.push(err); delete_result.failed_to_delete_files += 1; } else { if dry_run { if is_hardlinking { delete_result.infos.push(format!( "Would hardlink: \"{}\" to \"{}\"", file_entry.get_path().to_string_lossy(), file_entry.get_path().to_string_lossy() )); } else { delete_result.infos.push(format!("Would delete: \"{}\"", file_entry.get_path().to_string_lossy())); } } delete_result.deleted_files += 1; delete_result.gained_bytes += file_entry.get_size(); } } if !dry_run { info!( "{} items deleted, {} bytes gained, {} failed to delete", delete_result.deleted_files, format_size(delete_result.gained_bytes, BINARY), delete_result.failed_to_delete_files ); } delete_result } #[allow(clippy::print_stdout)] fn debug_print_common(&self) { println!("---------------DEBUG PRINT COMMON---------------"); println!("Tool type: {:?}", self.get_cd().tool_type); println!("Directories: {:?}", self.get_cd().directories); println!("Extensions: {:?}", self.get_cd().extensions); println!("Excluded items: {:?}", self.get_cd().excluded_items); println!("Recursive search: {}", self.get_cd().recursive_search); println!("Maximal file size: {}", self.get_cd().maximal_file_size); println!("Minimal file size: {}", self.get_cd().minimal_file_size); println!("Stopped search: {}", self.get_cd().stopped_search); println!("Use cache: {}", self.get_cd().use_cache); println!("Delete outdated cache: {}", self.get_cd().delete_outdated_cache); println!("Save also as json: {}", self.get_cd().save_also_as_json); println!("Delete method: {:?}", self.get_cd().delete_method); println!("Use reference folders: {}", self.get_cd().use_reference_folders); println!("Dry run: {}", self.get_cd().dry_run); println!("---------------DEBUG PRINT MESSAGES---------------"); println!("Errors size - {}", self.get_cd().text_messages.errors.len()); println!("Warnings size - {}", self.get_cd().text_messages.warnings.len()); println!("Messages size - {}", self.get_cd().text_messages.messages.len()); } } czkawka_core-10.0.0/src/common/traits.rs000064400000000000000000000076131046102023000163070ustar 00000000000000use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use serde::Serialize; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonData; pub trait DebugPrint { fn debug_print(&self); } pub trait PrintResults { fn write_results(&self, writer: &mut T) -> std::io::Result<()>; #[fun_time(message = "print_results_to_output", level = "debug")] fn print_results_to_output(&self) { let stdout = std::io::stdout(); let mut handle = stdout.lock(); // Panics here are allowed, because it is used only in CLI self.write_results(&mut handle).expect("Error while writing to stdout"); handle.flush().expect("Error while flushing stdout"); } #[fun_time(message = "print_results_to_file", level = "debug")] fn print_results_to_file(&self, file_name: &str) -> std::io::Result<()> { let file_name: String = match file_name { "" => "results.txt".to_string(), k => k.to_string(), }; let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); self.write_results(&mut writer)?; writer.flush()?; Ok(()) } #[fun_time(message = "print_results_to_writer", level = "debug")] fn print_results_to_writer(&self, writer: &mut T) -> std::io::Result<()> { self.write_results(writer) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()>; fn save_results_to_file_as_json_internal(&self, file_name: &str, item_to_serialize: &T, pretty_print: bool) -> std::io::Result<()> { if pretty_print { self.save_results_to_file_as_json_pretty(file_name, item_to_serialize) } else { self.save_results_to_file_as_json_compact(file_name, item_to_serialize) } } #[fun_time(message = "save_results_to_file_as_json_pretty", level = "debug")] fn save_results_to_file_as_json_pretty(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> { let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); serde_json::to_writer_pretty(&mut writer, item_to_serialize)?; Ok(()) } #[fun_time(message = "save_results_to_file_as_json_compact", level = "debug")] fn save_results_to_file_as_json_compact(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> { let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); serde_json::to_writer(&mut writer, item_to_serialize)?; Ok(()) } fn save_all_in_one(&self, folder: &str, base_file_name: &str) -> std::io::Result<()> { let pretty_name = format!("{folder}/{base_file_name}_pretty.json"); self.save_results_to_file_as_json(&pretty_name, true)?; let compact_name = format!("{folder}/{base_file_name}_compact.json"); self.save_results_to_file_as_json(&compact_name, false)?; let txt_name = format!("{folder}/{base_file_name}.txt"); self.print_results_to_file(&txt_name)?; Ok(()) } } pub trait DeletingItems { #[must_use] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus; } pub trait ResultEntry { fn get_path(&self) -> &Path; fn get_modified_date(&self) -> u64; fn get_size(&self) -> u64; } pub trait Search { fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>); } pub trait AllTraits: DebugPrint + PrintResults + DeletingItems + CommonData + Search {} czkawka_core-10.0.0/src/helpers/debug_timer.rs000064400000000000000000000054311046102023000174350ustar 00000000000000use std::time::{Duration, Instant}; /// Timer for measuring elapsed time between checkpoints. /// /// # How to use - examples /// /// Basic usage: /// ``` /// use czkawka_core::helpers::debug_timer::Timer; /// use std::thread::sleep; /// use std::time::Duration; /// /// let mut timer = Timer::new("MyTimer"); /// sleep(Duration::from_millis(50)); /// timer.checkpoint("step1"); /// sleep(Duration::from_millis(30)); /// timer.checkpoint("step2"); /// let report = timer.report("all_steps", false); /// println!("{}", report); /// ``` /// /// Output example: /// ```text /// MyTimer - step1: 50.0ms, /// MyTimer - step2: 30.0ms, /// MyTimer - all_steps: 80.0ms /// ``` /// /// One-line output: /// ``` /// use czkawka_core::helpers::debug_timer::Timer; /// use std::thread::sleep; /// use std::time::Duration; /// /// let mut timer = Timer::new("MyTimer"); /// sleep(Duration::from_millis(10)); /// timer.checkpoint("a"); /// sleep(Duration::from_millis(20)); /// timer.checkpoint("b"); /// let report = timer.report("total", true); /// println!("{}", report); /// ``` /// /// Output example: /// ```text /// MyTimer - a: 10.0ms, b: 20.0ms, total: 30.0ms /// ``` pub struct Timer { /// Name or label for the timer. base: String, /// Time when the timer was started. start_time: Instant, /// Time of the last checkpoint. last_time: Instant, /// List of (checkpoint name, duration since last checkpoint). times: Vec<(String, Duration)>, } impl Timer { /// Creates a new timer with a given label. pub fn new(base: &str) -> Self { Self { base: base.to_string(), start_time: Instant::now(), last_time: Instant::now(), times: Vec::new(), } } /// Records a checkpoint with the given name. pub fn checkpoint(&mut self, name: &str) { let elapsed = self.last_time.elapsed(); self.times.push((name.to_string(), elapsed)); self.last_time = Instant::now(); } /// Returns a formatted report of all checkpoints and total time. /// /// If `in_one_line` is true, outputs all checkpoints in a single line. /// Otherwise, outputs each checkpoint on a separate line. pub fn report(&mut self, all_steps_name: &str, in_one_line: bool) -> String { let all_elapsed = self.start_time.elapsed(); self.times.push((all_steps_name.to_string(), all_elapsed)); if in_one_line { let times = self.times.iter().map(|(name, time)| format!("{name}: {time:?}")).collect::>().join(", "); format!("{} - {}", self.base, times) } else { self.times .iter() .map(|(name, time)| format!("{} - {name}: {time:?}", self.base)) .collect::>() .join(", \n") } } } czkawka_core-10.0.0/src/helpers/delayed_sender.rs000064400000000000000000000060031046102023000201120ustar 00000000000000//! DelayedSender: A utility for batching or throttling messages sent between threads. use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; /// A sender that delays sending values until a specified wait time has passed since the last sent value. /// /// This is useful for batching updates or reducing the frequency of sending messages in a multi-threaded environment. /// Note: Using mutexes in the send function from multiple threads can lead to performance issues (waiting for mutex release), /// but for now, the performance impact is minimal. In the future, a more efficient channel could be used. pub struct DelayedSender { slot: Arc>>, stop_flag: Arc, } impl DelayedSender { /// Creates a new DelayedSender. /// /// # Arguments /// * `sender` - The channel sender to forward values to. /// * `wait_time` - The minimum duration to wait between sends. pub fn new(sender: crossbeam_channel::Sender, wait_time: Duration) -> Self { let slot = Arc::new(Mutex::new(None)); let slot_clone = Arc::clone(&slot); let stop_flag = Arc::new(AtomicBool::new(false)); let stop_flag_clone = Arc::clone(&stop_flag); let _join = thread::spawn(move || { let mut last_send_time: Option = None; let duration_between_checks = Duration::from_secs_f64(wait_time.as_secs_f64() / 5.0); loop { if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { break; } if let Some(last_send_time) = last_send_time { if last_send_time.elapsed() < wait_time { thread::sleep(duration_between_checks); continue; } } let Some(value) = slot_clone.lock().expect("Failed to lock slot in DelayedSender").take() else { thread::sleep(duration_between_checks); continue; }; if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { break; } if let Err(e) = sender.send(value) { log::error!("Failed to send value: {e:?}"); }; last_send_time = Some(Instant::now()); } }); Self { slot, stop_flag } } /// Sends a value, replacing any previous value that has not yet been sent. pub fn send(&self, value: T) { let mut slot = self.slot.lock().expect("Failed to lock slot in DelayedSender"); *slot = Some(value); } } impl Drop for DelayedSender { fn drop(&mut self) { // After dropping DelayedSender, no more values will be sent. // Previously, some values were cached and sent after later operations. self.stop_flag.store(true, std::sync::atomic::Ordering::Relaxed); } } czkawka_core-10.0.0/src/helpers/messages.rs000064400000000000000000000057711046102023000167650ustar 00000000000000//! Messages: Utility for collecting and printing messages, warnings, and errors. /// Stores messages, warnings, and errors for reporting. #[derive(Debug, Default, Clone)] pub struct Messages { /// Informational messages. pub messages: Vec, /// Warning messages. pub warnings: Vec, /// Error messages. pub errors: Vec, } impl Messages { /// Creates a new, empty `Messages` struct. pub fn new() -> Self { Default::default() } /// Creates a new `Messages` struct with errors. pub fn new_from_errors(errors: Vec) -> Self { Self { errors, ..Default::default() } } /// Creates a new `Messages` struct with warnings. pub fn new_from_warnings(warnings: Vec) -> Self { Self { warnings, ..Default::default() } } /// Creates a new `Messages` struct with messages. pub fn new_from_messages(messages: Vec) -> Self { Self { messages, ..Default::default() } } /// Prints all messages, warnings, and errors to the provided writer. pub fn print_messages_to_writer(&self, writer: &mut T) -> std::io::Result<()> { let text = self.create_messages_text(); writer.write_all(text.as_bytes()) } /// Creates a formatted string containing all messages, warnings, and errors. pub fn create_messages_text(&self) -> String { let mut text_to_return: String = String::new(); if !self.messages.is_empty() { text_to_return += "-------------------------------MESSAGES--------------------------------\n"; for i in &self.messages { text_to_return += i; text_to_return += "\n"; } text_to_return += "---------------------------END OF MESSAGES-----------------------------\n"; } if !self.warnings.is_empty() { text_to_return += "-------------------------------WARNINGS--------------------------------\n"; for i in &self.warnings { text_to_return += i; text_to_return += "\n"; } text_to_return += "---------------------------END OF WARNINGS-----------------------------\n"; } if !self.errors.is_empty() { text_to_return += "--------------------------------ERRORS---------------------------------\n"; for i in &self.errors { text_to_return += i; text_to_return += "\n"; } text_to_return += "----------------------------END OF ERRORS------------------------------\n"; } text_to_return } /// Extends this `Messages` struct with another, appending all messages, warnings, and errors. pub fn extend_with_another_messages(&mut self, messages: Self) { let (messages, warnings, errors) = (messages.messages, messages.warnings, messages.errors); self.messages.extend(messages); self.warnings.extend(warnings); self.errors.extend(errors); } } czkawka_core-10.0.0/src/helpers/mod.rs000064400000000000000000000002371046102023000157250ustar 00000000000000//! Helper modules: generic utilities, traits, structs, ready to copy/paste to other projects. pub mod debug_timer; pub mod delayed_sender; pub mod messages; czkawka_core-10.0.0/src/lib.rs000064400000000000000000000010201046102023000142410ustar 00000000000000#![allow(clippy::collapsible_else_if)] #![allow(clippy::type_complexity)] #![allow(clippy::needless_late_init)] #![allow(clippy::too_many_arguments)] #![warn(clippy::unwrap_used)] #![warn(clippy::print_stderr)] #![warn(clippy::print_stdout)] #![warn(clippy::dbg_macro)] #![warn(clippy::string_slice)] #[macro_use] extern crate bitflags; extern crate core; pub mod common; pub mod helpers; pub mod localizer_core; pub mod tools; pub const CZKAWKA_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const TOOLS_NUMBER: usize = 11; czkawka_core-10.0.0/src/localizer_core.rs000064400000000000000000000027351046102023000165050ustar 00000000000000use std::collections::HashMap; use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; use once_cell::sync::Lazy; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; pub static LANGUAGE_LOADER_CORE: Lazy = Lazy::new(|| { let loader: FluentLanguageLoader = fluent_language_loader!(); loader.load_fallback_language(&Localizations).expect("Error while loading fallback language"); loader }); #[macro_export] macro_rules! flc { ($message_id:literal) => {{ i18n_embed_fl::fl!($crate::localizer_core::LANGUAGE_LOADER_CORE, $message_id) }}; ($message_id:literal, $($args:expr),*) => {{ i18n_embed_fl::fl!($crate::localizer_core::LANGUAGE_LOADER_CORE, $message_id, $($args), *) }}; } // Get the `Localizer` to be used for localizing this library. pub fn localizer_core() -> Box { Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_CORE, &Localizations)) } pub fn generate_translation_hashmap(vec: Vec<(&'static str, String)>) -> HashMap<&'static str, String> { let mut hashmap: HashMap<&'static str, String> = Default::default(); for (key, value) in vec { hashmap.insert(key, value); } hashmap } pub fn fnc_get_similarity_very_high() -> String { flc!("core_similarity_very_high") } pub fn fnc_get_similarity_minimal() -> String { flc!("core_similarity_minimal") } czkawka_core-10.0.0/src/tools/bad_extensions/core.rs000064400000000000000000000177521046102023000206130ustar 00000000000000use std::collections::{BTreeSet, HashMap}; use std::mem; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use mime_guess::get_mime_extensions; use rayon::prelude::*; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{FileEntry, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::bad_extensions::workarounds::{DISABLED_EXTENSIONS, WORKAROUNDS}; use crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters, BadFileEntry, Info}; impl BadExtensions { pub fn new(params: BadExtensionsParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BadExtensions), information: Info::default(), files_to_check: Default::default(), bad_extensions_files: Default::default(), params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries.into_values().flatten().collect(); self.common_data.text_messages.warnings.extend(warnings); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "look_for_bad_extensions_files", level = "debug")] pub(crate) fn look_for_bad_extensions_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::BadExtensionsChecking, self.files_to_check.len(), self.get_test_type(), 0); let files_to_check = mem::take(&mut self.files_to_check); let mut hashmap_workarounds: HashMap<&str, Vec<&str>> = Default::default(); for (proper, found) in WORKAROUNDS { hashmap_workarounds.entry(found).or_default().push(proper); } self.bad_extensions_files = self.verify_extensions(files_to_check, progress_handler.items_counter(), stop_flag, &hashmap_workarounds); progress_handler.join_thread(); // Break if stop was clicked if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } self.information.number_of_files_with_bad_extension = self.bad_extensions_files.len(); debug!("Found {} files with invalid extension.", self.information.number_of_files_with_bad_extension); WorkContinueStatus::Continue } fn verify_extension_of_file(&self, file_entry: FileEntry, hashmap_workarounds: &HashMap<&str, Vec<&str>>) -> Option { // Check what exactly content file contains let kind = match infer::get_from_path(&file_entry.path) { Ok(k) => k?, Err(_) => return None, }; let proper_extension = kind.extension(); let current_extension = Self::get_and_validate_extension(&file_entry, proper_extension)?; // Check for all extensions that file can use(not sure if it is worth to do it) let (mut all_available_extensions, valid_extensions) = Self::check_for_all_extensions_that_file_can_use(hashmap_workarounds, ¤t_extension, proper_extension); if all_available_extensions.is_empty() { // Not found any extension return None; } else if current_extension.is_empty() { if !self.params.include_files_without_extension { return None; } } else if all_available_extensions.take(¤t_extension).is_some() { // Found proper extension return None; } Some(BadFileEntry { path: file_entry.path, modified_date: file_entry.modified_date, size: file_entry.size, current_extension, proper_extensions_group: valid_extensions, proper_extension: proper_extension.to_string(), }) } #[fun_time(message = "verify_extensions", level = "debug")] fn verify_extensions( &self, files_to_check: Vec, items_counter: &Arc, stop_flag: &Arc, hashmap_workarounds: &HashMap<&str, Vec<&str>>, ) -> Vec { files_to_check .into_par_iter() .map(|file_entry| { if check_if_stop_received(stop_flag) { return None; } let res = self.verify_extension_of_file(file_entry, hashmap_workarounds); items_counter.fetch_add(1, Ordering::Relaxed); Some(res) }) .while_some() .flatten() .collect::>() } fn get_and_validate_extension(file_entry: &FileEntry, proper_extension: &str) -> Option { let current_extension; // Extract current extension from file if let Some(extension) = file_entry.path.extension() { let extension = extension.to_string_lossy().to_lowercase(); if DISABLED_EXTENSIONS.contains(&extension.as_str()) { return None; } // Text longer than 10 characters is not considered as extension if extension.len() > 10 { current_extension = String::new(); } else { current_extension = extension; } } else { current_extension = String::new(); } // Already have proper extension, no need to do more things if current_extension == proper_extension { return None; } Some(current_extension) } fn check_for_all_extensions_that_file_can_use(hashmap_workarounds: &HashMap<&str, Vec<&str>>, current_extension: &str, proper_extension: &str) -> (BTreeSet, String) { let mut all_available_extensions: BTreeSet = Default::default(); // TODO Isn't this a bug? // Why to file without extensions we set this as empty let valid_extensions = if current_extension.is_empty() { String::new() } else { for mim in mime_guess::from_ext(proper_extension) { if let Some(all_ext) = get_mime_extensions(&mim) { for ext in all_ext { all_available_extensions.insert((*ext).to_string()); } } } // Workarounds if let Some(vec_pre) = hashmap_workarounds.get(current_extension) { for pre in vec_pre { if all_available_extensions.contains(*pre) { all_available_extensions.insert(current_extension.to_string()); break; } } } let mut guessed_multiple_extensions = format!("({proper_extension}) - "); for ext in &all_available_extensions { guessed_multiple_extensions.push_str(ext); guessed_multiple_extensions.push(','); } guessed_multiple_extensions.pop(); guessed_multiple_extensions }; (all_available_extensions, valid_extensions) } } czkawka_core-10.0.0/src/tools/bad_extensions/mod.rs000064400000000000000000000027031046102023000204300ustar 00000000000000pub mod core; pub mod traits; mod workarounds; use std::path::{Path, PathBuf}; use serde::Serialize; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; #[derive(Clone, Serialize, Debug)] pub struct BadFileEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub current_extension: String, pub proper_extensions_group: String, pub proper_extension: String, } impl ResultEntry for BadFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(Default, Clone)] pub struct Info { pub number_of_files_with_bad_extension: usize, } #[derive(Clone)] pub struct BadExtensionsParameters { pub include_files_without_extension: bool, } impl BadExtensionsParameters { pub fn new() -> Self { Self { include_files_without_extension: false, } } } impl Default for BadExtensionsParameters { fn default() -> Self { Self::new() } } pub struct BadExtensions { common_data: CommonToolData, information: Info, files_to_check: Vec, bad_extensions_files: Vec, params: BadExtensionsParameters, } impl BadExtensions { pub const fn get_bad_extensions_files(&self) -> &Vec { &self.bad_extensions_files } } czkawka_core-10.0.0/src/tools/bad_extensions/traits.rs000064400000000000000000000062301046102023000211560ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters, Info}; impl AllTraits for BadExtensions {} impl Search for BadExtensions { #[fun_time(message = "find_bad_extensions_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.look_for_bad_extensions_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } self.debug_print(); } } impl DeletingItems for BadExtensions { fn delete_files(&mut self, _stop_flag: &Arc, _progress_sender: Option<&Sender>) -> WorkContinueStatus { todo!() } } impl DebugPrint for BadExtensions { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for BadExtensions { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { writeln!( writer, "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; writeln!(writer, "Found {} files with invalid extension.\n", self.information.number_of_files_with_bad_extension)?; for file_entry in &self.bad_extensions_files { writeln!(writer, "\"{}\" ----- {}", file_entry.path.to_string_lossy(), file_entry.proper_extensions_group)?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.bad_extensions_files, pretty_print) } } impl CommonData for BadExtensions { type Info = Info; type Parameters = BadExtensionsParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.get_information().number_of_files_with_bad_extension > 0 } } czkawka_core-10.0.0/src/tools/bad_extensions/workarounds.rs000064400000000000000000000122001046102023000222200ustar 00000000000000pub(crate) const DISABLED_EXTENSIONS: &[&str] = &["file", "cache", "bak", "data", "tmp"]; // Such files can have any type inside // This adds several workarounds for bugs/invalid recognizing types by external libraries // ("real_content_extension", "current_file_extension") pub(crate) const WORKAROUNDS: &[(&str, &str)] = &[ // Wine/Windows ("der", "cat"), ("exe", "acm"), ("exe", "ax"), ("exe", "bck"), ("exe", "com"), ("exe", "cpl"), ("exe", "dll16"), ("exe", "dll"), ("exe", "drv16"), ("exe", "drv"), ("exe", "ds"), ("exe", "efi"), ("exe", "exe16"), ("exe", "fon"), // Type of font or something else ("exe", "mod16"), ("exe", "msstyles"), ("exe", "mui"), ("exe", "mun"), ("exe", "orig"), ("exe", "ps1xml"), ("exe", "rll"), ("exe", "rs"), ("exe", "scr"), ("exe", "signed"), ("exe", "sys"), ("exe", "tlb"), ("exe", "tsp"), ("exe", "vdm"), ("exe", "vxd"), ("exe", "winmd"), ("gz", "loggz"), ("xml", "adml"), ("xml", "admx"), ("xml", "camp"), ("xml", "cdmp"), ("xml", "cdxml"), ("xml", "dgml"), ("xml", "diagpkg"), ("xml", "gmmp"), ("xml", "library-ms"), ("xml", "man"), ("xml", "manifest"), ("xml", "msc"), ("xml", "mum"), ("xml", "resx"), ("zip", "wmz"), // Games specific extensions - cannot be used here common extensions like zip ("gz", "h3m"), // Heroes 3 ("zip", "hashdb"), // Gog ("c2", "zip"), // King of the Dark Age ("c2", "bmp"), // King of the Dark Age ("c2", "avi"), // King of the Dark Age ("c2", "exe"), // King of the Dark Age // Raw images ("tif", "nef"), ("tif", "dng"), ("tif", "arw"), // Other ("der", "keystore"), // Godot/Android keystore ("exe", "pyd"), // Python/Mingw ("gz", "blend"), // Blender ("gz", "crate"), // Cargo ("gz", "svgz"), // Archive svg ("gz", "tgz"), // Archive ("heic", "heif"), // Image ("heif", "heic"), // Image ("html", "dtd"), // Mingw ("html", "ent"), // Mingw ("html", "md"), // Markdown ("html", "svelte"), // Svelte ("jpg", "jfif"), // Photo format ("m4v", "mp4"), // m4v and mp4 are interchangeable ("mobi", "azw3"), // Ebook format ("mpg", "vob"), // Weddings in parts have usually vob extension ("obj", "bin"), // Multiple apps, Czkawka, Nvidia, Windows ("obj", "o"), // Compilators ("odp", "otp"), // LibreOffice ("ods", "ots"), // Libreoffice ("odt", "ott"), // Libreoffice ("ogg", "ogv"), // Audio format ("pem", "key"), // curl, openssl ("png", "kpp"), // Krita presets ("pptx", "ppsx"), // Powerpoint ("sh", "bash"), // Linux ("sh", "guess"), // GNU ("sh", "lua"), // Lua ("sh", "js"), // Javascript ("sh", "pl"), // Gnome/Linux ("sh", "pm"), // Gnome/Linux ("sh", "py"), // Python ("sh", "pyx"), // Python ("sh", "rs"), // Rust ("sh", "sample"), // Git ("xml", "bsp"), // Quartus ("xml", "cbp"), // CodeBlocks config ("xml", "cfg"), // Multiple apps - Godot ("xml", "cmb"), // Cambalache ("xml", "conf"), // Multiple apps - Python ("xml", "config"), // Multiple apps - QT Creator ("xml", "dae"), // 3D models ("xml", "docbook"), // ("xml", "fb2"), // ("xml", "filters"), // Visual studio ("xml", "gir"), // GTK ("xml", "glade"), // Glade ("xml", "iml"), // Intelij Idea ("xml", "kdenlive"), // KDenLive ("xml", "lang"), // ? ("xml", "nuspec"), // Nuget ("xml", "policy"), // SystemD ("xml", "qsys"), // Quartus ("xml", "sopcinfo"), // Quartus ("xml", "svg"), // SVG ("xml", "ui"), // Cambalache, Glade ("xml", "user"), // Qtcreator ("xml", "vbox"), // VirtualBox ("xml", "vbox-prev"), // VirtualBox ("xml", "vcproj"), // VisualStudio ("xml", "vcxproj"), // VisualStudio ("xml", "xba"), // Libreoffice ("xml", "xcd"), // Libreoffice files ("zip", "apk"), // Android apk ("zip", "cbr"), // Comics ("zip", "dat"), // Multiple - python, brave ("zip", "doc"), // Word ("zip", "docx"), // Word ("zip", "epub"), // Ebook format ("zip", "jar"), // Java ("zip", "kra"), // Krita ("zip", "kgm"), // Krita ("zip", "nupkg"), // Nuget packages ("zip", "odg"), // Libreoffice ("zip", "pptx"), // Powerpoint ("zip", "whl"), // Python packages ("zip", "xlsx"), // Excel ("zip", "xpi"), // Firefox extensions ("zip", "zcos"), // Scilab // Probably invalid ("html", "svg"), ("xml", "html"), // Probably bug in external library ("msi", "ppt"), // Not sure why ppt is not recognized ("msi", "doc"), // Not sure why doc is not recognized ("exe", "xls"), // Not sure why xls is not recognized ]; czkawka_core-10.0.0/src/tools/big_file/core.rs000064400000000000000000000042441046102023000173360ustar 00000000000000use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode}; impl BigFile { pub fn new(params: BigFileParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BigFile), information: Info::default(), big_files: Default::default(), params, } } #[fun_time(message = "look_for_big_files", level = "debug")] pub(crate) fn look_for_big_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .minimal_file_size(1) .maximal_file_size(u64::MAX) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { let mut all_files = grouped_file_entries.into_values().flatten().collect::>(); all_files.par_sort_unstable_by_key(|fe| fe.size); if self.get_params().search_mode == SearchMode::BiggestFiles { all_files.reverse(); } all_files.truncate(self.get_params().number_of_files_to_check); self.big_files = all_files; self.common_data.text_messages.warnings.extend(warnings); self.information.number_of_real_files = self.big_files.len(); debug!("check_files - Found {} biggest/smallest files.", self.big_files.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } } czkawka_core-10.0.0/src/tools/big_file/mod.rs000064400000000000000000000016121046102023000171610ustar 00000000000000pub mod core; pub mod traits; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum SearchMode { BiggestFiles, SmallestFiles, } #[derive(Debug, Default, Clone)] pub struct Info { pub number_of_real_files: usize, } #[derive(Clone)] pub struct BigFileParameters { pub number_of_files_to_check: usize, pub search_mode: SearchMode, } impl BigFileParameters { pub fn new(number_of_files: usize, search_mode: SearchMode) -> Self { Self { number_of_files_to_check: number_of_files.max(1), search_mode, } } } pub struct BigFile { common_data: CommonToolData, information: Info, big_files: Vec, params: BigFileParameters, } impl BigFile { pub const fn get_big_files(&self) -> &Vec { &self.big_files } } czkawka_core-10.0.0/src/tools/big_file/traits.rs000064400000000000000000000101011046102023000177010ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode}; impl AllTraits for BigFile {} impl DeletingItems for BigFile { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.big_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl DebugPrint for BigFile { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("### INDIVIDUAL DEBUG PRINT ###"); println!("Info: {:?}", self.information); println!("Number of files to check - {}", self.get_params().number_of_files_to_check); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for BigFile { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { writeln!( writer, "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; if self.information.number_of_real_files != 0 { if self.get_params().search_mode == SearchMode::BiggestFiles { writeln!(writer, "{} the biggest files.\n\n", self.information.number_of_real_files)?; } else { writeln!(writer, "{} the smallest files.\n\n", self.information.number_of_real_files)?; } for file_entry in &self.big_files { writeln!( writer, "{} ({}) - \"{}\"", format_size(file_entry.size, BINARY), file_entry.size, file_entry.path.to_string_lossy() )?; } } else { writeln!(writer, "Not found any files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.big_files, pretty_print) } } impl Search for BigFile { #[fun_time(message = "find_big_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.look_for_big_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl CommonData for BigFile { type Info = Info; type Parameters = BigFileParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_real_files > 0 } } czkawka_core-10.0.0/src/tools/broken_files/core.rs000064400000000000000000000413701046102023000202410ustar 00000000000000use std::collections::{BTreeMap, HashSet}; use std::fs::File; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{mem, panic}; use crossbeam_channel::Sender; use fun_time::fun_time; use log::{debug, error}; use lopdf::Document; use rayon::prelude::*; use crate::common::cache::{CACHE_VERSION, extract_loaded_cache, load_cache_from_file_generalized_by_path, save_cache_to_file_generalized}; use crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, PDF_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS}; use crate::common::create_crash_message; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::tools::broken_files::{BrokenEntry, BrokenFiles, BrokenFilesParameters, CheckedTypes, Info, TypeOfFile}; impl BrokenFiles { pub fn new(params: BrokenFilesParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BrokenFiles), information: Info::default(), files_to_check: Default::default(), broken_files: Default::default(), params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let zip_extensions = ZIP_FILES_EXTENSIONS.iter().collect::>(); let audio_extensions = AUDIO_FILES_EXTENSIONS.iter().collect::>(); let pdf_extensions = PDF_FILES_EXTENSIONS.iter().collect::>(); let images_extensions = IMAGE_RS_BROKEN_FILES_EXTENSIONS.iter().collect::>(); let mut extensions = Vec::new(); let vec_extensions = [ (CheckedTypes::PDF, PDF_FILES_EXTENSIONS), (CheckedTypes::AUDIO, AUDIO_FILES_EXTENSIONS), (CheckedTypes::ARCHIVE, ZIP_FILES_EXTENSIONS), (CheckedTypes::IMAGE, IMAGE_RS_BROKEN_FILES_EXTENSIONS), ]; for (checked_type, extensions_to_add) in &vec_extensions { if self.get_params().checked_types.contains(*checked_type) { extensions.extend_from_slice(extensions_to_add); } } self.common_data.extensions.set_and_validate_allowed_extensions(&extensions); // TODO, responsibility should be moved to CLI/GUI // assert!(self.common_data.extensions.set_any_extensions(), "This should be checked before"); if !self.common_data.extensions.set_any_extensions() { return WorkContinueStatus::Continue; } let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries .into_values() .flatten() .map(|fe| { let mut broken_entry = fe.into_broken_entry(); broken_entry.type_of_file = check_extension_availability(broken_entry.get_path(), &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions); (broken_entry.path.to_string_lossy().to_string(), broken_entry) }) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} files to check.", self.files_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn check_broken_image(&self, mut file_entry: BrokenEntry) -> BrokenEntry { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { if let Err(e) = image::open(&file_entry.path) { file_entry.error_string = e.to_string(); } file_entry }) .unwrap_or_else(|_| { let message = create_crash_message("Image-rs", &file_entry_clone.path.to_string_lossy(), "https://github.com/Serial-ATA/lofty-rs"); error!("{message}"); file_entry_clone.error_string = message; file_entry_clone }) } fn check_broken_zip(&self, mut file_entry: BrokenEntry) -> Option { match File::open(&file_entry.path) { Ok(file) => { if let Err(e) = zip::ZipArchive::new(file) { file_entry.error_string = e.to_string(); } Some(file_entry) } Err(_inspected) => None, } } fn check_broken_audio(&self, mut file_entry: BrokenEntry) -> Option { match File::open(&file_entry.path) { Ok(file) => { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { if let Err(e) = audio_checker::parse_audio_file(file) { file_entry.error_string = e.to_string(); } Some(file_entry) }) .unwrap_or_else(|_| { let message = create_crash_message("Symphonia", &file_entry_clone.path.to_string_lossy(), "https://github.com/pdeljanov/Symphonia"); error!("{message}"); file_entry_clone.error_string = message; Some(file_entry_clone) }) } Err(_inspected) => None, } } fn check_broken_pdf(&self, mut file_entry: BrokenEntry) -> BrokenEntry { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { match File::open(&file_entry.path) { Ok(file) => { if let Err(e) = Document::load_from(file) { file_entry.error_string = e.to_string(); } } Err(e) => { file_entry.error_string = e.to_string(); } } file_entry }) .unwrap_or_else(|_| { let message = create_crash_message("lopdf", &file_entry_clone.path.to_string_lossy(), "https://github.com/J-F-Liu/lopdf"); error!("{message}"); file_entry_clone.error_string = message; file_entry_clone }) } #[fun_time(message = "load_cache", level = "debug")] fn load_cache(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { let loaded_hash_map; let mut records_already_cached: BTreeMap = Default::default(); let mut non_cached_files_to_check: BTreeMap = Default::default(); let files_to_check = mem::take(&mut self.files_to_check); if self.common_data.use_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_path::(&get_broken_files_cache_file(), self.get_delete_outdated_cache(), &files_to_check); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); extract_loaded_cache(&loaded_hash_map, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check); } else { loaded_hash_map = Default::default(); non_cached_files_to_check = files_to_check; } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } fn check_file(&self, file_entry: BrokenEntry) -> Option { match file_entry.type_of_file { TypeOfFile::Image => Some(self.check_broken_image(file_entry)), TypeOfFile::ArchiveZip => self.check_broken_zip(file_entry), TypeOfFile::Audio => self.check_broken_audio(file_entry), TypeOfFile::PDF => Some(self.check_broken_pdf(file_entry)), // This means that cache read invalid value because maybe cache comes from different czkawka version TypeOfFile::Unknown => None, } } #[fun_time(message = "look_for_broken_files", level = "debug")] pub(crate) fn look_for_broken_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::BrokenFilesChecking, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|item| item.size).sum::(), ); let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::>(); debug!("look_for_broken_files - started finding for broken files"); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .with_max_len(3) .map(|(_, file_entry)| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let res = self.check_file(file_entry); progress_handler.increase_items(1); progress_handler.increase_size(size); Some(res) }) .while_some() .flatten() .collect::>(); debug!("look_for_broken_files - ended finding for broken files"); progress_handler.join_thread(); // Just connect loaded results with already calculated vec_file_entry.extend(records_already_cached.into_values()); self.save_to_cache(&vec_file_entry, loaded_hash_map); self.broken_files = vec_file_entry.into_iter().filter_map(|f| if f.error_string.is_empty() { None } else { Some(f) }).collect(); self.information.number_of_broken_files = self.broken_files.len(); debug!("Found {} broken files.", self.information.number_of_broken_files); // Clean unused data self.files_to_check = Default::default(); WorkContinueStatus::Continue } #[fun_time(message = "save_to_cache", level = "debug")] fn save_to_cache(&mut self, vec_file_entry: &[BrokenEntry], loaded_hash_map: BTreeMap) { if self.common_data.use_cache { // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = Default::default(); for file_entry in vec_file_entry.iter().cloned() { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } for (_name, file_entry) in loaded_hash_map { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } let messages = save_cache_to_file_generalized(&get_broken_files_cache_file(), &all_results, self.common_data.save_also_as_json, 0); self.get_text_messages_mut().extend_with_another_messages(messages); } } } #[allow(clippy::string_slice)] // Valid, because we address go to dot, which is known ascii character fn check_extension_availability( full_name: &Path, images_extensions: &HashSet<&&'static str>, zip_extensions: &HashSet<&&'static str>, audio_extensions: &HashSet<&&'static str>, pdf_extensions: &HashSet<&&'static str>, ) -> TypeOfFile { let Some(file_name) = full_name.file_name() else { error!("Missing file name in file - \"{}\"", full_name.to_string_lossy()); debug_assert!(false, "Missing file name in file - \"{}\"", full_name.to_string_lossy()); return TypeOfFile::Unknown; }; let Some(file_name_str) = file_name.to_str() else { return TypeOfFile::Unknown }; let Some(extension_idx) = file_name_str.rfind('.') else { return TypeOfFile::Unknown }; let extension_str = &file_name_str[extension_idx + 1..]; let extension_lowercase = extension_str.to_ascii_lowercase(); if images_extensions.contains(&extension_lowercase.as_str()) { TypeOfFile::Image } else if zip_extensions.contains(&extension_lowercase.as_str()) { TypeOfFile::ArchiveZip } else if audio_extensions.contains(&extension_lowercase.as_str()) { TypeOfFile::Audio } else if pdf_extensions.contains(&extension_lowercase.as_str()) { TypeOfFile::PDF } else { error!("File with unknown extension: \"{}\" - {extension_lowercase}", full_name.to_string_lossy()); debug_assert!(false, "File with unknown extension - \"{}\" - {extension_lowercase}", full_name.to_string_lossy()); TypeOfFile::Unknown } } pub fn get_broken_files_cache_file() -> String { format!("cache_broken_files_{CACHE_VERSION}.bin") } #[cfg(test)] mod tests { use std::collections::HashSet; use std::path::Path; use super::*; #[test] fn test_check_extension_availability_image() { let images_extensions: HashSet<&&str> = ["jpg", "png", "gif"].iter().collect(); let zip_extensions: HashSet<&&str> = HashSet::new(); let audio_extensions: HashSet<&&str> = HashSet::new(); let pdf_extensions: HashSet<&&str> = HashSet::new(); let path = Path::new("test.jpg"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::Image ); } #[test] fn test_check_extension_availability_zip() { let images_extensions: HashSet<&&str> = HashSet::new(); let zip_extensions: HashSet<&&str> = ["zip", "rar"].iter().collect(); let audio_extensions: HashSet<&&str> = HashSet::new(); let pdf_extensions: HashSet<&&str> = HashSet::new(); let path = Path::new("test.zip"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::ArchiveZip ); } #[test] fn test_check_extension_availability_audio() { let images_extensions: HashSet<&&str> = HashSet::new(); let zip_extensions: HashSet<&&str> = HashSet::new(); let audio_extensions: HashSet<&&str> = ["mp3", "wav"].iter().collect(); let pdf_extensions: HashSet<&&str> = HashSet::new(); let path = Path::new("test.mp3"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::Audio ); } #[test] fn test_check_extension_availability_pdf() { let images_extensions: HashSet<&&str> = HashSet::new(); let zip_extensions: HashSet<&&str> = HashSet::new(); let audio_extensions: HashSet<&&str> = HashSet::new(); let pdf_extensions: HashSet<&&str> = std::iter::once(&"pdf").collect(); let path = Path::new("test.pdf"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::PDF ); } #[test] fn test_check_extension_availability_no_extension() { let images_extensions: HashSet<&&str> = HashSet::new(); let zip_extensions: HashSet<&&str> = HashSet::new(); let audio_extensions: HashSet<&&str> = HashSet::new(); let pdf_extensions: HashSet<&&str> = HashSet::new(); let path = Path::new("test"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::Unknown ); } #[test] fn test_check_no_extension() { let images_extensions: HashSet<&&str> = HashSet::new(); let zip_extensions: HashSet<&&str> = HashSet::new(); let audio_extensions: HashSet<&&str> = ["mp3", "wav"].iter().collect(); let pdf_extensions: HashSet<&&str> = HashSet::new(); let path = Path::new("/home/.mp3"); assert_eq!( check_extension_availability(path, &images_extensions, &zip_extensions, &audio_extensions, &pdf_extensions), TypeOfFile::Audio ); } } czkawka_core-10.0.0/src/tools/broken_files/mod.rs000064400000000000000000000042151046102023000200650ustar 00000000000000pub mod core; pub mod traits; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct BrokenEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub type_of_file: TypeOfFile, pub error_string: String, } impl ResultEntry for BrokenEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_broken_entry(self) -> BrokenEntry { BrokenEntry { size: self.size, path: self.path, modified_date: self.modified_date, type_of_file: TypeOfFile::Unknown, error_string: String::new(), } } } #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum TypeOfFile { Unknown = -1, Image = 0, ArchiveZip, Audio, PDF, } bitflags! { #[derive(PartialEq, Copy, Clone, Debug)] pub struct CheckedTypes : u32 { const NONE = 0; const PDF = 0b1; const AUDIO = 0b10; const IMAGE = 0b100; const ARCHIVE = 0b1000; } } #[derive(Default, Clone)] pub struct Info { pub number_of_broken_files: usize, } #[derive(Clone)] pub struct BrokenFilesParameters { pub checked_types: CheckedTypes, } impl BrokenFilesParameters { pub fn new(checked_types: CheckedTypes) -> Self { Self { checked_types } } } pub struct BrokenFiles { common_data: CommonToolData, information: Info, files_to_check: BTreeMap, broken_files: Vec, params: BrokenFilesParameters, } impl BrokenFiles { pub const fn get_broken_files(&self) -> &Vec { &self.broken_files } pub(crate) fn get_params(&self) -> &BrokenFilesParameters { &self.params } pub const fn get_information(&self) -> &Info { &self.information } } czkawka_core-10.0.0/src/tools/broken_files/traits.rs000064400000000000000000000070631046102023000206200ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::*; use crate::tools::broken_files::{BrokenFiles, BrokenFilesParameters, Info}; impl AllTraits for BrokenFiles {} impl Search for BrokenFiles { #[fun_time(message = "find_broken_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.look_for_broken_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for BrokenFiles { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } self.debug_print_common(); } } impl PrintResults for BrokenFiles { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { writeln!( writer, "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; if !self.broken_files.is_empty() { writeln!(writer, "Found {} broken files.", self.information.number_of_broken_files)?; for file_entry in &self.broken_files { writeln!(writer, "\"{}\" - {}", file_entry.path.to_string_lossy(), file_entry.error_string)?; } } else { write!(writer, "Not found any broken files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.broken_files, pretty_print) } } impl DeletingItems for BrokenFiles { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.broken_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl CommonData for BrokenFiles { type Info = Info; type Parameters = BrokenFilesParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_broken_files > 0 } } czkawka_core-10.0.0/src/tools/duplicate/core.rs000064400000000000000000001061761046102023000175570ustar 00000000000000use std::collections::{BTreeMap, HashMap}; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{mem, thread}; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use log::debug; use rayon::prelude::*; use crate::common::cache::{CACHE_DUPLICATE_VERSION, load_cache_from_file_generalized_by_size, save_cache_to_file_generalized}; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{CheckingMethod, FileEntry, HashType, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::*; use crate::tools::duplicate::{ DuplicateEntry, DuplicateFinder, DuplicateFinderParameters, Info, PREHASHING_BUFFER_SIZE, THREAD_BUFFER, filter_hard_links, hash_calculation, hash_calculation_limit, }; impl DuplicateFinder { pub fn new(params: DuplicateFinderParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::Duplicate), information: Info::default(), files_with_identical_names: Default::default(), files_with_identical_size: Default::default(), files_with_identical_size_names: Default::default(), files_with_identical_hashes: Default::default(), files_with_identical_names_referenced: Default::default(), files_with_identical_size_names_referenced: Default::default(), files_with_identical_size_referenced: Default::default(), files_with_identical_hashes_referenced: Default::default(), params, } } #[fun_time(message = "check_files_name", level = "debug")] pub(crate) fn check_files_name(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let group_by_func = if self.get_params().case_sensitive_name_comparison { |fe: &FileEntry| { fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\"", fe.path.to_string_lossy())) .to_string_lossy() .to_string() } } else { |fe: &FileEntry| { fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\"", fe.path.to_string_lossy())) .to_string_lossy() .to_lowercase() } }; let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(group_by_func) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(CheckingMethod::Name) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); // Create new BTreeMap without single size entries(files have not duplicates) self.files_with_identical_names = grouped_file_entries .into_iter() .filter_map(|(name, vector)| { if vector.len() > 1 { Some((name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_names) .into_iter() .filter_map(|(_name, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_names_referenced.insert(fe.path.to_string_lossy().to_string(), (fe, vec_fe)); } } self.calculate_name_stats(); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_name_stats(&mut self) { if self.common_data.use_reference_folders { for (_fe, vector) in self.files_with_identical_names_referenced.values() { self.information.number_of_duplicated_files_by_name += vector.len(); self.information.number_of_groups_by_name += 1; } } else { for vector in self.files_with_identical_names.values() { self.information.number_of_duplicated_files_by_name += vector.len() - 1; self.information.number_of_groups_by_name += 1; } } } #[fun_time(message = "check_files_size_name", level = "debug")] pub(crate) fn check_files_size_name(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let group_by_func = if self.get_params().case_sensitive_name_comparison { |fe: &FileEntry| { ( fe.size, fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\"", fe.path.to_string_lossy())) .to_string_lossy() .to_string(), ) } } else { |fe: &FileEntry| { ( fe.size, fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\"", fe.path.to_string_lossy())) .to_string_lossy() .to_lowercase(), ) } }; let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(group_by_func) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(CheckingMethod::SizeName) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); self.files_with_identical_size_names = grouped_file_entries .into_iter() .filter_map(|(size_name, vector)| { if vector.len() > 1 { Some((size_name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_size_names) .into_iter() .filter_map(|(_size, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_size_names_referenced .insert((fe.size, fe.path.to_string_lossy().to_string()), (fe, vec_fe)); } } self.calculate_size_name_stats(); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_size_name_stats(&mut self) { if self.common_data.use_reference_folders { for ((size, _name), (_fe, vector)) in &self.files_with_identical_size_names_referenced { self.information.number_of_duplicated_files_by_size_name += vector.len(); self.information.number_of_groups_by_size_name += 1; self.information.lost_space_by_size += (vector.len() as u64) * size; } } else { for ((size, _name), vector) in &self.files_with_identical_size_names { self.information.number_of_duplicated_files_by_size_name += vector.len() - 1; self.information.number_of_groups_by_size_name += 1; self.information.lost_space_by_size += (vector.len() as u64 - 1) * size; } } } #[fun_time(message = "check_files_size", level = "debug")] pub(crate) fn check_files_size(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|fe| fe.size) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(self.get_params().check_method) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); let grouped_file_entries: Vec<(u64, Vec)> = grouped_file_entries.into_iter().collect(); let rayon_max_len = if self.get_params().ignore_hard_links { 3 } else { 100 }; self.files_with_identical_size = grouped_file_entries .into_par_iter() .with_max_len(rayon_max_len) .filter_map(|(size, vec)| { if vec.len() <= 1 { return None; } let vector = if self.get_params().ignore_hard_links { filter_hard_links(&vec) } else { vec }; if vector.len() > 1 { Some((size, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); self.filter_reference_folders_by_size(); self.calculate_size_stats(); debug!( "check_file_size - after calculating size stats/duplicates, found in {} groups, {} files with same size | referenced {} groups, {} files", self.files_with_identical_size.len(), self.files_with_identical_size.values().map(Vec::len).sum::(), self.files_with_identical_size_referenced.len(), self.files_with_identical_size_referenced.values().map(|(_fe, vec)| vec.len()).sum::() ); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_size_stats(&mut self) { if self.common_data.use_reference_folders { for (size, (_fe, vector)) in &self.files_with_identical_size_referenced { self.information.number_of_duplicated_files_by_size += vector.len(); self.information.number_of_groups_by_size += 1; self.information.lost_space_by_size += (vector.len() as u64) * size; } } else { for (size, vector) in &self.files_with_identical_size { self.information.number_of_duplicated_files_by_size += vector.len() - 1; self.information.number_of_groups_by_size += 1; self.information.lost_space_by_size += (vector.len() as u64 - 1) * size; } } } #[fun_time(message = "filter_reference_folders_by_size", level = "debug")] fn filter_reference_folders_by_size(&mut self) { if self.common_data.use_reference_folders && self.get_params().check_method == CheckingMethod::Size { let vec = mem::take(&mut self.files_with_identical_size) .into_iter() .filter_map(|(_size, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_size_referenced.insert(fe.size, (fe, vec_fe)); } } } #[fun_time(message = "prehash_load_cache_at_start", level = "debug")] fn prehash_load_cache_at_start(&mut self) -> (BTreeMap>, BTreeMap>, BTreeMap>) { // Cache algorithm // - Load data from cache // - Convert from BT> to BT // - Save to proper values let loaded_hash_map; let mut records_already_cached: BTreeMap> = Default::default(); let mut non_cached_files_to_check: BTreeMap> = Default::default(); if self.get_params().use_prehash_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_size::( &get_duplicate_cache_file(&self.get_params().hash_type, true), self.get_delete_outdated_cache(), &self.files_with_identical_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); Self::diff_loaded_and_prechecked_files( "prehash_load_cache_at_start", mem::take(&mut self.files_with_identical_size), &loaded_hash_map, &mut records_already_cached, &mut non_cached_files_to_check, ); } else { loaded_hash_map = Default::default(); mem::swap(&mut self.files_with_identical_size, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } #[fun_time(message = "prehash_save_cache_at_exit", level = "debug")] fn prehash_save_cache_at_exit( &mut self, loaded_hash_map: BTreeMap>, pre_hash_results: Vec<(u64, BTreeMap>, Vec)>, ) { if self.get_params().use_prehash_cache { // All results = records already cached + computed results let mut save_cache_to_hashmap: BTreeMap = Default::default(); for (size, vec_file_entry) in loaded_hash_map { if size >= self.get_params().minimal_prehash_cache_file_size { for file_entry in vec_file_entry { save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone()); } } } for (size, hash_map, _errors) in pre_hash_results { if size >= self.get_params().minimal_prehash_cache_file_size { for vec_file_entry in hash_map.into_values() { for file_entry in vec_file_entry { save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone()); } } } } let messages = save_cache_to_file_generalized( &get_duplicate_cache_file(&self.get_params().hash_type, true), &save_cache_to_hashmap, self.common_data.save_also_as_json, self.get_params().minimal_prehash_cache_file_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); } } #[fun_time(message = "prehashing", level = "debug")] fn prehashing( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, pre_checked_map: &mut BTreeMap>, ) -> WorkContinueStatus { if self.files_with_identical_size.is_empty() { return WorkContinueStatus::Continue; } let check_type = self.get_params().hash_type; let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheLoading, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.prehash_load_cache_at_start(); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::DuplicatePreHashing, non_cached_files_to_check.values().map(Vec::len).sum(), self.get_test_type(), non_cached_files_to_check .iter() .map(|(size, items)| items.len() as u64 * PREHASHING_BUFFER_SIZE.min(*size)) .sum::(), ); // Convert to vector to be able to use with_max_len method from rayon let non_cached_files_to_check: Vec<(u64, Vec)> = non_cached_files_to_check.into_iter().collect(); debug!("Starting calculating prehash"); #[allow(clippy::type_complexity)] let pre_hash_results: Vec<(u64, BTreeMap>, Vec)> = non_cached_files_to_check .into_par_iter() .with_max_len(3) // Vectors and BTreeMaps for really big inputs, leave some jobs to 0 thread, to avoid that I minimized max tasks for each thread to 3, which improved performance .map(|(size, vec_file_entry)| { let mut hashmap_with_hash: BTreeMap> = Default::default(); let mut errors: Vec = Vec::new(); THREAD_BUFFER.with_borrow_mut(|buffer| { for mut file_entry in vec_file_entry { if check_if_stop_received(stop_flag) { return None; } match hash_calculation_limit(buffer, &file_entry, check_type, PREHASHING_BUFFER_SIZE, progress_handler.size_counter()) { Ok(hash_string) => { file_entry.hash = hash_string.clone(); hashmap_with_hash.entry(hash_string).or_default().push(file_entry); } Err(s) => errors.push(s), } progress_handler.increase_items(1); } Some(()) })?; Some((size, hashmap_with_hash, errors)) }) .while_some() .collect(); debug!("Completed calculating prehash"); progress_handler.join_thread(); // Saving into cache let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheSaving, 0, self.get_test_type(), 0); // Add data from cache for (size, mut vec_file_entry) in records_already_cached { pre_checked_map.entry(size).or_default().append(&mut vec_file_entry); } // Check results for (size, hash_map, errors) in &pre_hash_results { if !errors.is_empty() { self.common_data.text_messages.warnings.append(&mut errors.clone()); } for vec_file_entry in hash_map.values() { if vec_file_entry.len() > 1 { pre_checked_map.entry(*size).or_default().append(&mut vec_file_entry.clone()); } } } self.prehash_save_cache_at_exit(loaded_hash_map, pre_hash_results); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } fn diff_loaded_and_prechecked_files( function_name: &str, used_map: BTreeMap>, loaded_hash_map: &BTreeMap>, records_already_cached: &mut BTreeMap>, non_cached_files_to_check: &mut BTreeMap>, ) { debug!("{function_name} - started diff between loaded and prechecked files"); for (size, mut vec_file_entry) in used_map { if let Some(cached_vec_file_entry) = loaded_hash_map.get(&size) { // TODO maybe hashmap is not needed when using < 4 elements let mut cached_path_entries: HashMap<&Path, DuplicateEntry> = HashMap::new(); for file_entry in cached_vec_file_entry { cached_path_entries.insert(&file_entry.path, file_entry.clone()); } for file_entry in vec_file_entry { if let Some(cached_file_entry) = cached_path_entries.remove(file_entry.path.as_path()) { records_already_cached.entry(size).or_default().push(cached_file_entry); } else { non_cached_files_to_check.entry(size).or_default().push(file_entry); } } } else { non_cached_files_to_check.entry(size).or_default().append(&mut vec_file_entry); } } debug!( "{function_name} - completed diff between loaded and prechecked files - {}({}) non cached, {}({}) already cached", non_cached_files_to_check.len(), format_size(non_cached_files_to_check.values().map(|v| v.iter().map(|e| e.size).sum::()).sum::(), BINARY), records_already_cached.len(), format_size(records_already_cached.values().map(|v| v.iter().map(|e| e.size).sum::()).sum::(), BINARY), ); } #[fun_time(message = "full_hashing_load_cache_at_start", level = "debug")] fn full_hashing_load_cache_at_start( &mut self, mut pre_checked_map: BTreeMap>, ) -> (BTreeMap>, BTreeMap>, BTreeMap>) { let loaded_hash_map; let mut records_already_cached: BTreeMap> = Default::default(); let mut non_cached_files_to_check: BTreeMap> = Default::default(); if self.common_data.use_cache { debug!("full_hashing_load_cache_at_start - using cache"); let (messages, loaded_items) = load_cache_from_file_generalized_by_size::( &get_duplicate_cache_file(&self.get_params().hash_type, false), self.get_delete_outdated_cache(), &pre_checked_map, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); Self::diff_loaded_and_prechecked_files( "full_hashing_load_cache_at_start", pre_checked_map, &loaded_hash_map, &mut records_already_cached, &mut non_cached_files_to_check, ); } else { debug!("full_hashing_load_cache_at_start - not using cache"); loaded_hash_map = Default::default(); mem::swap(&mut pre_checked_map, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } #[fun_time(message = "full_hashing_save_cache_at_exit", level = "debug")] fn full_hashing_save_cache_at_exit( &mut self, records_already_cached: BTreeMap>, full_hash_results: &mut Vec<(u64, BTreeMap>, Vec)>, loaded_hash_map: BTreeMap>, ) { if !self.common_data.use_cache { return; } 'main: for (size, vec_file_entry) in records_already_cached { // Check if size already exists, if exists we must to change it outside because cannot have mut and non mut reference to full_hash_results for (full_size, full_hashmap, _errors) in &mut (*full_hash_results) { if size == *full_size { for file_entry in vec_file_entry { full_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry); } continue 'main; } } // Size doesn't exists add results to files let mut temp_hashmap: BTreeMap> = Default::default(); for file_entry in vec_file_entry { temp_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry); } full_hash_results.push((size, temp_hashmap, Vec::new())); } // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = Default::default(); for (_size, vec_file_entry) in loaded_hash_map { for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } } for (_size, hashmap, _errors) in full_hash_results { for vec_file_entry in hashmap.values() { for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone()); } } } let messages = save_cache_to_file_generalized( &get_duplicate_cache_file(&self.get_params().hash_type, false), &all_results, self.common_data.save_also_as_json, self.get_params().minimal_cache_file_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); } #[fun_time(message = "full_hashing", level = "debug")] fn full_hashing( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, pre_checked_map: BTreeMap>, ) -> WorkContinueStatus { if pre_checked_map.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheLoading, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.full_hashing_load_cache_at_start(pre_checked_map); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::DuplicateFullHashing, non_cached_files_to_check.values().map(Vec::len).sum(), self.get_test_type(), non_cached_files_to_check.iter().map(|(size, items)| (*size) * items.len() as u64).sum::(), ); let non_cached_files_to_check: Vec<(u64, Vec)> = non_cached_files_to_check.into_iter().collect(); let check_type = self.get_params().hash_type; debug!( "Starting full hashing of {} files", non_cached_files_to_check.iter().map(|(_size, v)| v.len() as u64).sum::() ); let mut full_hash_results: Vec<(u64, BTreeMap>, Vec)> = non_cached_files_to_check .into_par_iter() .with_max_len(3) .map(|(size, vec_file_entry)| { let mut hashmap_with_hash: BTreeMap> = Default::default(); let mut errors: Vec = Vec::new(); THREAD_BUFFER.with_borrow_mut(|buffer| { for mut file_entry in vec_file_entry { if check_if_stop_received(stop_flag) { return None; } match hash_calculation(buffer, &file_entry, check_type, progress_handler.size_counter(), stop_flag) { Ok(hash_string) => { if let Some(hash_string) = hash_string { file_entry.hash = hash_string.clone(); hashmap_with_hash.entry(hash_string).or_default().push(file_entry); } else { return None; } } Err(s) => errors.push(s), }; progress_handler.increase_items(1); } Some(()) })?; Some((size, hashmap_with_hash, errors)) }) .while_some() .collect(); debug!("Finished full hashing"); // Even if clicked stop, save items to cache and show results progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheSaving, 0, self.get_test_type(), 0); self.full_hashing_save_cache_at_exit(records_already_cached, &mut full_hash_results, loaded_hash_map); progress_handler.join_thread(); for (size, hash_map, mut errors) in full_hash_results { self.common_data.text_messages.warnings.append(&mut errors); for (_hash, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { self.files_with_identical_hashes.entry(size).or_default().push(vec_file_entry); } } } WorkContinueStatus::Continue } #[fun_time(message = "hash_reference_folders", level = "debug")] fn hash_reference_folders(&mut self) { // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_hashes) .into_iter() .filter_map(|(_size, vec_vec_file_entry)| { let mut all_results_with_same_size = Vec::new(); for vec_file_entry in vec_vec_file_entry { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { continue; } if let Some(file) = files_from_referenced_folders.pop() { all_results_with_same_size.push((file, normal_files)); } } if all_results_with_same_size.is_empty() { None } else { Some(all_results_with_same_size) } }) .collect::)>>>(); for vec_of_vec in vec { self.files_with_identical_hashes_referenced.insert(vec_of_vec[0].0.size, vec_of_vec); } } if self.common_data.use_reference_folders { for (size, vector_vectors) in &self.files_with_identical_hashes_referenced { for (_fe, vector) in vector_vectors { self.information.number_of_duplicated_files_by_hash += vector.len(); self.information.number_of_groups_by_hash += 1; self.information.lost_space_by_hash += (vector.len() as u64) * size; } } } else { for (size, vector_vectors) in &self.files_with_identical_hashes { for vector in vector_vectors { self.information.number_of_duplicated_files_by_hash += vector.len() - 1; self.information.number_of_groups_by_hash += 1; self.information.lost_space_by_hash += (vector.len() as u64 - 1) * size; } } } } #[fun_time(message = "check_files_hash", level = "debug")] pub(crate) fn check_files_hash(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { assert_eq!(self.get_params().check_method, CheckingMethod::Hash); let mut pre_checked_map: BTreeMap> = Default::default(); if self.prehashing(stop_flag, progress_sender, &mut pre_checked_map) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } if self.full_hashing(stop_flag, progress_sender, pre_checked_map) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } self.hash_reference_folders(); // Clean unused data let files_with_identical_size = mem::take(&mut self.files_with_identical_size); thread::spawn(move || drop(files_with_identical_size)); WorkContinueStatus::Continue } } pub fn get_duplicate_cache_file(type_of_hash: &HashType, is_prehash: bool) -> String { let prehash_str = if is_prehash { "_prehash" } else { "" }; format!("cache_duplicates_{type_of_hash:?}{prehash_str}_{CACHE_DUPLICATE_VERSION}.bin") } czkawka_core-10.0.0/src/tools/duplicate/mod.rs000064400000000000000000000322301046102023000173730ustar 00000000000000pub mod core; pub mod traits; use std::cell::RefCell; use std::collections::{BTreeMap, HashSet}; use std::fmt::Debug; use std::fs; use std::fs::File; use std::hash::Hasher; use std::io::prelude::*; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use serde::{Deserialize, Serialize}; use static_assertions::const_assert; use xxhash_rust::xxh3::Xxh3; use crate::common::model::{CheckingMethod, FileEntry, HashType}; use crate::common::progress_stop_handler::check_if_stop_received; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; pub const PREHASHING_BUFFER_SIZE: u64 = 4 * 1024; pub const THREAD_BUFFER_SIZE: usize = 2 * 1024 * 1024; thread_local! { static THREAD_BUFFER: RefCell> = RefCell::new(vec![0u8; THREAD_BUFFER_SIZE]); } #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct DuplicateEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub hash: String, } impl ResultEntry for DuplicateEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_duplicate_entry(self) -> DuplicateEntry { DuplicateEntry { size: self.size, path: self.path, modified_date: self.modified_date, hash: String::new(), } } } #[derive(Default, Clone)] pub struct Info { pub number_of_groups_by_size: usize, pub number_of_duplicated_files_by_size: usize, pub number_of_groups_by_hash: usize, pub number_of_duplicated_files_by_hash: usize, pub number_of_groups_by_name: usize, pub number_of_duplicated_files_by_name: usize, pub number_of_groups_by_size_name: usize, pub number_of_duplicated_files_by_size_name: usize, pub lost_space_by_size: u64, pub lost_space_by_hash: u64, } #[derive(Clone)] pub struct DuplicateFinderParameters { pub check_method: CheckingMethod, pub hash_type: HashType, pub ignore_hard_links: bool, pub use_prehash_cache: bool, pub minimal_cache_file_size: u64, pub minimal_prehash_cache_file_size: u64, pub case_sensitive_name_comparison: bool, } impl DuplicateFinderParameters { pub fn new( check_method: CheckingMethod, hash_type: HashType, ignore_hard_links: bool, use_prehash_cache: bool, minimal_cache_file_size: u64, minimal_prehash_cache_file_size: u64, case_sensitive_name_comparison: bool, ) -> Self { Self { check_method, hash_type, ignore_hard_links, use_prehash_cache, minimal_cache_file_size, minimal_prehash_cache_file_size, case_sensitive_name_comparison, } } } pub struct DuplicateFinder { common_data: CommonToolData, information: Info, // File Size, File Entry files_with_identical_names: BTreeMap>, // File (Size, Name), File Entry files_with_identical_size_names: BTreeMap<(u64, String), Vec>, // File Size, File Entry files_with_identical_size: BTreeMap>, // File Size, next grouped by file size, next grouped by hash files_with_identical_hashes: BTreeMap>>, // File Size, File Entry files_with_identical_names_referenced: BTreeMap)>, // File (Size, Name), File Entry files_with_identical_size_names_referenced: BTreeMap<(u64, String), (DuplicateEntry, Vec)>, // File Size, File Entry files_with_identical_size_referenced: BTreeMap)>, // File Size, next grouped by file size, next grouped by hash files_with_identical_hashes_referenced: BTreeMap)>>, params: DuplicateFinderParameters, } #[cfg(target_family = "windows")] fn filter_hard_links(vec_file_entry: &[FileEntry]) -> Vec { let mut inodes: HashSet = HashSet::with_capacity(vec_file_entry.len()); let mut identical: Vec = Vec::with_capacity(vec_file_entry.len()); for f in vec_file_entry { if let Ok(meta) = file_id::get_high_res_file_id(&f.path) { if let file_id::FileId::HighRes { file_id, .. } = meta { if !inodes.insert(file_id) { continue; } } } identical.push(f.clone()); } identical } #[cfg(target_family = "unix")] fn filter_hard_links(vec_file_entry: &[FileEntry]) -> Vec { let mut inodes: HashSet = HashSet::with_capacity(vec_file_entry.len()); let mut identical: Vec = Vec::with_capacity(vec_file_entry.len()); for f in vec_file_entry { if let Ok(meta) = fs::metadata(&f.path) { if !inodes.insert(meta.ino()) { continue; } } identical.push(f.clone()); } identical } pub trait MyHasher { fn update(&mut self, bytes: &[u8]); fn finalize(&self) -> String; } impl DuplicateFinder { pub fn get_params(&self) -> &DuplicateFinderParameters { &self.params } pub const fn get_files_sorted_by_names(&self) -> &BTreeMap> { &self.files_with_identical_names } pub const fn get_files_sorted_by_size(&self) -> &BTreeMap> { &self.files_with_identical_size } pub const fn get_files_sorted_by_size_name(&self) -> &BTreeMap<(u64, String), Vec> { &self.files_with_identical_size_names } pub const fn get_files_sorted_by_hash(&self) -> &BTreeMap>> { &self.files_with_identical_hashes } pub const fn get_information(&self) -> &Info { &self.information } pub fn set_dry_run(&mut self, dry_run: bool) { self.common_data.dry_run = dry_run; } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } pub fn get_files_with_identical_hashes_referenced(&self) -> &BTreeMap)>> { &self.files_with_identical_hashes_referenced } pub fn get_files_with_identical_name_referenced(&self) -> &BTreeMap)> { &self.files_with_identical_names_referenced } pub fn get_files_with_identical_size_referenced(&self) -> &BTreeMap)> { &self.files_with_identical_size_referenced } pub fn get_files_with_identical_size_names_referenced(&self) -> &BTreeMap<(u64, String), (DuplicateEntry, Vec)> { &self.files_with_identical_size_names_referenced } } pub(crate) fn hash_calculation_limit(buffer: &mut [u8], file_entry: &DuplicateEntry, hash_type: HashType, limit: u64, size_counter: &Arc) -> Result { // This function is used only to calculate hash of file with limit // We must ensure that buffer is big enough to store all data // We don't need to check that each time const_assert!(PREHASHING_BUFFER_SIZE <= THREAD_BUFFER_SIZE as u64); let mut file_handler = match File::open(&file_entry.path) { Ok(t) => t, Err(e) => { size_counter.fetch_add(limit, Ordering::Relaxed); return Err(format!("Unable to check hash of file {:?}, reason {e}", file_entry.path)); } }; let hasher = &mut *hash_type.hasher(); let n = match file_handler.read(&mut buffer[..limit as usize]) { Ok(t) => t, Err(e) => return Err(format!("Error happened when checking hash of file {:?}, reason {}", file_entry.path, e)), }; hasher.update(&buffer[..n]); size_counter.fetch_add(n as u64, Ordering::Relaxed); Ok(hasher.finalize()) } pub fn hash_calculation( buffer: &mut [u8], file_entry: &DuplicateEntry, hash_type: HashType, size_counter: &Arc, stop_flag: &Arc, ) -> Result, String> { let mut file_handler = match File::open(&file_entry.path) { Ok(t) => t, Err(e) => { size_counter.fetch_add(file_entry.size, Ordering::Relaxed); return Err(format!("Unable to check hash of file {:?}, reason {e}", file_entry.path)); } }; let hasher = &mut *hash_type.hasher(); loop { let n = match file_handler.read(buffer) { Ok(0) => break, Ok(t) => t, Err(e) => return Err(format!("Error happened when checking hash of file {:?}, reason {}", file_entry.path, e)), }; hasher.update(&buffer[..n]); size_counter.fetch_add(n as u64, Ordering::Relaxed); if check_if_stop_received(stop_flag) { return Ok(None); } } Ok(Some(hasher.finalize())) } impl MyHasher for blake3::Hasher { fn update(&mut self, bytes: &[u8]) { self.update(bytes); } fn finalize(&self) -> String { self.finalize().to_hex().to_string() } } impl MyHasher for crc32fast::Hasher { fn update(&mut self, bytes: &[u8]) { self.write(bytes); } fn finalize(&self) -> String { self.finish().to_string() } } impl MyHasher for Xxh3 { fn update(&mut self, bytes: &[u8]) { self.write(bytes); } fn finalize(&self) -> String { self.finish().to_string() } } #[cfg(test)] mod tests { use std::fs::File; use std::io; use super::*; #[test] fn test_filter_hard_links_empty() { let expected: Vec = Default::default(); assert_eq!(expected, filter_hard_links(&[])); } #[cfg(target_family = "unix")] #[test] fn test_filter_hard_links() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; fs::hard_link(src.clone(), dst.clone())?; let e1 = FileEntry { path: src, ..Default::default() }; let e2 = FileEntry { path: dst, ..Default::default() }; let actual = filter_hard_links(&[e1.clone(), e2]); assert_eq!(vec![e1], actual); Ok(()) } #[test] fn test_filter_hard_links_regular_files() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; File::create(&dst)?; let e1 = FileEntry { path: src, ..Default::default() }; let e2 = FileEntry { path: dst, ..Default::default() }; let actual = filter_hard_links(&[e1.clone(), e2.clone()]); assert_eq!(vec![e1, e2], actual); Ok(()) } #[test] fn test_hash_calculation() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1 << 10]; let src = dir.path().join("a"); let mut file = File::create(&src)?; file.write_all(b"aaAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFFFFFGGGGGGGGG")?; let e = DuplicateEntry { path: src, ..Default::default() }; let size_counter = Arc::new(AtomicU64::new(0)); let r = hash_calculation(&mut buf, &e, HashType::Blake3, &size_counter, &Arc::default()) .expect("hash_calculation failed") .expect("hash_calculation returned None"); assert!(!r.is_empty()); assert_eq!(size_counter.load(Ordering::Relaxed), 45); Ok(()) } #[test] fn test_hash_calculation_limit() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1000]; let src = dir.path().join("a"); let mut file = File::create(&src)?; file.write_all(b"aa")?; let e = DuplicateEntry { path: src, ..Default::default() }; let size_counter_1 = Arc::new(AtomicU64::new(0)); let size_counter_2 = Arc::new(AtomicU64::new(0)); let size_counter_3 = Arc::new(AtomicU64::new(0)); let r1 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1, &size_counter_1).expect("hash_calculation failed"); let r2 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 2, &size_counter_2).expect("hash_calculation failed"); let r3 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1000, &size_counter_3).expect("hash_calculation failed"); assert_ne!(r1, r2); assert_eq!(r2, r3); assert_eq!(1, size_counter_1.load(Ordering::Relaxed)); assert_eq!(2, size_counter_2.load(Ordering::Relaxed)); assert_eq!(2, size_counter_3.load(Ordering::Relaxed)); Ok(()) } #[test] fn test_hash_calculation_invalid_file() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1 << 10]; let src = dir.path().join("a"); let e = DuplicateEntry { path: src, ..Default::default() }; let r = hash_calculation(&mut buf, &e, HashType::Blake3, &Arc::default(), &Arc::default()).expect_err("hash_calculation succeeded"); assert!(!r.is_empty()); Ok(()) } } czkawka_core-10.0.0/src/tools/duplicate/traits.rs000064400000000000000000000454551046102023000201370ustar 00000000000000use std::io::prelude::*; use std::io::{self}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::{CheckingMethod, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::*; use crate::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters, Info}; impl AllTraits for DuplicateFinder {} impl DeletingItems for DuplicateFinder { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.common_data.delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = match self.get_params().check_method { CheckingMethod::Name => self.files_with_identical_names.values().cloned().collect::>(), CheckingMethod::SizeName => self.files_with_identical_size_names.values().cloned().collect::>(), CheckingMethod::Hash => self.files_with_identical_hashes.values().flatten().cloned().collect::>(), CheckingMethod::Size => self.files_with_identical_size.values().cloned().collect::>(), _ => panic!(), }; self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } impl Search for DuplicateFinder { #[fun_time(message = "find_duplicates", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty(); match self.get_params().check_method { CheckingMethod::Name => { self.common_data.stopped_search = self.check_files_name(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::SizeName => { self.common_data.stopped_search = self.check_files_size_name(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::Size => { self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::Hash => { self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } self.common_data.stopped_search = self.check_files_hash(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } _ => panic!(), } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for DuplicateFinder { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!( "Number of duplicated files by size(in groups) - {} ({})", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size ); println!( "Number of duplicated files by hash(in groups) - {} ({})", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash ); println!( "Number of duplicated files by name(in groups) - {} ({})", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name ); println!( "Lost space by size - {} ({} bytes)", format_size(self.information.lost_space_by_size, BINARY), self.information.lost_space_by_size ); println!( "Lost space by hash - {} ({} bytes)", format_size(self.information.lost_space_by_hash, BINARY), self.information.lost_space_by_hash ); println!("### Other"); println!("Files list size - {}", self.files_with_identical_size.len()); println!("Hashed Files list size - {}", self.files_with_identical_hashes.len()); println!("Files with identical names - {}", self.files_with_identical_names.len()); println!("Files with identical size names - {}", self.files_with_identical_size_names.len()); println!("Files with identical names referenced - {}", self.files_with_identical_names_referenced.len()); println!("Files with identical size names referenced - {}", self.files_with_identical_size_names_referenced.len()); println!("Files with identical size referenced - {}", self.files_with_identical_size_referenced.len()); println!("Files with identical hashes referenced - {}", self.files_with_identical_hashes_referenced.len()); println!("Checking Method - {:?}", self.get_params().check_method); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for DuplicateFinder { fn write_results(&self, writer: &mut T) -> io::Result<()> { writeln!( writer, "Results of searching {:?} (reference directories {:?}) with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.reference_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; match self.get_params().check_method { CheckingMethod::Name => { if !self.files_with_identical_names.is_empty() { writeln!( writer, "-------------------------------------------------Files with same names-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same name(may have different content)", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name, )?; for (name, vector) in self.files_with_identical_names.iter().rev() { writeln!(writer, "Name - {} - {} files ", name, vector.len())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else if !self.files_with_identical_names_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same names in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same name(may have different content)", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name, )?; for (name, (file_entry, vector)) in self.files_with_identical_names_referenced.iter().rev() { writeln!(writer, "Name - {} - {} files ", name, vector.len())?; writeln!(writer, "Reference file - {:?}", file_entry.path)?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else { write!(writer, "Not found any files with same names.")?; } } CheckingMethod::SizeName => { if !self.files_with_identical_names.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size and names-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same size and name(may have different content)", self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name, )?; for ((size, name), vector) in self.files_with_identical_size_names.iter().rev() { writeln!(writer, "Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else if !self.files_with_identical_names_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size and names in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same size and name(may have different content)", self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name, )?; for ((size, name), (file_entry, vector)) in self.files_with_identical_size_names_referenced.iter().rev() { writeln!(writer, "Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len())?; writeln!(writer, "Reference file - {:?}", file_entry.path)?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else { write!(writer, "Not found any files with same size and names.")?; } } CheckingMethod::Size => { if !self.files_with_identical_size.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size, format_size(self.information.lost_space_by_size, BINARY) )?; for (size, vector) in self.files_with_identical_size.iter().rev() { write!(writer, "\n---- Size {} ({}) - {} files \n", format_size(*size, BINARY), size, vector.len())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } else if !self.files_with_identical_size_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size, format_size(self.information.lost_space_by_size, BINARY) )?; for (size, (file_entry, vector)) in self.files_with_identical_size_referenced.iter().rev() { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; writeln!(writer, "Reference file - {:?}", file_entry.path)?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } else { write!(writer, "Not found any duplicates.")?; } } CheckingMethod::Hash => { if !self.files_with_identical_hashes.is_empty() { writeln!( writer, "-------------------------------------------------Files with same hashes-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash, format_size(self.information.lost_space_by_hash, BINARY) )?; for (size, vectors_vector) in self.files_with_identical_hashes.iter().rev() { for vector in vectors_vector { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } } else if !self.files_with_identical_hashes_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same hashes in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash, format_size(self.information.lost_space_by_hash, BINARY) )?; for (size, vectors_vector) in self.files_with_identical_hashes_referenced.iter().rev() { for (file_entry, vector) in vectors_vector { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; writeln!(writer, "Reference file - \"{}\"", file_entry.path.to_string_lossy())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } } else { write!(writer, "Not found any duplicates.")?; } } _ => panic!(), } Ok(()) } // TODO - check if is possible to save also data in header about size and name in SizeName mode - https://github.com/qarmin/czkawka/issues/1137 fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> io::Result<()> { if self.get_use_reference() { match self.get_params().check_method { CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names_referenced, pretty_print), CheckingMethod::SizeName => { self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names_referenced.values().collect::>(), pretty_print) } CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_referenced, pretty_print), CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes_referenced, pretty_print), _ => panic!(), } } else { match self.get_params().check_method { CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names, pretty_print), CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names.values().collect::>(), pretty_print), CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size, pretty_print), CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes, pretty_print), _ => panic!(), } } } } impl CommonData for DuplicateFinder { type Info = Info; type Parameters = DuplicateFinderParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn get_check_method(&self) -> CheckingMethod { self.get_params().check_method } fn found_any_broken_files(&self) -> bool { self.get_information().number_of_duplicated_files_by_hash > 0 || self.get_information().number_of_duplicated_files_by_name > 0 || self.get_information().number_of_duplicated_files_by_size > 0 || self.get_information().number_of_duplicated_files_by_size_name > 0 } } czkawka_core-10.0.0/src/tools/empty_files/core.rs000064400000000000000000000033071046102023000201150ustar 00000000000000use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonToolData; use crate::tools::empty_files::{EmptyFiles, Info}; impl EmptyFiles { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::EmptyFiles), information: Info::default(), empty_files: vec![], } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .minimal_file_size(0) .maximal_file_size(0) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.empty_files = grouped_file_entries.into_values().flatten().collect(); self.information.number_of_empty_files = self.empty_files.len(); self.common_data.text_messages.warnings.extend(warnings); debug!("Found {} empty files.", self.information.number_of_empty_files); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } } czkawka_core-10.0.0/src/tools/empty_files/mod.rs000064400000000000000000000011271046102023000177420ustar 00000000000000pub mod core; pub mod traits; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; #[derive(Default, Clone)] pub struct Info { pub number_of_empty_files: usize, } pub struct EmptyFiles { common_data: CommonToolData, information: Info, empty_files: Vec, } impl Default for EmptyFiles { fn default() -> Self { Self::new() } } impl EmptyFiles { pub const fn get_empty_files(&self) -> &Vec { &self.empty_files } pub const fn get_information(&self) -> &Info { &self.information } } czkawka_core-10.0.0/src/tools/empty_files/traits.rs000064400000000000000000000067131046102023000204770ustar 00000000000000use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::*; use crate::tools::empty_files::{EmptyFiles, Info}; impl AllTraits for EmptyFiles {} impl Search for EmptyFiles { #[fun_time(message = "find_empty_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for EmptyFiles { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Empty list size - {}", self.empty_files.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for EmptyFiles { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { writeln!( writer, "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; if !self.empty_files.is_empty() { writeln!(writer, "Found {} empty files.", self.information.number_of_empty_files)?; for file_entry in &self.empty_files { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } else { write!(writer, "Not found any empty files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.empty_files, pretty_print) } } impl CommonData for EmptyFiles { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_empty_files > 0 } } impl DeletingItems for EmptyFiles { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.empty_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } czkawka_core-10.0.0/src/tools/empty_folder/core.rs000064400000000000000000000234271046102023000202730ustar 00000000000000use std::collections::HashMap; use std::fs::DirEntry; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::dir_traversal::{common_get_entry_data, common_get_metadata_dir, common_read_dir, get_modified_time}; use crate::common::directories::Directories; use crate::common::items::ExcludedItems; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::empty_folder::{EmptyFolder, FolderEmptiness, FolderEntry, Info}; impl EmptyFolder { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::EmptyFolders), information: Default::default(), empty_folder_list: Default::default(), } } pub const fn get_empty_folder_list(&self) -> &HashMap { &self.empty_folder_list } pub const fn get_information(&self) -> &Info { &self.information } pub(crate) fn optimize_folders(&mut self) { let mut new_directory_folders: HashMap = Default::default(); for (name, folder_entry) in &self.empty_folder_list { match &folder_entry.parent_path { Some(t) => { if !self.empty_folder_list.contains_key(t) { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } None => { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } } self.empty_folder_list = new_directory_folders; self.information.number_of_empty_folders = self.empty_folder_list.len(); } #[fun_time(message = "check_for_empty_folders", level = "debug")] pub(crate) fn check_for_empty_folders(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let mut folders_to_check: Vec = self.common_data.directories.included_directories.clone(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0); let excluded_items = self.common_data.excluded_items.clone(); let directories = self.common_data.directories.clone(); let mut non_empty_folders: Vec = vec![]; let mut start_folder_entries = Vec::with_capacity(folders_to_check.len()); let mut new_folder_entries_list = Vec::new(); for dir in &folders_to_check { start_folder_entries.push(FolderEntry { path: dir.clone(), parent_path: None, is_empty: FolderEmptiness::Maybe, modified_date: 0, }); } while !folders_to_check.is_empty() { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let segments: Vec<_> = folders_to_check .into_par_iter() .map(|current_folder| { let mut dir_result = vec![]; let mut warnings = vec![]; let mut non_empty_folder = None; let mut folder_entries_list = vec![]; let current_folder_as_string = current_folder.to_string_lossy().to_string(); let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return (dir_result, warnings, Some(current_folder_as_string), folder_entries_list); }; let mut counter = 0; // Check every sub folder/file/link etc. for entry in read_dir { let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, ¤t_folder) else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue }; if file_type.is_dir() { counter += 1; Self::process_dir_in_dir_mode( ¤t_folder, ¤t_folder_as_string, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items, &mut non_empty_folder, &mut folder_entries_list, ); } else { if non_empty_folder.is_none() { non_empty_folder = Some(current_folder_as_string.clone()); } } } if counter > 0 { // Increase counter in batch, because usually it may be slow to add multiple times atomic value progress_handler.increase_items(counter); } (dir_result, warnings, non_empty_folder, folder_entries_list) }) .collect(); let required_size = segments.iter().map(|(segment, _, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, non_empty_folder, fe_list) in segments { folders_to_check.extend(segment); if !warnings.is_empty() { self.common_data.text_messages.warnings.extend(warnings); } if let Some(non_empty_folder) = non_empty_folder { non_empty_folders.push(non_empty_folder); } new_folder_entries_list.push(fe_list); } } let mut folder_entries: HashMap = HashMap::with_capacity(start_folder_entries.len() + new_folder_entries_list.iter().map(Vec::len).sum::()); for fe in start_folder_entries { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } for fe_list in new_folder_entries_list { for fe in fe_list { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } } // Start to for current_folder in non_empty_folders.into_iter().rev() { Self::set_as_not_empty_folder(&mut folder_entries, ¤t_folder); } for (name, folder_entry) in folder_entries { if folder_entry.is_empty != FolderEmptiness::No { self.empty_folder_list.insert(name, folder_entry); } } debug!("Found {} empty folders.", self.empty_folder_list.len()); progress_handler.join_thread(); WorkContinueStatus::Continue } pub(crate) fn set_as_not_empty_folder(folder_entries: &mut HashMap, current_folder: &str) { let mut d = folder_entries .get_mut(current_folder) .unwrap_or_else(|| panic!("Folder {current_folder} not found in folder_entries")); if d.is_empty == FolderEmptiness::No { return; // Already set as non empty by one of his child } // Loop to recursively set as non empty this and all his parent folders loop { d.is_empty = FolderEmptiness::No; if let Some(parent_path) = &d.parent_path { let cf = parent_path.clone(); d = folder_entries.get_mut(&cf).unwrap_or_else(|| panic!("Folder {cf} not found in folder_entries")); if d.is_empty == FolderEmptiness::No { break; // Already set as non empty, so one of child already set it to non empty } } else { break; } } } fn process_dir_in_dir_mode( current_folder: &Path, current_folder_as_str: &str, entry_data: &DirEntry, directories: &Directories, dir_result: &mut Vec, warnings: &mut Vec, excluded_items: &ExcludedItems, non_empty_folder: &mut Option, folder_entries_list: &mut Vec, ) { let next_folder = entry_data.path(); if excluded_items.is_excluded(&next_folder) || directories.is_excluded(&next_folder) { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&next_folder) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &next_folder) else { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; }; dir_result.push(next_folder.clone()); folder_entries_list.push(FolderEntry { path: next_folder, parent_path: Some(current_folder_as_str.to_string()), is_empty: FolderEmptiness::Maybe, modified_date: get_modified_time(&metadata, warnings, current_folder, true), }); } } czkawka_core-10.0.0/src/tools/empty_folder/mod.rs000064400000000000000000000023121046102023000201100ustar 00000000000000pub mod core; pub mod traits; use std::collections::HashMap; use std::path::{Path, PathBuf}; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Clone, Debug)] pub struct FolderEntry { pub path: PathBuf, pub(crate) parent_path: Option, // Usable only when finding pub(crate) is_empty: FolderEmptiness, pub modified_date: u64, } impl ResultEntry for FolderEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { 0 } } pub struct EmptyFolder { common_data: CommonToolData, information: Info, empty_folder_list: HashMap, // Path, FolderEntry } /// Enum with values which show if folder is empty. /// In function "`optimize_folders`" automatically "Maybe" is changed to "Yes", so it is not necessary to put it here #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub(crate) enum FolderEmptiness { No, Maybe, } #[derive(Default, Clone)] pub struct Info { pub number_of_empty_folders: usize, } impl Default for EmptyFolder { fn default() -> Self { Self::new() } } czkawka_core-10.0.0/src/tools/empty_folder/traits.rs000064400000000000000000000071741046102023000206520ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use rayon::prelude::*; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::empty_folder::{EmptyFolder, Info}; impl AllTraits for EmptyFolder {} impl Search for EmptyFolder { #[fun_time(message = "find_empty_folders", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_for_empty_folders(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } self.optimize_folders(); if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for EmptyFolder { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Number of empty folders - {}", self.information.number_of_empty_folders); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for EmptyFolder { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.empty_folder_list.is_empty() { writeln!(writer, "--------------------------Empty folder list--------------------------")?; writeln!(writer, "Found {} empty folders", self.information.number_of_empty_folders)?; let mut empty_folder_list = self.empty_folder_list.keys().collect::>(); empty_folder_list.par_sort_unstable(); for name in empty_folder_list { writeln!(writer, "{name}")?; } } else { write!(writer, "Not found any empty folders.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.empty_folder_list.keys().collect::>(), pretty_print) } } impl CommonData for EmptyFolder { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_empty_folders > 0 } } impl DeletingItems for EmptyFolder { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages( stop_flag, progress_sender, DeleteItemType::DeletingFolders(self.empty_folder_list.values().cloned().collect::>()), ), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } czkawka_core-10.0.0/src/tools/invalid_symlinks/core.rs000064400000000000000000000071011046102023000211500ustar 00000000000000use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use crate::common::dir_traversal::{Collect, DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonToolData; use crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks, MAX_NUMBER_OF_SYMLINK_JUMPS, SymlinkInfo}; impl InvalidSymlinks { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::InvalidSymlinks), information: Info::default(), invalid_symlinks: vec![], } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .collect(Collect::InvalidSymlinks) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.invalid_symlinks = grouped_file_entries .into_values() .flatten() .filter_map(|e| { let (destination_path, type_of_error) = Self::check_invalid_symlinks(&e.path)?; Some(e.into_symlinks_entry(SymlinkInfo { destination_path, type_of_error })) }) .collect(); self.information.number_of_invalid_symlinks = self.invalid_symlinks.len(); self.common_data.text_messages.warnings.extend(warnings); debug!("Found {} invalid symlinks.", self.information.number_of_invalid_symlinks); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn check_invalid_symlinks(current_file_name: &Path) -> Option<(PathBuf, ErrorType)> { let mut destination_path = PathBuf::new(); let type_of_error; match current_file_name.read_link() { Ok(t) => { destination_path.push(t); let mut number_of_loop = 0; let mut current_path = current_file_name.to_path_buf(); loop { if number_of_loop == 0 && !current_path.exists() { type_of_error = ErrorType::NonExistentFile; break; } if number_of_loop == MAX_NUMBER_OF_SYMLINK_JUMPS { type_of_error = ErrorType::InfiniteRecursion; break; } current_path = match current_path.read_link() { Ok(t) => t, Err(_inspected) => { // Looks that some next symlinks are broken, but we do nothing with it - TODO why they are broken return None; } }; number_of_loop += 1; } } Err(_inspected) => { // Failed to load info about it type_of_error = ErrorType::NonExistentFile; } } Some((destination_path, type_of_error)) } } czkawka_core-10.0.0/src/tools/invalid_symlinks/mod.rs000064400000000000000000000045501046102023000210040ustar 00000000000000pub mod core; pub mod traits; use std::fmt::Display; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; use crate::flc; #[derive(Default, Clone)] pub struct Info { pub number_of_invalid_symlinks: usize, } const MAX_NUMBER_OF_SYMLINK_JUMPS: i32 = 20; #[derive(Clone, Debug, PartialEq, Eq, Copy, Deserialize, Serialize)] pub enum ErrorType { InfiniteRecursion, NonExistentFile, } impl ErrorType { pub fn translate(&self) -> String { match *self { Self::InfiniteRecursion => flc!("core_invalid_symlink_infinite_recursion"), Self::NonExistentFile => flc!("core_invalid_symlink_non_existent_destination"), } } } impl Display for ErrorType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InfiniteRecursion => write!(f, "Infinite recursion"), Self::NonExistentFile => write!(f, "Non existent file"), } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct SymlinkInfo { pub destination_path: PathBuf, pub type_of_error: ErrorType, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SymlinksFileEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub symlink_info: SymlinkInfo, } impl ResultEntry for SymlinksFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_symlinks_entry(self, symlink_info: SymlinkInfo) -> SymlinksFileEntry { SymlinksFileEntry { size: self.size, path: self.path, modified_date: self.modified_date, symlink_info, } } } pub struct InvalidSymlinks { common_data: CommonToolData, information: Info, invalid_symlinks: Vec, } impl Default for InvalidSymlinks { fn default() -> Self { Self::new() } } impl InvalidSymlinks { pub const fn get_invalid_symlinks(&self) -> &Vec { &self.invalid_symlinks } pub const fn get_information(&self) -> &Info { &self.information } } czkawka_core-10.0.0/src/tools/invalid_symlinks/traits.rs000064400000000000000000000072071046102023000215350ustar 00000000000000use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::*; use crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks}; impl AllTraits for InvalidSymlinks {} impl Search for InvalidSymlinks { #[fun_time(message = "find_invalid_links", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for InvalidSymlinks { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Invalid symlinks list size - {}", self.invalid_symlinks.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for InvalidSymlinks { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.invalid_symlinks.is_empty() { writeln!(writer, "Found {} invalid symlinks.", self.information.number_of_invalid_symlinks)?; for file_entry in &self.invalid_symlinks { writeln!( writer, "\"{}\"\t\t\"{}\"\t\t{}", file_entry.path.to_string_lossy(), file_entry.symlink_info.destination_path.to_string_lossy(), match file_entry.symlink_info.type_of_error { ErrorType::InfiniteRecursion => "Infinite Recursion", ErrorType::NonExistentFile => "Non Existent File", } )?; } } else { write!(writer, "Not found any invalid symlinks.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.invalid_symlinks, pretty_print) } } impl CommonData for InvalidSymlinks { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_invalid_symlinks > 0 } } impl DeletingItems for InvalidSymlinks { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.invalid_symlinks.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } czkawka_core-10.0.0/src/tools/mod.rs000064400000000000000000000003571046102023000154260ustar 00000000000000pub mod bad_extensions; pub mod big_file; pub mod broken_files; pub mod duplicate; pub mod empty_files; pub mod empty_folder; pub mod invalid_symlinks; pub mod same_music; pub mod similar_images; pub mod similar_videos; pub mod temporary; czkawka_core-10.0.0/src/tools/same_music/core.rs000064400000000000000000001037371046102023000177320ustar 00000000000000use std::collections::{BTreeMap, HashSet}; use std::fs::File; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::{mem, panic}; use anyhow::Context; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use lofty::file::{AudioFile, TaggedFileExt}; use lofty::prelude::*; use lofty::read_from; use log::{debug, error}; use rayon::prelude::*; use rusty_chromaprint::{Configuration, Fingerprinter, match_fingerprints}; use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions}; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use crate::common::cache::{CACHE_VERSION, extract_loaded_cache, load_cache_from_file_generalized_by_path, save_cache_to_file_generalized}; use crate::common::consts::AUDIO_FILES_EXTENSIONS; use crate::common::create_crash_message; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::*; use crate::tools::same_music::{GroupedFilesToCheck, Info, MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters}; impl SameMusic { pub fn new(params: SameMusicParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SameMusic), information: Info::default(), music_entries: Vec::with_capacity(2048), duplicated_music_entries: vec![], music_to_check: Default::default(), duplicated_music_entries_referenced: vec![], hash_preset_config: Configuration::preset_test1(), // TODO allow to change this and move to parameters params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { self.common_data.extensions.set_and_validate_allowed_extensions(AUDIO_FILES_EXTENSIONS); if !self.common_data.extensions.set_any_extensions() { return WorkContinueStatus::Continue; } let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .checking_method(self.params.check_type) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.music_to_check = grouped_file_entries .into_values() .flatten() .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_music_entry())) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} music files.", self.music_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "load_cache", level = "debug")] fn load_cache(&mut self, checking_tags: bool) -> (BTreeMap, BTreeMap, BTreeMap) { let loaded_hash_map; let mut records_already_cached: BTreeMap = Default::default(); let mut non_cached_files_to_check: BTreeMap = Default::default(); if self.common_data.use_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_path::(&get_similar_music_cache_file(checking_tags), self.get_delete_outdated_cache(), &self.music_to_check); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); debug!("load_cache - Starting to check for differences"); extract_loaded_cache( &loaded_hash_map, mem::take(&mut self.music_to_check), &mut records_already_cached, &mut non_cached_files_to_check, ); debug!( "load_cache - completed diff between loaded and prechecked files, {}({}) - non cached, {}({}) - already cached", non_cached_files_to_check.len(), format_size(non_cached_files_to_check.values().map(|e| e.size).sum::(), BINARY), records_already_cached.len(), format_size(records_already_cached.values().map(|e| e.size).sum::(), BINARY), ); } else { loaded_hash_map = Default::default(); mem::swap(&mut self.music_to_check, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } #[fun_time(message = "save_cache", level = "debug")] fn save_cache(&mut self, vec_file_entry: Vec, loaded_hash_map: BTreeMap, checking_tags: bool) { if !self.common_data.use_cache { return; } // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = loaded_hash_map; for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } let messages = save_cache_to_file_generalized(&get_similar_music_cache_file(checking_tags), &all_results, self.common_data.save_also_as_json, 0); self.get_text_messages_mut().extend_with_another_messages(messages); } #[fun_time(message = "calculate_fingerprint", level = "debug")] pub(crate) fn calculate_fingerprint(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } // We only calculate fingerprints, for files with similar titles // This saves a lot of time, because we don't need to calculate and later compare fingerprints for files with different titles if self.params.compare_fingerprints_only_with_similar_titles { let grouped_by_title: BTreeMap> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries)); self.music_to_check = grouped_by_title .into_iter() .filter_map(|(_title, entries)| if entries.len() >= 2 { Some(entries) } else { None }) .flatten() .map(|e| (e.path.to_string_lossy().to_string(), e)) .collect(); } else { self.music_to_check = mem::take(&mut self.music_entries).into_iter().map(|e| (e.path.to_string_lossy().to_string(), e)).collect(); } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingFingerprints, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(false); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SameMusicCalculatingFingerprints, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|e| e.size).sum::(), ); let configuration = &self.hash_preset_config; let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::>(); debug!("calculate_fingerprint - starting fingerprinting"); let mut vec_file_entry = non_cached_files_to_check .into_par_iter() .with_max_len(2) .map(|(path, mut music_entry)| { if check_if_stop_received(stop_flag) { return None; } let res = calc_fingerprint_helper(path, configuration); progress_handler.increase_size(music_entry.size); progress_handler.increase_items(1); let Ok(fingerprint) = res else { return Some(None); }; music_entry.fingerprint = fingerprint; Some(Some(music_entry)) }) .while_some() .flatten() .collect::>(); debug!("calculate_fingerprint - ended fingerprinting"); progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingFingerprints, 0, self.get_test_type(), 0); // Just connect loaded results with already calculated vec_file_entry.extend(records_already_cached.into_values()); self.music_entries = vec_file_entry.clone(); self.save_cache(vec_file_entry, loaded_hash_map, false); // Break if stop was clicked after saving to cache progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "read_tags", level = "debug")] pub(crate) fn read_tags(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_to_check.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingTags, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(true); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SameMusicReadingTags, non_cached_files_to_check.len(), self.get_test_type(), 0, ); debug!("read_tags - starting reading tags"); // Clean for duplicate files let mut vec_file_entry = non_cached_files_to_check .into_par_iter() .map(|(path, music_entry)| { if check_if_stop_received(stop_flag) { return None; } let res = read_single_file_tags(&path, music_entry); progress_handler.increase_items(1); Some(res) }) .while_some() .flatten() .collect::>(); debug!("read_tags - ended reading tags"); progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingTags, 0, self.get_test_type(), 0); // Just connect loaded results with already calculated vec_file_entry.extend(records_already_cached.into_values()); self.music_entries = vec_file_entry.clone(); self.save_cache(vec_file_entry, loaded_hash_map, true); // Break if stop was clicked after saving to cache progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "check_for_duplicate_tags", level = "debug")] pub(crate) fn check_for_duplicate_tags(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingTags, self.music_entries.len(), self.get_test_type(), 0); let mut old_duplicates: Vec> = vec![self.music_entries.clone()]; let mut new_duplicates: Vec> = Vec::new(); if (self.params.music_similarity & MusicSimilarity::TRACK_TITLE) == MusicSimilarity::TRACK_TITLE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| &fe.track_title, self.params.approximate_comparison); } if (self.params.music_similarity & MusicSimilarity::TRACK_ARTIST) == MusicSimilarity::TRACK_ARTIST { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| &fe.track_artist, self.params.approximate_comparison); } if (self.params.music_similarity & MusicSimilarity::YEAR) == MusicSimilarity::YEAR { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| &fe.year, false); } if (self.params.music_similarity & MusicSimilarity::LENGTH) == MusicSimilarity::LENGTH { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| &fe.length, false); } if (self.params.music_similarity & MusicSimilarity::GENRE) == MusicSimilarity::GENRE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| &fe.genre, false); } if (self.params.music_similarity & MusicSimilarity::BITRATE) == MusicSimilarity::BITRATE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let old_duplicates_len = old_duplicates.len(); for vec_file_entry in old_duplicates { let mut hash_map: BTreeMap> = Default::default(); for file_entry in vec_file_entry { if file_entry.bitrate != 0 { let thing = file_entry.bitrate.to_string(); if !thing.is_empty() { hash_map.entry(thing.clone()).or_default().push(file_entry); } } } for (_title, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { new_duplicates.push(vec_file_entry); } } } progress_handler.increase_items(old_duplicates_len); old_duplicates = new_duplicates; } progress_handler.join_thread(); self.duplicated_music_entries = old_duplicates; if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries)); } if self.common_data.use_reference_folders { for (_fe, vector) in &self.duplicated_music_entries_referenced { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.duplicated_music_entries { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clear unused data self.music_entries.clear(); WorkContinueStatus::Continue } fn split_fingerprints_to_base_and_files_to_compare(&self, music_data: Vec) -> (Vec, Vec) { if self.common_data.use_reference_folders { music_data.into_iter().partition(|f| self.common_data.directories.is_in_referenced_directory(f.get_path())) } else { (music_data.clone(), music_data) } } fn get_entries_grouped_by_title(music_data: Vec) -> BTreeMap> { let mut entries_grouped_by_title: BTreeMap> = BTreeMap::new(); for entry in music_data { let simplified_track_title = get_simplified_name(&entry.track_title); // TODO maybe add as option to check for empty titles? if simplified_track_title.is_empty() { continue; } entries_grouped_by_title.entry(simplified_track_title).or_default().push(entry); } entries_grouped_by_title } fn split_fingerprints_to_check(&mut self) -> Vec { if self.params.compare_fingerprints_only_with_similar_titles { let entries_grouped_by_title: BTreeMap> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries)); entries_grouped_by_title .into_iter() .filter_map(|(_title, entries)| { let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries); // When there is 0 files in base files or files to compare there will be no comparison, so removing it from the list // Also when there is only one file in base files and files to compare and they are the same file, there will be no comparison if base_files.is_empty() || files_to_compare.is_empty() || (base_files.len() == 1 && files_to_compare.len() == 1 && (base_files[0].path == files_to_compare[0].path)) { return None; } Some(GroupedFilesToCheck { base_files, files_to_compare }) }) .collect() } else { let entries = mem::take(&mut self.music_entries); let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries); vec![GroupedFilesToCheck { base_files, files_to_compare }] } } fn compare_fingerprints( &mut self, stop_flag: &Arc, items_counter: &Arc, base_files: Vec, files_to_compare: &[MusicEntry], ) -> Option>> { let mut used_paths: HashSet = Default::default(); let configuration = &self.hash_preset_config; let minimum_segment_duration = self.params.minimum_segment_duration; let maximum_difference = self.params.maximum_difference; let mut duplicated_music_entries = Vec::new(); for f_entry in base_files { items_counter.fetch_add(1, Ordering::Relaxed); if check_if_stop_received(stop_flag) { return None; } let f_string = f_entry.path.to_string_lossy().to_string(); if used_paths.contains(&f_string) { continue; } let (mut collected_similar_items, errors): (Vec<_>, Vec<_>) = files_to_compare .par_iter() .map(|e_entry| { let e_string = e_entry.path.to_string_lossy().to_string(); if used_paths.contains(&e_string) || e_string == f_string { return None; } let mut segments = match match_fingerprints(&f_entry.fingerprint, &e_entry.fingerprint, configuration) { Ok(segments) => segments, Err(e) => return Some(Err(format!("Error while comparing fingerprints: {e}"))), }; segments.retain(|s| s.duration(configuration) > minimum_segment_duration && s.score < maximum_difference); if segments.is_empty() { None } else { Some(Ok((e_string, e_entry))) } }) .flatten() .partition_map(|res| match res { Ok(entry) => itertools::Either::Left(entry), Err(err) => itertools::Either::Right(err), }); self.common_data.text_messages.errors.extend(errors); collected_similar_items.retain(|(path, _entry)| !used_paths.contains(path)); if !collected_similar_items.is_empty() { let mut music_entries = Vec::new(); for (path, entry) in collected_similar_items { used_paths.insert(path); music_entries.push(entry.clone()); } used_paths.insert(f_string); music_entries.push(f_entry); duplicated_music_entries.push(music_entries); } } Some(duplicated_music_entries) } #[fun_time(message = "check_for_duplicate_fingerprints", level = "debug")] pub(crate) fn check_for_duplicate_fingerprints(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } let grouped_files_to_check = self.split_fingerprints_to_check(); let base_files_number = grouped_files_to_check.iter().map(|g| g.base_files.len()).sum::(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingFingerprints, base_files_number, self.get_test_type(), 0); let mut duplicated_music_entries = Vec::new(); for group in grouped_files_to_check { let GroupedFilesToCheck { base_files, files_to_compare } = group; let Some(temp_music_entries) = self.compare_fingerprints(stop_flag, progress_handler.items_counter(), base_files, &files_to_compare) else { progress_handler.join_thread(); return WorkContinueStatus::Stop; }; duplicated_music_entries.extend(temp_music_entries); } progress_handler.join_thread(); self.duplicated_music_entries = duplicated_music_entries; if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries)); } if self.common_data.use_reference_folders { for (_fe, vector) in &self.duplicated_music_entries_referenced { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.duplicated_music_entries { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clear unused data self.music_entries.clear(); WorkContinueStatus::Continue } #[fun_time(message = "check_music_item", level = "debug")] fn check_music_item( &self, old_duplicates: Vec>, items_counter: &Arc, get_item: fn(&MusicEntry) -> &str, approximate_comparison: bool, ) -> Vec> { let mut new_duplicates: Vec<_> = Default::default(); let old_duplicates_len = old_duplicates.len(); for vec_file_entry in old_duplicates { let mut hash_map: BTreeMap> = Default::default(); for file_entry in vec_file_entry { let mut thing = get_item(&file_entry).trim().to_lowercase(); if approximate_comparison { thing = get_simplified_name(&thing); } if !thing.is_empty() { hash_map.entry(thing).or_default().push(file_entry); } } for (_title, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { new_duplicates.push(vec_file_entry); } } } items_counter.fetch_add(old_duplicates_len, Ordering::Relaxed); new_duplicates } } // TODO this should be taken from rusty-chromaprint repo, not reimplemented here fn calc_fingerprint_helper(path: impl AsRef, config: &Configuration) -> anyhow::Result> { let path = path.as_ref().to_path_buf(); panic::catch_unwind(|| { let path = &path; let src = File::open(path).context("failed to open file")?; let mss = MediaSourceStream::new(Box::new(src), Default::default()); let mut hint = Hint::new(); if let Some(ext) = path.extension().and_then(std::ffi::OsStr::to_str) { hint.with_extension(ext); } let meta_opts: MetadataOptions = Default::default(); let fmt_opts: FormatOptions = Default::default(); let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts).context("unsupported format")?; let mut format = probed.format; let track = format .tracks() .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .context("no supported audio tracks")?; let dec_opts: DecoderOptions = Default::default(); let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts).context("unsupported codec")?; let track_id = track.id; let mut printer = Fingerprinter::new(config); let sample_rate = track.codec_params.sample_rate.context("missing sample rate")?; let channels = track.codec_params.channels.context("missing audio channels")?.count() as u32; printer.start(sample_rate, channels).context("initializing fingerprinter")?; let mut sample_buf = None; loop { let Ok(packet) = format.next_packet() else { break; }; if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(audio_buf) => { if sample_buf.is_none() { let spec = *audio_buf.spec(); let duration = audio_buf.capacity() as u64; sample_buf = Some(SampleBuffer::::new(duration, spec)); } if let Some(buf) = &mut sample_buf { buf.copy_interleaved_ref(audio_buf); printer.consume(buf.samples()); } } Err(symphonia::core::errors::Error::DecodeError(_)) => (), Err(_) => break, } } printer.finish(); Ok(printer.fingerprint().to_vec()) }) .unwrap_or_else(|_| { let message = create_crash_message("Symphonia", &path.to_string_lossy(), "https://github.com/pdeljanov/Symphonia"); error!("{message}"); Err(anyhow::anyhow!("{message}")) }) } fn read_single_file_tags(path: &str, mut music_entry: MusicEntry) -> Option { let Ok(mut file) = File::open(path) else { return None; }; let Ok(possible_tagged_file) = panic::catch_unwind(move || read_from(&mut file).ok()) else { let message = create_crash_message("Lofty", path, "https://github.com/Serial-ATA/lofty-rs"); error!("{message}"); return None; }; let Some(tagged_file) = possible_tagged_file else { return Some(music_entry) }; let properties = tagged_file.properties(); let mut track_title = String::new(); let mut track_artist = String::new(); let mut year = String::new(); let mut genre = String::new(); let bitrate = properties.audio_bitrate().unwrap_or(0); let mut length = properties.duration().as_millis().to_string(); if let Some(tag) = tagged_file.primary_tag() { track_title = tag.get_string(&ItemKey::TrackTitle).unwrap_or_default().to_string(); track_artist = tag.get_string(&ItemKey::TrackArtist).unwrap_or_default().to_string(); year = tag.get_string(&ItemKey::Year).unwrap_or_default().to_string(); genre = tag.get_string(&ItemKey::Genre).unwrap_or_default().to_string(); } for tag in tagged_file.tags() { if track_title.is_empty() { if let Some(tag_value) = tag.get_string(&ItemKey::TrackTitle) { track_title = tag_value.to_string(); } } if track_artist.is_empty() { if let Some(tag_value) = tag.get_string(&ItemKey::TrackArtist) { track_artist = tag_value.to_string(); } } if year.is_empty() { if let Some(tag_value) = tag.get_string(&ItemKey::Year) { year = tag_value.to_string(); } } if genre.is_empty() { if let Some(tag_value) = tag.get_string(&ItemKey::Genre) { genre = tag_value.to_string(); } } } if let Ok(old_length_number) = length.parse::() { let length_number = old_length_number / 60; let minutes = length_number / 1000; let seconds = (length_number % 1000) * 6 / 100; if minutes != 0 || seconds != 0 { length = format!("{minutes}:{seconds:02}"); } else if old_length_number > 0 { // That means, that audio have length smaller that second but not zero length = "0:01".to_string(); } else { length = String::new(); } } else { length = String::new(); } music_entry.track_title = track_title; music_entry.track_artist = track_artist; music_entry.year = year; music_entry.length = length; music_entry.genre = genre; music_entry.bitrate = bitrate; Some(music_entry) } fn get_simplified_name_internal(what: &str, ignore_numbers: bool) -> String { let mut new_what = String::with_capacity(what.len()); let mut tab_number = 0; let mut space_before = true; for character in what.chars().map(|e| if e.is_whitespace() { ' ' } else { e }) { match character { '(' | '[' => { tab_number += 1; } ')' | ']' => { if tab_number == 0 { // Nothing to do, not even save it to output } else { tab_number -= 1; } } ' ' => { if !space_before { new_what.push(' '); space_before = true; } } ch => { if tab_number == 0 { if ch.is_ascii_alphabetic() || (!ignore_numbers && ch.is_numeric()) { space_before = false; new_what.push(ch); } else { let new_items = deunicode::deunicode_char(character).map_or_else(|| vec![character; 1], |e| e.trim().to_string().chars().collect::>()); // If is equal, then we're trying to deunicode e.g. dot, comma etc. // We just ignore char, because it is mostly useless, but we add space instead it if it wasn't added already if new_items.first() == Some(&character) { if !space_before { new_what.push(' '); space_before = true; } } else { new_what.extend(new_items.into_iter()); space_before = false; } } } } } } if new_what.ends_with(' ') { new_what.pop(); } new_what } fn get_simplified_name(what: &str) -> String { let new_what = get_simplified_name_internal(what, true); if !new_what.is_empty() { return new_what; } let new_what = get_simplified_name_internal(what, false); if !new_what.is_empty() { return new_what; } let simplified_unicode = deunicode::deunicode(what).trim().to_string(); if !simplified_unicode.is_empty() { return simplified_unicode; } // If everything failed, we return original string // this is more useful than returning empty string, which is ignored by other functions what.trim().to_string() } pub fn get_similar_music_cache_file(checking_tags: bool) -> String { if checking_tags { format!("cache_same_music_tags_{CACHE_VERSION}.bin") } else { format!("cache_same_music_fingerprints_{CACHE_VERSION}.bin") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_simplified_names() { let cases = [ ("roman ( ziemniak ) ", "roman"), (" HH) ", "HH"), (" fsf.f. ", "fsf f"), (" śśśśćććć ", "sssscccc"), ("rr\t", "rr"), ("Kekistan (feat. roman) [Mix on Mix]", "Kekistan"), ("23", "23"), ("23 (random)", "23"), ("(23)", "(23)"), ]; for (input, expected) in cases { let res = get_simplified_name(input); assert_eq!(res, expected, "Input: {input}, Expected: {expected}, Got: {res}"); } } } czkawka_core-10.0.0/src/tools/same_music/mod.rs000064400000000000000000000077221046102023000175560ustar 00000000000000pub mod core; pub mod traits; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use rusty_chromaprint::Configuration; use serde::{Deserialize, Serialize}; use crate::common::model::{CheckingMethod, FileEntry}; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; bitflags! { #[derive(PartialEq, Copy, Clone, Debug)] pub struct MusicSimilarity : u32 { const NONE = 0; const TRACK_TITLE = 0b1; const TRACK_ARTIST = 0b10; const YEAR = 0b100; const LENGTH = 0b1000; const GENRE = 0b10000; const BITRATE = 0b10_0000; } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct MusicEntry { pub size: u64, pub path: PathBuf, pub modified_date: u64, pub fingerprint: Vec, pub track_title: String, pub track_artist: String, pub year: String, pub length: String, pub genre: String, pub bitrate: u32, } impl ResultEntry for MusicEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_music_entry(self) -> MusicEntry { MusicEntry { size: self.size, path: self.path, modified_date: self.modified_date, fingerprint: vec![], track_title: String::new(), track_artist: String::new(), year: String::new(), length: String::new(), genre: String::new(), bitrate: 0, } } } struct GroupedFilesToCheck { pub base_files: Vec, pub files_to_compare: Vec, } #[derive(Default, Clone)] pub struct Info { pub number_of_duplicates: usize, pub number_of_groups: u64, } #[derive(Clone)] pub struct SameMusicParameters { pub music_similarity: MusicSimilarity, pub approximate_comparison: bool, pub check_type: CheckingMethod, pub minimum_segment_duration: f32, pub maximum_difference: f64, pub compare_fingerprints_only_with_similar_titles: bool, } impl SameMusicParameters { pub fn new( music_similarity: MusicSimilarity, approximate_comparison: bool, check_type: CheckingMethod, minimum_segment_duration: f32, maximum_difference: f64, compare_fingerprints_only_with_similar_titles: bool, ) -> Self { assert!(!music_similarity.is_empty()); assert!([CheckingMethod::AudioTags, CheckingMethod::AudioContent].contains(&check_type)); Self { music_similarity, approximate_comparison, check_type, minimum_segment_duration, maximum_difference, compare_fingerprints_only_with_similar_titles, } } } pub struct SameMusic { common_data: CommonToolData, information: Info, music_to_check: BTreeMap, music_entries: Vec, duplicated_music_entries: Vec>, duplicated_music_entries_referenced: Vec<(MusicEntry, Vec)>, hash_preset_config: Configuration, params: SameMusicParameters, } impl SameMusic { pub const fn get_duplicated_music_entries(&self) -> &Vec> { &self.duplicated_music_entries } pub fn get_params(&self) -> &SameMusicParameters { &self.params } pub const fn get_information(&self) -> &Info { &self.information } pub fn get_similar_music_referenced(&self) -> &Vec<(MusicEntry, Vec)> { &self.duplicated_music_entries_referenced } pub fn get_number_of_base_duplicated_files(&self) -> usize { if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced.len() } else { self.duplicated_music_entries.len() } } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } } czkawka_core-10.0.0/src/tools/same_music/traits.rs000064400000000000000000000143011046102023000202740ustar 00000000000000use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::{CheckingMethod, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::*; use crate::tools::same_music::{Info, MusicEntry, SameMusic, SameMusicParameters}; impl AllTraits for SameMusic {} impl Search for SameMusic { #[fun_time(message = "find_same_music", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } match self.params.check_type { CheckingMethod::AudioTags => { if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_for_duplicate_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } } CheckingMethod::AudioContent => { if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.calculate_fingerprint(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_for_duplicate_fingerprints(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } } _ => panic!(), } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for SameMusic { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Found files music - {}", self.music_entries.len()); println!("Found duplicated files music - {}", self.duplicated_music_entries.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SameMusic { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.duplicated_music_entries.is_empty() { writeln!(writer, "{} music files which have similar friends\n\n.", self.duplicated_music_entries.len())?; for vec_file_entry in &self.duplicated_music_entries { writeln!(writer, "Found {} music files which have similar friends", vec_file_entry.len())?; for file_entry in vec_file_entry { write_music_entry(writer, file_entry)?; } writeln!(writer)?; } } else if !self.duplicated_music_entries_referenced.is_empty() { writeln!(writer, "{} music files which have similar friends\n\n.", self.duplicated_music_entries_referenced.len())?; for (file_entry, vec_file_entry) in &self.duplicated_music_entries_referenced { writeln!(writer, "Found {} music files which have similar friends", vec_file_entry.len())?; writeln!(writer)?; write_music_entry(writer, file_entry)?; for file_entry in vec_file_entry { write_music_entry(writer, file_entry)?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar music files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries_referenced, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries, pretty_print) } } } fn write_music_entry(writer: &mut T, file_entry: &MusicEntry) -> std::io::Result<()> { writeln!( writer, "TT: {} - TA: {} - Y: {} - L: {} - G: {} - B: {} - P: \"{}\"", file_entry.track_title, file_entry.track_artist, file_entry.year, file_entry.length, file_entry.genre, file_entry.bitrate, file_entry.path.to_string_lossy() ) } impl CommonData for SameMusic { type Info = Info; type Parameters = SameMusicParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn get_check_method(&self) -> CheckingMethod { self.get_params().check_type } fn found_any_broken_files(&self) -> bool { self.information.number_of_duplicates > 0 } } impl DeletingItems for SameMusic { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.duplicated_music_entries.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } czkawka_core-10.0.0/src/tools/similar_images/core.rs000064400000000000000000001401721046102023000205640ustar 00000000000000use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{mem, panic}; use bk_tree::BKTree; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use image::GenericImageView; use image_hasher::{FilterType, HashAlg, HasherConfig}; use log::{debug, error}; use rayon::prelude::*; use crate::common::cache::{CACHE_IMAGE_VERSION, extract_loaded_cache, load_cache_from_file_generalized_by_path, save_cache_to_file_generalized}; use crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, JXL_IMAGE_EXTENSIONS, RAW_IMAGE_EXTENSIONS}; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode}; use crate::common::image::get_dynamic_image_from_path; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::flc; use crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SIMILAR_VALUES, SimilarImages, SimilarImagesParameters, SimilarityPreset}; impl SimilarImages { pub fn new(params: SimilarImagesParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SimilarImages), information: Default::default(), bktree: BKTree::new(Hamming), similar_vectors: vec![], similar_referenced_vectors: vec![], params, images_to_check: Default::default(), image_hashes: Default::default(), } } #[fun_time(message = "check_for_similar_images", level = "debug")] pub(crate) fn check_for_similar_images(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if cfg!(feature = "heif") { self.common_data .extensions .set_and_validate_allowed_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, JXL_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat()); } else { self.common_data .extensions .set_and_validate_allowed_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, JXL_IMAGE_EXTENSIONS].concat()); } if !self.common_data.extensions.set_any_extensions() { return WorkContinueStatus::Continue; } let result = DirTraversalBuilder::new() .group_by(inode) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.images_to_check = grouped_file_entries .into_par_iter() .flat_map(if self.get_params().ignore_hard_links { |(_, fes)| fes } else { take_1_per_inode }) .map(|fe| { let fe_str = fe.path.to_string_lossy().to_string(); let image_entry = fe.into_images_entry(); (fe_str, image_entry) }) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} image files.", self.images_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "hash_images_load_cache", level = "debug")] fn hash_images_load_cache(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { let loaded_hash_map; let mut records_already_cached: BTreeMap = Default::default(); let mut non_cached_files_to_check: BTreeMap = Default::default(); if self.common_data.use_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_path::( &get_similar_images_cache_file(&self.get_params().hash_size, &self.get_params().hash_alg, &self.get_params().image_filter), self.get_delete_outdated_cache(), &self.images_to_check, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); debug!("hash_images-load_cache - starting calculating diff"); extract_loaded_cache( &loaded_hash_map, mem::take(&mut self.images_to_check), &mut records_already_cached, &mut non_cached_files_to_check, ); debug!( "hash_images_load_cache - completed diff between loaded and prechecked files, {}({}) - non cached, {}({}) - already cached", non_cached_files_to_check.len(), format_size(non_cached_files_to_check.values().map(|e| e.size).sum::(), BINARY), records_already_cached.len(), format_size(records_already_cached.values().map(|e| e.size).sum::(), BINARY), ); } else { loaded_hash_map = Default::default(); mem::swap(&mut self.images_to_check, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } // Cache algorithm: // - Load data from file // - Remove from data to search, already loaded entries from cache(size and modified date must match) // - Check hash of files which doesn't have saved entry // - Join already read hashes with hashes which were read from file // - Join all hashes and save it to file #[fun_time(message = "hash_images", level = "debug")] pub(crate) fn hash_images(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.images_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.hash_images_load_cache(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SimilarImagesCalculatingHashes, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|entry| entry.size).sum(), ); debug!("hash_images - start hashing images"); let (mut vec_file_entry, errors): (Vec, Vec) = non_cached_files_to_check .into_par_iter() .map(|(_s, file_entry)| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let res = self.collect_image_file_entry(file_entry); progress_handler.increase_items(1); progress_handler.increase_size(size); Some(res) }) .while_some() .partition_map(|res| match res { Ok(entry) => itertools::Either::Left(entry), Err(err) => itertools::Either::Right(err), }); self.common_data.text_messages.errors.extend(errors); debug!("hash_images - end hashing {} images", vec_file_entry.len()); progress_handler.join_thread(); // Just connect loaded results with already calculated hashes for file_entry in records_already_cached.into_values() { vec_file_entry.push(file_entry); } // All valid entries are used to create bktree used to check for hash similarity for file_entry in &vec_file_entry { // Only use to comparing, non broken hashes(all 0 or 255 hashes means that algorithm fails to decode them because e.g. contains a lot of alpha channel) if !(file_entry.hash.is_empty() || file_entry.hash.iter().all(|e| *e == 0) || file_entry.hash.iter().all(|e| *e == 255)) { self.image_hashes.entry(file_entry.hash.clone()).or_default().push(file_entry.clone()); } } self.save_to_cache(vec_file_entry, loaded_hash_map); // Break if stop was clicked after saving to cache if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "save_to_cache", level = "debug")] fn save_to_cache(&mut self, vec_file_entry: Vec, loaded_hash_map: BTreeMap) { if self.common_data.use_cache { // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = loaded_hash_map; for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } let messages = save_cache_to_file_generalized( &get_similar_images_cache_file(&self.get_params().hash_size, &self.get_params().hash_alg, &self.get_params().image_filter), &all_results, self.common_data.save_also_as_json, 0, ); self.get_text_messages_mut().extend_with_another_messages(messages); } } fn collect_image_file_entry(&self, mut file_entry: ImagesEntry) -> Result { let img = get_dynamic_image_from_path(&file_entry.path.to_string_lossy())?; let dimensions = img.dimensions(); file_entry.width = dimensions.0; file_entry.height = dimensions.1; let hasher_config = HasherConfig::new() .hash_size(self.get_params().hash_size as u32, self.get_params().hash_size as u32) .hash_alg(self.get_params().hash_alg) .resize_filter(self.get_params().image_filter); let hasher = hasher_config.to_hasher(); let hash = hasher.hash_image(&img); file_entry.hash = hash.as_bytes().to_vec(); Ok(file_entry) } // Split hashes at 2 parts, base hashes and hashes to compare, 3 argument is set of hashes with multiple images #[fun_time(message = "split_hashes", level = "debug")] fn split_hashes(&mut self, all_hashed_images: &HashMap>) -> (Vec, HashSet) { let hashes_with_multiple_images: HashSet = all_hashed_images .iter() .filter_map(|(hash, vec_file_entry)| { if vec_file_entry.len() >= 2 { return Some(hash.clone()); }; None }) .collect(); let mut base_hashes = Vec::new(); // Initial hashes if self.common_data.use_reference_folders { let mut files_from_referenced_folders: HashMap> = HashMap::new(); let mut normal_files: HashMap> = HashMap::new(); all_hashed_images.clone().into_iter().for_each(|(hash, vec_file_entry)| { for file_entry in vec_file_entry { if is_in_reference_folder(&self.common_data.directories.reference_directories, &file_entry.path) { files_from_referenced_folders.entry(hash.clone()).or_default().push(file_entry); } else { normal_files.entry(hash.clone()).or_default().push(file_entry); } } }); for hash in normal_files.into_keys() { self.bktree.add(hash); } for hash in files_from_referenced_folders.into_keys() { base_hashes.push(hash); } } else { for original_hash in all_hashed_images.keys() { self.bktree.add(original_hash.clone()); } base_hashes = all_hashed_images.keys().cloned().collect::>(); } (base_hashes, hashes_with_multiple_images) } #[fun_time(message = "collect_hash_compare_result", level = "debug")] fn collect_hash_compare_result( &self, hashes_parents: HashMap, hashes_with_multiple_images: &HashSet, all_hashed_images: &HashMap>, collected_similar_images: &mut HashMap>, hashes_similarity: HashMap, ) { // Collecting results to vector for (parent_hash, child_number) in hashes_parents { // If hash contains other hasher OR multiple images are available for checked hash if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) { let vec_fe = all_hashed_images[&parent_hash].clone(); collected_similar_images.insert(parent_hash.clone(), vec_fe); } } for (child_hash, (parent_hash, similarity)) in hashes_similarity { let mut vec_fe = all_hashed_images[&child_hash].clone(); for fe in &mut vec_fe { fe.similarity = similarity; } collected_similar_images .get_mut(&parent_hash) .expect("Cannot find parent hash - this should be added in previous step") .append(&mut vec_fe); } } #[fun_time(message = "compare_hashes_with_non_zero_tolerance", level = "debug")] fn compare_hashes_with_non_zero_tolerance( &mut self, all_hashed_images: &HashMap>, collected_similar_images: &mut HashMap>, progress_sender: Option<&Sender>, stop_flag: &Arc, tolerance: u32, ) -> WorkContinueStatus { // Don't use hashes with multiple images in bktree, because they will always be master of group and cannot be find by other hashes let (base_hashes, hashes_with_multiple_images) = self.split_hashes(all_hashed_images); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SimilarImagesComparingHashes, base_hashes.len(), self.get_test_type(), 0); let mut hashes_parents: HashMap = Default::default(); // Hashes used as parent (hash, children_number_of_hash) let mut hashes_similarity: HashMap = Default::default(); // Hashes used as child, (parent_hash, similarity) // Check them in chunks, to decrease number of used memory // println!(); let base_hashes_chunks = base_hashes.chunks(1000); for chunk in base_hashes_chunks { let partial_results = chunk .into_par_iter() .map(|hash_to_check| { progress_handler.increase_items(1); if check_if_stop_received(stop_flag) { return None; } let mut found_items = self .bktree .find(hash_to_check, tolerance) .filter(|(similarity, compared_hash)| { *similarity != 0 && !hashes_parents.contains_key(*compared_hash) && !hashes_with_multiple_images.contains(*compared_hash) }) .filter(|(similarity, compared_hash)| { if let Some((_, other_similarity_with_parent)) = hashes_similarity.get(*compared_hash) { // If current hash is more similar to other hash than to current parent hash, then skip check earlier // Because there is no way to be more similar to other hash than to current parent hash if *similarity >= *other_similarity_with_parent { return false; } } true }) .collect::>(); // Sort by tolerance found_items.sort_unstable_by_key(|f| f.0); Some((hash_to_check, found_items)) }) .while_some() .filter(|(original_hash, vec_similar_hashes)| !vec_similar_hashes.is_empty() || hashes_with_multiple_images.contains(*original_hash)) .collect::>(); // for (hash, vec) in &partial_results { // println!("{hash:?} --- {:?}", vec.iter().map(|e| e.1).collect::>()); // } if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } self.connect_results(partial_results, &mut hashes_parents, &mut hashes_similarity, &hashes_with_multiple_images); } progress_handler.join_thread(); debug_check_for_duplicated_things(self.common_data.use_reference_folders, &hashes_parents, &hashes_similarity, all_hashed_images, "LATTER"); self.collect_hash_compare_result(hashes_parents, &hashes_with_multiple_images, all_hashed_images, collected_similar_images, hashes_similarity); WorkContinueStatus::Continue } #[fun_time(message = "connect_results", level = "debug")] fn connect_results( &self, partial_results: Vec<(&ImHash, Vec<(u32, &ImHash)>)>, hashes_parents: &mut HashMap, hashes_similarity: &mut HashMap, hashes_with_multiple_images: &HashSet, ) { for (original_hash, vec_compared_hashes) in partial_results { let mut number_of_added_child_items = 0; for (similarity, compared_hash) in vec_compared_hashes { // If hash is already in results skip it // This check duplicates check from bktree.find, but it is needed to because when iterating over elements, this structure can change if hashes_parents.contains_key(compared_hash) { continue; } // If there is already record, with smaller sensitivity, then replace it let mut need_to_add = false; let mut need_to_check = false; // TODO consider to replace variables from above with closures // If current checked hash, have parent, first we must check if similarity between them is lower than checked item if let Some((current_parent_hash, current_similarity_with_parent)) = hashes_similarity.get(original_hash) { if *current_similarity_with_parent > similarity { need_to_check = true; *hashes_parents.get_mut(current_parent_hash).expect("Cannot find parent hash") -= 1; if hashes_parents.get(current_parent_hash) == Some(&0) && !hashes_with_multiple_images.contains(current_parent_hash) { hashes_parents.remove(current_parent_hash); } hashes_similarity .remove(original_hash) .expect("This should never fail, because we are iterating over this hash"); } } else { need_to_check = true; } if need_to_check { if let Some((other_parent_hash, other_similarity)) = hashes_similarity.get(compared_hash) { if *other_similarity > similarity { need_to_add = true; *hashes_parents.get_mut(other_parent_hash).expect("Cannot find parent hash") -= 1; } } // But when there is no record, just add it else { need_to_add = true; } } if need_to_add { hashes_similarity.insert(compared_hash.clone(), (original_hash.clone(), similarity)); number_of_added_child_items += 1; } } if number_of_added_child_items > 0 || hashes_with_multiple_images.contains(original_hash) { hashes_parents.insert((*original_hash).clone(), number_of_added_child_items); } } } #[fun_time(message = "find_similar_hashes", level = "debug")] pub(crate) fn find_similar_hashes(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.image_hashes.is_empty() { return WorkContinueStatus::Continue; } let tolerance = self.get_params().similarity; // Results let mut collected_similar_images: HashMap> = Default::default(); let all_hashed_images = mem::take(&mut self.image_hashes); // Checking entries with tolerance 0 is really easy and fast, because only entries with same hashes needs to be checked if tolerance == 0 { for (hash, vec_file_entry) in all_hashed_images { if vec_file_entry.len() >= 2 { collected_similar_images.insert(hash, vec_file_entry); } } } else if self.compare_hashes_with_non_zero_tolerance(&all_hashed_images, &mut collected_similar_images, progress_sender, stop_flag, tolerance) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } self.verify_duplicated_items(&collected_similar_images); // Info about hashes is not needed anymore, so we drop this info self.similar_vectors = collected_similar_images.into_values().collect(); self.exclude_items_with_same_size(); self.remove_multiple_records_from_reference_folders(); if self.common_data.use_reference_folders { for (_fe, vector) in &self.similar_referenced_vectors { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.similar_vectors { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clean unused data to save ram self.image_hashes = Default::default(); self.images_to_check = Default::default(); self.bktree = BKTree::new(Hamming); WorkContinueStatus::Continue } #[fun_time(message = "exclude_items_with_same_size", level = "debug")] fn exclude_items_with_same_size(&mut self) { if self.get_params().exclude_images_with_same_size { for vec_file_entry in mem::take(&mut self.similar_vectors) { let mut bt_sizes: BTreeSet = Default::default(); let mut vec_values = Vec::new(); for file_entry in vec_file_entry { if bt_sizes.insert(file_entry.size) { vec_values.push(file_entry); } } if vec_values.len() > 1 { self.similar_vectors.push(vec_values); } } } } #[fun_time(message = "remove_multiple_records_from_reference_folders", level = "debug")] fn remove_multiple_records_from_reference_folders(&mut self) { if self.common_data.use_reference_folders { self.similar_referenced_vectors = mem::take(&mut self.similar_vectors) .into_iter() .filter_map(|vec_file_entry| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); } } #[allow(unused_variables)] // TODO this probably not works good when reference folders are used pub(crate) fn verify_duplicated_items(&self, collected_similar_images: &HashMap>) { if !cfg!(debug_assertions) { return; } // Validating if group contains duplicated results let mut result_hashset: HashSet = Default::default(); let mut found = false; for vec_file_entry in collected_similar_images.values() { if vec_file_entry.is_empty() { error!("Found empty group"); found = true; continue; } if vec_file_entry.len() == 1 { error!("Found simple element {vec_file_entry:?}"); found = true; continue; } for file_entry in vec_file_entry { let st = file_entry.path.to_string_lossy().to_string(); if result_hashset.contains(&st) { found = true; error!("Duplicated Element {st}"); } else { result_hashset.insert(st); } } } assert!(!found, "Found Invalid entries, verify errors before"); } } fn is_in_reference_folder(reference_directories: &[PathBuf], path: &Path) -> bool { reference_directories.iter().any(|e| path.starts_with(e)) } pub fn get_string_from_similarity(similarity: &u32, hash_size: u8) -> String { let index_preset = match hash_size { 8 => 0, 16 => 1, 32 => 2, 64 => 3, _ => panic!("Invalid hash size {hash_size}"), }; if *similarity == 0 { flc!("core_similarity_original") } else if *similarity <= SIMILAR_VALUES[index_preset][0] { flc!("core_similarity_very_high") } else if *similarity <= SIMILAR_VALUES[index_preset][1] { flc!("core_similarity_high") } else if *similarity <= SIMILAR_VALUES[index_preset][2] { flc!("core_similarity_medium") } else if *similarity <= SIMILAR_VALUES[index_preset][3] { flc!("core_similarity_small") } else if *similarity <= SIMILAR_VALUES[index_preset][4] { flc!("core_similarity_very_small") } else if *similarity <= SIMILAR_VALUES[index_preset][5] { flc!("core_similarity_minimal") } else { panic!("Invalid similarity value {similarity} for hash size {hash_size} (index {index_preset})"); } } pub fn return_similarity_from_similarity_preset(similarity_preset: &SimilarityPreset, hash_size: u8) -> u32 { let index_preset = match hash_size { 8 => 0, 16 => 1, 32 => 2, 64 => 3, _ => panic!(), }; match similarity_preset { SimilarityPreset::Original => 0, SimilarityPreset::VeryHigh => SIMILAR_VALUES[index_preset][0], SimilarityPreset::High => SIMILAR_VALUES[index_preset][1], SimilarityPreset::Medium => SIMILAR_VALUES[index_preset][2], SimilarityPreset::Small => SIMILAR_VALUES[index_preset][3], SimilarityPreset::VerySmall => SIMILAR_VALUES[index_preset][4], SimilarityPreset::Minimal => SIMILAR_VALUES[index_preset][5], SimilarityPreset::None => panic!(""), } } pub(crate) fn convert_filters_to_string(image_filter: &FilterType) -> String { match image_filter { FilterType::Lanczos3 => "Lanczos3", FilterType::Nearest => "Nearest", FilterType::Triangle => "Triangle", FilterType::Gaussian => "Gaussian", FilterType::CatmullRom => "CatmullRom", } .to_string() } pub(crate) fn convert_algorithm_to_string(hash_alg: &HashAlg) -> String { match hash_alg { HashAlg::Mean => "Mean", HashAlg::Gradient => "Gradient", HashAlg::Blockhash => "Blockhash", HashAlg::VertGradient => "VertGradient", HashAlg::DoubleGradient => "DoubleGradient", HashAlg::Median => "Median", } .to_string() } #[allow(dead_code)] #[allow(unreachable_code)] #[allow(unused_variables)] // Function to validate if after first check there are any duplicated entries // E.g. /a.jpg is used also as master and similar image which is forbidden, because may // cause accidentally delete more pictures that user wanted fn debug_check_for_duplicated_things( use_reference_folders: bool, hashes_parents: &HashMap, hashes_similarity: &HashMap, all_hashed_images: &HashMap>, numm: &str, ) { if !cfg!(debug_assertions) { return; } if use_reference_folders { return; } let mut found_broken_thing = false; let mut hashmap_hashes: HashSet<_> = Default::default(); let mut hashmap_names: HashSet<_> = Default::default(); for (hash, number_of_children) in hashes_parents { if *number_of_children > 0 { if hashmap_hashes.contains(hash) { debug!("------1--HASH--{} {:?}", numm, all_hashed_images[hash]); found_broken_thing = true; } hashmap_hashes.insert((*hash).clone()); for i in &all_hashed_images[hash] { let name = i.path.to_string_lossy().to_string(); if hashmap_names.contains(&name) { debug!("------1--NAME--{numm} {name:?}"); found_broken_thing = true; } hashmap_names.insert(name); } } } for hash in hashes_similarity.keys() { if hashmap_hashes.contains(hash) { debug!("------2--HASH--{} {:?}", numm, all_hashed_images[hash]); found_broken_thing = true; } hashmap_hashes.insert((*hash).clone()); for i in &all_hashed_images[hash] { let name = i.path.to_string_lossy().to_string(); if hashmap_names.contains(&name) { debug!("------2--NAME--{numm} {name:?}"); found_broken_thing = true; } hashmap_names.insert(name); } } assert!(!found_broken_thing); } pub fn get_similar_images_cache_file(hash_size: &u8, hash_alg: &HashAlg, image_filter: &FilterType) -> String { format!( "cache_similar_images_{hash_size}_{}_{}_{CACHE_IMAGE_VERSION}.bin", convert_algorithm_to_string(hash_alg), convert_filters_to_string(image_filter), ) } #[cfg(test)] mod tests { use std::collections::HashMap; use std::path::PathBuf; use bk_tree::BKTree; use image::imageops::FilterType; use image_hasher::HashAlg; use super::*; use crate::common::tool_data::CommonData; use crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SimilarImages, SimilarImagesParameters}; fn get_default_parameters() -> SimilarImagesParameters { SimilarImagesParameters { hash_alg: HashAlg::Gradient, hash_size: 8, similarity: 0, image_filter: FilterType::Lanczos3, exclude_images_with_same_size: false, ignore_hard_links: false, } } #[test] fn test_compare_no_images() { use crate::common::traits::Search; for _ in 0..100 { let mut similar_images = SimilarImages::new(get_default_parameters()); similar_images.search(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 0); } } #[test] fn test_compare_tolerance_0_normal_mode() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 0; let mut similar_images = SimilarImages::new(parameters); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "cde.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "rrt.txt"); let fe5 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "bld.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1.clone(), fe2.clone(), fe3.clone(), fe4.clone(), fe5.clone()]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 2); let first_group = similar_images.get_similar_images()[0].iter().map(|e| &e.path).collect::>(); let second_group = similar_images.get_similar_images()[1].iter().map(|e| &e.path).collect::>(); // Initial order is not guaranteed, so we need to check both options if similar_images.get_similar_images()[0][0].hash == fe1.hash { assert_eq!(first_group, vec![&fe1.path, &fe2.path]); assert_eq!(second_group, vec![&fe3.path, &fe4.path, &fe5.path]); } else { assert_eq!(first_group, vec![&fe3.path, &fe4.path, &fe5.path]); assert_eq!(second_group, vec![&fe1.path, &fe2.path]); } } } #[test] fn test_simple_normal_one_group() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 1); } } #[test] fn test_simple_normal_one_group_extended() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 2; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 1); assert_eq!(similar_images.get_similar_images()[0].len(), 3); } } #[test] fn test_simple_referenced_same_group() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 0); } } #[test] fn test_simple_referenced_group_extended() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images_referenced().len(), 1); assert_eq!(similar_images.get_similar_images_referenced()[0].1.len(), 1); } } #[test] fn test_simple_referenced_group_extended2() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc2.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd2.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 2); assert!(res[0].1.iter().all(|e| e.path.starts_with("/home/kk/"))); } } #[test] fn test_simple_normal_too_small_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00001], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00100], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b10000], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images(); assert!(res.is_empty()); } } #[test] fn test_simple_normal_union_of_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 4; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0001], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1111], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0111_1111], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images(); assert_eq!(res.len(), 1); let mut path = res[0].iter().map(|e| e.path.to_string_lossy().to_string()).collect::>(); path.sort(); if res[0].len() == 3 { assert_eq!(path, vec!["abc.txt".to_string(), "bcd.txt".to_string(), "rrd.txt".to_string()]); } else if res[0].len() == 2 { assert!(path == vec!["abc.txt".to_string(), "bcd.txt".to_string()] || path == vec!["bcd.txt".to_string(), "rrd.txt".to_string()]); } else { panic!("Invalid number of items"); } } } #[test] fn test_reference_similarity_only_one() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 1); assert_eq!(res[0].0.path, PathBuf::from("/home/rr/abc.txt")); assert_eq!(res[0].1[0].path, PathBuf::from("/home/kk/bcd.txt")); } } #[test] fn test_reference_too_small_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0010], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 0); } } #[test] fn test_reference_minimal() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], "/home/kk/bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], "/home/kk/bcd2.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], "/home/rr/krkr.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 2); assert_eq!(res[0].1.len(), 1); assert_eq!(res[1].1.len(), 1); if res[0].1[0].path == PathBuf::from("/home/kk/bcd.txt") { assert_eq!(res[0].0.path, PathBuf::from("/home/rr/abc.txt")); assert_eq!(res[1].0.path, PathBuf::from("/home/rr/krkr.txt")); } else if res[0].1[0].path == PathBuf::from("/home/kk/bcd2.txt") { assert_eq!(res[0].0.path, PathBuf::from("/home/rr/krkr.txt")); assert_eq!(res[1].0.path, PathBuf::from("/home/rr/abc.txt")); } } } #[test] fn test_reference_same() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 1); } } #[test] fn test_reference_union() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.similarity = 10; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe0 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1000], "/home/rr/abc2.txt"); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1110], "/home/kk/bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], "/home/kk/bcd2.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], "/home/rr/krkr.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe0, fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 2); assert_eq!(res[0].0.path, PathBuf::from("/home/rr/krkr.txt")); } } #[test] fn test_tolerance() { // This test not really tests anything, but shows that current hamming distance works // in bits instead of bytes // I tried to make it work in bytes, but it was terrible, so Hamming should be really Ok let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 2]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 2); let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 3]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 1); let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0000]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1000]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 1); } fn add_hashes(hashmap: &mut HashMap>, file_entries: Vec) { for fe in file_entries { hashmap.entry(fe.hash.clone()).or_default().push(fe); } } fn create_random_file_entry(hash: Vec, name: &str) -> ImagesEntry { ImagesEntry { path: PathBuf::from(name.to_string()), size: 0, width: 100, height: 100, modified_date: 0, hash, similarity: 0, } } } czkawka_core-10.0.0/src/tools/similar_images/mod.rs000064400000000000000000000072741046102023000204200ustar 00000000000000pub mod core; pub mod traits; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use bk_tree::BKTree; use hamming_bitwise_fast::hamming_bitwise_fast; use image_hasher::{FilterType, HashAlg}; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; type ImHash = Vec; // 40 is, similar like previous 20 in 8 hash size is useless // But since Krowka have problems with proper changing max value in fly // hardcode 40 as max value pub const SIMILAR_VALUES: [[u32; 6]; 4] = [ [1, 2, 5, 7, 14, 40], // 8 [2, 5, 15, 30, 40, 40], // 16 [4, 10, 20, 40, 40, 40], // 32 [6, 20, 40, 40, 40, 40], // 64 ]; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ImagesEntry { pub path: PathBuf, pub size: u64, pub width: u32, pub height: u32, pub modified_date: u64, pub hash: ImHash, pub similarity: u32, } impl ResultEntry for ImagesEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_images_entry(self) -> ImagesEntry { ImagesEntry { size: self.size, path: self.path, modified_date: self.modified_date, width: 0, height: 0, hash: Vec::new(), similarity: 0, } } } #[derive(Clone, Debug, Copy)] pub enum SimilarityPreset { Original, VeryHigh, High, Medium, Small, VerySmall, Minimal, None, } struct Hamming; impl bk_tree::Metric for Hamming { fn distance(&self, a: &ImHash, b: &ImHash) -> u32 { hamming_bitwise_fast(a, b) } fn threshold_distance(&self, a: &ImHash, b: &ImHash, _threshold: u32) -> Option { Some(self.distance(a, b)) } } #[derive(Clone)] pub struct SimilarImagesParameters { pub similarity: u32, pub hash_size: u8, pub hash_alg: HashAlg, pub image_filter: FilterType, pub exclude_images_with_same_size: bool, pub ignore_hard_links: bool, } impl SimilarImagesParameters { pub fn new(similarity: u32, hash_size: u8, hash_alg: HashAlg, image_filter: FilterType, exclude_images_with_same_size: bool, ignore_hard_links: bool) -> Self { assert!([8, 16, 32, 64].contains(&hash_size)); Self { similarity, hash_size, hash_alg, image_filter, exclude_images_with_same_size, ignore_hard_links, } } } pub struct SimilarImages { common_data: CommonToolData, information: Info, bktree: BKTree, similar_vectors: Vec>, similar_referenced_vectors: Vec<(ImagesEntry, Vec)>, // Hashmap with image hashes and Vector with names of files image_hashes: HashMap>, images_to_check: BTreeMap, params: SimilarImagesParameters, } #[derive(Default, Clone)] pub struct Info { pub number_of_duplicates: usize, pub number_of_groups: u64, } impl SimilarImages { pub fn get_params(&self) -> &SimilarImagesParameters { &self.params } pub const fn get_similar_images(&self) -> &Vec> { &self.similar_vectors } pub fn get_similar_images_referenced(&self) -> &Vec<(ImagesEntry, Vec)> { &self.similar_referenced_vectors } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } pub const fn get_information(&self) -> &Info { &self.information } } czkawka_core-10.0.0/src/tools/similar_images/traits.rs000064400000000000000000000134761046102023000211500ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::similar_images::core::get_string_from_similarity; use crate::tools::similar_images::{Info, SimilarImages, SimilarImagesParameters}; impl AllTraits for SimilarImages {} impl Search for SimilarImages { #[fun_time(message = "find_similar_images", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty(); if self.check_for_similar_images(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.hash_images(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.find_similar_hashes(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DebugPrint for SimilarImages { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SimilarImages { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.similar_vectors.is_empty() { write!(writer, "{} images which have similar friends\n\n", self.similar_vectors.len())?; for struct_similar in &self.similar_vectors { writeln!(writer, "Found {} images which have similar friends", struct_similar.len())?; for file_entry in struct_similar { writeln!( writer, "\"{}\" - {}x{} - {} - {}", file_entry.path.to_string_lossy(), file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(&file_entry.similarity, self.get_params().hash_size) )?; } writeln!(writer)?; } } else if !self.similar_referenced_vectors.is_empty() { writeln!(writer, "{} images which have similar friends\n\n", self.similar_referenced_vectors.len())?; for (file_entry, vec_file_entry) in &self.similar_referenced_vectors { writeln!(writer, "Found {} images which have similar friends", vec_file_entry.len())?; writeln!(writer)?; writeln!( writer, "\"{}\" - {}x{} - {} - {}", file_entry.path.to_string_lossy(), file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(&file_entry.similarity, self.get_params().hash_size) )?; for file_entry in vec_file_entry { writeln!( writer, "{:?} - {}x{} - {} - {}", file_entry.path, file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(&file_entry.similarity, self.get_params().hash_size) )?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar images.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print) } } } impl CommonData for SimilarImages { type Info = Info; type Parameters = SimilarImagesParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_duplicates > 0 } } impl DeletingItems for SimilarImages { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.similar_vectors.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } czkawka_core-10.0.0/src/tools/similar_videos/core.rs000064400000000000000000000276011046102023000206110ustar 00000000000000use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::mem; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use vid_dup_finder_lib::{CreationOptions, Cropdetect, VideoHash, VideoHashBuilder}; use crate::common::cache::{CACHE_VIDEO_VERSION, extract_loaded_cache, load_cache_from_file_generalized_by_path, save_cache_to_file_generalized}; use crate::common::consts::VIDEO_FILES_EXTENSIONS; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::tools::similar_videos::{SimilarVideos, SimilarVideosParameters, VideosEntry}; impl SimilarVideos { pub fn new(params: SimilarVideosParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SimilarVideos), information: Default::default(), similar_vectors: vec![], videos_hashes: Default::default(), videos_to_check: Default::default(), similar_referenced_vectors: vec![], params, } } #[fun_time(message = "check_for_similar_videos", level = "debug")] pub(crate) fn check_for_similar_videos(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { self.common_data.extensions.set_and_validate_allowed_extensions(VIDEO_FILES_EXTENSIONS); if !self.common_data.extensions.set_any_extensions() { return WorkContinueStatus::Continue; } let result = DirTraversalBuilder::new() .group_by(inode) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.videos_to_check = grouped_file_entries .into_par_iter() .flat_map(if self.get_params().ignore_hard_links { |(_, fes)| fes } else { take_1_per_inode }) .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_videos_entry())) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} video files.", self.videos_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "load_cache_at_start", level = "debug")] fn load_cache_at_start(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { let loaded_hash_map; let mut records_already_cached: BTreeMap = Default::default(); let mut non_cached_files_to_check: BTreeMap = Default::default(); if self.common_data.use_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_path::( &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect), self.get_delete_outdated_cache(), &self.videos_to_check, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); extract_loaded_cache( &loaded_hash_map, mem::take(&mut self.videos_to_check), &mut records_already_cached, &mut non_cached_files_to_check, ); } else { loaded_hash_map = Default::default(); mem::swap(&mut self.videos_to_check, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } fn check_video_file_entry(&self, mut file_entry: VideosEntry) -> VideosEntry { let creation_options = CreationOptions { skip_forward_amount: self.params.skip_forward_amount as f64, duration: self.params.duration as f64, cropdetect: self.params.crop_detect, }; let vhash = match VideoHashBuilder::from_options(creation_options).hash(file_entry.path.clone()) { Ok(t) => t, Err(e) => { let path = file_entry.path.to_string_lossy(); file_entry.error = format!("Failed to hash file {path}: reason {e}"); return file_entry; } }; file_entry.vhash = vhash; file_entry } #[fun_time(message = "sort_videos", level = "debug")] pub(crate) fn sort_videos(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.videos_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache_at_start(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SimilarVideosCalculatingHashes, non_cached_files_to_check.len(), self.get_test_type(), 0, // non_cached_files_to_check.values().map(|e| e.size).sum(), // Looks, that at least for now, there is no big difference between checking big and small files, so at least for now, only tracking number of files is enough ); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .map(|(_, file_entry)| { if check_if_stop_received(stop_flag) { return None; } // Currently size is not too much relevant // let size = file_entry.size; let res = self.check_video_file_entry(file_entry); progress_handler.increase_items(1); // progress_handler.increase_size(size); Some(res) }) .while_some() .collect::>(); progress_handler.join_thread(); // Just connect loaded results with already calculated hashes vec_file_entry.extend(records_already_cached.into_values()); let mut hashmap_with_file_entries: HashMap = Default::default(); let mut vector_of_hashes: Vec = Vec::new(); for file_entry in &vec_file_entry { // 0 means that images was not hashed correctly, e.g. could be improperly if file_entry.error.is_empty() { hashmap_with_file_entries.insert(file_entry.vhash.src_path().to_string_lossy().to_string(), file_entry.clone()); vector_of_hashes.push(file_entry.vhash.clone()); } else { self.common_data.text_messages.warnings.push(file_entry.error.clone()); } } self.save_cache(vec_file_entry, loaded_hash_map); // Break if stop was clicked after saving to cache if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } self.match_groups_of_videos(vector_of_hashes, &hashmap_with_file_entries); self.remove_from_reference_folders(); if self.common_data.use_reference_folders { for (_fe, vector) in &self.similar_referenced_vectors { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.similar_vectors { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clean unused data self.videos_hashes = Default::default(); self.videos_to_check = Default::default(); WorkContinueStatus::Continue } #[fun_time(message = "save_cache", level = "debug")] fn save_cache(&mut self, vec_file_entry: Vec, loaded_hash_map: BTreeMap) { if self.common_data.use_cache { // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = loaded_hash_map; for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } let messages = save_cache_to_file_generalized( &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect), &all_results, self.common_data.save_also_as_json, 0, ); self.get_text_messages_mut().extend_with_another_messages(messages); } } #[fun_time(message = "match_groups_of_videos", level = "debug")] fn match_groups_of_videos(&mut self, vector_of_hashes: Vec, hashmap_with_file_entries: &HashMap) { // Tolerance in library is a value between 0 and 1 // Tolerance in this app is a value between 0 and 20 // Default tolerance in library is 0.30 // We need to allow to set value in range 0 - 0.5 let match_group = vid_dup_finder_lib::search(vector_of_hashes, self.get_params().tolerance as f64 / 40.0f64); let mut collected_similar_videos: Vec> = Default::default(); for i in match_group { let mut temp_vector: Vec = Vec::new(); let mut bt_size: BTreeSet = Default::default(); for j in i.duplicates() { let file_entry = &hashmap_with_file_entries[&j.to_string_lossy().to_string()]; if self.get_params().exclude_videos_with_same_size { if bt_size.insert(file_entry.size) { temp_vector.push(file_entry.clone()); } } else { temp_vector.push(file_entry.clone()); } } if temp_vector.len() > 1 { collected_similar_videos.push(temp_vector); } } self.similar_vectors = collected_similar_videos; } #[fun_time(message = "remove_from_reference_folders", level = "debug")] fn remove_from_reference_folders(&mut self) { if self.common_data.use_reference_folders { self.similar_referenced_vectors = mem::take(&mut self.similar_vectors) .into_iter() .filter_map(|vec_file_entry| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); } } } pub fn get_similar_videos_cache_file(skip_forward_amount: u32, duration: u32, crop_detect: Cropdetect) -> String { let crop_detect_str = match crop_detect { Cropdetect::None => "none", Cropdetect::Letterbox => "letterbox", Cropdetect::Motion => "motion", }; format!("cache_similar_videos_{CACHE_VIDEO_VERSION}__skip_{skip_forward_amount}__dur_{duration}__cd_{crop_detect_str}.bin") } czkawka_core-10.0.0/src/tools/similar_videos/mod.rs000064400000000000000000000100361046102023000204320ustar 00000000000000pub mod core; pub mod traits; use std::collections::BTreeMap; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use vid_dup_finder_lib::{Cropdetect, VideoHash}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; pub const MAX_TOLERANCE: i32 = 20; pub const DEFAULT_CROP_DETECT: Cropdetect = Cropdetect::Letterbox; pub const ALLOWED_SKIP_FORWARD_AMOUNT: RangeInclusive = 0..=300; pub const DEFAULT_SKIP_FORWARD_AMOUNT: u32 = 15; pub const ALLOWED_VID_HASH_DURATION: RangeInclusive = 2..=60; pub const DEFAULT_VID_HASH_DURATION: u32 = 10; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VideosEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub vhash: VideoHash, pub error: String, } impl ResultEntry for VideosEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_videos_entry(self) -> VideosEntry { VideosEntry { size: self.size, path: self.path, modified_date: self.modified_date, vhash: Default::default(), error: String::new(), } } } #[derive(Clone)] pub struct SimilarVideosParameters { pub tolerance: i32, pub exclude_videos_with_same_size: bool, pub ignore_hard_links: bool, pub skip_forward_amount: u32, pub duration: u32, pub crop_detect: Cropdetect, } pub fn crop_detect_from_str_opt(s: &str) -> Option { match s.to_lowercase().as_str() { "none" => Some(Cropdetect::None), "letterbox" => Some(Cropdetect::Letterbox), "motion" => Some(Cropdetect::Motion), _ => None, } } pub fn crop_detect_from_str(s: &str) -> Cropdetect { crop_detect_from_str_opt(s).unwrap_or(DEFAULT_CROP_DETECT) } pub fn crop_detect_to_str(crop_detect: Cropdetect) -> String { match crop_detect { Cropdetect::None => "none".to_string(), Cropdetect::Letterbox => "letterbox".to_string(), Cropdetect::Motion => "motion".to_string(), } } impl SimilarVideosParameters { pub fn new(tolerance: i32, exclude_videos_with_same_size: bool, ignore_hard_links: bool, skip_forward_amount: u32, duration: u32, crop_detect: Cropdetect) -> Self { assert!((0..=MAX_TOLERANCE).contains(&tolerance)); assert!(ALLOWED_SKIP_FORWARD_AMOUNT.contains(&skip_forward_amount)); assert!(ALLOWED_VID_HASH_DURATION.contains(&duration)); Self { tolerance, exclude_videos_with_same_size, ignore_hard_links, skip_forward_amount, duration, crop_detect, } } } pub struct SimilarVideos { common_data: CommonToolData, information: Info, similar_vectors: Vec>, similar_referenced_vectors: Vec<(VideosEntry, Vec)>, videos_hashes: BTreeMap, Vec>, videos_to_check: BTreeMap, params: SimilarVideosParameters, } #[derive(Default, Clone)] pub struct Info { pub number_of_duplicates: usize, pub number_of_groups: u64, } impl SimilarVideos { pub fn get_params(&self) -> &SimilarVideosParameters { &self.params } pub const fn get_similar_videos(&self) -> &Vec> { &self.similar_vectors } pub const fn get_information(&self) -> &Info { &self.information } pub fn get_similar_videos_referenced(&self) -> &Vec<(VideosEntry, Vec)> { &self.similar_referenced_vectors } pub fn get_number_of_base_duplicated_files(&self) -> usize { if self.common_data.use_reference_folders { self.similar_referenced_vectors.len() } else { self.similar_vectors.len() } } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } } czkawka_core-10.0.0/src/tools/similar_videos/traits.rs000064400000000000000000000121771046102023000211710ustar 00000000000000use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::flc; use crate::tools::similar_videos::{Info, SimilarVideos, SimilarVideosParameters}; impl AllTraits for SimilarVideos {} impl Search for SimilarVideos { #[fun_time(message = "find_similar_videos", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { if !ffmpeg_cmdline_utils::ffmpeg_and_ffprobe_are_callable() { self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found")); #[cfg(target_os = "windows")] self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found_windows")); } else { self.prepare_items(); self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty(); if self.check_for_similar_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.sort_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DeletingItems for SimilarVideos { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.similar_vectors.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } impl DebugPrint for SimilarVideos { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Included directories - {:?}", self.common_data.directories.included_directories); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SimilarVideos { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.similar_vectors.is_empty() { write!(writer, "{} videos which have similar friends\n\n", self.similar_vectors.len())?; for struct_similar in &self.similar_vectors { writeln!(writer, "Found {} videos which have similar friends", struct_similar.len())?; for file_entry in struct_similar { writeln!(writer, "\"{}\" - {}", file_entry.path.to_string_lossy(), format_size(file_entry.size, BINARY))?; } writeln!(writer)?; } } else if !self.similar_referenced_vectors.is_empty() { write!(writer, "{} videos which have similar friends\n\n", self.similar_referenced_vectors.len())?; for (fe, struct_similar) in &self.similar_referenced_vectors { writeln!(writer, "Found {} videos which have similar friends", struct_similar.len())?; writeln!(writer)?; writeln!(writer, "\"{}\" - {}", fe.path.to_string_lossy(), format_size(fe.size, BINARY))?; for file_entry in struct_similar { writeln!(writer, "\"{}\" - {}", file_entry.path.to_string_lossy(), format_size(file_entry.size, BINARY))?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar videos.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print) } } } impl CommonData for SimilarVideos { type Info = Info; type Parameters = SimilarVideosParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_duplicates > 0 } } czkawka_core-10.0.0/src/tools/temporary/core.rs000064400000000000000000000132701046102023000176170ustar 00000000000000use std::fs::DirEntry; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crossbeam_channel::Sender; use fun_time::fun_time; use rayon::prelude::*; use crate::common::dir_traversal::{common_read_dir, get_modified_time}; use crate::common::directories::Directories; use crate::common::items::ExcludedItems; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::temporary::{Info, TEMP_EXTENSIONS, Temporary, TemporaryFileEntry}; impl Temporary { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::TemporaryFiles), information: Info::default(), temporary_files: vec![], } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let mut folders_to_check: Vec = self.common_data.directories.included_directories.clone(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0); while !folders_to_check.is_empty() { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let segments: Vec<_> = folders_to_check .into_par_iter() .map(|current_folder| { let mut dir_result = vec![]; let mut warnings = vec![]; let mut fe_result = vec![]; let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return (dir_result, warnings, fe_result); }; // Check every sub folder/file/link etc. for entry in read_dir { let Ok(entry_data) = entry else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue; }; if file_type.is_dir() { check_folder_children( &mut dir_result, &mut warnings, &entry_data, self.common_data.recursive_search, &self.common_data.directories, &self.common_data.excluded_items, ); } else if file_type.is_file() { if let Some(file_entry) = self.get_file_entry(progress_handler.items_counter(), &entry_data, &mut warnings) { fe_result.push(file_entry); } } } (dir_result, warnings, fe_result) }) .collect(); let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, fe_result) in segments { folders_to_check.extend(segment); self.common_data.text_messages.warnings.extend(warnings); for fe in fe_result { self.temporary_files.push(fe); } } } progress_handler.join_thread(); self.information.number_of_temporary_files = self.temporary_files.len(); WorkContinueStatus::Continue } pub(crate) fn get_file_entry(&self, items_counter: &Arc, entry_data: &DirEntry, warnings: &mut Vec) -> Option { items_counter.fetch_add(1, Ordering::Relaxed); let current_file_name = entry_data.path(); if self.common_data.excluded_items.is_excluded(¤t_file_name) { return None; } let file_name = entry_data.file_name(); let file_name_ascii_lowercase = file_name.to_ascii_lowercase(); let file_name_lowercase = file_name_ascii_lowercase.to_string_lossy(); if !TEMP_EXTENSIONS.iter().any(|f| file_name_lowercase.ends_with(f)) { return None; } let Ok(metadata) = entry_data.metadata() else { return None; }; // Creating new file entry Some(TemporaryFileEntry { modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), size: metadata.len(), path: current_file_name, }) } } pub(crate) fn check_folder_children( dir_result: &mut Vec, warnings: &mut Vec, entry_data: &DirEntry, recursive_search: bool, directories: &Directories, excluded_items: &ExcludedItems, ) { if !recursive_search { return; } let next_item = entry_data.path(); if directories.is_excluded(&next_item) { return; } if excluded_items.is_excluded(&next_item) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&next_item) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } dir_result.push(next_item); } czkawka_core-10.0.0/src/tools/temporary/mod.rs000064400000000000000000000023711046102023000174460ustar 00000000000000pub mod core; pub mod traits; use std::path::{Path, PathBuf}; use serde::Serialize; use crate::common::tool_data::CommonToolData; use crate::common::traits::*; const TEMP_EXTENSIONS: &[&str] = &[ "#", "thumbs.db", ".bak", "~", ".tmp", ".temp", ".ds_store", ".crdownload", ".part", ".cache", ".dmp", ".download", ".partial", ]; #[derive(Clone, Serialize, Debug)] pub struct TemporaryFileEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, } impl ResultEntry for TemporaryFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(Default, Clone)] pub struct Info { pub number_of_temporary_files: usize, } pub struct Temporary { common_data: CommonToolData, information: Info, temporary_files: Vec, } impl Default for Temporary { fn default() -> Self { Self::new() } } impl Temporary { pub const fn get_temporary_files(&self) -> &Vec { &self.temporary_files } pub const fn get_information(&self) -> &Info { &self.information } } czkawka_core-10.0.0/src/tools/temporary/traits.rs000064400000000000000000000063651046102023000202040ustar 00000000000000use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::*; use crate::tools::temporary::{Info, Temporary}; impl AllTraits for Temporary {} impl Search for Temporary { #[fun_time(message = "find_temporary_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { self.prepare_items(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; }; self.debug_print(); } } impl DeletingItems for Temporary { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.temporary_files.clone(); self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(files_to_delete)) } } impl PrintResults for Temporary { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { writeln!( writer, "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", self.common_data.directories.included_directories, self.common_data.directories.excluded_directories, self.common_data.excluded_items.get_excluded_items() )?; writeln!(writer, "Found {} temporary files.\n", self.information.number_of_temporary_files)?; for file_entry in &self.temporary_files { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.temporary_files, pretty_print) } } impl CommonData for Temporary { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_broken_files(&self) -> bool { self.information.number_of_temporary_files > 0 } } impl DebugPrint for Temporary { #[allow(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("### Information's"); println!("Temporary list size - {}", self.temporary_files.len()); self.debug_print_common(); } }