use std::sync::Arc;
use crate::control::ControlStore;
use crate::doc::{impl_doc, DocRef, ShardedMap};
use crate::error::Result;
use crate::sandbox::RepoRef;
pub const MIRROR_REF: &str = "refs/gunnar/mirrors";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MirrorCredentialKind {
#[default]
None,
SshKey,
Token,
}
impl MirrorCredentialKind {
pub fn is_set(self) -> bool {
!matches!(self, MirrorCredentialKind::None)
}
pub fn name(self) -> &'static str {
match self {
MirrorCredentialKind::None => "none",
MirrorCredentialKind::SshKey => "an ssh private key",
MirrorCredentialKind::Token => "a token",
}
}
}
pub fn credential_secret_key(repo: &RepoRef) -> String {
format!("mirror/{}", repo.key())
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MirrorRecord {
pub repo: String,
pub remote_url: String,
pub prune: bool,
pub added_at_unix_ms: i64,
pub last_sync_at_unix_ms: Option<i64>,
pub last_error: String,
pub sync_count: u64,
#[serde(default)]
pub credential: MirrorCredentialKind,
#[serde(default)]
pub credential_username: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub creates_repo: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub lfs_detected: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_started_at_unix_ms: Option<i64>,
#[serde(default, skip_serializing_if = "is_zero")]
pub interrupted: u32,
}
fn is_zero(n: &u32) -> bool {
*n == 0
}
pub const MAX_RESUMES: u32 = 3;
impl MirrorRecord {
pub fn new(
repo: &RepoRef,
remote_url: impl Into<String>,
prune: bool,
now_unix_ms: i64,
) -> Self {
MirrorRecord {
repo: repo.key(),
remote_url: remote_url.into(),
prune,
added_at_unix_ms: now_unix_ms,
last_sync_at_unix_ms: None,
last_error: String::new(),
sync_count: 0,
credential: MirrorCredentialKind::None,
credential_username: String::new(),
creates_repo: false,
lfs_detected: false,
sync_started_at_unix_ms: None,
interrupted: 0,
}
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct MirrorDoc {
#[serde(default)]
repos: ShardedMap<MirrorRecord>,
}
impl_doc!(MirrorDoc { repos });
#[derive(Clone, Debug)]
pub struct MirrorStore {
doc: DocRef<MirrorDoc>,
}
impl MirrorStore {
pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
Ok(MirrorStore {
doc: DocRef::new(store, MIRROR_REF)?,
})
}
pub fn ref_name(&self) -> &str {
self.doc.ref_name()
}
pub fn get(&self, repo: &RepoRef) -> Result<Option<MirrorRecord>> {
self.doc.load()?.repos.get(&repo.key())
}
pub fn all(&self) -> Result<Vec<MirrorRecord>> {
self.doc.load()?.repos.values()
}
pub fn set(&self, repo: &RepoRef, record: MirrorRecord) -> Result<MirrorRecord> {
let key = repo.key();
self.doc.update(|doc| {
doc.repos.insert(key.clone(), record.clone())?;
Ok(record.clone())
})
}
pub fn record_sync(
&self,
repo: &RepoRef,
at_unix_ms: i64,
error: Option<&str>,
) -> Result<Option<MirrorRecord>> {
let key = repo.key();
let error = error.map(str::to_owned);
self.doc.update(|doc| {
let Some(mut record) = doc.repos.get(&key)? else {
return Ok(None);
};
record.sync_started_at_unix_ms = None;
record.interrupted = 0;
match &error {
None => {
record.last_sync_at_unix_ms = Some(at_unix_ms);
record.last_error.clear();
record.sync_count += 1;
record.creates_repo = false;
}
Some(why) => record.last_error = why.clone(),
}
doc.repos.insert(key.clone(), record.clone())?;
Ok(Some(record))
})
}
pub fn mark_started(&self, repo: &RepoRef, at_unix_ms: i64) -> Result<Option<MirrorRecord>> {
let key = repo.key();
self.doc.update(|doc| {
let Some(mut record) = doc.repos.get(&key)? else {
return Ok(None);
};
record.sync_started_at_unix_ms = Some(at_unix_ms);
doc.repos.insert(key.clone(), record.clone())?;
Ok(Some(record))
})
}
pub fn settle_interrupted(&self, at_unix_ms: i64) -> Result<Vec<MirrorRecord>> {
self.doc.update(|doc| {
let mut settled = Vec::new();
for record in doc.repos.values()? {
let Some(started) = record.sync_started_at_unix_ms else {
continue;
};
let mut record = record;
record.sync_started_at_unix_ms = None;
record.interrupted = record.interrupted.saturating_add(1);
let minutes = (at_unix_ms - started).max(0) / 60_000;
record.last_error = if record.interrupted > MAX_RESUMES {
format!(
"The import stopped because the server restarted while it ran, {} times \
in a row (the last one {minutes} minute(s) in). It is not tried again by \
itself: this repository may be too big for this server's memory or \
disk. Press Sync to try once more.",
record.interrupted
)
} else {
format!(
"The import stopped because the server restarted while it ran \
({minutes} minute(s) in). It starts again by itself."
)
};
doc.repos.insert(record.repo.clone(), record.clone())?;
settled.push(record);
}
Ok(settled)
})
}
pub fn set_credential(
&self,
repo: &RepoRef,
kind: MirrorCredentialKind,
username: &str,
) -> Result<Option<MirrorRecord>> {
let key = repo.key();
let username = username.to_owned();
self.doc.update(|doc| {
let Some(mut record) = doc.repos.get(&key)? else {
return Ok(None);
};
record.credential = kind;
record.credential_username = if kind == MirrorCredentialKind::Token {
username.clone()
} else {
String::new()
};
doc.repos.insert(key.clone(), record.clone())?;
Ok(Some(record))
})
}
pub fn set_lfs_detected(&self, repo: &RepoRef, detected: bool) -> Result<Option<MirrorRecord>> {
let key = repo.key();
self.doc.update(|doc| {
let Some(mut record) = doc.repos.get(&key)? else {
return Ok(None);
};
if record.lfs_detected != detected {
record.lfs_detected = detected;
doc.repos.insert(key.clone(), record.clone())?;
}
Ok(Some(record))
})
}
pub fn remove(&self, repo: &RepoRef) -> Result<bool> {
let key = repo.key();
self.doc.update(|doc| Ok(doc.repos.remove(&key)?.is_some()))
}
}