use crate::allowance::Allowance;
use crate::error::{Error, Result};
use crate::limits::Limits;
use crate::oid::Oid;
use crate::operation::Operation;
use crate::store::LfsStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Plan {
Transfer,
AlreadyPresent,
Missing,
}
impl Plan {
pub fn ssh_action(self, operation: Operation) -> &'static str {
match (self, operation) {
(Plan::Transfer, Operation::Upload) => "upload",
(Plan::Transfer, Operation::Download) => "download",
(Plan::AlreadyPresent, _) => "noop",
(Plan::Missing, _) => "download",
}
}
pub fn needs_transfer(self) -> bool {
matches!(self, Plan::Transfer)
}
}
pub fn plan_object(
store: &dyn LfsStore,
limits: &Limits,
allowance: Option<&Allowance>,
taken: u64,
operation: Operation,
oid: &Oid,
size: u64,
) -> Result<Plan> {
if operation.may_upload() && size > limits.max_object_bytes {
return Err(Error::TooLarge(format!(
"object {oid} is {size} bytes; this server accepts at most {}",
limits.max_object_bytes
)));
}
if operation.may_upload() {
if let Some(allowance) = allowance {
allowance.admit(taken, size)?;
}
}
let present = store.size_of(oid)?.is_some();
Ok(match (operation, present) {
(Operation::Upload, false) | (Operation::Download, true) => Plan::Transfer,
(Operation::Upload, true) => Plan::AlreadyPresent,
(Operation::Download, false) => Plan::Missing,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::MemLfsStore;
fn limits() -> Limits {
Limits::default()
}
#[test]
fn the_four_cells_are_distinct_and_only_the_wire_folds_two_of_them() {
let store = MemLfsStore::new();
let here = store.insert(b"here");
let gone = Oid::parse(&"a".repeat(64)).expect("literal oid");
let cell = |operation, oid: &Oid| {
plan_object(&store, &limits(), None, 0, operation, oid, 4).expect("no refusal")
};
assert_eq!(cell(Operation::Upload, &gone), Plan::Transfer);
assert_eq!(cell(Operation::Upload, &here), Plan::AlreadyPresent);
assert_eq!(cell(Operation::Download, &here), Plan::Transfer);
assert_eq!(
cell(Operation::Download, &gone),
Plan::Missing,
"a download of an object the server does not hold is MISSING, not `noop`: git-lfs \
reads an object with no action and no error as one it already has, and the fetch \
then reports success having transferred nothing"
);
assert_ne!(
Plan::AlreadyPresent.ssh_action(Operation::Upload),
Plan::Missing.ssh_action(Operation::Download),
"a download batch answered `noop` for an object the server does not hold; \
git-lfs skips a no-action object silently and the fetch exits 0"
);
assert_eq!(Plan::AlreadyPresent.ssh_action(Operation::Upload), "noop");
assert_eq!(
Plan::Missing.ssh_action(Operation::Download),
"download",
"the client must be given an action, or it never asks and never \
reaches the 404 that is the only per-object error this protocol has"
);
assert_eq!(Plan::Transfer.ssh_action(Operation::Upload), "upload");
assert_eq!(Plan::Transfer.ssh_action(Operation::Download), "download");
assert_ne!(
Plan::Transfer.ssh_action(Operation::Upload),
Plan::Transfer.ssh_action(Operation::Download)
);
}
#[test]
fn the_quota_is_refused_before_the_store_is_touched_and_only_on_upload() {
let store = MemLfsStore::new();
let oid = Oid::parse(&"b".repeat(64)).expect("literal oid");
let tight = limits().max_object_bytes(16);
let err = plan_object(&store, &tight, None, 0, Operation::Upload, &oid, 17)
.expect_err("17 bytes must not fit under a 16-byte quota");
assert_eq!(err.status(), 413);
assert_eq!(
plan_object(&store, &tight, None, 0, Operation::Upload, &oid, 16).expect("16 fits"),
Plan::Transfer
);
assert_eq!(
plan_object(&store, &tight, None, 0, Operation::Download, &oid, 1 << 40)
.expect("no refusal"),
Plan::Missing
);
}
}