mirror of
https://codeberg.org/icewind/rss-webhook-trigger.git
synced 2026-06-03 18:04:09 +02:00
rate limited and cache controll attempt
This commit is contained in:
parent
18bd55cbcd
commit
988f2e4603
7 changed files with 544 additions and 199 deletions
172
Cargo.lock
generated
172
Cargo.lock
generated
|
|
@ -4,18 +4,18 @@ version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.24.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler"
|
name = "adler2"
|
||||||
version = "1.0.2"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
|
|
@ -63,17 +63,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.71"
|
version = "0.3.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cc",
|
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -102,9 +102,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
|
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
@ -135,33 +135,6 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-eyre"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
|
|
||||||
dependencies = [
|
|
||||||
"backtrace",
|
|
||||||
"color-spantrace",
|
|
||||||
"eyre",
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-spantrace"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
@ -276,7 +249,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -294,16 +267,6 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "eyre"
|
|
||||||
version = "0.6.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
|
||||||
dependencies = [
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "failure"
|
name = "failure"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
@ -389,9 +352,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.1"
|
version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
|
|
@ -441,9 +404,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.5.1"
|
version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
|
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
|
@ -460,9 +423,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.27.3"
|
version = "0.27.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -633,7 +596,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -663,12 +626,6 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indenter"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
|
|
@ -709,9 +666,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.168"
|
version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
|
|
@ -725,6 +682,12 @@ version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "main_error"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "155db5e86c6e45ee456bf32fad5a290ee1f7151c2faca27ea27097568da67d1a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
|
|
@ -739,11 +702,11 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.4"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -784,9 +747,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.2"
|
version = "0.36.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
@ -803,12 +766,6 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "owo-colors"
|
|
||||||
version = "3.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|
@ -886,7 +843,7 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.7",
|
"thiserror 2.0.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
@ -905,7 +862,7 @@ dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 2.0.7",
|
"thiserror 2.0.9",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
|
|
@ -913,9 +870,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-udp"
|
name = "quinn-udp"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527"
|
checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1042,12 +999,13 @@ dependencies = [
|
||||||
name = "rss-webhook-trigger"
|
name = "rss-webhook-trigger"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"main_error",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"secretfile",
|
"secretfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syndication",
|
"syndication",
|
||||||
|
"thiserror 2.0.9",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
@ -1142,14 +1100,14 @@ checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.133"
|
version = "1.0.134"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1275,9 +1233,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.90"
|
version = "2.0.91"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
|
|
@ -1332,7 +1290,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1346,11 +1304,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.7"
|
version = "2.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
|
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.7",
|
"thiserror-impl 2.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1361,18 +1319,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.7"
|
version = "2.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
|
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1428,9 +1386,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
|
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
|
|
@ -1466,7 +1424,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1538,7 +1496,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1551,16 +1509,6 @@ dependencies = [
|
||||||
"valuable",
|
"valuable",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-error"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
|
||||||
dependencies = [
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1681,7 +1629,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1716,7 +1664,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
@ -1940,7 +1888,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
"synstructure 0.13.1",
|
"synstructure 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1962,7 +1910,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1982,7 +1930,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
"synstructure 0.13.1",
|
"synstructure 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2011,5 +1959,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"syn 2.0.90",
|
"syn 2.0.91",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ syndication = "0.5.0"
|
||||||
reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "json"] }
|
reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "signal"] }
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.134"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
color-eyre = "0.6.3"
|
main_error = "0.1.2"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
time = { version = "0.3.37", features = ["serde", "serde-well-known"] }
|
time = { version = "0.3.37", features = ["serde", "serde-well-known"] }
|
||||||
|
thiserror = "2.0.9"
|
||||||
secretfile = "0.1.0"
|
secretfile = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use color_eyre::{eyre::WrapErr, Result};
|
|
||||||
use reqwest::header::{HeaderValue, InvalidHeaderValue};
|
use reqwest::header::{HeaderValue, InvalidHeaderValue};
|
||||||
use secretfile::{load, SecretError};
|
use secretfile::{load, SecretError};
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
|
|
@ -9,6 +8,7 @@ use std::convert::{TryFrom, TryInto};
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
use crate::error::ConfigError;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -27,10 +27,12 @@ pub struct FeedConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn from_file(path: &str) -> Result<Self> {
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
||||||
|
let path = path.as_ref();
|
||||||
let file = read_to_string(path)
|
let file = read_to_string(path)
|
||||||
.wrap_err_with(|| format!("Failed to open config file {}", path))?;
|
.map_err(|error| ConfigError::Read {error, path: path.into()})?;
|
||||||
toml::from_str(&file).wrap_err_with(|| format!("Failed to open config file {}", path))
|
toml::from_str(&file)
|
||||||
|
.map_err(|error| ConfigError::Parse {error, path: path.into()})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interval(&self) -> Duration {
|
pub fn interval(&self) -> Duration {
|
||||||
|
|
@ -55,7 +57,7 @@ impl<'de> Deserialize<'de> for HeaderVal {
|
||||||
impl TryFrom<&HeaderVal> for HeaderValue {
|
impl TryFrom<&HeaderVal> for HeaderValue {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
fn try_from(header: &HeaderVal) -> std::result::Result<Self, Self::Error> {
|
fn try_from(header: &HeaderVal) -> Result<Self, Self::Error> {
|
||||||
header.0.as_str().try_into()
|
header.0.as_str().try_into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
src/error.rs
Normal file
62
src/error.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ParseFeedError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Parse(<syndication::Feed as FromStr>::Err),
|
||||||
|
#[error("Empty feed")]
|
||||||
|
Empty,
|
||||||
|
#[error("No guid, pubDate or link set on feed item")]
|
||||||
|
MissingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FetchFeedError {
|
||||||
|
#[error("Error while fetching feed: {0:#}")]
|
||||||
|
Network(#[from] reqwest::Error),
|
||||||
|
#[error("Error while parsing feed: {0:#}")]
|
||||||
|
Parse(#[from] ParseFeedError),
|
||||||
|
#[error("Docker hub returned a server error {0}")]
|
||||||
|
ServerError(StatusCode),
|
||||||
|
#[error("Docker hub returned a client error {0}")]
|
||||||
|
ClientError(StatusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("Error while reading config file {}: {:#}", path.display(), error)]
|
||||||
|
Read {
|
||||||
|
error: std::io::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
#[error("Error while parse config file {}: {:#}", path.display(), error)]
|
||||||
|
Parse {
|
||||||
|
error: toml::de::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum HubError {
|
||||||
|
#[error("Error while fetching docker hub info: {0:#}")]
|
||||||
|
Network(#[from] reqwest::Error),
|
||||||
|
#[error("Error while parsing hub response: {0:#}")]
|
||||||
|
Parse(#[from] serde_json::Error),
|
||||||
|
#[error("Docker hub returned a server error {0}")]
|
||||||
|
ServerError(StatusCode),
|
||||||
|
#[error("Docker hub returned a client error {0}")]
|
||||||
|
ClientError(StatusCode),
|
||||||
|
#[error("Invalid hub url format")]
|
||||||
|
InvalidFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FetchError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Feed(#[from] FetchFeedError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Hub(#[from] HubError),
|
||||||
|
}
|
||||||
263
src/fetcher.rs
Normal file
263
src/fetcher.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
use reqwest::header::{
|
||||||
|
HeaderMap, HeaderValue, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED, RETRY_AFTER,
|
||||||
|
};
|
||||||
|
use reqwest::{Response, StatusCode};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use time::format_description::well_known::Rfc2822;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
/// waiting 6 hours after a 429 should be slow enough for everyone
|
||||||
|
const DEFAULT_BACKOFF: Duration = Duration::from_secs(6 * 60 * 60);
|
||||||
|
const ONE_SEC: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub enum FetchPlanInput {
|
||||||
|
Retry {
|
||||||
|
time: Instant,
|
||||||
|
headers: CacheHeaders,
|
||||||
|
},
|
||||||
|
WithCache {
|
||||||
|
headers: CacheHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchPlanInput {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn into_cache_headers(self) -> CacheHeaders {
|
||||||
|
match self {
|
||||||
|
FetchPlanInput::Retry { headers, .. } => headers,
|
||||||
|
FetchPlanInput::WithCache { headers } => headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct CacheHeaders {
|
||||||
|
etag: Option<String>,
|
||||||
|
last_modified: Option<OffsetDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheHeaders {
|
||||||
|
pub fn from_headers(headers: &HeaderMap) -> CacheHeaders {
|
||||||
|
let etag = headers
|
||||||
|
.get(ETAG)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.map(String::from);
|
||||||
|
let last_modified = headers
|
||||||
|
.get(LAST_MODIFIED)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.and_then(|s| OffsetDateTime::parse(s, &Rfc2822).ok());
|
||||||
|
CacheHeaders {
|
||||||
|
etag,
|
||||||
|
last_modified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_headers(&self, headers: &mut HeaderMap) {
|
||||||
|
match (&self.last_modified, &self.etag) {
|
||||||
|
(_, Some(etag)) => {
|
||||||
|
headers.insert(
|
||||||
|
IF_NONE_MATCH,
|
||||||
|
HeaderValue::from_str(etag).expect("malformed etag"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(Some(last_modified), None) => {
|
||||||
|
headers.insert(
|
||||||
|
IF_MODIFIED_SINCE,
|
||||||
|
HeaderValue::from_str(&last_modified.format(&Rfc2822).unwrap()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn headers(&self) -> HeaderMap {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
self.set_headers(&mut headers);
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FetchPlan {
|
||||||
|
pub time: Instant,
|
||||||
|
pub headers: CacheHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchPlan {
|
||||||
|
pub fn elapsed(&self) -> bool {
|
||||||
|
Instant::now() > self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FetchPlan {
|
||||||
|
fn default() -> FetchPlan {
|
||||||
|
FetchPlan {
|
||||||
|
time: Instant::now(),
|
||||||
|
headers: CacheHeaders::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// plan the next fetch, either on startup or right after we finished the previous fetch
|
||||||
|
pub fn next_fetch(base_interval: Duration, last_result: Option<FetchPlanInput>) -> FetchPlan {
|
||||||
|
let now = Instant::now();
|
||||||
|
match last_result {
|
||||||
|
Some(FetchPlanInput::Retry { time, headers }) => FetchPlan {
|
||||||
|
time: now.max(time),
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
Some(FetchPlanInput::WithCache { headers }) => FetchPlan {
|
||||||
|
time: now + base_interval,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
None => FetchPlan {
|
||||||
|
time: now + base_interval,
|
||||||
|
headers: CacheHeaders::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FetchResponse<T, E> {
|
||||||
|
Retry {
|
||||||
|
time: Instant,
|
||||||
|
headers: CacheHeaders,
|
||||||
|
},
|
||||||
|
Ok {
|
||||||
|
headers: CacheHeaders,
|
||||||
|
response: T,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
headers: CacheHeaders,
|
||||||
|
error: E,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> FetchResponse<T, E> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn plan(self) -> FetchPlanInput {
|
||||||
|
match self {
|
||||||
|
FetchResponse::Retry { time, headers } => FetchPlanInput::Retry { time, headers },
|
||||||
|
FetchResponse::Ok { headers, .. } => FetchPlanInput::WithCache { headers },
|
||||||
|
FetchResponse::Error { headers, .. } => FetchPlanInput::WithCache { headers },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_result(self) -> Result<(Option<T>, FetchPlanInput), (E, FetchPlanInput)> {
|
||||||
|
match self {
|
||||||
|
FetchResponse::Retry { time, headers } => {
|
||||||
|
Ok((None, FetchPlanInput::Retry { time, headers }))
|
||||||
|
}
|
||||||
|
FetchResponse::Ok { headers, response } => {
|
||||||
|
Ok((Some(response), FetchPlanInput::WithCache { headers }))
|
||||||
|
}
|
||||||
|
FetchResponse::Error { headers, error } => {
|
||||||
|
Err((error, FetchPlanInput::WithCache { headers }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> FetchResponse<Response, E> {
|
||||||
|
pub fn from_result(result: Result<Response, E>) -> FetchResponse<Response, E> {
|
||||||
|
match result {
|
||||||
|
Ok(response) => {
|
||||||
|
let cache_header = CacheHeaders::from_headers(response.headers());
|
||||||
|
if response.status() == StatusCode::TOO_MANY_REQUESTS {
|
||||||
|
let after = response
|
||||||
|
.headers()
|
||||||
|
.get(RETRY_AFTER)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.and_then(|str| str.parse::<u64>().ok())
|
||||||
|
.map(Duration::from_secs)
|
||||||
|
.unwrap_or(DEFAULT_BACKOFF);
|
||||||
|
FetchResponse::Retry {
|
||||||
|
time: Instant::now() + after + ONE_SEC,
|
||||||
|
headers: cache_header,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FetchResponse::Ok {
|
||||||
|
headers: cache_header,
|
||||||
|
response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => FetchResponse::Error {
|
||||||
|
error: err,
|
||||||
|
headers: CacheHeaders::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_status_code<Fc, Fs>(self, client_error: Fc, server_error: Fs) -> Self
|
||||||
|
where
|
||||||
|
Fc: Fn(StatusCode) -> E,
|
||||||
|
Fs: Fn(StatusCode) -> E,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
FetchResponse::Ok { headers, response } => {
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_client_error() {
|
||||||
|
FetchResponse::Error {
|
||||||
|
error: client_error(status),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
} else if status.is_server_error() {
|
||||||
|
FetchResponse::Error {
|
||||||
|
error: server_error(status),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FetchResponse::Ok { headers, response }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest => rest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> FetchResponse<T, E> {
|
||||||
|
pub async fn map<U, Fut, F>(self, f: F) -> FetchResponse<U, E>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = U>,
|
||||||
|
F: Fn(T) -> Fut,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
FetchResponse::Retry { time, headers } => FetchResponse::Retry { time, headers },
|
||||||
|
FetchResponse::Ok { headers, response } => FetchResponse::Ok {
|
||||||
|
headers,
|
||||||
|
response: f(response).await,
|
||||||
|
},
|
||||||
|
FetchResponse::Error { error, headers } => FetchResponse::Error { error, headers },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn map_err<U, F>(self, f: F) -> FetchResponse<T, U>
|
||||||
|
where
|
||||||
|
F: Fn(E) -> U,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
FetchResponse::Retry { time, headers } => FetchResponse::Retry { time, headers },
|
||||||
|
FetchResponse::Ok { headers, response } => FetchResponse::Ok { headers, response },
|
||||||
|
FetchResponse::Error { error, headers } => FetchResponse::Error {
|
||||||
|
error: f(error),
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> FetchResponse<Result<T, E>, E> {
|
||||||
|
pub fn flatten(self) -> FetchResponse<T, E> {
|
||||||
|
match self {
|
||||||
|
FetchResponse::Retry { time, headers } => FetchResponse::Retry { time, headers },
|
||||||
|
FetchResponse::Ok {
|
||||||
|
headers,
|
||||||
|
response: Ok(response),
|
||||||
|
} => FetchResponse::Ok { headers, response },
|
||||||
|
FetchResponse::Ok {
|
||||||
|
headers,
|
||||||
|
response: Err(error),
|
||||||
|
} => FetchResponse::Error { error, headers },
|
||||||
|
FetchResponse::Error { error, headers } => FetchResponse::Error { error, headers },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/hub.rs
38
src/hub.rs
|
|
@ -1,33 +1,37 @@
|
||||||
use color_eyre::eyre::WrapErr;
|
use crate::error::HubError;
|
||||||
use color_eyre::{eyre::ensure, Result};
|
use crate::fetcher::{CacheHeaders, FetchResponse};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
pub async fn tags(client: &Client, user: &str, repo: &str) -> Result<Vec<HubTag>> {
|
pub async fn tags(
|
||||||
|
client: &Client,
|
||||||
|
user: &str,
|
||||||
|
repo: &str,
|
||||||
|
cache_headers: &CacheHeaders,
|
||||||
|
) -> FetchResponse<Vec<HubTag>, HubError> {
|
||||||
let result = client
|
let result = client
|
||||||
.get(format!(
|
.get(format!(
|
||||||
"https://hub.docker.com/v2/repositories/{}/{}/tags",
|
"https://hub.docker.com/v2/repositories/{}/{}/tags",
|
||||||
user, repo
|
user, repo
|
||||||
))
|
))
|
||||||
|
.headers(cache_headers.headers())
|
||||||
.send()
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
FetchResponse::from_result(result)
|
||||||
|
.map_err(HubError::Network)
|
||||||
|
.check_status_code(HubError::ClientError, HubError::ServerError)
|
||||||
|
.map(|response| async {
|
||||||
|
response
|
||||||
|
.text()
|
||||||
.await
|
.await
|
||||||
.wrap_err("error with sending docker hub request")?;
|
.map_err(HubError::Network)
|
||||||
ensure!(
|
.and_then(|text| serde_json::from_str::<HubTagResponse>(&text).map_err(HubError::Parse))
|
||||||
!result.status().is_client_error(),
|
.map(|result| result.results)
|
||||||
"error with sending docker hub request {}/{}: {}", user, repo, result.status()
|
}).await.flatten()
|
||||||
);
|
|
||||||
ensure!(
|
|
||||||
!result.status().is_server_error(),
|
|
||||||
"docker hub request returned an error {}/{}: {}", user, repo, result.status()
|
|
||||||
);
|
|
||||||
Ok(result
|
|
||||||
.json::<HubTagResponse>()
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to parse hub response")?
|
|
||||||
.results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
145
src/main.rs
145
src/main.rs
|
|
@ -1,24 +1,27 @@
|
||||||
mod config;
|
mod config;
|
||||||
|
mod error;
|
||||||
|
mod fetcher;
|
||||||
mod hub;
|
mod hub;
|
||||||
|
|
||||||
use crate::config::{Config, FeedConfig};
|
use crate::config::{Config, FeedConfig};
|
||||||
use color_eyre::{
|
use crate::error::{FetchError, FetchFeedError, HubError, ParseFeedError};
|
||||||
eyre::{eyre, WrapErr},
|
use crate::fetcher::{next_fetch, CacheHeaders, FetchPlan, FetchResponse};
|
||||||
Result,
|
use main_error::MainResult;
|
||||||
};
|
use reqwest::{Client, Response};
|
||||||
use reqwest::Client;
|
|
||||||
use syndication::Feed;
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::future::ready;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tokio::time::sleep;
|
use std::time::{Duration};
|
||||||
use tokio::signal::ctrl_c;
|
use syndication::Feed;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing::{debug, error, info, instrument};
|
use tokio::signal::ctrl_c;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> MainResult {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
let mut args = std::env::args();
|
let mut args = std::env::args();
|
||||||
let bin = args.next().unwrap();
|
let bin = args.next().unwrap();
|
||||||
|
|
@ -33,9 +36,11 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let config = Config::from_file(&file)?;
|
let config = Config::from_file(&file)?;
|
||||||
|
|
||||||
println!("Running rss trigger for {} feeds", config.feed.len());
|
info!("Running rss trigger for {} feeds", config.feed.len());
|
||||||
|
|
||||||
let ctrl_c = async { ctrl_c().await.ok(); };
|
let ctrl_c = async {
|
||||||
|
ctrl_c().await.ok();
|
||||||
|
};
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
_ = ctrl_c => {},
|
_ = ctrl_c => {},
|
||||||
|
|
@ -45,7 +50,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn main_loop(config: Config) {
|
async fn main_loop(config: Config) {
|
||||||
let mut fetcher = FeedFetcher::default();
|
let mut fetcher = FeedFetcher::new(config.interval());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
for feed in config.feed.iter() {
|
for feed in config.feed.iter() {
|
||||||
|
|
@ -65,7 +70,9 @@ async fn main_loop(config: Config) {
|
||||||
#[instrument(skip_all, fields(feed = feed.feed))]
|
#[instrument(skip_all, fields(feed = feed.feed))]
|
||||||
async fn trigger(client: &Client, feed: &FeedConfig) {
|
async fn trigger(client: &Client, feed: &FeedConfig) {
|
||||||
info!("Triggering hook");
|
info!("Triggering hook");
|
||||||
let mut req = client.post(&feed.hook).header("user-agent", "rss-webhook-trigger");
|
let mut req = client
|
||||||
|
.post(&feed.hook)
|
||||||
|
.header("user-agent", "rss-webhook-trigger");
|
||||||
for (key, value) in &feed.headers {
|
for (key, value) in &feed.headers {
|
||||||
req = req.header(key, value);
|
req = req.header(key, value);
|
||||||
}
|
}
|
||||||
|
|
@ -78,76 +85,135 @@ async fn trigger(client: &Client, feed: &FeedConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct FeedFetcher {
|
pub struct FeedFetcher {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
base_interval: Duration,
|
||||||
cache: HashMap<String, u64>,
|
cache: HashMap<String, u64>,
|
||||||
|
fetch_plans: HashMap<String, FetchPlan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeedFetcher {
|
impl FeedFetcher {
|
||||||
#[instrument(skip(self))]
|
pub fn new(interval: Duration) -> Self {
|
||||||
pub async fn check_feed_updated(&mut self, feed: &str) -> Result<bool> {
|
FeedFetcher {
|
||||||
let new_key = self.get_feed_key(feed).await?;
|
client: Client::default(),
|
||||||
|
base_interval: interval,
|
||||||
|
cache: HashMap::default(),
|
||||||
|
fetch_plans: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(match self.cache.get_mut(feed) {
|
pub fn should_update(&self, feed: &str) -> bool {
|
||||||
Some(cached) => {
|
self.fetch_plans.get(feed).filter(|plan| FetchPlan::elapsed(plan)).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn check_feed_updated(&mut self, feed: &str) -> Result<bool, FetchError> {
|
||||||
|
if !self.should_update(feed) {
|
||||||
|
warn!("skipping feed util rate limited expires");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let plan = self.fetch_plans.remove(feed).unwrap_or_default();
|
||||||
|
|
||||||
|
let fetch_result = self.get_feed_key(feed, &plan.headers).await;
|
||||||
|
let new_key = match fetch_result.into_result() {
|
||||||
|
Ok((new_key, new_plan)) => {
|
||||||
|
self.fetch_plans.insert(feed.into(), next_fetch(self.base_interval, Some(new_plan)));
|
||||||
|
new_key
|
||||||
|
}
|
||||||
|
Err((err, new_plan)) => {
|
||||||
|
self.fetch_plans.insert(feed.into(), next_fetch(self.base_interval, Some(new_plan)));
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match (self.cache.get_mut(feed), new_key) {
|
||||||
|
(Some(cached), Some(new_key)) => {
|
||||||
debug!(cached, new_key, "checked existing feed");
|
debug!(cached, new_key, "checked existing feed");
|
||||||
if *cached != new_key {
|
if new_key != *cached {
|
||||||
*cached = new_key;
|
*cached = new_key;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
(None, Some(new_key)) => {
|
||||||
debug!(feed, "new feed");
|
debug!(feed, "new feed");
|
||||||
self.cache.insert(feed.into(), new_key);
|
self.cache.insert(feed.into(), new_key);
|
||||||
|
|
||||||
// don't trigger the actions on start
|
// don't trigger the actions on start
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
(_, None) => {
|
||||||
|
warn!("rate limited by server");
|
||||||
|
false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_feed_key(&self, feed: &str) -> Result<u64> {
|
async fn get_feed_key(
|
||||||
|
&self,
|
||||||
|
feed: &str,
|
||||||
|
cache_headers: &CacheHeaders,
|
||||||
|
) -> FetchResponse<u64, FetchError> {
|
||||||
if let Some(hub) = feed.strip_prefix("docker-hub://") {
|
if let Some(hub) = feed.strip_prefix("docker-hub://") {
|
||||||
if let Some((user, repo)) = hub.split_once('/') {
|
if let Some((user, repo)) = hub.split_once('/') {
|
||||||
let tags = hub::tags(&self.client, user, repo).await?;
|
hub::tags(&self.client, user, repo, cache_headers)
|
||||||
|
.await
|
||||||
|
.map(|tags| {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
tag.id.hash(&mut hasher);
|
tag.id.hash(&mut hasher);
|
||||||
tag.last_updated.hash(&mut hasher);
|
tag.last_updated.hash(&mut hasher);
|
||||||
}
|
}
|
||||||
|
ready(hasher.finish())
|
||||||
Ok(hasher.finish())
|
}).await
|
||||||
|
.map_err(FetchError::Hub)
|
||||||
} else {
|
} else {
|
||||||
Err(eyre!("Invalid hub format {}", feed))
|
FetchResponse::Error {
|
||||||
|
error: HubError::InvalidFormat.into(),
|
||||||
|
headers: CacheHeaders::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.get_rss_feed_key(feed).await
|
self.get_rss_feed_key(feed, cache_headers)
|
||||||
|
.await
|
||||||
|
.map_err(FetchError::Feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn get_rss_feed_key(&self, feed: &str) -> Result<u64> {
|
async fn get_rss_feed_key(
|
||||||
let content = self
|
&self,
|
||||||
|
feed: &str,
|
||||||
|
cache_headers: &CacheHeaders,
|
||||||
|
) -> FetchResponse<u64, FetchFeedError> {
|
||||||
|
let response = self
|
||||||
.client
|
.client
|
||||||
.get(feed)
|
.get(feed)
|
||||||
|
.headers(cache_headers.headers())
|
||||||
.send()
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let plan_result = FetchResponse::from_result(response);
|
||||||
|
plan_result
|
||||||
|
.map_err(FetchFeedError::Network)
|
||||||
|
.check_status_code(FetchFeedError::ClientError, FetchFeedError::ServerError)
|
||||||
|
.map(parse_rss_response)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| eyre!("Failed to load feed {}", feed))?
|
.flatten()
|
||||||
.text()
|
}
|
||||||
.await
|
}
|
||||||
.wrap_err_with(|| eyre!("Failed to load feed {}", feed))?;
|
|
||||||
let channel = Feed::from_str(&content)
|
async fn parse_rss_response(response: Response) -> Result<u64, FetchFeedError> {
|
||||||
.map_err(|_| eyre!("Failed to parse feed {}", feed))?;
|
let content = response.text().await?;
|
||||||
|
let channel = Feed::from_str(&content).map_err(ParseFeedError::Parse)?;
|
||||||
|
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
|
|
||||||
match channel {
|
match channel {
|
||||||
Feed::RSS(channel) => {
|
Feed::RSS(channel) => {
|
||||||
let item = channel.items.first().ok_or(eyre!("Empty feed"))?;
|
let item = channel.items.first().ok_or(ParseFeedError::Empty)?;
|
||||||
|
|
||||||
if let Some(guid) = item.guid() {
|
if let Some(guid) = item.guid() {
|
||||||
guid.value.hash(&mut hasher);
|
guid.value.hash(&mut hasher);
|
||||||
|
|
@ -156,15 +222,14 @@ impl FeedFetcher {
|
||||||
} else if let Some(link) = item.link() {
|
} else if let Some(link) = item.link() {
|
||||||
link.hash(&mut hasher);
|
link.hash(&mut hasher);
|
||||||
} else {
|
} else {
|
||||||
return Err(eyre!("No guid, pubDate or link set on feed item"));
|
return Err(ParseFeedError::MissingKey.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Feed::Atom(channel) => {
|
Feed::Atom(channel) => {
|
||||||
let item = channel.entries().first().ok_or(eyre!("Empty feed"))?;
|
let item = channel.entries().first().ok_or(ParseFeedError::Empty)?;
|
||||||
item.id().hash(&mut hasher);
|
item.id().hash(&mut hasher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(hasher.finish())
|
Ok(hasher.finish())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue