use crate::Result;
use crate::storage::Storage;
const TEMP_DIR: &str = ".hoodie/.temp";
const MARKER_TYPE_FILENAME: &str = "MARKERS.type";
const MARKERS_FILENAME_PREFIX: &str = "MARKERS";
const MARKER_EXTENSION: &str = ".marker.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MarkerIoType {
Create,
Merge,
}
impl MarkerIoType {
fn as_str(&self) -> &'static str {
match self {
MarkerIoType::Create => "CREATE",
MarkerIoType::Merge => "MERGE",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Marker {
pub partition_path: String,
pub file_name: String,
pub io_type: MarkerIoType,
}
impl Marker {
pub(crate) fn create(partition_path: &str, file_name: &str) -> Self {
Self {
partition_path: partition_path.to_string(),
file_name: file_name.to_string(),
io_type: MarkerIoType::Create,
}
}
pub(crate) fn merge(partition_path: &str, file_name: &str) -> Self {
Self {
partition_path: partition_path.to_string(),
file_name: file_name.to_string(),
io_type: MarkerIoType::Merge,
}
}
fn name(&self) -> String {
let suffix = format!(
"{}{}{}",
self.file_name,
MARKER_EXTENSION,
self.io_type.as_str()
);
if self.partition_path.is_empty() {
suffix
} else {
format!("{}/{suffix}", self.partition_path)
}
}
}
fn marker_dir(instant: &str) -> String {
format!("{TEMP_DIR}/{instant}")
}
pub(crate) async fn write_markers(
storage: &Storage,
instant: &str,
markers: &[Marker],
) -> Result<()> {
if markers.is_empty() {
return Ok(());
}
let dir = marker_dir(instant);
storage
.put_file(
&format!("{dir}/{MARKER_TYPE_FILENAME}"),
b"TIMELINE_SERVER_BASED".to_vec(),
)
.await?;
let mut body = String::new();
for marker in markers {
body.push_str(&marker.name());
body.push('\n');
}
storage
.put_file(
&format!("{dir}/{MARKERS_FILENAME_PREFIX}0"),
body.into_bytes(),
)
.await?;
Ok(())
}
pub(crate) async fn read_markers(storage: &Storage, instant: &str) -> Result<Vec<String>> {
let dir = marker_dir(instant);
let listed = match storage.list_files(Some(&dir)).await {
Ok(files) => files,
Err(crate::storage::error::StorageError::ObjectStoreError(
object_store::Error::NotFound { .. },
)) => return Ok(Vec::new()),
Err(error) => return Err(error.into()),
};
let mut markers = Vec::new();
let mut has_markers_files = false;
for file in &listed {
if file.name.starts_with(MARKERS_FILENAME_PREFIX) && file.name != MARKER_TYPE_FILENAME {
has_markers_files = true;
let bytes = storage
.get_file_data(&format!("{dir}/{}", file.name))
.await?;
let text = String::from_utf8_lossy(&bytes);
markers.extend(
text.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(String::from),
);
}
}
if has_markers_files {
return Ok(markers);
}
let mut markers: Vec<String> = list_files_recursive(storage, &dir)
.await?
.into_iter()
.filter(|name| name.contains(MARKER_EXTENSION))
.collect();
markers.sort();
markers.dedup();
Ok(markers)
}
async fn list_files_recursive(storage: &Storage, dir: &str) -> Result<Vec<String>> {
let mut out = Vec::new();
let mut stack = vec![String::new()];
while let Some(rel) = stack.pop() {
let full = if rel.is_empty() {
dir.to_string()
} else {
format!("{dir}/{rel}")
};
match storage.list_files(Some(&full)).await {
Ok(files) => {
for file in files {
out.push(if rel.is_empty() {
file.name
} else {
format!("{rel}/{}", file.name)
});
}
}
Err(crate::storage::error::StorageError::ObjectStoreError(
object_store::Error::NotFound { .. },
)) => {}
Err(error) => return Err(error.into()),
}
match storage.list_dirs(Some(&full)).await {
Ok(dirs) => {
for child in dirs {
stack.push(if rel.is_empty() {
child
} else {
format!("{rel}/{child}")
});
}
}
Err(crate::storage::error::StorageError::ObjectStoreError(
object_store::Error::NotFound { .. },
)) => {}
Err(error) => return Err(error.into()),
}
}
Ok(out)
}
pub(crate) async fn delete_marker_dir(storage: &Storage, instant: &str) {
let dir = marker_dir(instant);
if let Ok(names) = list_files_recursive(storage, &dir).await {
for name in names {
let _ = storage.delete_file(&format!("{dir}/{name}")).await;
}
}
}
pub(crate) fn parse_marker_name(marker: &str) -> Option<(String, String)> {
let idx = marker.rfind(MARKER_EXTENSION)?;
let file_path = marker[..idx].to_string();
let io_type = marker[idx + MARKER_EXTENSION.len()..].to_string();
Some((file_path, io_type))
}