Files
labweb/supabase-stack/docs/VUE_API_INTEGRATION.md
zly 9fa602f21b feat(supabase): 整理 Storage 文档和示例代码
- 创建 docs/ 目录存放所有文档
  - QUICK_START.md: 快速入门指南
  - OPERATIONS_GUIDE.md: 完整运维指南
  - VUE_API_INTEGRATION.md: Vue 集成文档
  - DEPLOYMENT_INFO.md: 部署配置信息
  - versions.md: 版本信息

- 创建 examples/ 目录存放示例代码
  - storage_client.py: Python 完整客户端
  - storage_client.js: JavaScript 完整客户端
  - test_https_storage.py: 功能测试脚本

- 新增 README_STORAGE.md 作为 Storage 使用指南

- 修复签名 URL 生成问题(需要 /storage/v1 前缀)
- 测试脚本支持资源已存在的情况
- 所有客户端代码已验证可用

功能特性:
✓ 公网 HTTPS 访问
✓ 文件上传/下载
✓ 生成临时下载链接
✓ 完整的 REST API 客户端
✓ 支持 Python 和 JavaScript
2025-11-22 21:03:00 +08:00

27 KiB
Raw Permalink Blame History

Supabase API 集成文档 - Vue 前端对接指南

📋 目录


基本信息

🌐 服务地址

Base URL: https://amiap.hzau.edu.cn/supa
内网地址: http://100.64.0.2:18000
Dashboard: http://100.64.0.2:18000 (内网访问)

🔑 认证密钥

// 从 .env 文件获取
const SUPABASE_URL = 'https://amiap.hzau.edu.cn/supa'
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'

// ⚠️ 仅后端使用!不要暴露给前端
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg'

快速开始

1. 安装 Supabase 客户端

npm install @supabase/supabase-js

2. 创建 Supabase 客户端

// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://amiap.hzau.edu.cn/supa'
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

3. 在 Vue 组件中使用

<script setup>
import { supabase } from '@/lib/supabaseClient'
import { ref, onMounted } from 'vue'

const users = ref([])

onMounted(async () => {
  const { data, error } = await supabase
    .from('users')
    .select('*')
  
  if (error) {
    console.error('Error:', error)
  } else {
    users.value = data
  }
})
</script>

API 端点

可用的 API 端点

服务 端点 用途 状态
REST API https://amiap.hzau.edu.cn/supa/rest/v1/ 数据库操作 可用
Auth API https://amiap.hzau.edu.cn/supa/auth/v1/ 用户认证 可用
Storage API https://amiap.hzau.edu.cn/supa/storage/v1/ 文件存储 可用
Realtime wss://amiap.hzau.edu.cn/supa/realtime/v1/ 实时订阅 可用
Edge Functions https://amiap.hzau.edu.cn/supa/functions/v1/ 云函数 ⚠️ 需配置

认证配置

环境变量配置

创建 .env 文件:

VITE_SUPABASE_URL=https://amiap.hzau.edu.cn/supa
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg

使用环境变量

// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

REST API 使用

查询数据

// 查询所有记录
const { data, error } = await supabase
  .from('users')
  .select('*')

// 条件查询
const { data, error } = await supabase
  .from('users')
  .select('id, name, email')
  .eq('status', 'active')
  .order('created_at', { ascending: false })
  .limit(10)

// 关联查询
const { data, error } = await supabase
  .from('posts')
  .select(`
    *,
    author:users(name, email),
    comments(*)
  `)

插入数据

const { data, error } = await supabase
  .from('users')
  .insert([
    { name: 'John Doe', email: 'john@example.com' }
  ])
  .select()

更新数据

const { data, error } = await supabase
  .from('users')
  .update({ status: 'inactive' })
  .eq('id', userId)
  .select()

删除数据

const { data, error } = await supabase
  .from('users')
  .delete()
  .eq('id', userId)

Auth API 使用

用户注册

const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123',
  options: {
    data: {
      first_name: 'John',
      last_name: 'Doe'
    }
  }
})

用户登录

// 邮箱密码登录
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password123'
})

// OAuth 登录(需要配置)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google'
})

获取当前用户

const { data: { user } } = await supabase.auth.getUser()

退出登录

const { error } = await supabase.auth.signOut()

