use std::io::{Read, Write};
use crate::error::{Error, Result};
use crate::oid::Oid;
pub struct LfsObject {
pub size: u64,
pub reader: Box<dyn Read + Send>,
}
impl std::fmt::Debug for LfsObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LfsObject")
.field("size", &self.size)
.finish()
}
}
pub trait PendingWrite: Write + Send {
fn commit(self: Box<Self>) -> Result<()>;
}
pub trait LfsStore: Send + Sync {
fn size_of(&self, oid: &Oid) -> Result<Option<u64>>;
fn open(&self, oid: &Oid) -> Result<Option<LfsObject>>;
fn open_at(&self, oid: &Oid, offset: u64) -> Result<Option<LfsObject>> {
let Some(mut object) = self.open(oid)? else {
return Ok(None);
};
if offset > 0 {
let skipped =
std::io::copy(&mut (&mut object.reader).take(offset), &mut std::io::sink())
.map_err(|err| Error::store_io(format!("skip to {offset} in {oid}"), err))?;
if skipped < offset.min(object.size) {
return Err(Error::store_io(
format!("skip to {offset} in {oid}"),
std::io::Error::from(std::io::ErrorKind::UnexpectedEof),
));
}
}
Ok(Some(object))
}
fn begin_write(&self, oid: &Oid, declared_size: u64) -> Result<Box<dyn PendingWrite + '_>>;
fn delete(&self, oid: &Oid) -> Result<bool> {
Err(Error::Io(std::io::Error::other(format!(
"this LFS store cannot delete objects (asked for {oid})"
))))
}
}
impl<T: LfsStore + ?Sized> LfsStore for std::sync::Arc<T> {
fn size_of(&self, oid: &Oid) -> Result<Option<u64>> {
(**self).size_of(oid)
}
fn open(&self, oid: &Oid) -> Result<Option<LfsObject>> {
(**self).open(oid)
}
fn open_at(&self, oid: &Oid, offset: u64) -> Result<Option<LfsObject>> {
(**self).open_at(oid, offset)
}
fn begin_write(&self, oid: &Oid, declared_size: u64) -> Result<Box<dyn PendingWrite + '_>> {
(**self).begin_write(oid, declared_size)
}
fn delete(&self, oid: &Oid) -> Result<bool> {
(**self).delete(oid)
}
}
impl<T: LfsStore + ?Sized> LfsStore for &T {
fn size_of(&self, oid: &Oid) -> Result<Option<u64>> {
(**self).size_of(oid)
}
fn open(&self, oid: &Oid) -> Result<Option<LfsObject>> {
(**self).open(oid)
}
fn open_at(&self, oid: &Oid, offset: u64) -> Result<Option<LfsObject>> {
(**self).open_at(oid, offset)
}
fn begin_write(&self, oid: &Oid, declared_size: u64) -> Result<Box<dyn PendingWrite + '_>> {
(**self).begin_write(oid, declared_size)
}
fn delete(&self, oid: &Oid) -> Result<bool> {
(**self).delete(oid)
}
}
#[derive(Debug)]
pub struct MemLfsStore {
objects: std::sync::Mutex<std::collections::BTreeMap<Oid, Vec<u8>>>,
ceiling: usize,
}
pub const MEM_STORE_CEILING: usize = 64 * 1024 * 1024;
impl Default for MemLfsStore {
fn default() -> Self {
MemLfsStore::new()
}
}
impl MemLfsStore {
pub fn new() -> Self {
MemLfsStore::with_ceiling(MEM_STORE_CEILING)
}
pub fn with_ceiling(ceiling: usize) -> Self {
MemLfsStore {
objects: std::sync::Mutex::new(std::collections::BTreeMap::new()),
ceiling,
}
}
pub fn insert(&self, bytes: &[u8]) -> Oid {
let oid = Oid::of(bytes);
self.objects
.lock()
.expect("lock")
.insert(oid, bytes.to_vec());
oid
}
pub fn get(&self, oid: &Oid) -> Option<Vec<u8>> {
self.objects.lock().expect("lock").get(oid).cloned()
}
pub fn len(&self) -> usize {
self.objects.lock().expect("lock").len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn held_bytes(&self) -> usize {
self.objects
.lock()
.expect("lock")
.values()
.map(Vec::len)
.sum()
}
}
impl LfsStore for MemLfsStore {
fn size_of(&self, oid: &Oid) -> Result<Option<u64>> {
Ok(self
.objects
.lock()
.expect("lock")
.get(oid)
.map(|bytes| bytes.len() as u64))
}
fn open(&self, oid: &Oid) -> Result<Option<LfsObject>> {
let bytes = match self.objects.lock().expect("lock").get(oid) {
Some(bytes) => bytes.clone(),
None => return Ok(None),
};
Ok(Some(LfsObject {
size: bytes.len() as u64,
reader: Box::new(std::io::Cursor::new(bytes)),
}))
}
fn begin_write(&self, oid: &Oid, declared_size: u64) -> Result<Box<dyn PendingWrite + '_>> {
let held = self.held_bytes() as u64;
if held + declared_size > self.ceiling as u64 {
return Err(Error::TooLarge(format!(
"the in-memory lfs store holds {held} bytes and its ceiling is {}; \
a {declared_size}-byte object does not fit. Use a streaming backend.",
self.ceiling
)));
}
Ok(Box::new(MemPendingWrite {
store: self,
oid: *oid,
buf: Vec::new(),
}))
}
}
struct MemPendingWrite<'a> {
store: &'a MemLfsStore,
oid: Oid,
buf: Vec<u8>,
}
impl Write for MemPendingWrite<'_> {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
if self.buf.len() + data.len() > self.store.ceiling {
return Err(std::io::Error::other(
"the in-memory lfs store's ceiling was reached mid-upload",
));
}
self.buf.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl PendingWrite for MemPendingWrite<'_> {
fn commit(self: Box<Self>) -> Result<()> {
self.store
.objects
.lock()
.expect("lock")
.insert(self.oid, self.buf);
Ok(())
}
}
#[derive(Debug, Default)]
pub struct DiscardingLfsStore {
accepted: std::sync::atomic::AtomicU64,
committed: std::sync::atomic::AtomicU64,
}
impl DiscardingLfsStore {
pub fn new() -> Self {
Self::default()
}
pub fn accepted(&self) -> u64 {
self.accepted.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn committed(&self) -> u64 {
self.committed.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl LfsStore for DiscardingLfsStore {
fn size_of(&self, _oid: &Oid) -> Result<Option<u64>> {
Ok(None)
}
fn open(&self, _oid: &Oid) -> Result<Option<LfsObject>> {
Ok(None)
}
fn begin_write(&self, _oid: &Oid, _declared_size: u64) -> Result<Box<dyn PendingWrite + '_>> {
Ok(Box::new(DiscardingWrite { store: self }))
}
}
struct DiscardingWrite<'a> {
store: &'a DiscardingLfsStore,
}
impl Write for DiscardingWrite<'_> {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.store
.accepted
.fetch_add(data.len() as u64, std::sync::atomic::Ordering::Relaxed);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl PendingWrite for DiscardingWrite<'_> {
fn commit(self: Box<Self>) -> Result<()> {
self.store
.committed
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_at_starts_the_reader_at_the_offset() {
let store = MemLfsStore::new();
let oid = store.insert(b"0123456789");
let mut object = store.open_at(&oid, 7).unwrap().expect("present");
assert_eq!(object.size, 10, "the whole object's size");
let mut out = Vec::new();
std::io::copy(&mut object.reader, &mut out).unwrap();
assert_eq!(out, b"789");
}
#[test]
fn a_mem_store_round_trips_an_object() {
let store = MemLfsStore::new();
let oid = store.insert(b"hello lfs");
assert_eq!(store.size_of(&oid).unwrap(), Some(9));
let mut out = Vec::new();
let mut object = store.open(&oid).unwrap().expect("present");
assert_eq!(object.size, 9);
std::io::copy(&mut object.reader, &mut out).unwrap();
assert_eq!(out, b"hello lfs");
let absent = Oid::parse(&"a".repeat(64)).unwrap();
assert_eq!(store.size_of(&absent).unwrap(), None);
assert!(store.open(&absent).unwrap().is_none());
}
#[test]
fn the_mem_store_refuses_before_the_bytes_arrive() {
let store = MemLfsStore::with_ceiling(1024);
let oid = Oid::parse(&"b".repeat(64)).unwrap();
let err = match store.begin_write(&oid, 4096) {
Err(err) => err,
Ok(_) => panic!("4 KiB must not fit under a 1 KiB ceiling"),
};
assert_eq!(err.status(), 413);
assert!(store.begin_write(&oid, 512).is_ok());
}
#[test]
fn an_uncommitted_write_is_not_visible() {
let store = MemLfsStore::new();
let oid = Oid::parse(&"c".repeat(64)).unwrap();
{
let mut pending = store.begin_write(&oid, 3).unwrap();
pending.write_all(b"abc").unwrap();
}
assert!(store.open(&oid).unwrap().is_none());
assert!(store.is_empty());
}
}