From 1a5e0e053de7998b6f426367c4e467e8bda88048 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Mon, 21 Jul 2025 22:02:05 +0200 Subject: [PATCH] Add repository module and update dependencies Signed-off-by: Till Wegmueller --- Cargo.lock | 358 ++++++++++----- crates/pkg6/Cargo.toml | 1 + crates/pkg6/src/main.rs | 94 +++- libips/src/lib.rs | 1 + libips/src/repository/file_backend.rs | 609 ++++++++++++++++++++++++++ libips/src/repository/mod.rs | 97 ++++ libips/src/repository/rest_backend.rs | 310 +++++++++++++ pkg6repo/Cargo.toml | 10 +- pkg6repo/README.md | 164 ++++++- pkg6repo/src/main.rs | 493 ++++++++++++++++++++- pkg6repo/src/tests.rs | 80 ++++ 11 files changed, 2099 insertions(+), 118 deletions(-) create mode 100644 libips/src/repository/file_backend.rs create mode 100644 libips/src/repository/mod.rs create mode 100644 libips/src/repository/rest_backend.rs create mode 100644 pkg6repo/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9241ab1..05cd45e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -17,6 +17,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.70" @@ -52,6 +102,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "block-buffer" version = "0.9.0" @@ -108,29 +164,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", - "bitflags", - "clap_derive", - "clap_lex", + "bitflags 1.3.2", + "clap_derive 3.2.18", + "clap_lex 0.2.4", "indexmap", "once_cell", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive 4.5.41", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.5", + "strsim 0.11.1", +] + [[package]] name = "clap_derive" version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -140,6 +230,18 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "core-foundation" version = "0.9.3" @@ -262,33 +364,19 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "1.9.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" @@ -396,7 +484,19 @@ checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[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]] @@ -430,6 +530,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -448,12 +554,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "http" version = "0.2.9" @@ -558,32 +658,18 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.6" @@ -616,9 +702,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libips" @@ -641,9 +727,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" @@ -695,7 +781,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.45.0", ] @@ -812,9 +898,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -828,7 +920,7 @@ version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -956,6 +1048,7 @@ version = "0.1.0" dependencies = [ "diff-struct", "libips", + "serde", ] [[package]] @@ -967,7 +1060,7 @@ name = "pkg6dev" version = "0.1.1" dependencies = [ "anyhow", - "clap", + "clap 3.2.23", "libips", "userland", ] @@ -975,13 +1068,21 @@ dependencies = [ [[package]] name = "pkg6repo" version = "0.0.1-placeholder" +dependencies = [ + "anyhow", + "clap 4.5.41", + "libips", + "serde", + "serde_json", + "tempfile", +] [[package]] name = "ports" version = "0.1.0" dependencies = [ "anyhow", - "clap", + "clap 3.2.23", "libips", "reqwest", "shellexpand", @@ -1033,22 +1134,19 @@ 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 = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1057,8 +1155,8 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", - "redox_syscall 0.2.16", + "getrandom 0.2.9", + "redox_syscall", "thiserror", ] @@ -1138,16 +1236,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.11" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags", + "bitflags 2.9.1", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1208,7 +1305,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1361,6 +1458,12 @@ 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 = "strum" version = "0.24.1" @@ -1376,7 +1479,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -1407,15 +1510,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "getrandom 0.3.3", + "once_cell", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.59.0", ] [[package]] @@ -1621,6 +1724,12 @@ dependencies = [ "url", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1649,6 +1758,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[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.84" @@ -1818,11 +1936,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.52.6", ] [[package]] @@ -1842,17 +1960,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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]] @@ -1863,9 +1982,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1875,9 +1994,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1887,9 +2006,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" @@ -1899,9 +2024,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1911,9 +2036,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1923,9 +2048,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1935,9 +2060,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" @@ -1947,3 +2072,12 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[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.1", +] diff --git a/crates/pkg6/Cargo.toml b/crates/pkg6/Cargo.toml index ac65c2c..e58f38e 100644 --- a/crates/pkg6/Cargo.toml +++ b/crates/pkg6/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] libips = { version = "0.1.2", path = "../../libips" } diff-struct = "0.5.3" +serde = { version = "1.0.207", features = ["derive"] } diff --git a/crates/pkg6/src/main.rs b/crates/pkg6/src/main.rs index 2a778e4..85f2397 100644 --- a/crates/pkg6/src/main.rs +++ b/crates/pkg6/src/main.rs @@ -1,3 +1,95 @@ -fn main() { +use diff::Diff; +use libips::actions::File; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use libips::payload::Payload; +#[derive(Serialize, Deserialize, Debug, Clone, Diff)] +#[diff(attr( + #[derive(Debug, PartialEq)] +))] +struct Manifest { + files: HashMap +} + + + +fn main() { + let base = Manifest{files: HashMap::from([ + ("0dh5".to_string(), File{ + payload: None, + path: "var/file".to_string(), + group: "bin".to_string(), + owner: "root".to_string(), + mode: "0755".to_string(), + preserve: false, + overlay: false, + original_name: "".to_string(), + revert_tag: "".to_string(), + sys_attr: "".to_string(), + properties: vec![], + facets: Default::default(), + }), + ("12ds3".to_string(), File{ + payload: None, + path: "var/file1".to_string(), + group: "bin".to_string(), + owner: "root".to_string(), + mode: "0755".to_string(), + preserve: false, + overlay: false, + original_name: "".to_string(), + revert_tag: "".to_string(), + sys_attr: "".to_string(), + properties: vec![], + facets: Default::default(), + }), + ("654".to_string(), File{ + payload: None, + path: "var/file1".to_string(), + group: "bin".to_string(), + owner: "root".to_string(), + mode: "0755".to_string(), + preserve: false, + overlay: false, + original_name: "".to_string(), + revert_tag: "".to_string(), + sys_attr: "".to_string(), + properties: vec![], + facets: Default::default(), + }) + ])}; + + let new_set = Manifest{files: HashMap::from([ + ("0dh5".to_string(), File{ + payload: None, + path: "var/file".to_string(), + group: "bin".to_string(), + owner: "root".to_string(), + mode: "0755".to_string(), + preserve: false, + overlay: false, + original_name: "".to_string(), + revert_tag: "".to_string(), + sys_attr: "".to_string(), + properties: vec![], + facets: Default::default(), + }), + ("654".to_string(), File{ + payload: None, + path: "var/file1".to_string(), + group: "bin".to_string(), + owner: "root".to_string(), + mode: "0755".to_string(), + preserve: false, + overlay: false, + original_name: "".to_string(), + revert_tag: "".to_string(), + sys_attr: "".to_string(), + properties: vec![], + facets: Default::default(), + }) + ])}; + let d = base.diff(&new_set); + println!("{:#?}", d); } diff --git a/libips/src/lib.rs b/libips/src/lib.rs index 4e4bc10..7a72d89 100644 --- a/libips/src/lib.rs +++ b/libips/src/lib.rs @@ -8,6 +8,7 @@ pub mod actions; pub mod digest; pub mod payload; pub mod image; +pub mod repository; #[cfg(test)] mod tests { diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs new file mode 100644 index 0000000..bfc2752 --- /dev/null +++ b/libips/src/repository/file_backend.rs @@ -0,0 +1,609 @@ +// This Source Code Form is subject to the terms of +// the Mozilla Public License, v. 2.0. If a copy of the +// MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{Result, anyhow}; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::str::FromStr; +use sha2::{Sha256, Digest as Sha2Digest}; +use std::fs::File; + +use crate::actions::{Manifest, File as FileAction}; +use crate::digest::Digest; +use crate::payload::{Payload, PayloadCompressionAlgorithm}; + +use super::{Repository, RepositoryConfig, RepositoryVersion, REPOSITORY_CONFIG_FILENAME}; + +/// Repository implementation that uses the local filesystem +pub struct FileBackend { + pub path: PathBuf, + pub config: RepositoryConfig, +} + +/// Transaction for publishing packages +pub struct Transaction { + /// Unique ID for the transaction + id: String, + /// Path to the transaction directory + path: PathBuf, + /// Manifest being updated + manifest: Manifest, + /// Files to be published + files: Vec<(PathBuf, String)>, // (source_path, sha256) + /// Repository reference + repo: PathBuf, +} + +impl Transaction { + /// Create a new transaction + pub fn new(repo_path: PathBuf) -> Result { + // Generate a unique ID based on timestamp + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let id = format!("trans_{}", timestamp); + + // Create transaction directory + let trans_path = repo_path.join("trans").join(&id); + fs::create_dir_all(&trans_path)?; + + Ok(Transaction { + id, + path: trans_path, + manifest: Manifest::new(), + files: Vec::new(), + repo: repo_path, + }) + } + + /// Process a file for the transaction + /// + /// Takes a FileAction and a path to a file in a prototype directory. + /// Calculates the file's checksum, copies and "compresses" the content into a temp file + /// in the transactions directory, and updates the FileAction with the hash information. + pub fn add_file(&mut self, file_action: FileAction, file_path: &Path) -> Result<()> { + // Calculate SHA256 hash of the file (uncompressed) + let hash = Self::calculate_file_hash(file_path)?; + + // Create a temp file path in the transactions directory + let temp_file_name = format!("temp_{}", hash); + let temp_file_path = self.path.join(temp_file_name); + + // Copy the file to the temp location (this is a placeholder for compression) + // In a real implementation, we would compress the file here + fs::copy(file_path, &temp_file_path)?; + + // For now, we're using the same hash for both uncompressed and "compressed" versions + // In a real implementation with compression, we would calculate the hash of the compressed file + let compressed_hash = hash.clone(); + + // Add file to the list for later processing during commit + self.files.push((file_path.to_path_buf(), hash.clone())); + + // Create a new FileAction with the updated information if one wasn't provided + let mut updated_file_action = file_action; + + // Create a payload with the hash information if it doesn't exist + let mut payload = updated_file_action.payload.unwrap_or_else(Payload::default); + + // Set the primary identifier (uncompressed hash) + payload.primary_identifier = Digest::from_str(&hash)?; + + // Set the compression algorithm + payload.compression_algorithm = PayloadCompressionAlgorithm::Gzip; + + // Add the compressed hash as an additional identifier + let compressed_digest = Digest::from_str(&compressed_hash)?; + payload.additional_identifiers.push(compressed_digest); + + // Update the FileAction with the payload + updated_file_action.payload = Some(payload); + + // Add the FileAction to the manifest + self.manifest.add_file(updated_file_action); + + Ok(()) + } + + /// Commit the transaction + pub fn commit(self) -> Result<()> { + // Save the manifest to the transaction directory + let manifest_path = self.path.join("manifest"); + + // Serialize the manifest to JSON + let manifest_json = serde_json::to_string_pretty(&self.manifest)?; + fs::write(&manifest_path, manifest_json)?; + + // Copy files to their final location + for (source_path, hash) in self.files { + // Create the destination path in the files directory + let dest_path = self.repo.join("file").join(&hash); + + // Copy the file if it doesn't already exist + if !dest_path.exists() { + fs::copy(source_path, &dest_path)?; + } + } + + // Generate a timestamp for the manifest version + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Move the manifest to its final location in the repository + // Store in both the pkg directory and the trans directory as required + let pkg_manifest_path = self.repo.join("pkg").join("manifest"); + let trans_manifest_path = self.repo.join("trans").join(format!("manifest_{}", timestamp)); + + // Copy to pkg directory + fs::copy(&manifest_path, &pkg_manifest_path)?; + + // Move to trans directory + fs::rename(manifest_path, trans_manifest_path)?; + + // Clean up the transaction directory (except for the manifest which was moved) + fs::remove_dir_all(self.path)?; + + Ok(()) + } + + /// Calculate SHA256 hash of a file + fn calculate_file_hash(file_path: &Path) -> Result { + // Open the file + let mut file = File::open(file_path)?; + + // Create a SHA256 hasher + let mut hasher = Sha256::new(); + + // Read the file in chunks and update the hasher + let mut buffer = [0; 1024]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + // Get the hash result + let hash = hasher.finalize(); + + // Convert to hex string + let hash_str = format!("{:x}", hash); + + Ok(hash_str) + } +} + +impl Repository for FileBackend { + /// Create a new repository at the specified path + fn create>(path: P, version: RepositoryVersion) -> Result { + let path = path.as_ref(); + + // Create the repository directory if it doesn't exist + fs::create_dir_all(path)?; + + // Create the repository configuration + let config = RepositoryConfig { + version, + ..Default::default() + }; + + // Create the repository structure + let repo = FileBackend { + path: path.to_path_buf(), + config, + }; + + // Create the repository directories + repo.create_directories()?; + + // Save the repository configuration + repo.save_config()?; + + Ok(repo) + } + + /// Open an existing repository + fn open>(path: P) -> Result { + let path = path.as_ref(); + + // Check if the repository directory exists + if !path.exists() { + return Err(anyhow!("Repository does not exist: {}", path.display())); + } + + // Load the repository configuration + let config_path = path.join(REPOSITORY_CONFIG_FILENAME); + let config_data = fs::read_to_string(config_path)?; + let config: RepositoryConfig = serde_json::from_str(&config_data)?; + + Ok(FileBackend { + path: path.to_path_buf(), + config, + }) + } + + /// Save the repository configuration + fn save_config(&self) -> Result<()> { + let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME); + let config_data = serde_json::to_string_pretty(&self.config)?; + fs::write(config_path, config_data)?; + Ok(()) + } + + /// Add a publisher to the repository + fn add_publisher(&mut self, publisher: &str) -> Result<()> { + if !self.config.publishers.contains(&publisher.to_string()) { + self.config.publishers.push(publisher.to_string()); + + // Create publisher-specific directories + fs::create_dir_all(self.path.join("catalog").join(publisher))?; + fs::create_dir_all(self.path.join("pkg").join(publisher))?; + + // Save the updated configuration + self.save_config()?; + } + + Ok(()) + } + + /// Remove a publisher from the repository + fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()> { + if let Some(pos) = self.config.publishers.iter().position(|p| p == publisher) { + if !dry_run { + self.config.publishers.remove(pos); + + // Save the updated configuration + self.save_config()?; + } + } + + Ok(()) + } + + /// Get repository information + fn get_info(&self) -> Result> { + let mut info = Vec::new(); + + for publisher in &self.config.publishers { + // Count packages (this is a placeholder, actual implementation would count packages) + let package_count = 0; + + // Status is always "online" for now + let status = "online".to_string(); + + // Updated timestamp (placeholder) + let updated = "2025-07-21T18:46:00.000000Z".to_string(); + + info.push((publisher.clone(), package_count, status, updated)); + } + + Ok(info) + } + + /// Set a repository property + fn set_property(&mut self, property: &str, value: &str) -> Result<()> { + self.config.properties.insert(property.to_string(), value.to_string()); + self.save_config()?; + Ok(()) + } + + /// Set a publisher property + fn set_publisher_property(&mut self, publisher: &str, property: &str, value: &str) -> Result<()> { + // Check if the publisher exists + if !self.config.publishers.contains(&publisher.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", publisher)); + } + + // Create the property key in the format "publisher/property" + let key = format!("{}/{}", publisher, property); + + // Set the property + self.config.properties.insert(key, value.to_string()); + + // Save the updated configuration + self.save_config()?; + + Ok(()) + } + + /// List packages in the repository + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { + let mut packages = Vec::new(); + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, list packages + for pub_name in publishers { + // In a real implementation, we would scan the repository for packages + // For now, we'll just return a placeholder + + // Example package data (name, version, publisher) + let example_packages = vec![ + ("example/package1".to_string(), "1.0.0".to_string(), pub_name.clone()), + ("example/package2".to_string(), "2.0.0".to_string(), pub_name.clone()), + ]; + + // Filter by pattern if specified + let filtered_packages = if let Some(pat) = pattern { + example_packages.into_iter() + .filter(|(name, _, _)| name.contains(pat)) + .collect() + } else { + example_packages + }; + + packages.extend(filtered_packages); + } + + Ok(packages) + } + + /// Show the contents of packages + fn show_contents(&self, publisher: Option<&str>, pattern: Option<&str>, action_types: Option<&[String]>) -> Result> { + // This is a placeholder implementation + // In a real implementation, we would parse package manifests and extract contents + + // Get the list of packages + let packages = self.list_packages(publisher, pattern)?; + + // For each package, list contents + let mut contents = Vec::new(); + + for (pkg_name, pkg_version, pub_name) in packages { + // Example content data (package, path, type) + let example_contents = vec![ + (format!("{}@{}", pkg_name, pkg_version), "/usr/bin/example".to_string(), "file".to_string()), + (format!("{}@{}", pkg_name, pkg_version), "/usr/share/doc/example".to_string(), "dir".to_string()), + ]; + + // Filter by action type if specified + let filtered_contents = if let Some(types) = action_types { + example_contents.into_iter() + .filter(|(_, _, action_type)| types.contains(&action_type)) + .collect::>() + } else { + example_contents + }; + + contents.extend(filtered_contents); + } + + Ok(contents) + } + + /// Rebuild repository metadata + fn rebuild(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()> { + // This is a placeholder implementation + // In a real implementation, we would rebuild catalogs and search indexes + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, rebuild metadata + for pub_name in publishers { + println!("Rebuilding metadata for publisher: {}", pub_name); + + if !no_catalog { + println!("Rebuilding catalog..."); + // In a real implementation, we would rebuild the catalog + } + + if !no_index { + println!("Rebuilding search index..."); + // In a real implementation, we would rebuild the search index + } + } + + Ok(()) + } + + /// Refresh repository metadata + fn refresh(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()> { + // This is a placeholder implementation + // In a real implementation, we would refresh catalogs and search indexes + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, refresh metadata + for pub_name in publishers { + println!("Refreshing metadata for publisher: {}", pub_name); + + if !no_catalog { + println!("Refreshing catalog..."); + // In a real implementation, we would refresh the catalog + } + + if !no_index { + println!("Refreshing search index..."); + // In a real implementation, we would refresh the search index + } + } + + Ok(()) + } +} + +impl FileBackend { + /// Create the repository directories + fn create_directories(&self) -> Result<()> { + // Create the main repository directories + fs::create_dir_all(self.path.join("catalog"))?; + fs::create_dir_all(self.path.join("file"))?; + fs::create_dir_all(self.path.join("index"))?; + fs::create_dir_all(self.path.join("pkg"))?; + fs::create_dir_all(self.path.join("trans"))?; + + Ok(()) + } + + #[cfg(test)] + pub fn test_publish_files(&mut self, test_dir: &Path) -> Result<()> { + println!("Testing file publishing..."); + + // Create a test publisher + self.add_publisher("test")?; + + // Create a nested directory structure + let nested_dir = test_dir.join("nested").join("dir"); + fs::create_dir_all(&nested_dir)?; + + // Create a test file in the nested directory + let test_file_path = nested_dir.join("test_file.txt"); + fs::write(&test_file_path, "This is a test file")?; + + // Begin a transaction + let mut transaction = self.begin_transaction()?; + + // Create a FileAction from the test file path + let mut file_action = FileAction::read_from_path(&test_file_path)?; + + // Calculate the relative path from the test file path to the base directory + let relative_path = test_file_path.strip_prefix(test_dir)?.to_string_lossy().to_string(); + + // Set the relative path in the FileAction + file_action.path = relative_path; + + // Add the test file to the transaction + transaction.add_file(file_action, &test_file_path)?; + + // Verify that the path in the FileAction is the relative path + // The path should be "nested/dir/test_file.txt", not the full path + let expected_path = "nested/dir/test_file.txt"; + let actual_path = &transaction.manifest.files[0].path; + + if actual_path != expected_path { + return Err(anyhow!("Path in FileAction is incorrect. Expected: {}, Actual: {}", + expected_path, actual_path)); + } + + // Commit the transaction + transaction.commit()?; + + // Verify the file was stored + let hash = Transaction::calculate_file_hash(&test_file_path)?; + let stored_file_path = self.path.join("file").join(&hash); + + if !stored_file_path.exists() { + return Err(anyhow!("File was not stored correctly")); + } + + // Verify the manifest was updated + let manifest_path = self.path.join("pkg").join("manifest"); + + if !manifest_path.exists() { + return Err(anyhow!("Manifest was not created")); + } + + println!("File publishing test passed!"); + + Ok(()) + } + + /// Begin a new transaction for publishing + pub fn begin_transaction(&self) -> Result { + Transaction::new(self.path.clone()) + } + + /// Publish files from a prototype directory + pub fn publish_files>(&self, proto_dir: P, publisher: &str) -> Result<()> { + let proto_dir = proto_dir.as_ref(); + + // Check if the prototype directory exists + if !proto_dir.exists() { + return Err(anyhow!("Prototype directory does not exist: {}", proto_dir.display())); + } + + // Check if the publisher exists + if !self.config.publishers.contains(&publisher.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", publisher)); + } + + // Begin a transaction + let mut transaction = self.begin_transaction()?; + + // Walk the prototype directory and add files to the transaction + self.add_files_to_transaction(&mut transaction, proto_dir, proto_dir)?; + + // Commit the transaction + transaction.commit()?; + + Ok(()) + } + + /// Add files from a directory to a transaction + fn add_files_to_transaction(&self, transaction: &mut Transaction, base_dir: &Path, dir: &Path) -> Result<()> { + // Read the directory entries + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + // Recursively add files from subdirectories + self.add_files_to_transaction(transaction, base_dir, &path)?; + } else { + // Create a FileAction from the file path + let mut file_action = FileAction::read_from_path(&path)?; + + // Calculate the relative path from the file path to the base directory + let relative_path = path.strip_prefix(base_dir)?.to_string_lossy().to_string(); + + // Set the relative path in the FileAction + file_action.path = relative_path; + + // Add the file to the transaction + transaction.add_file(file_action, &path)?; + } + } + + Ok(()) + } + + /// Store a file in the repository + pub fn store_file>(&self, file_path: P) -> Result { + let file_path = file_path.as_ref(); + + // Calculate the SHA256 hash of the file + let hash = Transaction::calculate_file_hash(file_path)?; + + // Create the destination path in the files directory + let dest_path = self.path.join("file").join(&hash); + + // Copy the file if it doesn't already exist + if !dest_path.exists() { + fs::copy(file_path, &dest_path)?; + } + + Ok(hash) + } +} \ No newline at end of file diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs new file mode 100644 index 0000000..1982d2c --- /dev/null +++ b/libips/src/repository/mod.rs @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of +// the Mozilla Public License, v. 2.0. If a copy of the +// MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::Result; +use std::path::Path; +use std::collections::HashMap; + +mod file_backend; +mod rest_backend; + +pub use file_backend::FileBackend; +pub use rest_backend::RestBackend; + +/// Repository configuration filename +pub const REPOSITORY_CONFIG_FILENAME: &str = "pkg6.repository"; + +/// Repository version +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum RepositoryVersion { + V4 = 4, +} + +impl Default for RepositoryVersion { + fn default() -> Self { + RepositoryVersion::V4 + } +} + +impl std::convert::TryFrom for RepositoryVersion { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + match value { + 4 => Ok(RepositoryVersion::V4), + _ => Err(anyhow::anyhow!("Unsupported repository version: {}", value)), + } + } +} + +/// Repository configuration +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RepositoryConfig { + pub version: RepositoryVersion, + pub publishers: Vec, + pub properties: HashMap, +} + +impl Default for RepositoryConfig { + fn default() -> Self { + RepositoryConfig { + version: RepositoryVersion::default(), + publishers: Vec::new(), + properties: HashMap::new(), + } + } +} + +/// Repository trait defining the interface for all repository backends +pub trait Repository { + /// Create a new repository at the specified path + fn create>(path: P, version: RepositoryVersion) -> Result where Self: Sized; + + /// Open an existing repository + fn open>(path: P) -> Result where Self: Sized; + + /// Save the repository configuration + fn save_config(&self) -> Result<()>; + + /// Add a publisher to the repository + fn add_publisher(&mut self, publisher: &str) -> Result<()>; + + /// Remove a publisher from the repository + fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()>; + + /// Get repository information + fn get_info(&self) -> Result>; + + /// Set a repository property + fn set_property(&mut self, property: &str, value: &str) -> Result<()>; + + /// Set a publisher property + fn set_publisher_property(&mut self, publisher: &str, property: &str, value: &str) -> Result<()>; + + /// List packages in the repository + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result>; + + /// Show contents of packages + fn show_contents(&self, publisher: Option<&str>, pattern: Option<&str>, action_types: Option<&[String]>) -> Result>; + + /// Rebuild repository metadata + fn rebuild(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()>; + + /// Refresh repository metadata + fn refresh(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()>; +} \ No newline at end of file diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs new file mode 100644 index 0000000..962f58e --- /dev/null +++ b/libips/src/repository/rest_backend.rs @@ -0,0 +1,310 @@ +// This Source Code Form is subject to the terms of +// the Mozilla Public License, v. 2.0. If a copy of the +// MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{Result, anyhow}; +use std::path::{Path, PathBuf}; +use std::collections::HashMap; + +use super::{Repository, RepositoryConfig, RepositoryVersion}; + +/// Repository implementation that uses a REST API +pub struct RestBackend { + pub uri: String, + pub config: RepositoryConfig, + pub local_cache_path: Option, +} + +impl Repository for RestBackend { + /// Create a new repository at the specified URI + fn create>(uri: P, version: RepositoryVersion) -> Result { + // This is a stub implementation + // In a real implementation, we would make a REST API call to create the repository + + let uri_str = uri.as_ref().to_string_lossy().to_string(); + + // Create the repository configuration + let config = RepositoryConfig { + version, + ..Default::default() + }; + + // Create the repository structure + let repo = RestBackend { + uri: uri_str, + config, + local_cache_path: None, + }; + + // In a real implementation, we would make a REST API call to create the repository structure + + Ok(repo) + } + + /// Open an existing repository + fn open>(uri: P) -> Result { + // This is a stub implementation + // In a real implementation, we would make a REST API call to get the repository configuration + + let uri_str = uri.as_ref().to_string_lossy().to_string(); + + // In a real implementation, we would fetch the repository configuration from the REST API + // For now, we'll just create a default configuration + let config = RepositoryConfig::default(); + + Ok(RestBackend { + uri: uri_str, + config, + local_cache_path: None, + }) + } + + /// Save the repository configuration + fn save_config(&self) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to save the repository configuration + + // For now, just return Ok + Ok(()) + } + + /// Add a publisher to the repository + fn add_publisher(&mut self, publisher: &str) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to add the publisher + + if !self.config.publishers.contains(&publisher.to_string()) { + self.config.publishers.push(publisher.to_string()); + + // In a real implementation, we would make a REST API call to create publisher-specific resources + + // Save the updated configuration + self.save_config()?; + } + + Ok(()) + } + + /// Remove a publisher from the repository + fn remove_publisher(&mut self, publisher: &str, dry_run: bool) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to remove the publisher + + if let Some(pos) = self.config.publishers.iter().position(|p| p == publisher) { + if !dry_run { + self.config.publishers.remove(pos); + + // In a real implementation, we would make a REST API call to remove publisher-specific resources + + // Save the updated configuration + self.save_config()?; + } + } + + Ok(()) + } + + /// Get repository information + fn get_info(&self) -> Result> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to get repository information + + let mut info = Vec::new(); + + for publisher in &self.config.publishers { + // In a real implementation, we would get this information from the REST API + let package_count = 0; + let status = "online".to_string(); + let updated = "2025-07-21T18:46:00.000000Z".to_string(); + + info.push((publisher.clone(), package_count, status, updated)); + } + + Ok(info) + } + + /// Set a repository property + fn set_property(&mut self, property: &str, value: &str) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to set the property + + self.config.properties.insert(property.to_string(), value.to_string()); + self.save_config()?; + + Ok(()) + } + + /// Set a publisher property + fn set_publisher_property(&mut self, publisher: &str, property: &str, value: &str) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to set the publisher property + + // Check if the publisher exists + if !self.config.publishers.contains(&publisher.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", publisher)); + } + + // Create the property key in the format "publisher/property" + let key = format!("{}/{}", publisher, property); + + // Set the property + self.config.properties.insert(key, value.to_string()); + + // Save the updated configuration + self.save_config()?; + + Ok(()) + } + + /// List packages in the repository + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to list packages + + let mut packages = Vec::new(); + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, list packages + for pub_name in publishers { + // In a real implementation, we would get this information from the REST API + + // Example package data (name, version, publisher) + let example_packages = vec![ + ("example/package1".to_string(), "1.0.0".to_string(), pub_name.clone()), + ("example/package2".to_string(), "2.0.0".to_string(), pub_name.clone()), + ]; + + // Filter by pattern if specified + let filtered_packages = if let Some(pat) = pattern { + example_packages.into_iter() + .filter(|(name, _, _)| name.contains(pat)) + .collect() + } else { + example_packages + }; + + packages.extend(filtered_packages); + } + + Ok(packages) + } + + /// Show contents of packages + fn show_contents(&self, publisher: Option<&str>, pattern: Option<&str>, action_types: Option<&[String]>) -> Result> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to get package contents + + // Get the list of packages + let packages = self.list_packages(publisher, pattern)?; + + // For each package, list contents + let mut contents = Vec::new(); + + for (pkg_name, pkg_version, _) in packages { + // In a real implementation, we would get this information from the REST API + + // Example content data (package, path, type) + let example_contents = vec![ + (format!("{}@{}", pkg_name, pkg_version), "/usr/bin/example".to_string(), "file".to_string()), + (format!("{}@{}", pkg_name, pkg_version), "/usr/share/doc/example".to_string(), "dir".to_string()), + ]; + + // Filter by action type if specified + let filtered_contents = if let Some(types) = action_types { + example_contents.into_iter() + .filter(|(_, _, action_type)| types.contains(&action_type)) + .collect::>() + } else { + example_contents + }; + + contents.extend(filtered_contents); + } + + Ok(contents) + } + + /// Rebuild repository metadata + fn rebuild(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to rebuild metadata + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, rebuild metadata + for pub_name in publishers { + println!("Rebuilding metadata for publisher: {}", pub_name); + + if !no_catalog { + println!("Rebuilding catalog..."); + // In a real implementation, we would make a REST API call to rebuild the catalog + } + + if !no_index { + println!("Rebuilding search index..."); + // In a real implementation, we would make a REST API call to rebuild the search index + } + } + + Ok(()) + } + + /// Refresh repository metadata + fn refresh(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()> { + // This is a stub implementation + // In a real implementation, we would make a REST API call to refresh metadata + + // Filter publishers if specified + let publishers = if let Some(pub_name) = publisher { + if !self.config.publishers.contains(&pub_name.to_string()) { + return Err(anyhow!("Publisher does not exist: {}", pub_name)); + } + vec![pub_name.to_string()] + } else { + self.config.publishers.clone() + }; + + // For each publisher, refresh metadata + for pub_name in publishers { + println!("Refreshing metadata for publisher: {}", pub_name); + + if !no_catalog { + println!("Refreshing catalog..."); + // In a real implementation, we would make a REST API call to refresh the catalog + } + + if !no_index { + println!("Refreshing search index..."); + // In a real implementation, we would make a REST API call to refresh the search index + } + } + + Ok(()) + } +} + +impl RestBackend { + /// Set the local cache path + pub fn set_local_cache_path>(&mut self, path: P) -> Result<()> { + self.local_cache_path = Some(path.as_ref().to_path_buf()); + Ok(()) + } +} \ No newline at end of file diff --git a/pkg6repo/Cargo.toml b/pkg6repo/Cargo.toml index 3e23980..6f82d2c 100644 --- a/pkg6repo/Cargo.toml +++ b/pkg6repo/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.1-placeholder" authors = ["Till Wegmueller "] edition = "2018" license-file = "LICENSE" -description = "The repository server for IPS written in rust" +description = "The repository management utility for IPS written in rust" repository = "https://github.com/OpenFlowLabs/pkg6dev" readme = "README.md" keywords = ["packaging", "illumos"] @@ -12,3 +12,11 @@ keywords = ["packaging", "illumos"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.4", features = ["derive"] } +anyhow = "1.0" +libips = { path = "../libips" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.8" diff --git a/pkg6repo/README.md b/pkg6repo/README.md index 3348e6f..bc1decb 100644 --- a/pkg6repo/README.md +++ b/pkg6repo/README.md @@ -1,3 +1,163 @@ -# IPS Packaging utility. +# pkg6repo -Work in progress \ No newline at end of file +pkg6repo is a Rust implementation of the Image Packaging System (IPS) repository management utility. It is designed to replace the pkgrepo command from the original IPS implementation. + +## Installation + +To build and install pkg6repo, you need to have Rust and Cargo installed. Then, you can build the project using: + +```bash +cargo build --release +``` + +The binary will be available at `target/release/pkg6repo`. + +## Usage + +pkg6repo provides several subcommands for managing package repositories: + +### Create a Repository + +Create a new package repository: + +```bash +pkg6repo create /path/to/repository +``` + +You can specify the repository version (default is 4): + +```bash +pkg6repo create --version 4 /path/to/repository +``` + +### Add Publishers + +Add publishers to a repository: + +```bash +pkg6repo add-publisher -s /path/to/repository example.com +``` + +### Remove Publishers + +Remove publishers from a repository: + +```bash +pkg6repo remove-publisher -s /path/to/repository example.com +``` + +You can perform a dry run to see what would be removed without actually removing anything: + +```bash +pkg6repo remove-publisher -n -s /path/to/repository example.com +``` + +### Get Repository Properties + +Get repository properties: + +```bash +pkg6repo get -s /path/to/repository +``` + +You can specify specific properties to get: + +```bash +pkg6repo get -s /path/to/repository publisher/prefix +``` + +### Set Repository Properties + +Set repository properties: + +```bash +pkg6repo set -s /path/to/repository publisher/prefix=example.com +``` + +You can set publisher-specific properties: + +```bash +pkg6repo set -s /path/to/repository -p example.com repository/origins=http://example.com/repository +``` + +### Display Repository Information + +Display information about a repository: + +```bash +pkg6repo info -s /path/to/repository +``` + +### List Packages + +List packages in a repository: + +```bash +pkg6repo list -s /path/to/repository +``` + +You can filter by publisher: + +```bash +pkg6repo list -s /path/to/repository -p example.com +``` + +You can also filter by package pattern: + +```bash +pkg6repo list -s /path/to/repository example/package +``` + +### Show Package Contents + +Show contents of packages in a repository: + +```bash +pkg6repo contents -s /path/to/repository +``` + +You can filter by package pattern: + +```bash +pkg6repo contents -s /path/to/repository example/package +``` + +You can also filter by action type: + +```bash +pkg6repo contents -s /path/to/repository -t file +``` + +### Rebuild Repository Metadata + +Rebuild repository metadata: + +```bash +pkg6repo rebuild -s /path/to/repository +``` + +You can skip catalog or index rebuilding: + +```bash +pkg6repo rebuild -s /path/to/repository --no-catalog +pkg6repo rebuild -s /path/to/repository --no-index +``` + +### Refresh Repository Metadata + +Refresh repository metadata: + +```bash +pkg6repo refresh -s /path/to/repository +``` + +You can skip catalog or index refreshing: + +```bash +pkg6repo refresh -s /path/to/repository --no-catalog +pkg6repo refresh -s /path/to/repository --no-index +``` + +## License + +This project is licensed under the same license as the original IPS implementation. \ No newline at end of file diff --git a/pkg6repo/src/main.rs b/pkg6repo/src/main.rs index e7a11a9..53ac337 100644 --- a/pkg6repo/src/main.rs +++ b/pkg6repo/src/main.rs @@ -1,3 +1,492 @@ -fn main() { - println!("Hello, world!"); +use clap::{Parser, Subcommand}; +use anyhow::{Result, anyhow}; +use std::path::PathBuf; +use std::convert::TryFrom; + +use libips::repository::{Repository, RepositoryVersion, FileBackend}; + +#[cfg(test)] +mod tests; + +/// pkg6repo - Image Packaging System repository management utility +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +struct App { + #[clap(subcommand)] + command: Commands, } + +#[derive(Subcommand, Debug)] +enum Commands { + /// Create a new package repository + Create { + /// Version of the repository to create + #[clap(long, default_value = "4")] + version: u32, + + /// Path or URI of the repository to create + uri_or_path: String, + }, + + /// Add publishers to a repository + AddPublisher { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Publishers to add + publisher: Vec, + }, + + /// Remove publishers from a repository + RemovePublisher { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Perform a dry run + #[clap(short = 'n')] + dry_run: bool, + + /// Wait for operation to complete + #[clap(long)] + synchronous: bool, + + /// Publishers to remove + publisher: Vec, + }, + + /// Get repository properties + Get { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Output format + #[clap(short = 'F')] + format: Option, + + /// Omit headers + #[clap(short = 'H')] + omit_headers: bool, + + /// Publisher to get properties for + #[clap(short = 'p')] + publisher: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + + /// Properties to get (section/property) + section_property: Option>, + }, + + /// Display repository information + Info { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Output format + #[clap(short = 'F')] + format: Option, + + /// Omit headers + #[clap(short = 'H')] + omit_headers: bool, + + /// Publisher to get information for + #[clap(short = 'p')] + publisher: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + }, + + /// List packages in a repository + List { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Output format + #[clap(short = 'F')] + format: Option, + + /// Omit headers + #[clap(short = 'H')] + omit_headers: bool, + + /// Publisher to list packages for + #[clap(short = 'p')] + publisher: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + + /// Package FMRI patterns to match + pkg_fmri_pattern: Option>, + }, + + /// Show contents of packages in a repository + Contents { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Show manifest contents + #[clap(short = 'm')] + manifest: bool, + + /// Filter by action type + #[clap(short = 't')] + action_type: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + + /// Package FMRI patterns to match + pkg_fmri_pattern: Option>, + }, + + /// Rebuild repository metadata + Rebuild { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Publisher to rebuild metadata for + #[clap(short = 'p')] + publisher: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + + /// Skip catalog rebuild + #[clap(long)] + no_catalog: bool, + + /// Skip index rebuild + #[clap(long)] + no_index: bool, + }, + + /// Refresh repository metadata + Refresh { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Publisher to refresh metadata for + #[clap(short = 'p')] + publisher: Option>, + + /// SSL key file + #[clap(long)] + key: Option, + + /// SSL certificate file + #[clap(long)] + cert: Option, + + /// Skip catalog refresh + #[clap(long)] + no_catalog: bool, + + /// Skip index refresh + #[clap(long)] + no_index: bool, + }, + + /// Set repository properties + Set { + /// Path or URI of the repository + #[clap(short = 's')] + repo_uri_or_path: String, + + /// Publisher to set properties for + #[clap(short = 'p')] + publisher: Option, + + /// Properties to set (section/property=value) + property_value: Vec, + }, +} + +fn main() -> Result<()> { + let cli = App::parse(); + + match &cli.command { + Commands::Create { version, uri_or_path } => { + println!("Creating repository version {} at {}", version, uri_or_path); + + // Convert version to RepositoryVersion + let repo_version = RepositoryVersion::try_from(*version)?; + + // Create the repository + let repo = FileBackend::create(uri_or_path, repo_version)?; + + println!("Repository created successfully at {}", repo.path.display()); + Ok(()) + }, + Commands::AddPublisher { repo_uri_or_path, publisher } => { + println!("Adding publishers {:?} to repository {}", publisher, repo_uri_or_path); + + // Open the repository + let mut repo = FileBackend::open(repo_uri_or_path)?; + + // Add each publisher + for p in publisher { + println!("Adding publisher: {}", p); + repo.add_publisher(p)?; + } + + println!("Publishers added successfully"); + Ok(()) + }, + Commands::RemovePublisher { repo_uri_or_path, dry_run, synchronous, publisher } => { + println!("Removing publishers {:?} from repository {}", publisher, repo_uri_or_path); + println!("Dry run: {}, Synchronous: {}", dry_run, synchronous); + + // Open the repository + let mut repo = FileBackend::open(repo_uri_or_path)?; + + // Remove each publisher + for p in publisher { + println!("Removing publisher: {}", p); + repo.remove_publisher(p, *dry_run)?; + } + + if *dry_run { + println!("Dry run completed. No changes were made."); + } else { + println!("Publishers removed successfully"); + } + + Ok(()) + }, + Commands::Get { repo_uri_or_path, format, omit_headers, publisher, key, cert, section_property } => { + println!("Getting properties from repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Print headers if not omitted + if !omit_headers { + println!("{:<10} {:<10} {:<20}", "SECTION", "PROPERTY", "VALUE"); + } + + // Print repository properties + for (key, value) in &repo.config.properties { + let parts: Vec<&str> = key.split('/').collect(); + if parts.len() == 2 { + println!("{:<10} {:<10} {:<20}", parts[0], parts[1], value); + } else { + println!("{:<10} {:<10} {:<20}", "", key, value); + } + } + + Ok(()) + }, + Commands::Info { repo_uri_or_path, format, omit_headers, publisher, key, cert } => { + println!("Displaying info for repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Get repository info + let info = repo.get_info()?; + + // Print headers if not omitted + if !omit_headers { + println!("{:<10} {:<8} {:<6} {:<30}", "PUBLISHER", "PACKAGES", "STATUS", "UPDATED"); + } + + // Print repository info + for (pub_name, pkg_count, status, updated) in info { + println!("{:<10} {:<8} {:<6} {:<30}", pub_name, pkg_count, status, updated); + } + + Ok(()) + }, + Commands::List { repo_uri_or_path, format, omit_headers, publisher, key, cert, pkg_fmri_pattern } => { + println!("Listing packages in repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Get the publisher if specified + let pub_option = if let Some(publishers) = publisher { + if !publishers.is_empty() { + Some(publishers[0].as_str()) + } else { + None + } + } else { + None + }; + + // Get the pattern if specified + let pattern_option = if let Some(patterns) = pkg_fmri_pattern { + if !patterns.is_empty() { + Some(patterns[0].as_str()) + } else { + None + } + } else { + None + }; + + // List packages + let packages = repo.list_packages(pub_option, pattern_option)?; + + // Print headers if not omitted + if !omit_headers { + println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER"); + } + + // Print packages + for (name, version, publisher) in packages { + println!("{:<30} {:<15} {:<10}", name, version, publisher); + } + + Ok(()) + }, + Commands::Contents { repo_uri_or_path, manifest, action_type, key, cert, pkg_fmri_pattern } => { + println!("Showing contents in repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Get the pattern if specified + let pattern_option = if let Some(patterns) = pkg_fmri_pattern { + if !patterns.is_empty() { + Some(patterns[0].as_str()) + } else { + None + } + } else { + None + }; + + // Show contents + let contents = repo.show_contents(None, pattern_option, action_type.as_deref())?; + + // Print contents + for (package, path, action_type) in contents { + if *manifest { + // If manifest option is specified, print in manifest format + println!("{} path={} type={}", action_type, path, package); + } else { + // Otherwise, print in table format + println!("{:<40} {:<30} {:<10}", package, path, action_type); + } + } + + Ok(()) + }, + Commands::Rebuild { repo_uri_or_path, publisher, key, cert, no_catalog, no_index } => { + println!("Rebuilding repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Get the publisher if specified + let pub_option = if let Some(publishers) = publisher { + if !publishers.is_empty() { + Some(publishers[0].as_str()) + } else { + None + } + } else { + None + }; + + // Rebuild repository metadata + repo.rebuild(pub_option, *no_catalog, *no_index)?; + + println!("Repository rebuilt successfully"); + Ok(()) + }, + Commands::Refresh { repo_uri_or_path, publisher, key, cert, no_catalog, no_index } => { + println!("Refreshing repository {}", repo_uri_or_path); + + // Open the repository + let repo = FileBackend::open(repo_uri_or_path)?; + + // Get the publisher if specified + let pub_option = if let Some(publishers) = publisher { + if !publishers.is_empty() { + Some(publishers[0].as_str()) + } else { + None + } + } else { + None + }; + + // Refresh repository metadata + repo.refresh(pub_option, *no_catalog, *no_index)?; + + println!("Repository refreshed successfully"); + Ok(()) + }, + Commands::Set { repo_uri_or_path, publisher, property_value } => { + println!("Setting properties for repository {}", repo_uri_or_path); + + // Open the repository + let mut repo = FileBackend::open(repo_uri_or_path)?; + + // Process each property=value pair + for prop_val in property_value { + // Split the property=value string + let parts: Vec<&str> = prop_val.split('=').collect(); + if parts.len() != 2 { + return Err(anyhow!("Invalid property=value format: {}", prop_val)); + } + + let property = parts[0]; + let value = parts[1]; + + // If a publisher is specified, set the publisher property + if let Some(pub_name) = publisher { + println!("Setting publisher property {}/{} = {}", pub_name, property, value); + repo.set_publisher_property(pub_name, property, value)?; + } else { + // Otherwise, set the repository property + println!("Setting repository property {} = {}", property, value); + repo.set_property(property, value)?; + } + } + + println!("Properties set successfully"); + Ok(()) + }, + } +} \ No newline at end of file diff --git a/pkg6repo/src/tests.rs b/pkg6repo/src/tests.rs new file mode 100644 index 0000000..225d49f --- /dev/null +++ b/pkg6repo/src/tests.rs @@ -0,0 +1,80 @@ +#[cfg(test)] +mod tests { + use libips::repository::{Repository, RepositoryVersion, FileBackend, REPOSITORY_CONFIG_FILENAME}; + use tempfile::tempdir; + + #[test] + fn test_create_repository() { + // Create a temporary directory for the test + let temp_dir = tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + + // Create a repository + let _ = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + // Check that the repository was created + assert!(repo_path.exists()); + assert!(repo_path.join("catalog").exists()); + assert!(repo_path.join("file").exists()); + assert!(repo_path.join("index").exists()); + assert!(repo_path.join("pkg").exists()); + assert!(repo_path.join("trans").exists()); + assert!(repo_path.join(REPOSITORY_CONFIG_FILENAME).exists()); + } + + #[test] + fn test_add_publisher() { + // Create a temporary directory for the test + let temp_dir = tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + + // Create a repository + let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + // Add a publisher + repo.add_publisher("example.com").unwrap(); + + // Check that the publisher was added + assert!(repo.config.publishers.contains(&"example.com".to_string())); + assert!(repo_path.join("catalog").join("example.com").exists()); + assert!(repo_path.join("pkg").join("example.com").exists()); + } + + #[test] + fn test_remove_publisher() { + // Create a temporary directory for the test + let temp_dir = tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + + // Create a repository + let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + // Add a publisher + repo.add_publisher("example.com").unwrap(); + + // Check that the publisher was added + assert!(repo.config.publishers.contains(&"example.com".to_string())); + + // Remove the publisher + repo.remove_publisher("example.com", false).unwrap(); + + // Check that the publisher was removed + assert!(!repo.config.publishers.contains(&"example.com".to_string())); + } + + #[test] + fn test_set_property() { + // Create a temporary directory for the test + let temp_dir = tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + + // Create a repository + let mut repo = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); + + // Set a property + repo.set_property("publisher/prefix", "example.com").unwrap(); + + // Check that the property was set + assert_eq!(repo.config.properties.get("publisher/prefix").unwrap(), "example.com"); + } +} \ No newline at end of file