监听认证状态

supabase.auth.onAuthStateChange((event, session) => {
  console.log(event, session)
  
  if (event === 'SIGNED_IN') {
    console.log('用户已登录:', session.user)
  }
  if (event === 'SIGNED_OUT') {
    console.log('用户已退出')
  }
})

Vue 3 Composition API 示例

<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'
import { useRouter } from 'vue-router'

const router = useRouter()
const email = ref('')
const password = ref('')
const user = ref(null)

// 登录
const signIn = async () => {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: email.value,
    password: password.value
  })
  
  if (error) {
    alert('登录失败: ' + error.message)
  } else {
    user.value = data.user
    router.push('/dashboard')
  }
}

// 退出
const signOut = async () => {
  await supabase.auth.signOut()
  user.value = null
  router.push('/login')
}

// 检查当前用户
onMounted(async () => {
  const { data: { user: currentUser } } = await supabase.auth.getUser()
  user.value = currentUser
  
  // 监听认证状态变化
  supabase.auth.onAuthStateChange((event, session) => {
    user.value = session?.user || null
  })
})
</script>

<template>
  <div v-if="!user">
    <input v-model="email" type="email" placeholder="邮箱" />
    <input v-model="password" type="password" placeholder="密码" />
    <button @click="signIn">登录</button>
  </div>
  <div v-else>
    <p>欢迎, {{ user.email }}</p>
    <button @click="signOut">退出</button>
  </div>
</template>

Storage API 使用

上传文件

const file = event.target.files[0]
const fileName = `${Date.now()}-${file.name}`

const { data, error } = await supabase.storage
  .from('avatars')
  .upload(fileName, file)

if (error) {
  console.error('上传失败:', error)
} else {
  console.log('上传成功:', data)
}

下载文件

const { data, error } = await supabase.storage
  .from('avatars')
  .download('path/to/file.jpg')

获取公开 URL

const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('path/to/file.jpg')

console.log('公开URL:', data.publicUrl)

删除文件

const { data, error } = await supabase.storage
  .from('avatars')
  .remove(['path/to/file.jpg'])

Vue 文件上传组件示例

<script setup>
import { ref } from 'vue'
import { supabase } from '@/lib/supabaseClient'

const uploading = ref(false)
const fileUrl = ref('')

const uploadFile = async (event) => {
  try {
    uploading.value = true
    const file = event.target.files[0]
    const fileExt = file.name.split('.').pop()
    const fileName = `${Math.random()}.${fileExt}`
    const filePath = `${fileName}`

    const { error: uploadError } = await supabase.storage
      .from('avatars')
      .upload(filePath, file)

    if (uploadError) throw uploadError

    const { data } = supabase.storage
      .from('avatars')
      .getPublicUrl(filePath)

    fileUrl.value = data.publicUrl
  } catch (error) {
    alert('上传失败: ' + error.message)
  } finally {
    uploading.value = false
  }
}
</script>

<template>
  <div>
    <input type="file" @change="uploadFile" :disabled="uploading" />
    <div v-if="uploading">上传中...</div>
    <img v-if="fileUrl" :src="fileUrl" alt="上传的图片" />
  </div>
</template>

Realtime 使用

订阅数据变化

// 订阅表的所有变化
const channel = supabase
  .channel('public:users')
  .on('postgres_changes', 
    { event: '*', schema: 'public', table: 'users' },
    (payload) => {
      console.log('数据变化:', payload)
    }
  )
  .subscribe()

// 订阅特定操作
const channel = supabase
  .channel('public:posts')
  .on('postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('新增记录:', payload.new)
    }
  )
  .on('postgres_changes',
    { event: 'UPDATE', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('更新记录:', payload.new)
    }
  )
  .on('postgres_changes',
    { event: 'DELETE', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('删除记录:', payload.old)
    }
  )
  .subscribe()

取消订阅

const removeChannel = async () => {
  await supabase.removeChannel(channel)
}

Vue 实时数据组件示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'

const messages = ref([])
let channel = null

