use std::collections::HashMap;
use async_trait::async_trait;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::path::RemotePath;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectVersion {
pub key: String,
pub version_id: String,
pub is_latest: bool,
pub is_delete_marker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_modified: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub etag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectInfo {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size_human: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_modified: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub etag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_class: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
pub is_dir: bool,
}
impl ObjectInfo {
pub fn file(key: impl Into<String>, size: i64) -> Self {
Self {
key: key.into(),
size_bytes: Some(size),
size_human: Some(humansize::format_size(size as u64, humansize::BINARY)),
last_modified: None,
etag: None,
storage_class: None,
content_type: None,
metadata: None,
is_dir: false,
}
}
pub fn dir(key: impl Into<String>) -> Self {
Self {
key: key.into(),
size_bytes: None,
size_human: None,
last_modified: None,
etag: None,
storage_class: None,
content_type: None,
metadata: None,
is_dir: true,
}
}
pub fn bucket(name: impl Into<String>) -> Self {
Self {
key: name.into(),
size_bytes: None,
size_human: None,
last_modified: None,
etag: None,
storage_class: None,
content_type: None,
metadata: None,
is_dir: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResult {
pub items: Vec<ObjectInfo>,
pub truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub continuation_token: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ListOptions {
pub max_keys: Option<i32>,
pub delimiter: Option<String>,
pub prefix: Option<String>,
pub continuation_token: Option<String>,
pub recursive: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Capabilities {
pub versioning: bool,
pub object_lock: bool,
pub tagging: bool,
pub anonymous: bool,
pub select: bool,
pub notifications: bool,
}
#[async_trait]
pub trait ObjectStore: Send + Sync {
async fn list_buckets(&self) -> Result<Vec<ObjectInfo>>;
async fn list_objects(&self, path: &RemotePath, options: ListOptions) -> Result<ListResult>;
async fn head_object(&self, path: &RemotePath) -> Result<ObjectInfo>;
async fn bucket_exists(&self, bucket: &str) -> Result<bool>;
async fn create_bucket(&self, bucket: &str) -> Result<()>;
async fn delete_bucket(&self, bucket: &str) -> Result<()>;
async fn capabilities(&self) -> Result<Capabilities>;
async fn get_object(&self, path: &RemotePath) -> Result<Vec<u8>>;
async fn put_object(
&self,
path: &RemotePath,
data: Vec<u8>,
content_type: Option<&str>,
) -> Result<ObjectInfo>;
async fn delete_object(&self, path: &RemotePath) -> Result<()>;
async fn delete_objects(&self, bucket: &str, keys: Vec<String>) -> Result<Vec<String>>;
async fn copy_object(&self, src: &RemotePath, dst: &RemotePath) -> Result<ObjectInfo>;
async fn presign_get(&self, path: &RemotePath, expires_secs: u64) -> Result<String>;
async fn presign_put(
&self,
path: &RemotePath,
expires_secs: u64,
content_type: Option<&str>,
) -> Result<String>;
async fn get_versioning(&self, bucket: &str) -> Result<Option<bool>>;
async fn set_versioning(&self, bucket: &str, enabled: bool) -> Result<()>;
async fn list_object_versions(
&self,
path: &RemotePath,
max_keys: Option<i32>,
) -> Result<Vec<ObjectVersion>>;
async fn get_object_tags(
&self,
path: &RemotePath,
) -> Result<std::collections::HashMap<String, String>>;
async fn get_bucket_tags(
&self,
bucket: &str,
) -> Result<std::collections::HashMap<String, String>>;
async fn set_object_tags(
&self,
path: &RemotePath,
tags: std::collections::HashMap<String, String>,
) -> Result<()>;
async fn set_bucket_tags(
&self,
bucket: &str,
tags: std::collections::HashMap<String, String>,
) -> Result<()>;
async fn delete_object_tags(&self, path: &RemotePath) -> Result<()>;
async fn delete_bucket_tags(&self, bucket: &str) -> Result<()>;
async fn get_bucket_policy(&self, bucket: &str) -> Result<Option<String>>;
async fn set_bucket_policy(&self, bucket: &str, policy: &str) -> Result<()>;
async fn delete_bucket_policy(&self, bucket: &str) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_info_file() {
let info = ObjectInfo::file("test.txt", 1024);
assert_eq!(info.key, "test.txt");
assert_eq!(info.size_bytes, Some(1024));
assert!(!info.is_dir);
}
#[test]
fn test_object_info_dir() {
let info = ObjectInfo::dir("path/to/dir/");
assert_eq!(info.key, "path/to/dir/");
assert!(info.is_dir);
assert!(info.size_bytes.is_none());
}
#[test]
fn test_object_info_bucket() {
let info = ObjectInfo::bucket("my-bucket");
assert_eq!(info.key, "my-bucket");
assert!(info.is_dir);
}
#[test]
fn test_object_info_metadata_default_none() {
let info = ObjectInfo::file("test.txt", 1024);
assert!(info.metadata.is_none());
}
#[test]
fn test_object_info_metadata_set() {
let mut info = ObjectInfo::file("test.txt", 1024);
let mut meta = HashMap::new();
meta.insert("content-disposition".to_string(), "attachment".to_string());
meta.insert("custom-key".to_string(), "custom-value".to_string());
info.metadata = Some(meta);
let metadata = info.metadata.as_ref().expect("metadata should be Some");
assert_eq!(metadata.len(), 2);
assert_eq!(metadata.get("content-disposition").unwrap(), "attachment");
assert_eq!(metadata.get("custom-key").unwrap(), "custom-value");
}
}