supabase_rust_storage/
lib.rs

1//! Supabase Storage client for Rust
2//!
3//! This crate provides storage functionality for Supabase,
4//! allowing for uploading, downloading, and managing files.
5
6use reqwest::Client;
7use reqwest::multipart::{Form, Part};
8use serde::{Serialize, Deserialize};
9use thiserror::Error;
10use std::collections::HashMap;
11use url::Url;
12use bytes::Bytes;
13use std::path::Path;
14use tokio::fs::File;
15use tokio::io::AsyncReadExt;
16
17// Note: wiremock should only be used in tests
18#[cfg(test)]
19use wiremock::{MockServer, Mock, ResponseTemplate};
20#[cfg(test)]
21use wiremock::matchers::{method, path, query_param};
22#[cfg(test)]
23use serde_json::json;
24
25/// A specialized Result type for storage operations
26pub type Result<T> = std::result::Result<T, StorageError>;
27
28/// エラー型
29#[derive(Error, Debug)]
30pub enum StorageError {
31    #[error("API error: {0}")]
32    ApiError(String),
33    
34    #[error("Network error: {0}")]
35    NetworkError(#[from] reqwest::Error),
36    
37    #[error("JSON serialization error: {0}")]
38    SerializationError(#[from] serde_json::Error),
39    
40    #[error("URL parse error: {0}")]
41    UrlParseError(#[from] url::ParseError),
42    
43    #[error("Storage error: {0}")]
44    StorageError(String),
45    
46    #[error("File not found: {0}")]
47    FileNotFound(String),
48    
49    #[error("IO error: {0}")]
50    IoError(#[from] std::io::Error),
51
52    #[error("Request error: {0}")]
53    RequestError(String),
54    
55    #[error("Deserialization error: {0}")]
56    DeserializationError(String),
57}
58
59impl StorageError {
60    pub fn new(message: String) -> Self {
61        Self::StorageError(message)
62    }
63}
64
65/// ファイルアップロードオプション
66#[derive(Debug, Clone, Serialize, Default)]
67pub struct FileOptions {
68    pub cache_control: Option<String>,
69    pub content_type: Option<String>,
70    pub upsert: Option<bool>,
71}
72
73impl FileOptions {
74    /// 新しいファイルオプションを作成
75    pub fn new() -> Self {
76        Self::default()
77    }
78    
79    /// キャッシュコントロールを設定
80    pub fn with_cache_control(mut self, cache_control: &str) -> Self {
81        self.cache_control = Some(cache_control.to_string());
82        self
83    }
84    
85    /// コンテンツタイプを設定
86    pub fn with_content_type(mut self, content_type: &str) -> Self {
87        self.content_type = Some(content_type.to_string());
88        self
89    }
90    
91    /// アップサートを設定
92    pub fn with_upsert(mut self, upsert: bool) -> Self {
93        self.upsert = Some(upsert);
94        self
95    }
96}
97
98/// ファイル一覧取得オプション
99#[derive(Debug, Clone, Serialize, Default)]
100pub struct ListOptions {
101    pub limit: Option<i32>,
102    pub offset: Option<i32>,
103    pub sort_by: Option<SortBy>,
104    pub search: Option<String>,
105}
106
107impl ListOptions {
108    /// 新しい一覧オプションを作成
109    pub fn new() -> Self {
110        Self::default()
111    }
112    
113    /// 取得上限を設定
114    pub fn limit(mut self, limit: i32) -> Self {
115        self.limit = Some(limit);
116        self
117    }
118    
119    /// オフセットを設定
120    pub fn offset(mut self, offset: i32) -> Self {
121        self.offset = Some(offset);
122        self
123    }
124    
125    /// ソート順を設定
126    pub fn sort_by(mut self, column: &str, order: SortOrder) -> Self {
127        self.sort_by = Some(SortBy {
128            column: column.to_string(),
129            order,
130        });
131        self
132    }
133    
134    /// 検索キーワードを設定
135    pub fn search(mut self, search: &str) -> Self {
136        self.search = Some(search.to_string());
137        self
138    }
139}
140
141/// ソート設定
142#[derive(Debug, Clone, Serialize)]
143pub struct SortBy {
144    pub column: String,
145    pub order: SortOrder,
146}
147
148impl ToString for SortBy {
149    fn to_string(&self) -> String {
150        format!("{}:{:?}", self.column, self.order).to_lowercase()
151    }
152}
153
154/// ソート順
155#[derive(Debug, Clone, Copy, Serialize)]
156#[serde(rename_all = "lowercase")]
157pub enum SortOrder {
158    Asc,
159    Desc,
160}
161
162/// 画像変換オプション
163#[derive(Debug, Clone, Serialize, Default)]
164pub struct ImageTransformOptions {
165    pub width: Option<u32>,
166    pub height: Option<u32>,
167    pub resize: Option<String>,
168    pub format: Option<String>,
169    pub quality: Option<u32>,
170}
171
172impl ImageTransformOptions {
173    /// 新しい画像変換オプションを作成
174    pub fn new() -> Self {
175        Self::default()
176    }
177    
178    /// 幅を設定
179    pub fn with_width(mut self, width: u32) -> Self {
180        self.width = Some(width);
181        self
182    }
183    
184    /// 高さを設定
185    pub fn with_height(mut self, height: u32) -> Self {
186        self.height = Some(height);
187        self
188    }
189    
190    /// リサイズモードを設定 (cover, contain, fill)
191    pub fn with_resize(mut self, resize: &str) -> Self {
192        self.resize = Some(resize.to_string());
193        self
194    }
195    
196    /// 出力フォーマットを設定 (webp, png, jpeg, etc)
197    pub fn with_format(mut self, format: &str) -> Self {
198        self.format = Some(format.to_string());
199        self
200    }
201    
202    /// 画質を設定 (1-100)
203    pub fn with_quality(mut self, quality: u32) -> Self {
204        self.quality = Some(quality.min(100));
205        self
206    }
207    
208    /// URLクエリパラメータに変換
209    fn to_query_params(&self) -> String {
210        let mut params = Vec::new();
211        
212        if let Some(width) = self.width {
213            params.push(format!("width={}", width));
214        }
215        
216        if let Some(height) = self.height {
217            params.push(format!("height={}", height));
218        }
219        
220        if let Some(resize) = &self.resize {
221            params.push(format!("resize={}", resize));
222        }
223        
224        if let Some(format) = &self.format {
225            params.push(format!("format={}", format));
226        }
227        
228        if let Some(quality) = self.quality {
229            params.push(format!("quality={}", quality));
230        }
231        
232        if params.is_empty() {
233            String::new()
234        } else {
235            format!("?{}", params.join("&"))
236        }
237    }
238}
239
240/// ファイル情報
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct FileObject {
243    pub name: String,
244    pub bucket_id: String,
245    pub owner: String,
246    pub id: String,
247    pub updated_at: String,
248    pub created_at: String,
249    pub last_accessed_at: String,
250    pub metadata: Option<serde_json::Value>,
251    pub mime_type: Option<String>,
252    pub size: i64,
253}
254
255/// バケット情報
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct Bucket {
258    pub id: String,
259    pub name: String,
260    pub owner: String,
261    pub public: bool,
262    pub created_at: String,
263    pub updated_at: String,
264}
265
266/// チャンクアップロードの初期化結果
267#[derive(Debug, Clone, Deserialize)]
268pub struct InitiateMultipartUploadResponse {
269    pub id: String,
270    #[serde(rename = "uploadId")]
271    pub upload_id: String,
272    pub key: String,
273    pub bucket: String,
274}
275
276/// アップロードされたチャンク情報
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct UploadedPartInfo {
279    #[serde(rename = "partNumber")]
280    pub part_number: u32,
281    pub etag: String,
282}
283
284/// チャンクアップロードの完了リクエスト
285#[derive(Debug, Clone, Serialize)]
286struct CompleteMultipartUploadRequest {
287    #[serde(rename = "uploadId")]
288    pub upload_id: String,
289    pub parts: Vec<UploadedPartInfo>,
290}
291
292/// ストレージバケットクライアント
293pub struct StorageBucketClient<'a> {
294    parent: &'a StorageClient,
295    bucket_id: String,
296}
297
298/// ストレージクライアント
299pub struct StorageClient {
300    base_url: String,
301    api_key: String,
302    http_client: Client,
303}
304
305impl StorageClient {
306    /// 新しいストレージクライアントを作成
307    pub fn new(base_url: &str, api_key: &str, http_client: Client) -> Self {
308        Self {
309            base_url: base_url.to_string(),
310            api_key: api_key.to_string(),
311            http_client,
312        }
313    }
314    
315    /// バケットを指定
316    pub fn from<'a>(&'a self, bucket_id: &str) -> StorageBucketClient<'a> {
317        StorageBucketClient {
318            parent: self,
319            bucket_id: bucket_id.to_string(),
320        }
321    }
322    
323    /// バケット一覧を取得
324    pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
325        let url = format!("{}/storage/v1/bucket", self.base_url);
326        
327        let response = self.http_client.get(&url)
328            .header("apikey", &self.api_key)
329            .send()
330            .await?;
331            
332        if !response.status().is_success() {
333            let error_text = response.text().await?;
334            return Err(StorageError::ApiError(error_text));
335        }
336        
337        let buckets = response.json::<Vec<Bucket>>().await?;
338        
339        Ok(buckets)
340    }
341    
342    /// バケットを作成
343    pub async fn create_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
344        let url = format!("{}/storage/v1/bucket", self.base_url);
345        
346        let payload = serde_json::json!({
347            "id": bucket_id,
348            "name": bucket_id,
349            "public": is_public
350        });
351        
352        let response = self.http_client.post(&url)
353            .header("apikey", &self.api_key)
354            .header("Content-Type", "application/json")
355            .json(&payload)
356            .send()
357            .await?;
358            
359        if !response.status().is_success() {
360            let error_text = response.text().await?;
361            return Err(StorageError::ApiError(error_text));
362        }
363        
364        let bucket = response.json::<Bucket>().await?;
365        
366        Ok(bucket)
367    }
368    
369    /// バケットを削除
370    pub async fn delete_bucket(&self, bucket_id: &str) -> Result<()> {
371        let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
372        
373        let response = self.http_client.delete(&url)
374            .header("apikey", &self.api_key)
375            .send()
376            .await?;
377            
378        if !response.status().is_success() {
379            let error_text = response.text().await?;
380            return Err(StorageError::ApiError(error_text));
381        }
382        
383        Ok(())
384    }
385    
386    /// バケット情報を更新
387    pub async fn update_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
388        let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
389        
390        let payload = serde_json::json!({
391            "id": bucket_id,
392            "public": is_public
393        });
394        
395        let response = self.http_client.put(&url)
396            .header("apikey", &self.api_key)
397            .header("Content-Type", "application/json")
398            .json(&payload)
399            .send()
400            .await?;
401            
402        if !response.status().is_success() {
403            let error_text = response.text().await?;
404            return Err(StorageError::ApiError(error_text));
405        }
406        
407        let bucket = response.json::<Bucket>().await?;
408        
409        Ok(bucket)
410    }
411}
412
413impl<'a> StorageBucketClient<'a> {
414    /// ファイルをアップロード
415    pub async fn upload(&self, path: &str, file_path: &Path, options: Option<FileOptions>) -> Result<FileObject> {
416        let mut url = Url::parse(&self.parent.base_url)?;
417        url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
418        
419        // オプションをURLクエリとして設定
420        if let Some(opts) = &options {
421            let mut query_pairs = url.query_pairs_mut();
422            if let Some(cache_control) = &opts.cache_control {
423                query_pairs.append_pair("cache_control", cache_control);
424            }
425            if let Some(upsert) = &opts.upsert {
426                query_pairs.append_pair("upsert", &upsert.to_string());
427            }
428        }
429        
430        // ファイルの内容を読み込む
431        let mut file = File::open(file_path).await?;
432        let mut contents = Vec::new();
433        file.read_to_end(&mut contents).await?;
434        
435        // マルチパートフォームデータの作成
436        let part = Part::bytes(contents)
437            .file_name(file_path.file_name().unwrap().to_string_lossy().to_string());
438            
439        let form = Form::new().part("file", part);
440        
441        let response = self.parent.http_client
442            .post(url)
443            .header("apikey", &self.parent.api_key)
444            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
445            .multipart(form)
446            .send()
447            .await?;
448            
449        if !response.status().is_success() {
450            let error_text = response.text().await?;
451            return Err(StorageError::ApiError(error_text));
452        }
453        
454        let file_object = response.json::<FileObject>().await?;
455        
456        Ok(file_object)
457    }
458    
459    /// ファイルをダウンロード
460    pub async fn download(&self, path: &str) -> Result<Bytes> {
461        let mut url = Url::parse(&self.parent.base_url)?;
462        url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
463        
464        let response = self.parent.http_client
465            .get(url)
466            .header("apikey", &self.parent.api_key)
467            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
468            .send()
469            .await?;
470            
471        if !response.status().is_success() {
472            let error_text = response.text().await?;
473            return Err(StorageError::ApiError(error_text));
474        }
475        
476        let bytes = response.bytes().await?;
477        
478        Ok(bytes)
479    }
480    
481    /// ファイル一覧を取得
482    pub async fn list(&self, prefix: &str, options: Option<ListOptions>) -> Result<Vec<FileObject>> {
483        let mut url = Url::parse(&self.parent.base_url)?;
484        url.set_path(&format!("/storage/v1/object/list/{}", self.bucket_id));
485        
486        // プレフィックスと検索オプションをクエリとして設定
487        {
488            let mut query_pairs = url.query_pairs_mut();
489            query_pairs.append_pair("prefix", prefix);
490            
491            if let Some(opts) = &options {
492                if let Some(limit) = opts.limit {
493                    query_pairs.append_pair("limit", &limit.to_string());
494                }
495                if let Some(offset) = opts.offset {
496                    query_pairs.append_pair("offset", &offset.to_string());
497                }
498                if let Some(sort_by) = &opts.sort_by {
499                    query_pairs.append_pair("sortBy", &sort_by.to_string());
500                }
501                if let Some(search) = &opts.search {
502                    query_pairs.append_pair("search", search);
503                }
504            }
505        } // query_pairsのスコープはここで終了
506        
507        let response = self.parent.http_client
508            .get(url)
509            .header("apikey", &self.parent.api_key)
510            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
511            .send()
512            .await?;
513            
514        if !response.status().is_success() {
515            let error_text = response.text().await?;
516            return Err(StorageError::ApiError(error_text));
517        }
518        
519        let files = response.json::<Vec<FileObject>>().await?;
520        
521        Ok(files)
522    }
523    
524    /// ファイルを削除
525    pub async fn remove(&self, paths: Vec<&str>) -> Result<()> {
526        let url = format!("{}/storage/v1/object/{}", self.parent.base_url, self.bucket_id);
527        
528        let payload = serde_json::json!({
529            "prefixes": paths
530        });
531        
532        let response = self.parent.http_client.delete(&url)
533            .header("apikey", &self.parent.api_key)
534            .header("Content-Type", "application/json")
535            .json(&payload)
536            .send()
537            .await?;
538            
539        if !response.status().is_success() {
540            let error_text = response.text().await?;
541            return Err(StorageError::ApiError(error_text));
542        }
543        
544        Ok(())
545    }
546    
547    /// 公開URLを取得
548    pub fn get_public_url(&self, path: &str) -> String {
549        format!("{}/storage/v1/object/public/{}/{}", self.parent.base_url, self.bucket_id, path)
550    }
551    
552    /// 署名付きURLを作成
553    pub async fn create_signed_url(&self, path: &str, expires_in: i32) -> Result<String> {
554        let url = format!("{}/storage/v1/object/sign/{}/{}", self.parent.base_url, self.bucket_id, path);
555        
556        let payload = serde_json::json!({
557            "expiresIn": expires_in
558        });
559        
560        let response = self.parent.http_client.post(&url)
561            .header("apikey", &self.parent.api_key)
562            .header("Content-Type", "application/json")
563            .json(&payload)
564            .send()
565            .await?;
566            
567        if !response.status().is_success() {
568            let error_text = response.text().await?;
569            return Err(StorageError::ApiError(error_text));
570        }
571        
572        #[derive(Deserialize)]
573        struct SignedUrlResponse {
574            signed_url: String,
575        }
576        
577        let signed_url = response.json::<SignedUrlResponse>().await?;
578        
579        Ok(signed_url.signed_url)
580    }
581
582    /// マルチパートアップロードを初期化
583    pub async fn initiate_multipart_upload(
584        &self,
585        path: &str,
586        options: Option<FileOptions>
587    ) -> Result<InitiateMultipartUploadResponse> {
588        let url = format!(
589            "{}/storage/v1/upload/initiate",
590            self.parent.base_url
591        );
592        
593        let options = options.unwrap_or_default();
594        
595        let cache_control = options.cache_control.unwrap_or_else(|| "max-age=3600".to_string());
596        let content_type = options.content_type.unwrap_or_else(|| "application/octet-stream".to_string());
597        let upsert = options.upsert.unwrap_or(false);
598        
599        let payload = serde_json::json!({
600            "bucket": self.bucket_id,
601            "name": path,
602            "cacheControl": cache_control,
603            "contentType": content_type,
604            "upsert": upsert,
605        });
606        
607        let response = self.parent.http_client.post(&url)
608            .header("apikey", &self.parent.api_key)
609            .header("Content-Type", "application/json")
610            .json(&payload)
611            .send()
612            .await?;
613            
614        if !response.status().is_success() {
615            let error_text = response.text().await?;
616            return Err(StorageError::ApiError(error_text));
617        }
618        
619        let initiate_response: InitiateMultipartUploadResponse = response.json().await?;
620        
621        Ok(initiate_response)
622    }
623    
624    /// チャンクをアップロード
625    pub async fn upload_part(
626        &self,
627        upload_id: &str,
628        part_number: u32,
629        data: Bytes
630    ) -> Result<UploadedPartInfo> {
631        let url = format!(
632            "{}/storage/v1/upload/part",
633            self.parent.base_url
634        );
635        
636        let body = reqwest::Body::from(data);
637        
638        let response = self.parent.http_client.post(&url)
639            .header("apikey", &self.parent.api_key)
640            .query(&[
641                ("uploadId", upload_id),
642                ("partNumber", &part_number.to_string()),
643                ("bucket", &self.bucket_id),
644            ])
645            .body(body)
646            .send()
647            .await?;
648            
649        if !response.status().is_success() {
650            let error_text = response.text().await?;
651            return Err(StorageError::ApiError(error_text));
652        }
653        
654        let etag = response.headers()
655            .get("etag")
656            .ok_or_else(|| StorageError::new("ETag header not found in response".to_string()))?
657            .to_str()
658            .map_err(|e| StorageError::new(format!("Invalid ETag header: {}", e)))?
659            .to_string();
660        
661        let part_info = UploadedPartInfo {
662            part_number,
663            etag,
664        };
665        
666        Ok(part_info)
667    }
668    
669    /// マルチパートアップロードを完了
670    pub async fn complete_multipart_upload(
671        &self,
672        upload_id: &str,
673        path: &str,
674        parts: Vec<UploadedPartInfo>
675    ) -> Result<FileObject> {
676        let url = format!(
677            "{}/storage/v1/upload/complete",
678            self.parent.base_url
679        );
680
681        let request_data = CompleteMultipartUploadRequest {
682            upload_id: upload_id.to_string(),
683            parts,
684        };
685
686        let params = [
687            ("bucket", self.bucket_id.to_string()),
688            ("key", path.to_string()), // Convert &str to String
689        ];
690
691        let response = self.parent.http_client.post(&url)
692            .header("apikey", &self.parent.api_key)
693            .header("Content-Type", "application/json")
694            .json(&request_data)
695            .query(&params)
696            .send()
697            .await?;
698            
699        if !response.status().is_success() {
700            let error_text = response.text().await?;
701            return Err(StorageError::ApiError(error_text));
702        }
703        
704        let file_object: FileObject = response.json().await?;
705        
706        Ok(file_object)
707    }
708    
709    /// マルチパートアップロードを中止
710    pub async fn abort_multipart_upload(
711        &self,
712        upload_id: &str,
713        path: &str
714    ) -> Result<()> {
715        let url = format!(
716            "{}/storage/v1/upload/abort",
717            self.parent.base_url
718        );
719        
720        let payload = serde_json::json!({
721            "uploadId": upload_id,
722            "bucket": self.bucket_id,
723            "key": path,
724        });
725        
726        let response = self.parent.http_client.post(&url)
727            .header("apikey", &self.parent.api_key)
728            .header("Content-Type", "application/json")
729            .json(&payload)
730            .send()
731            .await?;
732            
733        if !response.status().is_success() {
734            let error_text = response.text().await?;
735            return Err(StorageError::ApiError(error_text));
736        }
737        
738        Ok(())
739    }
740    
741    /// 大容量ファイルをチャンクでアップロード
742    /// 
743    /// このメソッドは大きなファイルを自動的にチャンクに分割してアップロードします。
744    /// 各チャンクは非同期でアップロードされ、すべてのチャンクがアップロードされると
745    /// 自動的にマルチパートアップロードを完了します。
746    pub async fn upload_large_file(
747        &self,
748        path: &str,
749        file_path: &Path,
750        chunk_size: usize,
751        options: Option<FileOptions>
752    ) -> Result<FileObject> {
753        // ファイルを開く
754        let mut file = File::open(file_path).await?;
755        
756        // ファイルサイズを取得
757        let file_size = file.metadata().await?.len() as usize;
758        
759        // チャンク数を計算
760        let chunk_count = (file_size + chunk_size - 1) / chunk_size;
761        
762        if chunk_count == 0 {
763            return Err(StorageError::new("File is empty".to_string()));
764        }
765        
766        // マルチパートアップロードを初期化
767        let init_response = self.initiate_multipart_upload(path, options).await?;
768        
769        // 部分アップロード情報を保持するベクター
770        let mut uploaded_parts = Vec::with_capacity(chunk_count);
771        
772        // バッファを準備
773        let mut buffer = vec![0u8; chunk_size];
774        
775        // 各チャンクをアップロード
776        for part_number in 1..=chunk_count as u32 {
777            // バッファにデータを読み込む
778            let n = file.read(&mut buffer).await?;
779            
780            if n == 0 {
781                break;
782            }
783            
784            // 実際に読み込んだサイズに合わせてバッファを調整
785            let chunk_data = Bytes::from(buffer[0..n].to_vec());
786            
787            // チャンクをアップロード
788            let part_info = self.upload_part(&init_response.upload_id, part_number, chunk_data).await?;
789            
790            // アップロードした部分情報を保存
791            uploaded_parts.push(part_info);
792        }
793        
794        // マルチパートアップロードを完了
795        let file_object = self.complete_multipart_upload(
796            &init_response.upload_id,
797            path,
798            uploaded_parts
799        ).await?;
800        
801        Ok(file_object)
802    }
803    
804    /// 画像を変換して取得
805    pub async fn transform_image(&self, path: &str, options: ImageTransformOptions) -> Result<Bytes> {
806        let url = format!(
807            "{}/storage/v1/object/transform/{}/{}{}",
808            self.parent.base_url,
809            self.bucket_id,
810            path,
811            options.to_query_params()
812        );
813        
814        let response = self.parent.http_client.get(&url)
815            .header("apikey", &self.parent.api_key)
816            .send()
817            .await?;
818            
819        if !response.status().is_success() {
820            let error_text = response.text().await?;
821            return Err(StorageError::ApiError(error_text));
822        }
823        
824        let data = response.bytes().await?;
825        
826        Ok(data)
827    }
828    
829    /// 変換された画像のURLを取得(公開バケットの場合)
830    pub fn get_public_transform_url(&self, path: &str, options: ImageTransformOptions) -> String {
831        format!(
832            "{}/storage/v1/object/public/transform/{}/{}{}",
833            self.parent.base_url,
834            self.bucket_id,
835            path,
836            options.to_query_params()
837        )
838    }
839    
840    /// 変換された画像の署名付きURLを取得
841    pub async fn create_signed_transform_url(
842        &self,
843        path: &str,
844        options: ImageTransformOptions,
845        expires_in: i32
846    ) -> Result<String> {
847        let transform_path = format!("transform/{}/{}{}", self.bucket_id, path, options.to_query_params());
848        
849        let url = format!(
850            "{}/storage/v1/object/sign/{}",
851            self.parent.base_url,
852            transform_path,
853        );
854        
855        let params = serde_json::json!({
856            "expiresIn": expires_in
857        });
858        
859        let response = self.parent.http_client.post(&url)
860            .header("apikey", &self.parent.api_key)
861            .json(&params)
862            .send()
863            .await?;
864            
865        if !response.status().is_success() {
866            let error_text = response.text().await?;
867            return Err(StorageError::ApiError(error_text));
868        }
869        
870        #[derive(Deserialize)]
871        struct SignedUrlResponse {
872            signed_url: String,
873        }
874        
875        let result = response.json::<SignedUrlResponse>().await?;
876        
877        Ok(result.signed_url)
878    }
879
880    /// S3互換クライアントを取得
881    pub fn s3_compatible(&self, options: s3::S3Options) -> s3::S3BucketClient {
882        s3::S3BucketClient::new(
883            &self.parent.base_url,
884            &self.parent.api_key,
885            &self.bucket_id,
886            self.parent.http_client.clone(),
887            options
888        )
889    }
890}
891
892// S3互換API用のモジュールを追加
893pub mod s3 {
894    use crate::StorageError;
895    use crate::Result;
896    use reqwest::Client;
897    use serde_json::Value;
898    use std::collections::HashMap;
899    use bytes::Bytes;
900    use serde::{Serialize, Deserialize};
901    use serde_json;
902
903    /// S3互換APIのオプション
904    #[derive(Debug, Clone, Serialize, Deserialize)]
905    pub struct S3Options {
906        /// アクセスキー
907        pub access_key_id: String,
908        /// シークレットキー
909        pub secret_access_key: String,
910        /// リージョン(デフォルトは「auto」)
911        #[serde(skip_serializing_if = "Option::is_none")]
912        pub region: Option<String>,
913        /// エンドポイントURL(デフォルトはSupabaseのストレージURL)
914        #[serde(skip_serializing_if = "Option::is_none")]
915        pub endpoint: Option<String>,
916        /// フォースパス形式を使用するかどうか
917        #[serde(skip_serializing_if = "Option::is_none")]
918        pub force_path_style: Option<bool>,
919    }
920
921    impl Default for S3Options {
922        fn default() -> Self {
923            Self {
924                access_key_id: String::new(),
925                secret_access_key: String::new(),
926                region: Some("auto".to_string()),
927                endpoint: None,
928                force_path_style: Some(true),
929            }
930        }
931    }
932
933    /// S3 API互換クライアント
934    pub struct S3Client {
935        pub options: S3Options,
936        pub base_url: String,
937        pub api_key: String,
938        pub http_client: Client,
939    }
940
941    impl S3Client {
942        /// 新しいS3互換クライアントを作成
943        pub fn new(base_url: &str, api_key: &str, http_client: Client, options: S3Options) -> Self {
944            Self {
945                options,
946                base_url: base_url.to_string(),
947                api_key: api_key.to_string(),
948                http_client,
949            }
950        }
951
952        /// バケットの作成
953        pub async fn create_bucket(&self, bucket_name: &str, is_public: bool) -> Result<()> {
954            let url = format!("{}/storage/v1/bucket", self.base_url);
955            
956            let payload = serde_json::json!({
957                "name": bucket_name,
958                "public": is_public,
959                "file_size_limit": null,
960                "allowed_mime_types": null
961            });
962            
963            let response = self.http_client
964                .post(&url)
965                .header("apikey", &self.api_key)
966                .header("Authorization", format!("Bearer {}", &self.api_key))
967                .json(&payload)
968                .send()
969                .await
970                .map_err(|e| StorageError::RequestError(e.to_string()))?;
971            
972            if !response.status().is_success() {
973                let error_text = response.text().await
974                    .unwrap_or_else(|_| "Unknown error".to_string());
975                return Err(StorageError::ApiError(error_text));
976            }
977            
978            Ok(())
979        }
980        
981        /// バケットの削除
982        pub async fn delete_bucket(&self, bucket_name: &str) -> Result<()> {
983            let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_name);
984            
985            let response = self.http_client
986                .delete(&url)
987                .header("apikey", &self.api_key)
988                .header("Authorization", format!("Bearer {}", &self.api_key))
989                .send()
990                .await
991                .map_err(|e| StorageError::RequestError(e.to_string()))?;
992            
993            if !response.status().is_success() {
994                let error_text = response.text().await
995                    .unwrap_or_else(|_| "Unknown error".to_string());
996                return Err(StorageError::ApiError(error_text));
997            }
998            
999            Ok(())
1000        }
1001        
1002        /// バケットの一覧を取得
1003        pub async fn list_buckets(&self) -> Result<Vec<serde_json::Value>> {
1004            let url = format!("{}/storage/v1/bucket", self.base_url);
1005            
1006            let response = self.http_client
1007                .get(&url)
1008                .header("apikey", &self.api_key)
1009                .header("Authorization", format!("Bearer {}", &self.api_key))
1010                .send()
1011                .await
1012                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1013            
1014            if !response.status().is_success() {
1015                let error_text = response.text().await
1016                    .unwrap_or_else(|_| "Unknown error".to_string());
1017                return Err(StorageError::ApiError(error_text));
1018            }
1019            
1020            let buckets = response.json::<Vec<serde_json::Value>>().await
1021                .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1022            
1023            Ok(buckets)
1024        }
1025        
1026        /// バケットを取得し、S3互換操作のためのクライアントを返す
1027        pub fn bucket(&self, bucket_name: &str) -> S3BucketClient {
1028            S3BucketClient::new(
1029                &self.base_url,
1030                &self.api_key,
1031                bucket_name,
1032                self.http_client.clone(),
1033                self.options.clone()
1034            )
1035        }
1036    }
1037    
1038    /// S3バケット操作用クライアント
1039    pub struct S3BucketClient {
1040        pub base_url: String,
1041        pub api_key: String,
1042        pub bucket_name: String,
1043        pub http_client: Client,
1044        pub options: S3Options,
1045    }
1046    
1047    impl S3BucketClient {
1048        /// 新しいS3バケットクライアントを作成
1049        pub fn new(
1050            base_url: &str,
1051            api_key: &str,
1052            bucket_name: &str,
1053            http_client: Client,
1054            options: S3Options
1055        ) -> Self {
1056            Self {
1057                base_url: base_url.to_string(),
1058                api_key: api_key.to_string(),
1059                bucket_name: bucket_name.to_string(),
1060                http_client,
1061                options,
1062            }
1063        }
1064        
1065        /// オブジェクトをアップロード(S3互換API)
1066        pub async fn put_object(&self, path: &str, data: Bytes, content_type: Option<String>, metadata: Option<HashMap<String, String>>) -> Result<()> {
1067            let url = format!(
1068                "{}/storage/v1/object/{}/{}",
1069                self.base_url,
1070                self.bucket_name,
1071                path.trim_start_matches('/')
1072            );
1073            
1074            let content_type = content_type.unwrap_or_else(|| "application/octet-stream".to_string());
1075            
1076            let mut request = self.http_client
1077                .put(&url)
1078                .header("apikey", &self.api_key)
1079                .header("Authorization", format!("Bearer {}", &self.api_key))
1080                .header("Content-Type", content_type)
1081                .body(data);
1082            
1083            // メタデータがある場合は追加
1084            if let Some(metadata) = metadata {
1085                for (key, value) in metadata {
1086                    request = request.header(&format!("x-amz-meta-{}", key), value);
1087                }
1088            }
1089            
1090            let response = request
1091                .send()
1092                .await
1093                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1094            
1095            if !response.status().is_success() {
1096                let error_text = response.text().await
1097                    .unwrap_or_else(|_| "Unknown error".to_string());
1098                return Err(StorageError::ApiError(error_text));
1099            }
1100            
1101            Ok(())
1102        }
1103        
1104        /// オブジェクトをダウンロード(S3互換API)
1105        pub async fn get_object(&self, path: &str) -> Result<Bytes> {
1106            let url = format!(
1107                "{}/storage/v1/object/{}/{}",
1108                self.base_url,
1109                self.bucket_name,
1110                path.trim_start_matches('/')
1111            );
1112            
1113            let response = self.http_client
1114                .get(&url)
1115                .header("apikey", &self.api_key)
1116                .header("Authorization", format!("Bearer {}", &self.api_key))
1117                .send()
1118                .await
1119                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1120            
1121            if !response.status().is_success() {
1122                let error_text = response.text().await
1123                    .unwrap_or_else(|_| "Unknown error".to_string());
1124                return Err(StorageError::ApiError(error_text));
1125            }
1126            
1127            let data = response.bytes().await
1128                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1129            
1130            Ok(data)
1131        }
1132        
1133        /// オブジェクトのメタデータを取得(S3互換API)
1134        pub async fn head_object(&self, path: &str) -> Result<HashMap<String, String>> {
1135            let url = format!(
1136                "{}/storage/v1/object/{}/{}",
1137                self.base_url,
1138                self.bucket_name,
1139                path.trim_start_matches('/')
1140            );
1141            
1142            let response = self.http_client
1143                .head(&url)
1144                .header("apikey", &self.api_key)
1145                .header("Authorization", format!("Bearer {}", &self.api_key))
1146                .send()
1147                .await
1148                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1149            
1150            if !response.status().is_success() {
1151                return Err(StorageError::ApiError("Object not found".to_string()));
1152            }
1153            
1154            let mut metadata = HashMap::new();
1155            
1156            // レスポンスヘッダーからメタデータを抽出
1157            for (key, value) in response.headers() {
1158                let key_str = key.to_string();
1159                if key_str.starts_with("x-amz-meta-") {
1160                    let meta_key = key_str.trim_start_matches("x-amz-meta-").to_string();
1161                    metadata.insert(meta_key, value.to_str().unwrap_or_default().to_string());
1162                }
1163            }
1164            
1165            Ok(metadata)
1166        }
1167        
1168        /// オブジェクトを削除(S3互換API)
1169        pub async fn delete_object(&self, path: &str) -> Result<()> {
1170            let url = format!(
1171                "{}/storage/v1/object/{}/{}",
1172                self.base_url,
1173                self.bucket_name,
1174                path.trim_start_matches('/')
1175            );
1176            
1177            let response = self.http_client
1178                .delete(&url)
1179                .header("apikey", &self.api_key)
1180                .header("Authorization", format!("Bearer {}", &self.api_key))
1181                .send()
1182                .await
1183                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1184            
1185            if !response.status().is_success() {
1186                let error_text = response.text().await
1187                    .unwrap_or_else(|_| "Unknown error".to_string());
1188                return Err(StorageError::ApiError(error_text));
1189            }
1190            
1191            Ok(())
1192        }
1193        
1194        /// オブジェクトの一覧を取得(S3互換API)
1195        pub async fn list_objects(&self, prefix: Option<&str>, delimiter: Option<&str>, max_keys: Option<i32>) -> Result<serde_json::Value> {
1196            let mut url = format!(
1197                "{}/storage/v1/object/list/{}",
1198                self.base_url,
1199                self.bucket_name
1200            );
1201            
1202            // クエリパラメータを追加
1203            let mut query_params = Vec::new();
1204            
1205            if let Some(prefix) = prefix {
1206                query_params.push(format!("prefix={}", prefix));
1207            }
1208            
1209            if let Some(delimiter) = delimiter {
1210                query_params.push(format!("delimiter={}", delimiter));
1211            }
1212            
1213            if let Some(max_keys) = max_keys {
1214                query_params.push(format!("max-keys={}", max_keys));
1215            }
1216            
1217            if !query_params.is_empty() {
1218                url = format!("{}?{}", url, query_params.join("&"));
1219            }
1220            
1221            let response = self.http_client
1222                .get(&url)
1223                .header("apikey", &self.api_key)
1224                .header("Authorization", format!("Bearer {}", &self.api_key))
1225                .send()
1226                .await
1227                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1228            
1229            if !response.status().is_success() {
1230                let error_text = response.text().await
1231                    .unwrap_or_else(|_| "Unknown error".to_string());
1232                return Err(StorageError::ApiError(error_text));
1233            }
1234            
1235            let objects = response.json::<serde_json::Value>().await
1236                .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1237            
1238            Ok(objects)
1239        }
1240        
1241        /// オブジェクトのコピー(S3互換API)
1242        pub async fn copy_object(&self, source_path: &str, destination_path: &str) -> Result<()> {
1243            let url = format!(
1244                "{}/storage/v1/object/copy",
1245                self.base_url
1246            );
1247            
1248            let payload = serde_json::json!({
1249                "bucketId": self.bucket_name,
1250                "sourceKey": source_path,
1251                "destinationKey": destination_path
1252            });
1253            
1254            let response = self.http_client
1255                .post(&url)
1256                .header("apikey", &self.api_key)
1257                .header("Authorization", format!("Bearer {}", &self.api_key))
1258                .json(&payload)
1259                .send()
1260                .await
1261                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1262            
1263            if !response.status().is_success() {
1264                let error_text = response.text().await
1265                    .unwrap_or_else(|_| "Unknown error".to_string());
1266                return Err(StorageError::ApiError(error_text));
1267            }
1268            
1269            Ok(())
1270        }
1271    }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276    use super::*;
1277    
1278    #[tokio::test]
1279    async fn test_list_buckets() {
1280        // TODO: モック実装を用いたテスト
1281    }
1282
1283    #[tokio::test]
1284    async fn test_multipart_upload() {
1285        // このテストは実際のAPIと通信するため、モックサーバーを使用するべきですが、
1286        // 簡略化のため省略しています。実際の実装ではモックサーバーを使用してください。
1287    }
1288    
1289    #[tokio::test]
1290    async fn test_transform_image() {
1291        let mock_server = MockServer::start().await;
1292        
1293        // 画像変換のモック
1294        Mock::given(method("GET"))
1295            .and(path("/storage/v1/object/transform/test-bucket/image.jpg"))
1296            .and(query_param("width", "300"))
1297            .and(query_param("height", "200"))
1298            .and(query_param("format", "webp"))
1299            .respond_with(ResponseTemplate::new(200)
1300                .set_body_bytes(vec![0, 1, 2, 3, 4]) // ダミー画像データ
1301                .append_header("Content-Type", "image/webp")
1302            )
1303            .mount(&mock_server)
1304            .await;
1305        
1306        let http_client = reqwest::Client::new();
1307        let storage_client = StorageClient::new(&mock_server.uri(), "fake-key", http_client);
1308        let bucket_client = storage_client.from("test-bucket");
1309        
1310        let options = ImageTransformOptions::new()
1311            .with_width(300)
1312            .with_height(200)
1313            .with_format("webp");
1314        
1315        let result = bucket_client.transform_image("image.jpg", options).await;
1316        
1317        assert!(result.is_ok());
1318        let image_data = result.unwrap();
1319        assert_eq!(image_data.len(), 5);
1320    }
1321    
1322    #[tokio::test]
1323    async fn test_get_public_transform_url() {
1324        let http_client = reqwest::Client::new();
1325        let storage_client = StorageClient::new("https://example.com", "fake-key", http_client);
1326        let bucket_client = storage_client.from("test-bucket");
1327        
1328        let options = ImageTransformOptions::new()
1329            .with_width(300)
1330            .with_height(200)
1331            .with_format("webp");
1332        
1333        let url = bucket_client.get_public_transform_url("image.jpg", options);
1334        
1335        assert!(url.contains("https://example.com/storage/v1/object/public/transform/test-bucket/image.jpg"));
1336        assert!(url.contains("width=300"));
1337        assert!(url.contains("height=200"));
1338        assert!(url.contains("format=webp"));
1339    }
1340    
1341    #[tokio::test]
1342    async fn test_create_signed_transform_url() {
1343        let mock_server = MockServer::start().await;
1344        
1345        // 署名付きURL生成のモック
1346        Mock::given(method("POST"))
1347            .and(path("/storage/v1/object/sign/transform/test-bucket/image.jpg"))
1348            .respond_with(ResponseTemplate::new(200)
1349                .set_body_json(json!({
1350                    "signed_url": "https://example.com/storage/v1/object/signed/transform/test-bucket/image.jpg?width=300&height=200&format=webp&token=abc123"
1351                }))
1352            )
1353            .mount(&mock_server)
1354            .await;
1355        
1356        let http_client = reqwest::Client::new();
1357        let storage_client = StorageClient::new(&mock_server.uri(), "fake-key", http_client);
1358        let bucket_client = storage_client.from("test-bucket");
1359        
1360        let options = ImageTransformOptions::new()
1361            .with_width(300)
1362            .with_height(200)
1363            .with_format("webp");
1364        
1365        let result = bucket_client.create_signed_transform_url("image.jpg", options, 60).await;
1366        
1367        assert!(result.is_ok());
1368        let url = result.unwrap();
1369        assert!(url.contains("https://example.com/storage/v1/object/signed/transform/test-bucket/image.jpg"));
1370        assert!(url.contains("token=abc123"));
1371    }
1372}