onMounted(async () => {
  // 加载初始数据
  const { data } = await supabase
    .from('messages')
    .select('*')
    .order('created_at', { ascending: false })
  
  messages.value = data || []

  // 订阅实时更新
  channel = supabase
    .channel('public:messages')
    .on('postgres_changes',
      { event: 'INSERT', schema: 'public', table: 'messages' },
      (payload) => {
        messages.value = [payload.new, ...messages.value]
      }
    )
    .on('postgres_changes',
      { event: 'DELETE', schema: 'public', table: 'messages' },
      (payload) => {
        messages.value = messages.value.filter(m => m.id !== payload.old.id)
      }
    )
    .subscribe()
})

onUnmounted(() => {
  if (channel) {
    supabase.removeChannel(channel)
  }
})
</script>

<template>
  <div>
    <h2>实时消息</h2>
    <ul>
      <li v-for="message in messages" :key="message.id">
        {{ message.content }}
      </li>
    </ul>
  </div>
</template>

完整示例

完整的 CRUD 示例

<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'

// 状态
const items = ref([])
const loading = ref(false)
const newItem = ref({ name: '', description: '' })

// 读取数据
const fetchItems = async () => {
  loading.value = true
  const { data, error } = await supabase
    .from('items')
    .select('*')
    .order('created_at', { ascending: false })
  
  if (error) {
    console.error('获取数据失败:', error)
  } else {
    items.value = data
  }
  loading.value = false
}

// 创建数据
const createItem = async () => {
  const { data, error } = await supabase
    .from('items')
    .insert([newItem.value])
    .select()
  
  if (error) {
    alert('创建失败: ' + error.message)
  } else {
    items.value = [data[0], ...items.value]
    newItem.value = { name: '', description: '' }
  }
}

// 更新数据
const updateItem = async (id, updates) => {
  const { data, error } = await supabase
    .from('items')
    .update(updates)
    .eq('id', id)
    .select()
  
  if (error) {
    alert('更新失败: ' + error.message)
  } else {
    const index = items.value.findIndex(item => item.id === id)
    if (index !== -1) {
      items.value[index] = data[0]
    }
  }
}

// 删除数据
const deleteItem = async (id) => {
  if (!confirm('确认删除?')) return
  
  const { error } = await supabase
    .from('items')
    .delete()
    .eq('id', id)
  
  if (error) {
    alert('删除失败: ' + error.message)
  } else {
    items.value = items.value.filter(item => item.id !== id)
  }
}

onMounted(() => {
  fetchItems()
})
</script>

<template>
  <div>
    <h1>数据管理</h1>
    
    <!-- 创建表单 -->
    <div class="form">
      <input v-model="newItem.name" placeholder="名称" />
      <input v-model="newItem.description" placeholder="描述" />
      <button @click="createItem">创建</button>
    </div>
    
    <!-- 列表 -->
    <div v-if="loading">加载中...</div>
    <ul v-else>
      <li v-for="item in items" :key="item.id">
        <span>{{ item.name }} - {{ item.description }}</span>
        <button @click="updateItem(item.id, { name: '已更新' })">更新</button>
        <button @click="deleteItem(item.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

错误处理

统一错误处理

// src/lib/errorHandler.js
export const handleSupabaseError = (error) => {
  if (!error) return null
  
  // 认证错误
  if (error.status === 401) {
    console.error('认证失败,请重新登录')
    // 跳转到登录页
    router.push('/login')
  }
  
  // 权限错误
  if (error.status === 403) {
    console.error('权限不足')
  }
  
  // 网络错误
  if (error.message && error.message.includes('fetch')) {
    console.error('网络连接失败')
  }
  
  return error.message || '操作失败'
}

使用错误处理

import { handleSupabaseError } from '@/lib/errorHandler'

const fetchData = async () => {
  const { data, error } = await supabase
    .from('users')
    .select('*')
  
  if (error) {
    const errorMsg = handleSupabaseError(error)
    alert(errorMsg)
    return
  }
  
  // 处理成功的数据
  console.log(data)
}

🔧 数据库 Schema 示例

创建表(在 Dashboard 中执行)

