Files
markbase/markbase-core/src/vfs/async_s3_fs.rs
Warren 9b02bbac27 A: Code quality improvements - fix clippy warnings
- Remove unused imports in server.rs (Body, HeaderValue, RwLock)
- Remove unused imports in forward_acl.rs (tests still need Ipv4Addr)
- Remove unused imports in host_key.rs (Read, Write)
- Remove unused imports in kex_exchange.rs (HostKeyType)
- Remove unused imports in known_hosts.rs (tests need Ipv4Addr)
- Remove unused imports in multiplex.rs (Arc)
- Auto-fix other unused imports via clippy --fix

Tests: 303 passed, 0 failed (4 new tests added)
2026-06-21 23:08:07 +08:00

454 lines
14 KiB
Rust

use std::path::Path;
use std::pin::Pin;
use std::future::Future;
use std::io::{SeekFrom};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use reqwest::Client;
use rusty_s3::{Bucket, Credentials, S3Action, actions, UrlStyle};
use url::Url;
use super::{VfsError, VfsStat, VfsDirEntry, open_flags::OpenFlags};
pub struct AsyncS3Vfs {
bucket: Bucket,
credentials: Credentials,
client: Client,
}
struct AsyncS3FileState {
key: String,
mode: FileMode,
position: u64,
size: u64,
data: Vec<u8>,
write_buffer: Vec<u8>,
mtime: std::time::SystemTime,
}
enum FileMode {
Read,
Write,
}
pub struct AsyncS3File {
inner: Arc<Mutex<AsyncS3FileState>>,
vfs: AsyncS3Vfs,
}
impl AsyncS3Vfs {
pub fn new(
endpoint: &str,
region: &str,
bucket_name: &str,
access_key: &str,
secret_key: &str,
) -> Result<Self, VfsError> {
let endpoint_url = Url::parse(endpoint.trim_end_matches('/'))
.map_err(|e| VfsError::Io(format!("Invalid S3 endpoint URL: {}", e)))?;
let bucket = Bucket::new(
endpoint_url,
UrlStyle::Path,
bucket_name.to_string(),
region.to_string(),
).map_err(|e| VfsError::Io(format!("Failed to create S3 bucket config: {}", e)))?;
let credentials = Credentials::new(access_key, secret_key);
let client = Client::new();
Ok(Self { bucket, credentials, client })
}
fn path_to_key(path: &Path) -> String {
let s = path.to_string_lossy();
s.strip_prefix('/').unwrap_or(&s).to_string()
}
async fn head_object(&self, key: &str) -> Result<(u64, std::time::SystemTime, String), VfsError> {
let action = actions::HeadObject::new(&self.bucket, Some(&self.credentials), key);
let url = action.sign(Duration::from_secs(3600));
let resp = self.client
.head(url.as_str())
.send()
.await
.map_err(|e| VfsError::Io(format!("S3 HEAD failed: {}", e)))?;
let status = resp.status();
if status == 404 {
return Err(VfsError::NotFound(key.to_string()));
}
if !status.is_success() {
return Err(VfsError::Io(format!("HeadObject returned {}", status)));
}
let content_len: u64 = resp
.headers()
.get("Content-Length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let last_modified = parse_last_modified(
resp.headers()
.get("Last-Modified")
.and_then(|v| v.to_str().ok())
);
let etag = resp
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.map(|s| s.replace('"', ""))
.unwrap_or_default();
Ok((content_len, last_modified, etag))
}
async fn get_object(&self, key: &str) -> Result<Vec<u8>, VfsError> {
let action = actions::GetObject::new(&self.bucket, Some(&self.credentials), key);
let url = action.sign(Duration::from_secs(3600));
let resp = self.client
.get(url.as_str())
.send()
.await
.map_err(|e| VfsError::Io(format!("S3 GET failed: {}", e)))?;
let status = resp.status();
if status == 404 {
return Err(VfsError::NotFound(key.to_string()));
}
if !status.is_success() {
return Err(VfsError::Io(format!("GetObject returned {}", status)));
}
let bytes = resp.bytes().await
.map_err(|e| VfsError::Io(format!("Failed to read response body: {}", e)))?;
Ok(bytes.to_vec())
}
async fn put_object(&self, key: &str, data: &[u8]) -> Result<String, VfsError> {
let action = actions::PutObject::new(&self.bucket, Some(&self.credentials), key);
let url = action.sign(Duration::from_secs(3600));
let resp = self.client
.put(url.as_str())
.body(data.to_vec())
.send()
.await
.map_err(|e| VfsError::Io(format!("S3 PUT failed: {}", e)))?;
if !resp.status().is_success() {
return Err(VfsError::Io(format!("PutObject returned {}", resp.status())));
}
let etag = resp
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.map(|s| s.replace('"', ""))
.unwrap_or_default();
Ok(etag)
}
async fn delete_object(&self, key: &str) -> Result<(), VfsError> {
let action = actions::DeleteObject::new(&self.bucket, Some(&self.credentials), key);
let url = action.sign(Duration::from_secs(3600));
let resp = self.client
.delete(url.as_str())
.send()
.await
.map_err(|e| VfsError::Io(format!("S3 DELETE failed: {}", e)))?;
if !resp.status().is_success() {
return Err(VfsError::Io(format!("DeleteObject returned {}", resp.status())));
}
Ok(())
}
async fn list_objects(&self, prefix: &str) -> Result<Vec<VfsDirEntry>, VfsError> {
let mut action = actions::ListObjectsV2::new(&self.bucket, Some(&self.credentials));
if !prefix.is_empty() {
action.with_prefix(prefix);
}
action.with_delimiter("/");
let url = action.sign(Duration::from_secs(3600));
let resp = self.client
.get(url.as_str())
.send()
.await
.map_err(|e| VfsError::Io(format!("S3 LIST failed: {}", e)))?;
if !resp.status().is_success() {
return Err(VfsError::Io(format!("ListObjectsV2 returned {}", resp.status())));
}
let body = resp.text().await
.map_err(|e| VfsError::Io(format!("Failed to read LIST response: {}", e)))?;
// Use rusty-s3's built-in parser
let list_response = actions::ListObjectsV2::parse_response(&body)
.map_err(|e| VfsError::Io(format!("Failed to parse LIST response: {}", e)))?;
// Convert to VfsDirEntry
let mut entries = Vec::new();
for obj in list_response.contents {
let name = obj.key.strip_prefix(prefix).unwrap_or(&obj.key).to_string();
entries.push(VfsDirEntry {
name,
long_name: obj.key.clone(),
stat: VfsStat {
size: obj.size,
mode: 0o644,
uid: 0,
gid: 0,
atime: std::time::SystemTime::UNIX_EPOCH,
mtime: std::time::SystemTime::UNIX_EPOCH,
is_dir: false,
is_symlink: false,
},
});
}
for prefix_elem in list_response.common_prefixes {
let name = prefix_elem.prefix.strip_prefix(prefix).unwrap_or(&prefix_elem.prefix).trim_end_matches('/').to_string();
entries.push(VfsDirEntry {
name,
long_name: prefix_elem.prefix.clone(),
stat: VfsStat {
size: 0,
mode: 0o755,
uid: 0,
gid: 0,
atime: std::time::SystemTime::UNIX_EPOCH,
mtime: std::time::SystemTime::UNIX_EPOCH,
is_dir: true,
is_symlink: false,
},
});
}
Ok(entries)
}
}
impl Clone for AsyncS3Vfs {
fn clone(&self) -> Self {
Self {
bucket: self.bucket.clone(),
credentials: self.credentials.clone(),
client: self.client.clone(),
}
}
}
impl AsyncS3File {
pub async fn new_read(vfs: AsyncS3Vfs, key: String) -> Result<Self, VfsError> {
let (size, mtime, _) = vfs.head_object(&key).await?;
Ok(Self {
inner: Arc::new(Mutex::new(AsyncS3FileState {
key,
mode: FileMode::Read,
position: 0,
size,
data: Vec::new(),
write_buffer: Vec::new(),
mtime,
})),
vfs,
})
}
pub fn new_write(vfs: AsyncS3Vfs, key: String) -> Self {
Self {
inner: Arc::new(Mutex::new(AsyncS3FileState {
key,
mode: FileMode::Write,
position: 0,
size: 0,
data: Vec::new(),
write_buffer: Vec::new(),
mtime: std::time::SystemTime::now(),
})),
vfs,
}
}
}
impl super::AsyncVfsFile for AsyncS3File {
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Pin<Box<dyn Future<Output = Result<usize, VfsError>> + Send + 'a>> {
let inner = self.inner.clone();
let vfs = self.vfs.clone();
Box::pin(async move {
let mut state = inner.lock().await;
if state.position >= state.size {
return Ok(0);
}
if state.data.is_empty() {
let key = state.key.clone();
state.data = vfs.get_object(&key).await?;
}
let remaining = state.size - state.position;
let to_read = buf.len().min(remaining as usize);
let start = state.position as usize;
let end = start + to_read;
buf[..to_read].copy_from_slice(&state.data[start..end]);
state.position += to_read as u64;
Ok(to_read)
})
}
fn write<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn Future<Output = Result<usize, VfsError>> + Send + 'a>> {
let inner = self.inner.clone();
Box::pin(async move {
let mut state = inner.lock().await;
state.write_buffer.extend_from_slice(buf);
Ok(buf.len())
})
}
fn seek<'a>(&'a mut self, pos: SeekFrom) -> Pin<Box<dyn Future<Output = Result<u64, VfsError>> + Send + 'a>> {
let inner = self.inner.clone();
Box::pin(async move {
let mut state = inner.lock().await;
let new_pos = match pos {
SeekFrom::Start(offset) => offset,
SeekFrom::Current(offset) => {
((state.position as i64) + offset).max(0) as u64
}
SeekFrom::End(offset) => {
((state.size as i64) + offset).max(0) as u64
}
};
state.position = new_pos.min(state.size);
Ok(state.position)
})
}
fn flush<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
let inner = self.inner.clone();
let vfs = self.vfs.clone();
Box::pin(async move {
let mut state = inner.lock().await;
if !state.write_buffer.is_empty() {
let key = state.key.clone();
let data = state.write_buffer.clone();
vfs.put_object(&key, &data).await?;
state.write_buffer.clear();
}
Ok(())
})
}
}
impl super::AsyncVfsBackend for AsyncS3Vfs {
fn clone_boxed(&self) -> Box<dyn super::AsyncVfsBackend> {
Box::new(self.clone())
}
fn read_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<Vec<VfsDirEntry>, VfsError>> + Send + 'a>> {
let prefix = Self::path_to_key(path);
Box::pin(async move {
self.list_objects(&prefix).await
})
}
fn open_file<'a>(&'a self, path: &'a Path, flags: &'a OpenFlags) -> Pin<Box<dyn Future<Output = Result<Box<dyn super::AsyncVfsFile>, VfsError>> + Send + 'a>> {
let key = Self::path_to_key(path);
let vfs = self.clone();
let is_write = flags.write;
Box::pin(async move {
if is_write {
Ok(Box::new(AsyncS3File::new_write(vfs, key)) as Box<dyn super::AsyncVfsFile>)
} else {
let file = AsyncS3File::new_read(vfs, key).await?;
Ok(Box::new(file) as Box<dyn super::AsyncVfsFile>)
}
})
}
fn stat<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<VfsStat, VfsError>> + Send + 'a>> {
let key = Self::path_to_key(path);
Box::pin(async move {
let (size, mtime, _) = self.head_object(&key).await?;
Ok(VfsStat {
size,
mode: 0o644,
uid: 0,
gid: 0,
atime: mtime,
mtime,
is_dir: false,
is_symlink: false,
})
})
}
fn create_dir<'a>(&'a self, path: &'a Path, _mode: u32) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
let key = Self::path_to_key(path);
if !key.ends_with('/') {
let _key = format!("{}/", key);
}
Box::pin(async move {
self.put_object(&key, &[]).await?;
Ok(())
})
}
fn remove_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
let key = Self::path_to_key(path);
let key = if key.ends_with('/') { key } else { format!("{}/", key) };
Box::pin(async move {
self.delete_object(&key).await?;
Ok(())
})
}
fn remove_file<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
let key = Self::path_to_key(path);
Box::pin(async move {
self.delete_object(&key).await?;
Ok(())
})
}
fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
let from_key = Self::path_to_key(from);
let to_key = Self::path_to_key(to);
Box::pin(async move {
let data = self.get_object(&from_key).await?;
self.put_object(&to_key, &data).await?;
self.delete_object(&from_key).await?;
Ok(())
})
}
fn exists<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
let key = Self::path_to_key(path);
Box::pin(async move {
self.head_object(&key).await.is_ok()
})
}
}
fn parse_last_modified(header: Option<&str>) -> std::time::SystemTime {
header
.and_then(|s| chrono::DateTime::parse_from_rfc2822(s).ok())
.map(|dt| std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(dt.timestamp() as u64))
.unwrap_or(std::time::SystemTime::now())
}