use crate::git;
use std::path::{Path, PathBuf};
pub const REPOSITORY: &str = "SEFY_GITHUB_REPO";
const AUTHOR_NAME: &str = "sefy";
const AUTHOR_EMAIL: &str = "sefy@localhost";
#[derive(Debug)]
pub enum Error {
BadRequest(String),
NoRepository,
NoDataDirectory,
NothingThere(String),
Io {
what: String,
source: std::io::Error,
},
Git(git::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BadRequest(reason) => {
write!(formatter, "the request could not be read: {reason}")
}
Self::NoRepository => write!(
formatter,
"{REPOSITORY} is not set\n\
point it at the repository to keep the vault in, for example:\n\
\x20 {REPOSITORY}=git@github.com:you/vault.git"
),
Self::NoDataDirectory => formatter.write_str(
"this system offers no per-user data directory to keep the working copy in",
),
Self::NothingThere(name) => write!(
formatter,
"the repository holds no copy called {name:?} yet\n\
push from a machine that has the vault first"
),
Self::Io { what, source } => write!(formatter, "{what}: {source}"),
Self::Git(error) => error.fmt(formatter),
}
}
}
impl std::error::Error for Error {}
impl From<git::Error> for Error {
fn from(error: git::Error) -> Self {
Self::Git(error)
}
}
#[derive(Debug, serde::Deserialize)]
struct Request {
operation: String,
file: PathBuf,
name: String,
}
pub fn handle(raw: &str) -> Result<String, Error> {
let request: Request =
serde_json::from_str(raw.trim()).map_err(|error| Error::BadRequest(error.to_string()))?;
let repository = std::env::var(REPOSITORY)
.ok()
.filter(|value| !value.trim().is_empty())
.ok_or(Error::NoRepository)?;
let working_copy = working_copy_path()?;
let checkout = prepare(&working_copy, &repository)?;
match request.operation.as_str() {
"push" => push(&checkout, &request),
"pull" => pull(&checkout, &request),
other => Err(Error::BadRequest(format!("unknown operation {other:?}"))),
}
}
fn prepare(working_copy: &Path, repository: &str) -> Result<PathBuf, Error> {
if working_copy.join(".git").is_dir() {
git::run(
Some(working_copy),
&["fetch", "--quiet", "origin"],
"fetching from the remote",
)?;
git::run(
Some(working_copy),
&["reset", "--quiet", "--hard", "origin/HEAD"],
"taking the remote's state",
)?;
return Ok(working_copy.to_path_buf());
}
if let Some(parent) = working_copy.parent() {
std::fs::create_dir_all(parent).map_err(|source| Error::Io {
what: format!("cannot create {}", parent.display()),
source,
})?;
}
git::run(
None,
&[
"clone",
"--quiet",
repository,
&working_copy.display().to_string(),
],
"cloning the repository",
)?;
Ok(working_copy.to_path_buf())
}
fn push(checkout: &Path, request: &Request) -> Result<String, Error> {
let destination = checkout.join(&request.name);
let incoming = std::fs::read(&request.file).map_err(|source| Error::Io {
what: "cannot read the vault".to_owned(),
source,
})?;
let present = std::fs::read(&destination).ok();
if present.as_deref() == Some(incoming.as_slice()) {
return Ok(format!(
"{:?} is already what the repository holds ({})",
request.name,
human(incoming.len() as u64)
));
}
std::fs::write(&destination, &incoming).map_err(|source| Error::Io {
what: "cannot write the vault into the working copy".to_owned(),
source,
})?;
git::run(
Some(checkout),
&["update-index", "--really-refresh"],
"refreshing what git knows about the working copy",
)
.ok();
git::run(
Some(checkout),
&["add", "--", &request.name],
"staging the vault",
)?;
git::run(
Some(checkout),
&[
"-c",
&format!("user.name={AUTHOR_NAME}"),
"-c",
&format!("user.email={AUTHOR_EMAIL}"),
"commit",
"--quiet",
"-m",
"update",
],
"recording the change",
)?;
git::run(
Some(checkout),
&["push", "--quiet"],
"pushing to the remote",
)?;
Ok(format!(
"pushed {:?} ({})",
request.name,
human(incoming.len() as u64)
))
}
fn pull(checkout: &Path, request: &Request) -> Result<String, Error> {
let source = checkout.join(&request.name);
if !source.is_file() {
return Err(Error::NothingThere(request.name.clone()));
}
std::fs::copy(&source, &request.file).map_err(|error| Error::Io {
what: "cannot write the fetched copy".to_owned(),
source: error,
})?;
let size = std::fs::metadata(&source).map(|data| data.len()).ok();
Ok(match size {
Some(bytes) => format!("fetched {:?} ({})", request.name, human(bytes)),
None => format!("fetched {:?}", request.name),
})
}
fn working_copy_path() -> Result<PathBuf, Error> {
if let Some(override_path) = std::env::var_os("SEFY_GITHUB_DIR")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
{
return Ok(override_path);
}
data_directory()
.map(|base| base.join("sefy-plugin-github").join("repository"))
.ok_or(Error::NoDataDirectory)
}
fn data_directory() -> Option<PathBuf> {
#[cfg(windows)]
{
std::env::var_os("APPDATA")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
#[cfg(target_os = "macos")]
{
std::env::var_os("HOME")
.filter(|value| !value.is_empty())
.map(|home| {
PathBuf::from(home)
.join("Library")
.join("Application Support")
})
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let from_xdg = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute());
from_xdg.or_else(|| {
std::env::var_os("HOME")
.filter(|value| !value.is_empty())
.map(|home| PathBuf::from(home).join(".local").join("share"))
})
}
}
fn human(bytes: u64) -> String {
const KIB: f64 = 1024.0;
let bytes = bytes as f64;
if bytes < KIB {
return format!("{bytes:.0} B");
}
if bytes < KIB * KIB {
return format!("{:.1} KiB", bytes / KIB);
}
format!("{:.1} MiB", bytes / (KIB * KIB))
}