-- 创建用户表
CREATE TABLE users (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  avatar_url TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 创建帖子表
CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 启用 RLS (Row Level Security)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 创建策略
CREATE POLICY "用户可以查看所有用户" ON users
  FOR SELECT USING (true);

CREATE POLICY "用户只能更新自己的数据" ON users
  FOR UPDATE USING (auth.uid() = id);

CREATE POLICY "所有人可以查看已发布的帖子" ON posts
  FOR SELECT USING (published = true);

CREATE POLICY "用户可以创建自己的帖子" ON posts
  FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "用户可以更新自己的帖子" ON posts
  FOR UPDATE USING (auth.uid() = user_id);

📚 更多资源


⚠️ 注意事项

  1. ANON_KEY 是公开的:可以在前端代码中使用
  2. SERVICE_ROLE_KEY 必须保密:只能在后端使用
  3. 使用 RLS (Row Level Security):保护数据安全
  4. Dashboard 访问:只能通过内网 http://100.64.0.2:18000 访问
  5. API 端点:通过 https://amiap.hzau.edu.cn/supa 访问所有服务

🎯 下一步

  1. 在 Dashboard 中创建数据库表
  2. 配置 RLS 策略
  3. 在 Vue 应用中集成 Supabase
  4. 开发认证、CRUD、文件上传等功能
  5. 测试实时订阅功能

📦 S3 对象存储配置与使用

概述

Supabase Storage 已配置使用 MinIO 作为 S3 兼容的对象存储后端。所有文件都会保存在宿主机的 /vol1/1000/s3 目录。

🔧 启动 S3 存储服务

cd /vol1/1000/docker_server/traefik/supabase-stack
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d

说明

  • docker-compose.yml: 主配置文件(包含所有 Supabase 服务)
  • docker-compose.s3.yml: MinIO 配置文件(在 /vol1/1000/docker_server/traefik/supabase-stack/ 目录)
  • 所有对象文件实际存储在:/vol1/1000/s3/stub/<bucket_name>/...

🌐 S3 访问端点

统一 S3 端点(推荐)

https://amiap.hzau.edu.cn/supa/storage/v1/s3

这是标准的 Supabase Storage S3 协议端点,完全兼容 AWS S3 API。

MinIO 管理控制台(可选)

http://100.64.0.2:9001
用户名: supa-storage
密码: secret519521

🔑 S3 认证密钥

1. S3 协议访问密钥(推荐用于应用)

用于通过 Supabase Storage 的 S3 端点访问:

# 从 .env 文件
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
S3_PROTOCOL_REGION=stub

2. MinIO 根密钥(内部使用)

用于 storage 服务连接 MinIO一般不直接使用

MINIO_ROOT_USER=supa-storage
MINIO_ROOT_PASSWORD=secret1234

💻 客户端配置示例

Python (boto3)

import boto3
from botocore.client import Config

# 配置 S3 客户端连接到 Supabase Storage
s3_client = boto3.client(
    's3',
    endpoint_url='https://amiap.hzau.edu.cn/supa/storage/v1/s3',
    aws_access_key_id='supa-protocol-key',
    aws_secret_access_key='supa-protocol-secret',
    region_name='stub',
    config=Config(
        signature_version='s3v4',
        s3={'addressing_style': 'path'}
    ),
    verify=True  # HTTPS 验证
)

# 列出所有 buckets
response = s3_client.list_buckets()
for bucket in response['Buckets']:
    print(f"Bucket: {bucket['Name']}")

# 上传文件
with open('example.jpg', 'rb') as file:
    s3_client.upload_fileobj(
        file,
        'my-bucket',  # bucket 名称
        'uploads/example.jpg'  # 对象路径
    )

# 下载文件
s3_client.download_file(
    'my-bucket',
    'uploads/example.jpg',
    'downloaded.jpg'
)

# 生成预签名 URL临时访问链接
url = s3_client.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'my-bucket', 'Key': 'uploads/example.jpg'},
    ExpiresIn=3600  # 1小时过期
)
print(f"Presigned URL: {url}")

JavaScript/Node.js (AWS SDK v3)

import { S3Client, ListBucketsCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import fs from 'fs'

// 配置 S3 客户端
const s3Client = new S3Client({
  endpoint: 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
  region: 'stub',
  credentials: {
    accessKeyId: 'supa-protocol-key',
    secretAccessKey: 'supa-protocol-secret',
  },
  forcePathStyle: true,  // 使用路径风格访问
})

// 列出 buckets
const listBuckets = async () => {
  const command = new ListBucketsCommand({})
  const response = await s3Client.send(command)
  console.log('Buckets:', response.Buckets)
}

// 上传文件
const uploadFile = async () => {
  const fileContent = fs.readFileSync('example.jpg')
  
  const command = new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: 'uploads/example.jpg',
    Body: fileContent,
    ContentType: 'image/jpeg',
  })
  
  const response = await s3Client.send(command)
  console.log('Upload success:', response)
}

