1use std::path::Path;
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum ScanScheduleKind {
16 Webhook,
17 Poll,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "snake_case")]
22pub enum ScanScheduleProvider {
23 GitHub,
24 GitLab,
25 Bitbucket,
26 Any,
27}
28
29impl ScanScheduleProvider {
30 pub fn display_name(&self) -> &'static str {
31 match self {
32 Self::GitHub => "GitHub",
33 Self::GitLab => "GitLab",
34 Self::Bitbucket => "Bitbucket",
35 Self::Any => "Any / Poll",
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ScanSchedule {
44 pub id: Uuid,
45 pub label: String,
46 pub repo_url: String,
47 pub branch: String,
48 pub kind: ScanScheduleKind,
49 pub provider: ScanScheduleProvider,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub webhook_secret: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub interval_secs: Option<u64>,
54 pub last_scan_sha: Option<String>,
55 pub last_scan_at: Option<DateTime<Utc>>,
56 pub last_run_id: Option<String>,
57 pub enabled: bool,
58}
59
60impl ScanSchedule {
61 pub fn new_webhook(
62 repo_url: String,
63 branch: String,
64 provider: ScanScheduleProvider,
65 label: String,
66 ) -> Self {
67 Self {
68 id: Uuid::new_v4(),
69 label,
70 repo_url,
71 branch,
72 kind: ScanScheduleKind::Webhook,
73 provider,
74 webhook_secret: Some(generate_secret()),
75 interval_secs: None,
76 last_scan_sha: None,
77 last_scan_at: None,
78 last_run_id: None,
79 enabled: true,
80 }
81 }
82
83 pub fn new_poll(repo_url: String, branch: String, interval_secs: u64, label: String) -> Self {
84 Self {
85 id: Uuid::new_v4(),
86 label,
87 repo_url,
88 branch,
89 kind: ScanScheduleKind::Poll,
90 provider: ScanScheduleProvider::Any,
91 webhook_secret: None,
92 interval_secs: Some(interval_secs),
93 last_scan_sha: None,
94 last_scan_at: None,
95 last_run_id: None,
96 enabled: true,
97 }
98 }
99}
100
101fn generate_secret() -> String {
102 format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
103}
104
105#[derive(Debug, Default, Serialize, Deserialize)]
108pub struct ScheduleStore {
109 pub schedules: Vec<ScanSchedule>,
110}
111
112impl ScheduleStore {
113 pub fn load(path: &Path) -> Self {
114 std::fs::read_to_string(path)
115 .ok()
116 .and_then(|s| serde_json::from_str(&s).ok())
117 .unwrap_or_default()
118 }
119
120 pub fn save(&self, path: &Path) -> Result<()> {
121 let json = serde_json::to_string_pretty(self)?;
122 std::fs::write(path, json)?;
123 Ok(())
124 }
125
126 pub fn find_matching<'a>(&'a self, repo_url: &str, branch: &str) -> Vec<&'a ScanSchedule> {
127 self.schedules
128 .iter()
129 .filter(|s| s.enabled && urls_match(&s.repo_url, repo_url) && s.branch == branch)
130 .collect()
131 }
132
133 pub fn by_id_mut(&mut self, id: Uuid) -> Option<&mut ScanSchedule> {
134 self.schedules.iter_mut().find(|s| s.id == id)
135 }
136
137 pub fn remove(&mut self, id: Uuid) {
138 self.schedules.retain(|s| s.id != id);
139 }
140}
141
142fn urls_match(a: &str, b: &str) -> bool {
143 normalize_url(a) == normalize_url(b)
144}
145
146fn normalize_url(url: &str) -> String {
147 url.trim_end_matches('/')
148 .trim_end_matches(".git")
149 .to_lowercase()
150}