use std::{
cmp,
io::{self, Write},
mem,
path::PathBuf,
};
use block_on_place::HandleExt;
use opendal::FuturesAsyncWriter;
use tantivy::directory::{AntiCallToken, TerminatingWrite};
use tokio::{io::AsyncWriteExt, runtime::Handle};
use tokio_util::compat::{Compat, FuturesAsyncWriteCompatExt};
use crate::empty::Empty;
pub(crate) type Sink = Box<dyn TerminatingWrite + Send + Sync>;
pub(crate) type OpenSink = Box<dyn FnOnce() -> io::Result<Sink> + Send + Sync>;
pub(crate) type OnDone = Box<dyn FnOnce(Outcome) -> io::Result<()> + Send + Sync>;
pub(crate) enum Outcome {
Empty(Empty),
Standalone,
Bundled(Vec<u8>),
}
pub(crate) struct Writer {
path: PathBuf,
cap: usize,
bundle: bool,
state: State,
open: Option<OpenSink>,
on_done: Option<OnDone>,
}
enum State {
Buffering(Vec<u8>),
Streaming(Sink),
Done,
}
impl Writer {
pub fn new(path: PathBuf, cap: usize, bundle: bool, open: OpenSink, on_done: OnDone) -> Self {
Self {
path,
cap: cmp::max(cap, Empty::max_len()),
bundle,
state: State::Buffering(Vec::new()),
open: Some(open),
on_done: Some(on_done),
}
}
fn ensure_streaming(&mut self) -> io::Result<()> {
if let State::Buffering(buffer) = &mut self.state {
let buffer = mem::take(buffer);
let open = self
.open
.take()
.expect("the downstream sink can only be opened once");
let mut sink = open()?;
sink.write_all(&buffer)?;
self.state = State::Streaming(sink);
}
Ok(())
}
}
impl Write for Writer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let State::Buffering(buffer) = &mut self.state
&& buffer.len() + buf.len() <= self.cap
{
buffer.extend_from_slice(buf);
return Ok(buf.len());
}
self.ensure_streaming()?;
match &mut self.state {
State::Streaming(sink) => {
sink.write_all(buf)?;
Ok(buf.len())
}
State::Buffering(_) => unreachable!("ensure_streaming did not start streaming"),
State::Done => Err(io::Error::other("write after the file was finalized")),
}
}
fn flush(&mut self) -> io::Result<()> {
match &mut self.state {
State::Buffering(_) | State::Done => Ok(()),
State::Streaming(sink) => sink.flush(),
}
}
}
impl TerminatingWrite for Writer {
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()> {
let outcome = match mem::replace(&mut self.state, State::Done) {
State::Buffering(buffer) => {
if let Some(empty) = Empty::detect(&self.path, &buffer) {
Outcome::Empty(empty)
} else if self.bundle {
Outcome::Bundled(buffer)
} else {
let open = self
.open
.take()
.expect("the downstream sink can only be opened once");
let mut sink = open()?;
sink.write_all(&buffer)?;
sink.terminate()?;
Outcome::Standalone
}
}
State::Streaming(sink) => {
sink.terminate()?;
Outcome::Standalone
}
State::Done => return Err(io::Error::other("the file was already finalized")),
};
let on_done = self
.on_done
.take()
.expect("a file can only be finalized once");
on_done(outcome)
}
}
pub(crate) struct OpendalSink {
rt: Handle,
writer: Compat<FuturesAsyncWriter>,
}
impl OpendalSink {
pub fn boxed(writer: opendal::Writer, rt: Handle) -> Sink {
let writer = writer.into_futures_async_write().compat_write();
Box::new(Self { rt, writer })
}
}
impl Write for OpendalSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.rt
.block_on_place(async { self.writer.write(buf).await })
}
fn flush(&mut self) -> io::Result<()> {
self.rt.block_on_place(async { self.writer.flush().await })
}
}
impl TerminatingWrite for OpendalSink {
fn terminate_ref(&mut self, _: AntiCallToken) -> io::Result<()> {
self.rt
.clone()
.block_on_place(async { self.writer.shutdown().await })
}
}