// 生成预签名 URL
const getPresignedUrl = async () => {
  const command = new GetObjectCommand({
    Bucket: 'my-bucket',
    Key: 'uploads/example.jpg',
  })
  
  const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
  console.log('Presigned URL:', url)
  return url
}

// 执行
await listBuckets()
await uploadFile()
await getPresignedUrl()

PostgreSQL Foreign Data Wrapper (FDW)

在 PostgreSQL 中直接查询 S3 对象:

-- 1. 启用扩展
CREATE EXTENSION IF NOT EXISTS wrappers CASCADE;

-- 2. 创建 S3 服务器连接
CREATE SERVER s3_server
  FOREIGN DATA WRAPPER s3_wrapper
  OPTIONS (
    aws_access_key_id 'supa-protocol-key',
    aws_secret_access_key 'supa-protocol-secret',
    aws_region 'stub',
    endpoint_url 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
    path_style_url 'true'
  );

-- 3. 创建外部表映射到 S3 对象(例如 Parquet 文件)
CREATE FOREIGN TABLE my_s3_table (
  id bigint,
  name text,
  created_at timestamp
)
SERVER s3_server
OPTIONS (
  uri 's3://my-bucket/data/file.parquet',
  format 'parquet'
);

-- 4. 查询 S3 数据
SELECT * FROM my_s3_table WHERE created_at > NOW() - INTERVAL '7 days';

AWS CLI

# 配置 AWS CLI
export AWS_ACCESS_KEY_ID=supa-protocol-key
export AWS_SECRET_ACCESS_KEY=supa-protocol-secret
export AWS_DEFAULT_REGION=stub

# 或创建 AWS 配置文件 ~/.aws/config
# [profile supabase]
# region = stub
# output = json

# ~/.aws/credentials
# [supabase]
# aws_access_key_id = supa-protocol-key
# aws_secret_access_key = supa-protocol-secret

# 列出 buckets
aws s3 ls --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3

# 列出 bucket 中的对象
aws s3 ls s3://my-bucket/ --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3

# 上传文件
aws s3 cp example.jpg s3://my-bucket/uploads/ \
  --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3

# 下载文件
aws s3 cp s3://my-bucket/uploads/example.jpg downloaded.jpg \
  --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3

# 同步目录
aws s3 sync ./local-dir s3://my-bucket/backup/ \
  --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3

Vue 3 + Supabase Client推荐

<script setup>
import { ref } from 'vue'
import { supabase } from '@/lib/supabaseClient'

const uploading = ref(false)
const uploadedUrl = ref('')

// 上传文件到 S3
const uploadToS3 = async (event) => {
  try {
    uploading.value = true
    const file = event.target.files[0]
    const fileExt = file.name.split('.').pop()
    const fileName = `${Math.random()}.${fileExt}`
    const filePath = `uploads/${fileName}`

    // Supabase Storage 会自动使用配置的 S3 后端MinIO
    const { data, error } = await supabase.storage
      .from('my-bucket')  // bucket 名称
      .upload(filePath, file, {
        cacheControl: '3600',
        upsert: false
      })

    if (error) throw error

    // 获取公开 URL
    const { data: urlData } = supabase.storage
      .from('my-bucket')
      .getPublicUrl(filePath)

    uploadedUrl.value = urlData.publicUrl
    console.log('文件已上传到 S3:', uploadedUrl.value)
  } catch (error) {
    console.error('上传失败:', error.message)
  } finally {
    uploading.value = false
  }
}

// 从 S3 下载文件
const downloadFromS3 = async (filePath) => {
  const { data, error } = await supabase.storage
    .from('my-bucket')
    .download(filePath)

  if (error) {
    console.error('下载失败:', error)
    return
  }

  // 创建下载链接
  const url = URL.createObjectURL(data)
  const a = document.createElement('a')
  a.href = url
  a.download = filePath.split('/').pop()
  a.click()
}

