use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use ffi_sdk::BoxedAttachmentHandle;
use safer_ffi::prelude::*;
use serde::ser::SerializeMap;
use super::ditto_attachment_token::DittoAttachmentToken;
use crate::{
ditto::{TryUpgrade, WeakDittoHandleWrapper},
prelude::DittoError,
utils::prelude::ErrorKind,
};
pub mod fetch_event;
pub mod fetcher;
pub mod token;
#[derive(Debug)]
pub struct DittoAttachment {
id: Box<[u8]>,
len: u64,
metadata: HashMap<String, String>,
ditto: WeakDittoHandleWrapper,
attachment_handle: BoxedAttachmentHandle,
}
impl DittoAttachment {
pub fn id(&self) -> String {
crate::utils::base64_encode_unpadded(&self.id)
}
pub fn len(&self) -> u64 {
self.len
}
pub fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
}
impl serde::Serialize for DittoAttachment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry(
"_ditto_internal_type_jkb12973t4b",
&(::ffi_sdk::DittoCrdtType::Attachment as u64),
)?;
map.serialize_entry("_id", ::serde_bytes::Bytes::new(&self.id[..]))?;
map.serialize_entry("_len", &self.len)?;
map.serialize_entry("_meta", &self.metadata)?;
map.end()
}
}
impl DittoAttachment {
pub(crate) fn new(
id: Box<[u8]>,
len: u64,
metadata: HashMap<String, String>,
ditto: WeakDittoHandleWrapper,
attachment_handle: BoxedAttachmentHandle,
) -> Self {
Self {
id,
len,
metadata,
ditto,
attachment_handle,
}
}
pub(crate) fn from_file_and_metadata(
filepath: &(impl ?Sized + AsRef<Path>),
metadata: HashMap<String, String>,
ditto: &Arc<ffi_sdk::BoxedDitto>,
) -> Result<DittoAttachment, DittoError> {
let source_path = char_p::new(filepath.as_ref().to_str().unwrap());
let file_operation = ffi_sdk::AttachmentFileOperation::Copy;
let mut slot = ::core::mem::MaybeUninit::<ffi_sdk::Attachment>::uninit();
let status = {
ffi_sdk::ditto_new_attachment_from_file(
ditto,
source_path.as_ref(),
file_operation,
slot.as_out(),
)
};
if status != 0 {
Err(DittoError::from_ffi(ErrorKind::InvalidInput))
} else {
let attachment = unsafe { slot.assume_init() }; let ret = DittoAttachment::new(
attachment.id.into(),
attachment.len,
metadata,
Arc::downgrade(ditto),
attachment.handle,
);
Ok(ret)
}
}
pub(crate) fn from_bytes_and_metadata(
bytes: &(impl ?Sized + AsRef<[u8]>),
metadata: HashMap<String, String>,
ditto: &Arc<ffi_sdk::BoxedDitto>,
) -> Result<DittoAttachment, DittoError> {
let mut slot = ::core::mem::MaybeUninit::<ffi_sdk::Attachment>::uninit();
let status = {
ffi_sdk::ditto_new_attachment_from_bytes(ditto, bytes.as_ref().into(), slot.as_out())
};
if status != 0 {
Err(DittoError::from_ffi(ErrorKind::InvalidInput))
} else {
let attachment = unsafe { slot.assume_init() }; let ret = DittoAttachment::new(
attachment.id.into(),
attachment.len,
metadata,
Arc::downgrade(ditto),
attachment.handle,
);
Ok(ret)
}
}
pub(crate) fn new_with_token(
token: DittoAttachmentToken,
ditto: WeakDittoHandleWrapper,
attachment_handle: BoxedAttachmentHandle,
) -> Self {
Self {
id: token.id,
len: token.len,
metadata: token.metadata,
ditto,
attachment_handle,
}
}
pub fn path(&self) -> PathBuf {
let ditto = self.ditto.try_upgrade().unwrap();
let p = ffi_sdk::ditto_get_complete_attachment_path(&ditto, &self.attachment_handle);
let p_string = p.to_string();
p_string.into()
}
}
#[cfg(test)]
mod tests {
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use serde_json::json;
use crate::{
prelude::*,
store::{
ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
ditto_attachment_token::DittoAttachmentToken,
},
test_helpers::setup_ditto,
};
#[::tokio::test]
async fn attachment_serialize() {
let ditto = setup_ditto().unwrap();
let store = ditto.store();
let collection = store.collection("test").unwrap();
let original_test_file_path = "tests/data/attachment_file_1.txt";
let metadata = {
let mut m = HashMap::new();
m.insert("key_1".to_string(), "value_1".to_string());
m.insert("key_2".to_string(), "value_2".to_string());
m
};
let attachment = store
.new_attachment(original_test_file_path, metadata.clone())
.await
.expect("new_attachment");
let attachment_id = attachment.id.clone();
let attachment_len = attachment.len;
let attachment_file_path = attachment.path();
assert_ne!(
original_test_file_path,
attachment_file_path
.clone()
.into_os_string()
.into_string()
.unwrap()
);
let id = collection
.upsert(json!({ "hello": "again", "att": attachment }))
.unwrap();
{
let mut map = HashMap::new();
map.insert("hello", serde_cbor::value::to_value("again").unwrap());
map.insert("att", serde_cbor::value::to_value(&attachment).unwrap());
let _other = collection.upsert(map).unwrap();
}
let mut doc = collection.find_by_id(id).exec().unwrap();
let set = doc.set("att_two", &attachment);
assert!(set.is_ok());
let attachment_token = doc.get::<DittoAttachmentToken>("att").unwrap();
assert_eq!(attachment_token.id, attachment_id);
assert_eq!(attachment_token.len, attachment_len);
assert_eq!(attachment_token.metadata, metadata);
let attachment_token_two = doc.get::<DittoAttachmentToken>("att_two").unwrap();
assert_eq!(attachment_token.id, attachment_token_two.id);
assert_eq!(attachment_token.len, attachment_token_two.len);
assert_eq!(attachment_token.metadata, attachment_token_two.metadata);
let test_file = std::fs::read(original_test_file_path).unwrap();
let attachment_file = std::fs::read(attachment_file_path).unwrap();
assert_eq!(test_file, attachment_file);
assert_eq!(test_file.len() as u64, attachment_len);
}
#[test]
fn attachment_fetch_legacy() {
#![allow(deprecated)]
let ditto = setup_ditto().unwrap();
let store = ditto.store();
let collection = store.collection("test").unwrap();
let original_test_file_path = "tests/data/attachment_file_1.txt";
let attachment = collection
.new_attachment(original_test_file_path, HashMap::new())
.expect("new_attachment");
let collection = store.collection("test").unwrap();
let id = collection.upsert(json!({"hello": "again"})).unwrap();
let mut doc = collection.find_by_id(id).exec().unwrap();
let set = doc.set("att", attachment);
assert!(set.is_ok());
let attachment_token = doc.get::<DittoAttachmentToken>("att").unwrap();
let finished = Arc::new(AtomicBool::new(false));
let finished_clone = Arc::clone(&finished);
let fetched_attachment_data: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));
let _fetcher = collection
.fetch_attachment(attachment_token, |event| {
if let DittoAttachmentFetchEvent::Completed { attachment } = event {
let att_data_mtx = &*fetched_attachment_data; if let Ok(mut fetched_attachment_data) = att_data_mtx.lock() {
*fetched_attachment_data = std::fs::read(attachment.path()).unwrap();
finished_clone.store(true, Ordering::SeqCst);
}
}
})
.unwrap();
while !finished.load(Ordering::SeqCst) {
std::thread::yield_now();
}
let test_file_data = std::fs::read(original_test_file_path).unwrap();
let fetched_att_data = fetched_attachment_data.lock().unwrap();
assert_eq!(test_file_data, *fetched_att_data);
}
#[::tokio::test]
async fn attachment_fetch() {
let ditto = setup_ditto().unwrap();
let store = ditto.store();
let collection = store.collection("test").unwrap();
let original_test_file_path = "tests/data/attachment_file_1.txt";
let attachment = store
.new_attachment(original_test_file_path, HashMap::new())
.await
.expect("new_attachment");
let id = collection.upsert(json!({"hello": "again"})).unwrap();
let mut doc = collection.find_by_id(id).exec().unwrap();
let set = doc.set("att", attachment);
assert!(set.is_ok());
let attachment_token = doc.get::<DittoAttachmentToken>("att").unwrap();
let finished = Arc::new(AtomicBool::new(false));
let finished_clone = Arc::clone(&finished);
let fetched_attachment_data: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));
let _fetcher = store
.fetch_attachment(attachment_token, {
let fetched_attachment_data = fetched_attachment_data.clone();
move |event| {
if let DittoAttachmentFetchEvent::Completed { attachment } = event {
let att_data_mtx = &*fetched_attachment_data; if let Ok(mut fetched_attachment_data) = att_data_mtx.lock() {
*fetched_attachment_data = std::fs::read(attachment.path()).unwrap();
finished_clone.store(true, Ordering::SeqCst);
}
}
}
})
.unwrap();
while !finished.load(Ordering::SeqCst) {
std::thread::yield_now();
}
let test_file_data = std::fs::read(original_test_file_path).unwrap();
let fetched_att_data = fetched_attachment_data.lock().unwrap();
assert_eq!(test_file_data, *fetched_att_data);
}
}