// 列出 bucket 中的文件
const listFiles = async () => {
  const { data, error } = await supabase.storage
    .from('my-bucket')
    .list('uploads/', {
      limit: 100,
      offset: 0,
      sortBy: { column: 'created_at', order: 'desc' }
    })

  if (error) {
    console.error('列表失败:', error)
    return
  }

  console.log('S3 文件列表:', data)
  return data
}
</script>

<template>
  <div>
    <h2>S3 文件上传</h2>
    <input type="file" @change="uploadToS3" :disabled="uploading" />
    <div v-if="uploading">上传中...</div>
    <div v-if="uploadedUrl">
      <p>上传成功</p>
      <a :href="uploadedUrl" target="_blank">查看文件</a>
    </div>
  </div>
</template>

🗂️ 数据存储位置

所有上传的文件实际保存在

/vol1/1000/s3/stub/<bucket_name>/<object_path>

示例:

/vol1/1000/s3/
└── stub/
    ├── my-bucket/
    │   ├── uploads/
    │   │   ├── image1.jpg
    │   │   └── image2.png
    │   └── documents/
    │       └── report.pdf
    └── avatars/
        └── user123.jpg

🔒 安全建议

  1. 生产环境密钥:修改 .env 中的默认密钥
  2. HTTPS 访问:外部访问使用 HTTPS 端点
  3. Bucket 权限:合理配置 RLS 策略
  4. MinIO 控制台:仅内网访问(端口 9001

常见问题

Q: 文件存在哪里? A: 所有文件物理存储在 /vol1/1000/s3/stub/<bucket_name>/

Q: 如何备份数据? A: 直接备份 /vol1/1000/s3 目录即可

Q: 可以直接访问 MinIO 吗? A: 可以,但推荐使用 Supabase Storage 的 S3 端点(更安全)

Q: 如何迁移现有文件到 S3 A: 使用 AWS CLI 或 Python 脚本批量上传到对应 bucket


📝 在 Dashboard 中创建 Bucket

  1. 访问 Dashboard: http://100.64.0.2:18000
  2. 登录后进入 Storage 页面
  3. 点击 New Bucket
  4. 输入 bucket 名称(例如:my-bucket
  5. 配置访问权限Public/Private
  6. 创建完成后,文件会保存在 /vol1/1000/s3/stub/my-bucket/

⚙️ 配置文件说明

.env 配置

# MinIO 内部认证
MINIO_ROOT_USER=supa-storage
MINIO_ROOT_PASSWORD=secret1234

# S3 协议访问密钥(用于外部客户端)
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
S3_PROTOCOL_REGION=stub

# S3 配置
GLOBAL_S3_BUCKET=stub
GLOBAL_S3_ENDPOINT=http://minio:9000
GLOBAL_S3_PROTOCOL=http
GLOBAL_S3_FORCE_PATH_STYLE=true
AWS_DEFAULT_REGION=stub

docker-compose.s3.yml

services:
  minio:
    image: minio/minio
    ports:
      - "9000:9000"  # S3 API
      - "9001:9001"  # Web 控制台
    environment:
      MINIO_ROOT_USER: supa-storage
      MINIO_ROOT_PASSWORD: secret1234
    command: server --console-address ":9001" /data
    volumes:
      - /vol1/1000/s3:/data:z  # 数据存储位置

  minio-createbucket:
    image: minio/mc
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      /usr/bin/mc alias set supa-minio http://minio:9000 supa-storage secret1234;
      /usr/bin/mc mb supa-minio/stub;
      exit 0;
      "

🔧 验证 S3 配置

1. 检查 MinIO 服务状态

docker compose ps minio

2. 使用 MinIO Client 验证

# 进入 MinIO 容器
docker exec -it $(docker ps -qf "name=minio") sh

# 配置 mc 别名
mc alias set local http://localhost:9000 supa-storage secret1234

# 列出 buckets
mc ls local/

# 查看 stub bucket
mc ls local/stub/

# 退出
exit

3. 测试 S3 API 端点

# 测试 endpoint 是否可访问
curl -I https://amiap.hzau.edu.cn/supa/storage/v1/s3

📚 相关资源