mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
revert: remove Google Drive storage backend
Remove all GDrive/Google Drive storage extension code: - Delete 7 GDrive-specific files (handler, service, tests, router, frontend) - Remove GDrive fields from setting service and DTOs - Remove GDrive routes, wire dependencies, i18n entries - Revert DataManagementView to S3-only storage management
This commit is contained in:
@@ -185,15 +185,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
opsSystemLogSink := service.ProvideOpsSystemLogSink(opsRepository)
|
||||
opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, userRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, opsSystemLogSink)
|
||||
soraS3Storage := service.NewSoraS3Storage(settingService)
|
||||
soraGDriveStorage := service.NewSoraGDriveStorage(settingService)
|
||||
soraStorageRouter := service.NewSoraStorageRouter(settingService, soraS3Storage, soraGDriveStorage)
|
||||
settingService.SetOnS3UpdateCallback(soraStorageRouter.RefreshAll)
|
||||
settingService.SetOnS3UpdateCallback(soraS3Storage.RefreshClient)
|
||||
soraGenerationRepository := repository.NewSoraGenerationRepository(db)
|
||||
soraQuotaService := service.NewSoraQuotaService(userRepository, groupRepository, settingService)
|
||||
soraGenerationService := service.NewSoraGenerationService(soraGenerationRepository, soraStorageRouter, soraQuotaService)
|
||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, soraS3Storage, soraGDriveStorage, soraGenerationService)
|
||||
soraGDriveOAuthService := service.NewSoraGDriveOAuthService(settingService)
|
||||
gdriveOAuthHandler := admin.NewGDriveOAuthHandler(settingService, soraGDriveOAuthService, soraGDriveStorage)
|
||||
soraGenerationService := service.NewSoraGenerationService(soraGenerationRepository, soraS3Storage, soraQuotaService)
|
||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, soraS3Storage)
|
||||
opsHandler := admin.NewOpsHandler(opsService)
|
||||
updateCache := repository.NewUpdateCache(redisClient)
|
||||
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||
@@ -221,7 +217,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository)
|
||||
scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService)
|
||||
channelHandler := admin.NewChannelHandler(channelService, billingService)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, gdriveOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler)
|
||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
||||
@@ -230,12 +226,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
soraSDKClient := service.ProvideSoraSDKClient(configConfig, httpUpstream, openAITokenProvider, accountRepository, soraAccountRepository)
|
||||
soraMediaStorage := service.ProvideSoraMediaStorage(configConfig)
|
||||
soraGatewayService := service.NewSoraGatewayService(soraSDKClient, rateLimitService, httpUpstream, configConfig)
|
||||
soraClientHandler := handler.NewSoraClientHandler(soraGenerationService, soraQuotaService, soraStorageRouter, soraGatewayService, gatewayService, soraMediaStorage, apiKeyService)
|
||||
soraClientHandler := handler.NewSoraClientHandler(soraGenerationService, soraQuotaService, soraS3Storage, soraGatewayService, gatewayService, soraMediaStorage, apiKeyService)
|
||||
soraTaskRepository := repository.NewSoraTaskRepository(db)
|
||||
soraTaskService := service.NewSoraTaskService(soraTaskRepository, accountRepository, soraSDKClient, httpUpstream)
|
||||
soraTaskWorker := service.NewSoraTaskWorker(soraTaskService, accountRepository, soraStorageRouter, soraMediaStorage, 60*time.Second)
|
||||
soraTaskWorker := service.NewSoraTaskWorker(soraTaskService, accountRepository, soraS3Storage, soraMediaStorage, 60*time.Second)
|
||||
soraTaskWorker.Start()
|
||||
soraVideosHandler := handler.NewSoraVideosHandler(soraTaskService, gatewayService, soraStorageRouter, soraMediaStorage, soraGatewayService)
|
||||
soraVideosHandler := handler.NewSoraVideosHandler(soraTaskService, gatewayService, soraS3Storage, soraMediaStorage, soraGatewayService)
|
||||
soraGatewayHandler := handler.NewSoraGatewayHandler(gatewayService, soraGatewayService, concurrencyService, billingCacheService, usageRecordWorkerPool, configConfig)
|
||||
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
|
||||
totpHandler := handler.NewTotpHandler(totpService)
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GDriveOAuthHandler 处理 Google Drive OAuth 授权流程。
|
||||
type GDriveOAuthHandler struct {
|
||||
settingService *service.SettingService
|
||||
gdriveOAuth *service.SoraGDriveOAuthService
|
||||
gdriveStorage *service.SoraGDriveStorage
|
||||
}
|
||||
|
||||
// NewGDriveOAuthHandler 创建 GDrive OAuth Handler。
|
||||
func NewGDriveOAuthHandler(settingService *service.SettingService, gdriveOAuth *service.SoraGDriveOAuthService, gdriveStorage *service.SoraGDriveStorage) *GDriveOAuthHandler {
|
||||
return &GDriveOAuthHandler{
|
||||
settingService: settingService,
|
||||
gdriveOAuth: gdriveOAuth,
|
||||
gdriveStorage: gdriveStorage,
|
||||
}
|
||||
}
|
||||
|
||||
// StartOAuthRequest 启动 OAuth 授权请求。
|
||||
type StartOAuthRequest struct {
|
||||
ClientID string `json:"client_id" binding:"required"`
|
||||
ClientSecret string `json:"client_secret" binding:"required"`
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
}
|
||||
|
||||
// StartOAuth 生成 Google OAuth 授权 URL。
|
||||
// POST /api/v1/admin/settings/sora-storage/gdrive-oauth/start
|
||||
func (h *GDriveOAuthHandler) StartOAuth(c *gin.Context) {
|
||||
if h.gdriveOAuth == nil {
|
||||
response.Error(c, http.StatusInternalServerError, "GDrive OAuth service not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
var req StartOAuthRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
authURL, state, err := h.gdriveOAuth.GenerateAuthURL(req.ClientID, req.ClientSecret, req.RedirectURI)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "生成授权 URL 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"auth_url": authURL,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// OAuthCallbackRequest OAuth 回调请求。
|
||||
type OAuthCallbackRequest struct {
|
||||
ClientID string `json:"client_id" binding:"required"`
|
||||
ClientSecret string `json:"client_secret" binding:"required"`
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
ProfileID string `json:"profile_id"` // 要保存到的 profile ID(可选)
|
||||
}
|
||||
|
||||
// OAuthCallback 用授权码换取 refresh_token 并保存到 profile。
|
||||
// POST /api/v1/admin/settings/sora-storage/gdrive-oauth/callback
|
||||
func (h *GDriveOAuthHandler) OAuthCallback(c *gin.Context) {
|
||||
if h.gdriveOAuth == nil {
|
||||
response.Error(c, http.StatusInternalServerError, "GDrive OAuth service not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
var req OAuthCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken, err := h.gdriveOAuth.ExchangeCode(c.Request.Context(), req.ClientID, req.ClientSecret, req.RedirectURI, req.Code)
|
||||
if err != nil {
|
||||
slog.Error("[GDriveOAuth] exchange failed",
|
||||
"client_id_len", len(req.ClientID),
|
||||
"client_secret_len", len(req.ClientSecret),
|
||||
"redirect_uri", req.RedirectURI,
|
||||
"code_len", len(req.Code),
|
||||
"error", err,
|
||||
)
|
||||
response.Error(c, http.StatusBadRequest, "换取 refresh_token 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果指定了 profile_id,自动保存 refresh_token 到 profile
|
||||
if req.ProfileID != "" {
|
||||
profiles, err := h.settingService.ListSoraS3Profiles(c.Request.Context())
|
||||
if err == nil {
|
||||
for _, p := range profiles.Items {
|
||||
if p.ProfileID == req.ProfileID {
|
||||
_, _ = h.settingService.UpdateSoraS3Profile(c.Request.Context(), req.ProfileID, &service.SoraS3Profile{
|
||||
Name: p.Name,
|
||||
Provider: p.Provider,
|
||||
AccessMode: p.AccessMode,
|
||||
Enabled: p.Enabled,
|
||||
Endpoint: p.Endpoint,
|
||||
Region: p.Region,
|
||||
Bucket: p.Bucket,
|
||||
AccessKeyID: p.AccessKeyID,
|
||||
Prefix: p.Prefix,
|
||||
ForcePathStyle: p.ForcePathStyle,
|
||||
CDNURL: p.CDNURL,
|
||||
DefaultStorageQuotaBytes: p.DefaultStorageQuotaBytes,
|
||||
AuthType: p.AuthType,
|
||||
ClientID: p.ClientID,
|
||||
FolderID: p.FolderID,
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"refresh_token": refreshToken,
|
||||
"message": "OAuth 授权成功",
|
||||
})
|
||||
}
|
||||
|
||||
// TestGDriveStorage 测试 GDrive 存储的完整上传→下载→删除流程。
|
||||
// POST /api/v1/admin/settings/sora-storage/gdrive-test
|
||||
func (h *GDriveOAuthHandler) TestGDriveStorage(c *gin.Context) {
|
||||
if h.gdriveStorage == nil {
|
||||
response.Error(c, http.StatusInternalServerError, "GDrive storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.gdriveStorage.TestFullCycle(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "GDrive 测试失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
@@ -37,25 +37,21 @@ func generateMenuItemID() (string, error) {
|
||||
|
||||
// SettingHandler 系统设置处理器
|
||||
type SettingHandler struct {
|
||||
settingService *service.SettingService
|
||||
emailService *service.EmailService
|
||||
turnstileService *service.TurnstileService
|
||||
opsService *service.OpsService
|
||||
soraS3Storage *service.SoraS3Storage
|
||||
soraGDriveStorage *service.SoraGDriveStorage
|
||||
soraGenerationService *service.SoraGenerationService
|
||||
settingService *service.SettingService
|
||||
emailService *service.EmailService
|
||||
turnstileService *service.TurnstileService
|
||||
opsService *service.OpsService
|
||||
soraS3Storage *service.SoraS3Storage
|
||||
}
|
||||
|
||||
// NewSettingHandler 创建系统设置处理器
|
||||
func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, opsService *service.OpsService, soraS3Storage *service.SoraS3Storage, soraGDriveStorage *service.SoraGDriveStorage, soraGenerationService *service.SoraGenerationService) *SettingHandler {
|
||||
func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, opsService *service.OpsService, soraS3Storage *service.SoraS3Storage) *SettingHandler {
|
||||
return &SettingHandler{
|
||||
settingService: settingService,
|
||||
emailService: emailService,
|
||||
turnstileService: turnstileService,
|
||||
opsService: opsService,
|
||||
soraS3Storage: soraS3Storage,
|
||||
soraGDriveStorage: soraGDriveStorage,
|
||||
soraGenerationService: soraGenerationService,
|
||||
settingService: settingService,
|
||||
emailService: emailService,
|
||||
turnstileService: turnstileService,
|
||||
opsService: opsService,
|
||||
soraS3Storage: soraS3Storage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,8 +1230,6 @@ func toSoraS3ProfileDTO(profile service.SoraS3Profile) dto.SoraS3Profile {
|
||||
ProfileID: profile.ProfileID,
|
||||
Name: profile.Name,
|
||||
IsActive: profile.IsActive,
|
||||
Provider: profile.GetProvider(),
|
||||
AccessMode: profile.AccessMode,
|
||||
Enabled: profile.Enabled,
|
||||
Endpoint: profile.Endpoint,
|
||||
Region: profile.Region,
|
||||
@@ -1247,13 +1241,6 @@ func toSoraS3ProfileDTO(profile service.SoraS3Profile) dto.SoraS3Profile {
|
||||
CDNURL: profile.CDNURL,
|
||||
DefaultStorageQuotaBytes: profile.DefaultStorageQuotaBytes,
|
||||
UpdatedAt: profile.UpdatedAt,
|
||||
// Google Drive 专属
|
||||
AuthType: profile.AuthType,
|
||||
ClientID: profile.ClientID,
|
||||
ClientSecretConfigured: profile.ClientSecretConfigured,
|
||||
RefreshTokenConfigured: profile.RefreshTokenConfigured,
|
||||
ServiceAccountConfigured: profile.ServiceAccountConfigured,
|
||||
FolderID: profile.FolderID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,8 +1320,6 @@ type CreateSoraS3ProfileRequest struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
Name string `json:"name"`
|
||||
SetActive bool `json:"set_active"`
|
||||
Provider string `json:"provider"` // "s3" / "gdrive"
|
||||
AccessMode string `json:"access_mode"` // "direct" / "proxy"
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
@@ -1345,19 +1330,10 @@ type CreateSoraS3ProfileRequest struct {
|
||||
ForcePathStyle bool `json:"force_path_style"`
|
||||
CDNURL string `json:"cdn_url"`
|
||||
DefaultStorageQuotaBytes int64 `json:"default_storage_quota_bytes"`
|
||||
// Google Drive 专属
|
||||
AuthType string `json:"auth_type,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ServiceAccountJSON string `json:"service_account_json,omitempty"`
|
||||
FolderID string `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSoraS3ProfileRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
AccessMode string `json:"access_mode"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
@@ -1368,13 +1344,6 @@ type UpdateSoraS3ProfileRequest struct {
|
||||
ForcePathStyle bool `json:"force_path_style"`
|
||||
CDNURL string `json:"cdn_url"`
|
||||
DefaultStorageQuotaBytes int64 `json:"default_storage_quota_bytes"`
|
||||
// Google Drive 专属
|
||||
AuthType string `json:"auth_type,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ServiceAccountJSON string `json:"service_account_json,omitempty"`
|
||||
FolderID string `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
// CreateSoraS3Profile 创建 Sora S3 配置
|
||||
@@ -1397,23 +1366,15 @@ func (h *SettingHandler) CreateSoraS3Profile(c *gin.Context) {
|
||||
response.BadRequest(c, "Profile ID is required")
|
||||
return
|
||||
}
|
||||
// S3 专属字段验证:仅当 provider 为 s3(或未指定)时校验
|
||||
provider := req.Provider
|
||||
if provider == "" {
|
||||
provider = "s3"
|
||||
}
|
||||
if provider == "s3" {
|
||||
if err := validateSoraS3RequiredWhenEnabled(req.Enabled, req.Endpoint, req.Bucket, req.AccessKeyID, req.SecretAccessKey, false); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
// S3 字段验证
|
||||
if err := validateSoraS3RequiredWhenEnabled(req.Enabled, req.Endpoint, req.Bucket, req.AccessKeyID, req.SecretAccessKey, false); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
created, err := h.settingService.CreateSoraS3Profile(c.Request.Context(), &service.SoraS3Profile{
|
||||
ProfileID: req.ProfileID,
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
AccessMode: req.AccessMode,
|
||||
Enabled: req.Enabled,
|
||||
Endpoint: req.Endpoint,
|
||||
Region: req.Region,
|
||||
@@ -1424,13 +1385,6 @@ func (h *SettingHandler) CreateSoraS3Profile(c *gin.Context) {
|
||||
ForcePathStyle: req.ForcePathStyle,
|
||||
CDNURL: req.CDNURL,
|
||||
DefaultStorageQuotaBytes: req.DefaultStorageQuotaBytes,
|
||||
// Google Drive 专属
|
||||
AuthType: req.AuthType,
|
||||
ClientID: req.ClientID,
|
||||
ClientSecret: req.ClientSecret,
|
||||
RefreshToken: req.RefreshToken,
|
||||
ServiceAccountJSON: req.ServiceAccountJSON,
|
||||
FolderID: req.FolderID,
|
||||
}, req.SetActive)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -1473,25 +1427,14 @@ func (h *SettingHandler) UpdateSoraS3Profile(c *gin.Context) {
|
||||
response.ErrorFrom(c, service.ErrSoraS3ProfileNotFound)
|
||||
return
|
||||
}
|
||||
// S3 专属字段验证
|
||||
provider := req.Provider
|
||||
if provider == "" && existing != nil {
|
||||
provider = existing.GetProvider()
|
||||
}
|
||||
if provider == "" {
|
||||
provider = "s3"
|
||||
}
|
||||
if provider == "s3" {
|
||||
if err := validateSoraS3RequiredWhenEnabled(req.Enabled, req.Endpoint, req.Bucket, req.AccessKeyID, req.SecretAccessKey, existing.SecretAccessKeyConfigured); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
// S3 字段验证
|
||||
if err := validateSoraS3RequiredWhenEnabled(req.Enabled, req.Endpoint, req.Bucket, req.AccessKeyID, req.SecretAccessKey, existing.SecretAccessKeyConfigured); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, updateErr := h.settingService.UpdateSoraS3Profile(c.Request.Context(), profileID, &service.SoraS3Profile{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
AccessMode: req.AccessMode,
|
||||
Enabled: req.Enabled,
|
||||
Endpoint: req.Endpoint,
|
||||
Region: req.Region,
|
||||
@@ -1502,13 +1445,6 @@ func (h *SettingHandler) UpdateSoraS3Profile(c *gin.Context) {
|
||||
ForcePathStyle: req.ForcePathStyle,
|
||||
CDNURL: req.CDNURL,
|
||||
DefaultStorageQuotaBytes: req.DefaultStorageQuotaBytes,
|
||||
// Google Drive 专属
|
||||
AuthType: req.AuthType,
|
||||
ClientID: req.ClientID,
|
||||
ClientSecret: req.ClientSecret,
|
||||
RefreshToken: req.RefreshToken,
|
||||
ServiceAccountJSON: req.ServiceAccountJSON,
|
||||
FolderID: req.FolderID,
|
||||
})
|
||||
if updateErr != nil {
|
||||
response.ErrorFrom(c, updateErr)
|
||||
@@ -1845,44 +1781,3 @@ func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
|
||||
ThresholdWindowMinutes: updatedSettings.ThresholdWindowMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// GetGDriveQuota 获取 Google Drive 配额信息。
|
||||
// GET /api/v1/admin/settings/sora-storage/gdrive-quota
|
||||
func (h *SettingHandler) GetGDriveQuota(c *gin.Context) {
|
||||
if h.soraGDriveStorage == nil {
|
||||
response.Error(c, http.StatusServiceUnavailable, "GDrive storage not configured")
|
||||
return
|
||||
}
|
||||
quota, err := h.soraGDriveStorage.GetQuotaInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, fmt.Sprintf("failed to get GDrive quota: %v", err))
|
||||
return
|
||||
}
|
||||
response.Success(c, quota)
|
||||
}
|
||||
|
||||
// GetStorageVideoStats 获取各存储类型的视频统计信息。
|
||||
// GET /api/v1/admin/settings/sora-storage/video-stats
|
||||
func (h *SettingHandler) GetStorageVideoStats(c *gin.Context) {
|
||||
if h.soraGenerationService == nil {
|
||||
response.Error(c, http.StatusServiceUnavailable, "generation service not configured")
|
||||
return
|
||||
}
|
||||
|
||||
storageTypes := []string{service.SoraStorageTypeS3, service.SoraStorageTypeGDrive}
|
||||
result := make(map[string]*service.StorageVideoStats, len(storageTypes))
|
||||
|
||||
for _, st := range storageTypes {
|
||||
completed, inProgress, err := h.soraGenerationService.CountByStorageType(c.Request.Context(), st)
|
||||
if err != nil {
|
||||
log.Printf("[SettingHandler] CountByStorageType(%s) error: %v", st, err)
|
||||
continue
|
||||
}
|
||||
result[st] = &service.StorageVideoStats{
|
||||
Completed: completed,
|
||||
InProgress: inProgress,
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
@@ -152,8 +152,6 @@ type SoraS3Profile struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Provider string `json:"provider"` // "s3" / "gdrive"
|
||||
AccessMode string `json:"access_mode"` // "direct" / "proxy"
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
@@ -165,14 +163,6 @@ type SoraS3Profile struct {
|
||||
CDNURL string `json:"cdn_url"`
|
||||
DefaultStorageQuotaBytes int64 `json:"default_storage_quota_bytes"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
|
||||
// --- Google Drive 专属 ---
|
||||
AuthType string `json:"auth_type,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecretConfigured bool `json:"client_secret_configured"`
|
||||
RefreshTokenConfigured bool `json:"refresh_token_configured"`
|
||||
ServiceAccountConfigured bool `json:"service_account_configured"`
|
||||
FolderID string `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
// ListSoraS3ProfilesResponse Sora S3 配置列表响应
|
||||
|
||||
@@ -30,7 +30,6 @@ type AdminHandlers struct {
|
||||
TLSFingerprintProfile *admin.TLSFingerprintProfileHandler
|
||||
APIKey *admin.AdminAPIKeyHandler
|
||||
ScheduledTest *admin.ScheduledTestHandler
|
||||
GDriveOAuth *admin.GDriveOAuthHandler
|
||||
Channel *admin.ChannelHandler
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ func ProvideAdminHandlers(
|
||||
openaiOAuthHandler *admin.OpenAIOAuthHandler,
|
||||
geminiOAuthHandler *admin.GeminiOAuthHandler,
|
||||
antigravityOAuthHandler *admin.AntigravityOAuthHandler,
|
||||
gdriveOAuthHandler *admin.GDriveOAuthHandler,
|
||||
proxyHandler *admin.ProxyHandler,
|
||||
redeemHandler *admin.RedeemHandler,
|
||||
promoHandler *admin.PromoHandler,
|
||||
@@ -61,7 +60,6 @@ func ProvideAdminHandlers(
|
||||
TLSFingerprintProfile: tlsFingerprintProfileHandler,
|
||||
APIKey: apiKeyHandler,
|
||||
ScheduledTest: scheduledTestHandler,
|
||||
GDriveOAuth: gdriveOAuthHandler,
|
||||
Channel: channelHandler,
|
||||
}
|
||||
}
|
||||
@@ -156,7 +154,6 @@ var ProviderSet = wire.NewSet(
|
||||
admin.NewTLSFingerprintProfileHandler,
|
||||
admin.NewAdminAPIKeyHandler,
|
||||
admin.NewScheduledTestHandler,
|
||||
admin.NewGDriveOAuthHandler,
|
||||
admin.NewChannelHandler,
|
||||
|
||||
// AdminHandlers and Handlers constructors
|
||||
|
||||
@@ -442,13 +442,6 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||
adminSettings.PUT("/sora-storage/profiles/:profile_id", h.Admin.Setting.UpdateSoraS3Profile)
|
||||
adminSettings.DELETE("/sora-storage/profiles/:profile_id", h.Admin.Setting.DeleteSoraS3Profile)
|
||||
adminSettings.POST("/sora-storage/profiles/:profile_id/activate", h.Admin.Setting.SetActiveSoraS3Profile)
|
||||
// Google Drive OAuth
|
||||
adminSettings.POST("/sora-storage/gdrive-oauth/start", h.Admin.GDriveOAuth.StartOAuth)
|
||||
adminSettings.POST("/sora-storage/gdrive-oauth/callback", h.Admin.GDriveOAuth.OAuthCallback)
|
||||
adminSettings.POST("/sora-storage/gdrive-test", h.Admin.GDriveOAuth.TestGDriveStorage)
|
||||
// Sora 存储统计
|
||||
adminSettings.GET("/sora-storage/gdrive-quota", h.Admin.Setting.GetGDriveQuota)
|
||||
adminSettings.GET("/sora-storage/video-stats", h.Admin.Setting.GetStorageVideoStats)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,49 +227,6 @@ func (s *SettingService) SetOnS3UpdateCallback(callback func()) {
|
||||
s.onS3Update = callback
|
||||
}
|
||||
|
||||
// SetOnStorageUpdateCallback 设置存储配置变更时的回调函数(用于刷新所有存储客户端缓存)。
|
||||
// 替代 SetOnS3UpdateCallback,支持 S3 + GDrive 统一刷新。
|
||||
func (s *SettingService) SetOnStorageUpdateCallback(callback func()) {
|
||||
s.onS3Update = callback
|
||||
}
|
||||
|
||||
// --- 统一存储 Profile 方法别名 ---
|
||||
|
||||
// ListSoraStorageProfiles 获取 Sora 存储多配置列表(统一方法名)。
|
||||
func (s *SettingService) ListSoraStorageProfiles(ctx context.Context) (*SoraS3ProfileList, error) {
|
||||
return s.ListSoraS3Profiles(ctx)
|
||||
}
|
||||
|
||||
// CreateSoraStorageProfile 创建 Sora 存储配置(统一方法名)。
|
||||
func (s *SettingService) CreateSoraStorageProfile(ctx context.Context, profile *SoraS3Profile, setActive bool) (*SoraS3Profile, error) {
|
||||
return s.CreateSoraS3Profile(ctx, profile, setActive)
|
||||
}
|
||||
|
||||
// UpdateSoraStorageProfile 更新 Sora 存储配置(统一方法名)。
|
||||
func (s *SettingService) UpdateSoraStorageProfile(ctx context.Context, profileID string, profile *SoraS3Profile) (*SoraS3Profile, error) {
|
||||
return s.UpdateSoraS3Profile(ctx, profileID, profile)
|
||||
}
|
||||
|
||||
// DeleteSoraStorageProfile 删除 Sora 存储配置(统一方法名)。
|
||||
func (s *SettingService) DeleteSoraStorageProfile(ctx context.Context, profileID string) error {
|
||||
return s.DeleteSoraS3Profile(ctx, profileID)
|
||||
}
|
||||
|
||||
// SetActiveSoraStorageProfile 设置激活的 Sora 存储配置(统一方法名)。
|
||||
func (s *SettingService) SetActiveSoraStorageProfile(ctx context.Context, profileID string) (*SoraS3Profile, error) {
|
||||
return s.SetActiveSoraS3Profile(ctx, profileID)
|
||||
}
|
||||
|
||||
// GetActiveStorageProfile 获取当前激活的存储配置 profile。
|
||||
func (s *SettingService) GetActiveStorageProfile(ctx context.Context) (*SoraS3Profile, error) {
|
||||
profiles, err := s.ListSoraS3Profiles(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
active := pickActiveSoraS3Profile(profiles.Items, profiles.ActiveProfileID)
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// SetVersion sets the application version for injection into public settings
|
||||
func (s *SettingService) SetVersion(version string) {
|
||||
s.version = version
|
||||
@@ -1635,8 +1592,6 @@ type soraS3ProfilesStore struct {
|
||||
type soraS3ProfileStoreItem struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider,omitempty"` // "s3" / "gdrive",空值视为 "s3"
|
||||
AccessMode string `json:"access_mode,omitempty"` // "direct" / "proxy",空值视为 "direct"
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
@@ -1648,14 +1603,6 @@ type soraS3ProfileStoreItem struct {
|
||||
CDNURL string `json:"cdn_url"`
|
||||
DefaultStorageQuotaBytes int64 `json:"default_storage_quota_bytes"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
|
||||
// --- Google Drive 专属 ---
|
||||
AuthType string `json:"auth_type,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ServiceAccountJSON string `json:"service_account_json,omitempty"`
|
||||
FolderID string `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
// GetSoraS3Settings 获取 Sora S3 存储配置(兼容旧单配置语义:返回当前激活配置)
|
||||
@@ -1767,8 +1714,6 @@ func (s *SettingService) CreateSoraS3Profile(ctx context.Context, profile *SoraS
|
||||
store.Items = append(store.Items, soraS3ProfileStoreItem{
|
||||
ProfileID: profileID,
|
||||
Name: name,
|
||||
Provider: profile.Provider,
|
||||
AccessMode: profile.AccessMode,
|
||||
Enabled: profile.Enabled,
|
||||
Endpoint: strings.TrimSpace(profile.Endpoint),
|
||||
Region: strings.TrimSpace(profile.Region),
|
||||
@@ -1780,13 +1725,6 @@ func (s *SettingService) CreateSoraS3Profile(ctx context.Context, profile *SoraS
|
||||
CDNURL: strings.TrimSpace(profile.CDNURL),
|
||||
DefaultStorageQuotaBytes: maxInt64(profile.DefaultStorageQuotaBytes, 0),
|
||||
UpdatedAt: now,
|
||||
// Google Drive 专属
|
||||
AuthType: profile.AuthType,
|
||||
ClientID: strings.TrimSpace(profile.ClientID),
|
||||
ClientSecret: profile.ClientSecret,
|
||||
RefreshToken: profile.RefreshToken,
|
||||
ServiceAccountJSON: profile.ServiceAccountJSON,
|
||||
FolderID: strings.TrimSpace(profile.FolderID),
|
||||
})
|
||||
|
||||
if setActive || store.ActiveProfileID == "" {
|
||||
@@ -1832,8 +1770,6 @@ func (s *SettingService) UpdateSoraS3Profile(ctx context.Context, profileID stri
|
||||
return nil, infraerrors.BadRequest("SORA_S3_PROFILE_NAME_REQUIRED", "name is required")
|
||||
}
|
||||
target.Name = name
|
||||
target.Provider = profile.Provider
|
||||
target.AccessMode = profile.AccessMode
|
||||
target.Enabled = profile.Enabled
|
||||
target.Endpoint = strings.TrimSpace(profile.Endpoint)
|
||||
target.Region = strings.TrimSpace(profile.Region)
|
||||
@@ -1846,19 +1782,6 @@ func (s *SettingService) UpdateSoraS3Profile(ctx context.Context, profileID stri
|
||||
if profile.SecretAccessKey != "" {
|
||||
target.SecretAccessKey = profile.SecretAccessKey
|
||||
}
|
||||
// Google Drive 专属
|
||||
target.AuthType = profile.AuthType
|
||||
target.ClientID = strings.TrimSpace(profile.ClientID)
|
||||
if profile.ClientSecret != "" {
|
||||
target.ClientSecret = profile.ClientSecret
|
||||
}
|
||||
if profile.RefreshToken != "" {
|
||||
target.RefreshToken = profile.RefreshToken
|
||||
}
|
||||
if profile.ServiceAccountJSON != "" {
|
||||
target.ServiceAccountJSON = profile.ServiceAccountJSON
|
||||
}
|
||||
target.FolderID = strings.TrimSpace(profile.FolderID)
|
||||
target.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
store.Items[targetIndex] = target
|
||||
|
||||
@@ -2161,8 +2084,6 @@ func convertSoraS3ProfilesStore(store *soraS3ProfilesStore) *SoraS3ProfileList {
|
||||
ProfileID: item.ProfileID,
|
||||
Name: item.Name,
|
||||
IsActive: item.ProfileID == store.ActiveProfileID,
|
||||
Provider: item.Provider,
|
||||
AccessMode: item.AccessMode,
|
||||
Enabled: item.Enabled,
|
||||
Endpoint: item.Endpoint,
|
||||
Region: item.Region,
|
||||
@@ -2175,16 +2096,6 @@ func convertSoraS3ProfilesStore(store *soraS3ProfilesStore) *SoraS3ProfileList {
|
||||
CDNURL: item.CDNURL,
|
||||
DefaultStorageQuotaBytes: item.DefaultStorageQuotaBytes,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
// Google Drive 专属
|
||||
AuthType: item.AuthType,
|
||||
ClientID: item.ClientID,
|
||||
ClientSecret: item.ClientSecret,
|
||||
ClientSecretConfigured: item.ClientSecret != "",
|
||||
RefreshToken: item.RefreshToken,
|
||||
RefreshTokenConfigured: item.RefreshToken != "",
|
||||
ServiceAccountJSON: item.ServiceAccountJSON,
|
||||
ServiceAccountConfigured: item.ServiceAccountJSON != "",
|
||||
FolderID: item.FolderID,
|
||||
})
|
||||
}
|
||||
return &SoraS3ProfileList{
|
||||
|
||||
@@ -136,8 +136,6 @@ type SoraS3Profile struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Provider string `json:"provider"` // "s3" / "gdrive",空值视为 "s3"
|
||||
AccessMode string `json:"access_mode"` // "direct" / "proxy",空值视为 "direct"
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
@@ -150,25 +148,6 @@ type SoraS3Profile struct {
|
||||
CDNURL string `json:"cdn_url"`
|
||||
DefaultStorageQuotaBytes int64 `json:"default_storage_quota_bytes"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
|
||||
// --- Google Drive 专属 ---
|
||||
AuthType string `json:"auth_type,omitempty"` // "oauth2" / "service_account"
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"-"`
|
||||
ClientSecretConfigured bool `json:"client_secret_configured"`
|
||||
RefreshToken string `json:"-"`
|
||||
RefreshTokenConfigured bool `json:"refresh_token_configured"`
|
||||
ServiceAccountJSON string `json:"-"`
|
||||
ServiceAccountConfigured bool `json:"service_account_configured"`
|
||||
FolderID string `json:"folder_id,omitempty"`
|
||||
}
|
||||
|
||||
// GetProvider 返回 Provider,空值视为 "s3"。
|
||||
func (p *SoraS3Profile) GetProvider() string {
|
||||
if p.Provider == "" {
|
||||
return SoraStorageTypeS3
|
||||
}
|
||||
return p.Provider
|
||||
}
|
||||
|
||||
// SoraS3ProfileList Sora S3 多配置列表
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/drive/v3"
|
||||
)
|
||||
|
||||
// SoraGDriveOAuthService 处理 Google Drive OAuth2 授权流程。
|
||||
type SoraGDriveOAuthService struct{}
|
||||
|
||||
// NewSoraGDriveOAuthService 创建 GDrive OAuth 服务。
|
||||
func NewSoraGDriveOAuthService(_ *SettingService) *SoraGDriveOAuthService {
|
||||
return &SoraGDriveOAuthService{}
|
||||
}
|
||||
|
||||
// GenerateAuthURL 生成 Google OAuth 授权 URL。
|
||||
func (s *SoraGDriveOAuthService) GenerateAuthURL(clientID, clientSecret, redirectURI string) (authURL, state string, err error) {
|
||||
if clientID == "" || clientSecret == "" || redirectURI == "" {
|
||||
return "", "", fmt.Errorf("client_id, client_secret, redirect_uri are required")
|
||||
}
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{drive.DriveFileScope},
|
||||
RedirectURL: redirectURI,
|
||||
}
|
||||
|
||||
// 生成随机 state
|
||||
stateBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(stateBytes); err != nil {
|
||||
return "", "", fmt.Errorf("generate state: %w", err)
|
||||
}
|
||||
state = hex.EncodeToString(stateBytes)
|
||||
|
||||
authURL = config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
||||
return authURL, state, nil
|
||||
}
|
||||
|
||||
// ExchangeCode 用授权码换取 refresh_token。
|
||||
func (s *SoraGDriveOAuthService) ExchangeCode(ctx context.Context, clientID, clientSecret, redirectURI, code string) (string, error) {
|
||||
if code == "" {
|
||||
return "", fmt.Errorf("authorization code is required")
|
||||
}
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{drive.DriveFileScope},
|
||||
RedirectURL: redirectURI,
|
||||
}
|
||||
|
||||
token, err := config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exchange code: %w", err)
|
||||
}
|
||||
|
||||
if token.RefreshToken == "" {
|
||||
return "", fmt.Errorf("no refresh_token received, please revoke app access and try again")
|
||||
}
|
||||
|
||||
return token.RefreshToken, nil
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/drive/v3"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
)
|
||||
|
||||
// SoraGDriveStorage 负责 Sora 媒体文件的 Google Drive 存储操作。
|
||||
type SoraGDriveStorage struct {
|
||||
settingService *SettingService
|
||||
|
||||
mu sync.RWMutex
|
||||
srv *drive.Service
|
||||
cfg *SoraS3Profile // 缓存当前 GDrive 配置
|
||||
healthCheckedAt time.Time
|
||||
healthErr error
|
||||
healthTTL time.Duration
|
||||
}
|
||||
|
||||
const defaultGDriveHealthTTL = 30 * time.Second
|
||||
|
||||
// NewSoraGDriveStorage 创建 Google Drive 存储服务实例。
|
||||
func NewSoraGDriveStorage(settingService *SettingService) *SoraGDriveStorage {
|
||||
return &SoraGDriveStorage{
|
||||
settingService: settingService,
|
||||
healthTTL: defaultGDriveHealthTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// StorageType 返回存储类型标识。
|
||||
func (s *SoraGDriveStorage) StorageType() string {
|
||||
return SoraStorageTypeGDrive
|
||||
}
|
||||
|
||||
// Enabled 返回 Google Drive 存储是否已启用。
|
||||
func (s *SoraGDriveStorage) Enabled(ctx context.Context) bool {
|
||||
profile := s.getActiveGDriveProfile(ctx)
|
||||
if profile == nil {
|
||||
return false
|
||||
}
|
||||
return profile.Enabled && s.hasValidCredentials(profile)
|
||||
}
|
||||
|
||||
// getActiveGDriveProfile 获取当前激活的 GDrive 配置。
|
||||
func (s *SoraGDriveStorage) getActiveGDriveProfile(ctx context.Context) *SoraS3Profile {
|
||||
if s.settingService == nil {
|
||||
return nil
|
||||
}
|
||||
profile, err := s.settingService.GetActiveStorageProfile(ctx)
|
||||
if err != nil || profile == nil {
|
||||
return nil
|
||||
}
|
||||
if profile.GetProvider() != SoraStorageTypeGDrive {
|
||||
return nil
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
// hasValidCredentials 检查 GDrive 配置是否有有效凭证。
|
||||
func (s *SoraGDriveStorage) hasValidCredentials(profile *SoraS3Profile) bool {
|
||||
switch profile.AuthType {
|
||||
case "oauth2":
|
||||
return profile.ClientID != "" && profile.ClientSecret != "" && profile.RefreshToken != ""
|
||||
case "service_account":
|
||||
return profile.ServiceAccountJSON != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getService 获取或初始化 Drive 服务(带缓存)。
|
||||
func (s *SoraGDriveStorage) getService(ctx context.Context) (*drive.Service, *SoraS3Profile, error) {
|
||||
s.mu.RLock()
|
||||
if s.srv != nil && s.cfg != nil {
|
||||
srv, cfg := s.srv, s.cfg
|
||||
s.mu.RUnlock()
|
||||
return srv, cfg, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
return s.initService(ctx)
|
||||
}
|
||||
|
||||
func (s *SoraGDriveStorage) initService(ctx context.Context) (*drive.Service, *SoraS3Profile, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 双重检查
|
||||
if s.srv != nil && s.cfg != nil {
|
||||
return s.srv, s.cfg, nil
|
||||
}
|
||||
|
||||
profile := s.getActiveGDriveProfile(ctx)
|
||||
if profile == nil {
|
||||
return nil, nil, fmt.Errorf("no active gdrive profile found")
|
||||
}
|
||||
if !profile.Enabled {
|
||||
return nil, nil, fmt.Errorf("gdrive storage is disabled")
|
||||
}
|
||||
|
||||
srv, err := s.buildDriveService(ctx, profile)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("build gdrive service: %w", err)
|
||||
}
|
||||
|
||||
s.srv = srv
|
||||
s.cfg = profile
|
||||
logger.LegacyPrintf("service.sora_gdrive", "[SoraGDrive] 客户端已初始化 auth_type=%s folder_id=%s", profile.AuthType, profile.FolderID)
|
||||
return srv, profile, nil
|
||||
}
|
||||
|
||||
// buildDriveService 根据认证类型创建 Google Drive 服务。
|
||||
func (s *SoraGDriveStorage) buildDriveService(ctx context.Context, profile *SoraS3Profile) (*drive.Service, error) {
|
||||
switch profile.AuthType {
|
||||
case "oauth2":
|
||||
return s.buildOAuth2Service(ctx, profile)
|
||||
case "service_account":
|
||||
return s.buildServiceAccountService(ctx, profile)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported auth_type: %s", profile.AuthType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoraGDriveStorage) buildOAuth2Service(ctx context.Context, profile *SoraS3Profile) (*drive.Service, error) {
|
||||
config := &oauth2.Config{
|
||||
ClientID: profile.ClientID,
|
||||
ClientSecret: profile.ClientSecret,
|
||||
Endpoint: google.Endpoint,
|
||||
Scopes: []string{drive.DriveFileScope},
|
||||
}
|
||||
token := &oauth2.Token{
|
||||
RefreshToken: profile.RefreshToken,
|
||||
}
|
||||
tokenSource := config.TokenSource(ctx, token)
|
||||
srv, err := drive.NewService(ctx, option.WithTokenSource(tokenSource))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create gdrive oauth2 service: %w", err)
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func (s *SoraGDriveStorage) buildServiceAccountService(ctx context.Context, profile *SoraS3Profile) (*drive.Service, error) {
|
||||
srv, err := drive.NewService(ctx, option.WithCredentialsJSON([]byte(profile.ServiceAccountJSON))) //nolint:staticcheck // SA1019: admin-controlled service account JSON, safe to use
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create gdrive service account service: %w", err)
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// RefreshClient 清除缓存的 Drive 客户端。
|
||||
func (s *SoraGDriveStorage) RefreshClient() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.srv = nil
|
||||
s.cfg = nil
|
||||
s.healthCheckedAt = time.Time{}
|
||||
s.healthErr = nil
|
||||
logger.LegacyPrintf("service.sora_gdrive", "[SoraGDrive] 客户端缓存已清除")
|
||||
}
|
||||
|
||||
// GDriveQuotaInfo 包含 Google Drive 配额信息。
|
||||
type GDriveQuotaInfo struct {
|
||||
LimitBytes int64 `json:"limit_bytes"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
}
|
||||
|
||||
// TestConnection 测试 Google Drive 连接。
|
||||
func (s *SoraGDriveStorage) TestConnection(ctx context.Context) error {
|
||||
srv, _, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = srv.About.Get().Fields("storageQuota").Context(ctx).Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gdrive About.Get failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetQuotaInfo 获取 Google Drive 配额信息(总量和已用量)。
|
||||
func (s *SoraGDriveStorage) GetQuotaInfo(ctx context.Context) (*GDriveQuotaInfo, error) {
|
||||
srv, _, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
about, err := srv.About.Get().Fields("storageQuota").Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gdrive About.Get failed: %w", err)
|
||||
}
|
||||
if about.StorageQuota == nil {
|
||||
return nil, fmt.Errorf("storageQuota not available")
|
||||
}
|
||||
return &GDriveQuotaInfo{
|
||||
LimitBytes: about.StorageQuota.Limit,
|
||||
UsedBytes: about.StorageQuota.Usage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestFullCycle 执行完整的上传→获取链接→删除测试。
|
||||
func (s *SoraGDriveStorage) TestFullCycle(ctx context.Context) (map[string]any, error) {
|
||||
srv, cfg, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init client: %w", err)
|
||||
}
|
||||
|
||||
result := map[string]any{}
|
||||
|
||||
// 1. 测试 API 连接
|
||||
about, err := srv.About.Get().Fields("storageQuota").Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API connection failed: %w", err)
|
||||
}
|
||||
if about.StorageQuota != nil {
|
||||
result["quota_limit_bytes"] = about.StorageQuota.Limit
|
||||
result["quota_used_bytes"] = about.StorageQuota.Usage
|
||||
}
|
||||
|
||||
// 2. 上传测试文件
|
||||
testContent := "sub2api GDrive test file - " + time.Now().Format(time.RFC3339)
|
||||
fileMeta := &drive.File{
|
||||
Name: "sub2api_test_" + uuid.NewString()[:8] + ".txt",
|
||||
MimeType: "text/plain",
|
||||
}
|
||||
if cfg.FolderID != "" {
|
||||
fileMeta.Parents = []string{cfg.FolderID}
|
||||
}
|
||||
uploaded, err := srv.Files.Create(fileMeta).
|
||||
Media(strings.NewReader(testContent)).
|
||||
Fields("id,name,size,webViewLink").
|
||||
Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload test file failed: %w", err)
|
||||
}
|
||||
result["uploaded_file_id"] = uploaded.Id
|
||||
result["uploaded_file_name"] = uploaded.Name
|
||||
result["uploaded_file_size"] = uploaded.Size
|
||||
result["web_view_link"] = uploaded.WebViewLink
|
||||
|
||||
// 3. 获取访问链接
|
||||
accessURL, err := s.GetAccessURL(ctx, uploaded.Id)
|
||||
if err != nil {
|
||||
// 即使获取链接失败,仍尝试清理
|
||||
_ = srv.Files.Delete(uploaded.Id).Context(ctx).Do()
|
||||
return nil, fmt.Errorf("get access URL failed: %w", err)
|
||||
}
|
||||
result["access_url"] = accessURL
|
||||
|
||||
// 4. 删除测试文件
|
||||
if err := srv.Files.Delete(uploaded.Id).Context(ctx).Do(); err != nil {
|
||||
result["delete_warning"] = fmt.Sprintf("delete failed (manual cleanup needed): %v", err)
|
||||
} else {
|
||||
result["deleted"] = true
|
||||
}
|
||||
|
||||
result["status"] = "ok"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// IsHealthy 返回 Google Drive 健康状态(带短缓存)。
|
||||
func (s *SoraGDriveStorage) IsHealthy(ctx context.Context) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
s.mu.RLock()
|
||||
lastCheck := s.healthCheckedAt
|
||||
lastErr := s.healthErr
|
||||
ttl := s.healthTTL
|
||||
s.mu.RUnlock()
|
||||
|
||||
if ttl <= 0 {
|
||||
ttl = defaultGDriveHealthTTL
|
||||
}
|
||||
if !lastCheck.IsZero() && now.Sub(lastCheck) < ttl {
|
||||
return lastErr == nil
|
||||
}
|
||||
|
||||
err := s.TestConnection(ctx)
|
||||
s.mu.Lock()
|
||||
s.healthCheckedAt = time.Now()
|
||||
s.healthErr = err
|
||||
s.mu.Unlock()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// UploadFromURL 从上游 URL 下载并上传到 Google Drive。
|
||||
// 返回 Google Drive 文件 ID 作为 objectKey、文件大小、存储类型。
|
||||
func (s *SoraGDriveStorage) UploadFromURL(ctx context.Context, userID int64, sourceURL string) (string, int64, string, error) {
|
||||
srv, cfg, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
|
||||
// 下载源文件
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("create download request: %w", err)
|
||||
}
|
||||
httpClient := &http.Client{Timeout: 5 * time.Minute}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("download from upstream: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", 0, "", &UpstreamDownloadError{StatusCode: resp.StatusCode}
|
||||
}
|
||||
|
||||
// 推断文件扩展名和 MIME
|
||||
ext := fileExtFromURL(sourceURL)
|
||||
if ext == "" {
|
||||
ext = fileExtFromContentType(resp.Header.Get("Content-Type"))
|
||||
}
|
||||
if ext == "" {
|
||||
ext = ".bin"
|
||||
}
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
datePath := time.Now().Format("2006-01-02")
|
||||
fileName := fmt.Sprintf("sora_%d_%s_%s%s", userID, datePath, uuid.NewString()[:8], ext)
|
||||
|
||||
// 创建文件元数据
|
||||
fileMeta := &drive.File{
|
||||
Name: fileName,
|
||||
MimeType: contentType,
|
||||
}
|
||||
if cfg.FolderID != "" {
|
||||
fileMeta.Parents = []string{cfg.FolderID}
|
||||
}
|
||||
|
||||
// 使用 CountingReader 统计大小
|
||||
cr := &countingReader{Reader: resp.Body}
|
||||
|
||||
// 上传到 Google Drive
|
||||
created, err := srv.Files.Create(fileMeta).
|
||||
Media(cr).
|
||||
Fields("id, size").
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("gdrive upload: %w", err)
|
||||
}
|
||||
|
||||
fileSize := cr.BytesRead
|
||||
if created.Size > 0 {
|
||||
fileSize = created.Size
|
||||
}
|
||||
|
||||
// 根据 access_mode 设置权限
|
||||
if cfg.AccessMode == "" || cfg.AccessMode == "direct" {
|
||||
// 设为任何人可读
|
||||
_, permErr := srv.Permissions.Create(created.Id, &drive.Permission{
|
||||
Type: "anyone",
|
||||
Role: "reader",
|
||||
}).Context(ctx).Do()
|
||||
if permErr != nil {
|
||||
logger.LegacyPrintf("service.sora_gdrive", "[SoraGDrive] 设置公开权限失败 fileID=%s err=%v", created.Id, permErr)
|
||||
}
|
||||
}
|
||||
|
||||
logger.LegacyPrintf("service.sora_gdrive", "[SoraGDrive] 上传完成 fileID=%s size=%d", created.Id, fileSize)
|
||||
return created.Id, fileSize, SoraStorageTypeGDrive, nil
|
||||
}
|
||||
|
||||
// DeleteObjects 删除一组 Google Drive 文件。
|
||||
func (s *SoraGDriveStorage) DeleteObjects(ctx context.Context, objectKeys []string) error {
|
||||
if len(objectKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
srv, _, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, fileID := range objectKeys {
|
||||
if err := srv.Files.Delete(fileID).Context(ctx).Do(); err != nil {
|
||||
logger.LegacyPrintf("service.sora_gdrive", "[SoraGDrive] 删除失败 fileID=%s err=%v", fileID, err)
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// GetAccessURL 获取 Google Drive 文件的访问 URL。
|
||||
func (s *SoraGDriveStorage) GetAccessURL(ctx context.Context, objectKey string) (string, error) {
|
||||
_, cfg, err := s.getService(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// CDN URL 优先
|
||||
if cfg.CDNURL != "" {
|
||||
cdnBase := strings.TrimRight(cfg.CDNURL, "/")
|
||||
return cdnBase + "/" + objectKey, nil
|
||||
}
|
||||
|
||||
// 默认使用 Google Drive 直链
|
||||
return fmt.Sprintf("https://drive.google.com/uc?export=download&id=%s", objectKey), nil
|
||||
}
|
||||
|
||||
// countingReader 包装 io.Reader 以统计读取的字节数。
|
||||
type countingReader struct {
|
||||
Reader io.Reader
|
||||
BytesRead int64
|
||||
}
|
||||
|
||||
func (r *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := r.Reader.Read(p)
|
||||
r.BytesRead += int64(n)
|
||||
return n, err
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSoraGDriveStorage_StorageType(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
assert.Equal(t, SoraStorageTypeGDrive, s.StorageType())
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_EnabledWithNilSettingService(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
assert.False(t, s.Enabled(context.Background()))
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_IsHealthyWithNilReceiver(t *testing.T) {
|
||||
var s *SoraGDriveStorage
|
||||
assert.False(t, s.IsHealthy(context.Background()))
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_GetServiceWithoutProfile(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
_, _, err := s.getService(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no active gdrive profile")
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_DeleteObjectsEmpty(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
err := s.DeleteObjects(context.Background(), []string{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_RefreshClient(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
// 不应 panic
|
||||
s.RefreshClient()
|
||||
assert.Nil(t, s.srv)
|
||||
assert.Nil(t, s.cfg)
|
||||
}
|
||||
|
||||
func TestSoraGDriveStorage_HasValidCredentials(t *testing.T) {
|
||||
s := NewSoraGDriveStorage(nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
profile *SoraS3Profile
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "oauth2 with all fields",
|
||||
profile: &SoraS3Profile{
|
||||
AuthType: "oauth2",
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
RefreshToken: "token",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "oauth2 missing refresh token",
|
||||
profile: &SoraS3Profile{
|
||||
AuthType: "oauth2",
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "service_account with json",
|
||||
profile: &SoraS3Profile{
|
||||
AuthType: "service_account",
|
||||
ServiceAccountJSON: `{"type":"service_account"}`,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "service_account without json",
|
||||
profile: &SoraS3Profile{
|
||||
AuthType: "service_account",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unknown auth type",
|
||||
profile: &SoraS3Profile{
|
||||
AuthType: "unknown",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := s.hasValidCredentials(tt.profile)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoraStorageRouter_DefaultsToS3(t *testing.T) {
|
||||
s3 := NewSoraS3Storage(nil)
|
||||
router := NewSoraStorageRouter(nil, s3, nil)
|
||||
// settingService 为 nil,应返回 s3Storage
|
||||
backend := router.activeBackend(context.Background())
|
||||
assert.Equal(t, s3, backend)
|
||||
}
|
||||
|
||||
func TestSoraStorageRouter_StorageType(t *testing.T) {
|
||||
router := NewSoraStorageRouter(nil, nil, nil)
|
||||
assert.Equal(t, SoraStorageTypeS3, router.StorageType())
|
||||
}
|
||||
|
||||
func TestSoraStorageRouter_RefreshAllNoPanic(t *testing.T) {
|
||||
router := NewSoraStorageRouter(nil, nil, nil)
|
||||
// 不应 panic
|
||||
router.RefreshAll()
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import "context"
|
||||
|
||||
// SoraObjectStorage 是 Sora 媒体文件的通用对象存储接口。
|
||||
// S3 和 Google Drive 等存储后端均实现此接口。
|
||||
type SoraObjectStorage interface {
|
||||
// Enabled 返回存储是否已启用且配置有效。
|
||||
Enabled(ctx context.Context) bool
|
||||
@@ -15,7 +14,7 @@ type SoraObjectStorage interface {
|
||||
TestConnection(ctx context.Context) error
|
||||
|
||||
// UploadFromURL 从上游 URL 下载并上传到存储。
|
||||
// 返回 object key(S3 key 或 GDrive file ID)、文件大小、实际使用的存储类型。
|
||||
// 返回 object key、文件大小、实际使用的存储类型。
|
||||
UploadFromURL(ctx context.Context, userID int64, sourceURL string) (objectKey string, sizeBytes int64, storageType string, err error)
|
||||
|
||||
// DeleteObjects 删除一组存储对象。
|
||||
@@ -27,11 +26,11 @@ type SoraObjectStorage interface {
|
||||
// RefreshClient 清除缓存客户端,配置变更时调用。
|
||||
RefreshClient()
|
||||
|
||||
// StorageType 返回存储类型标识("s3" / "gdrive")。
|
||||
// StorageType 返回存储类型标识。
|
||||
StorageType() string
|
||||
}
|
||||
|
||||
// IsObjectStorageType 判断是否为对象存储类型(S3 或 Google Drive)。
|
||||
// IsObjectStorageType 判断是否为对象存储类型。
|
||||
func IsObjectStorageType(t string) bool {
|
||||
return t == SoraStorageTypeS3 || t == SoraStorageTypeGDrive
|
||||
return t == SoraStorageTypeS3
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
)
|
||||
|
||||
// SoraStorageRouter 根据激活 profile 的 provider 字段路由到对应存储实现。
|
||||
// 实现 SoraObjectStorage 接口。
|
||||
type SoraStorageRouter struct {
|
||||
settingService *SettingService
|
||||
s3Storage *SoraS3Storage
|
||||
gdriveStorage SoraObjectStorage // 可为 nil(GDrive 未实现时)
|
||||
}
|
||||
|
||||
// NewSoraStorageRouter 创建存储路由。
|
||||
func NewSoraStorageRouter(
|
||||
settingService *SettingService,
|
||||
s3Storage *SoraS3Storage,
|
||||
gdriveStorage SoraObjectStorage,
|
||||
) *SoraStorageRouter {
|
||||
return &SoraStorageRouter{
|
||||
settingService: settingService,
|
||||
s3Storage: s3Storage,
|
||||
gdriveStorage: gdriveStorage,
|
||||
}
|
||||
}
|
||||
|
||||
// activeBackend 返回当前激活 profile 对应的存储后端。
|
||||
func (r *SoraStorageRouter) activeBackend(ctx context.Context) SoraObjectStorage {
|
||||
if r.settingService == nil {
|
||||
return r.s3Storage // 默认 S3
|
||||
}
|
||||
|
||||
profile, err := r.settingService.GetActiveStorageProfile(ctx)
|
||||
if err != nil || profile == nil {
|
||||
return r.s3Storage // 默认 S3
|
||||
}
|
||||
|
||||
switch profile.GetProvider() {
|
||||
case SoraStorageTypeGDrive:
|
||||
if r.gdriveStorage != nil {
|
||||
return r.gdriveStorage
|
||||
}
|
||||
logger.LegacyPrintf("service.storage_router", "[StorageRouter] GDrive 后端未初始化,降级到 S3")
|
||||
return r.s3Storage
|
||||
default:
|
||||
return r.s3Storage
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) Enabled(ctx context.Context) bool {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return false
|
||||
}
|
||||
return backend.Enabled(ctx)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) IsHealthy(ctx context.Context) bool {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return false
|
||||
}
|
||||
return backend.IsHealthy(ctx)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) TestConnection(ctx context.Context) error {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return fmt.Errorf("no storage backend available")
|
||||
}
|
||||
return backend.TestConnection(ctx)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) UploadFromURL(ctx context.Context, userID int64, sourceURL string) (string, int64, string, error) {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return "", 0, "", fmt.Errorf("no storage backend available")
|
||||
}
|
||||
return backend.UploadFromURL(ctx, userID, sourceURL)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) DeleteObjects(ctx context.Context, objectKeys []string) error {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return fmt.Errorf("no storage backend available")
|
||||
}
|
||||
return backend.DeleteObjects(ctx, objectKeys)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) GetAccessURL(ctx context.Context, objectKey string) (string, error) {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return "", fmt.Errorf("no storage backend available")
|
||||
}
|
||||
return backend.GetAccessURL(ctx, objectKey)
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) RefreshClient() {
|
||||
if r.s3Storage != nil {
|
||||
r.s3Storage.RefreshClient()
|
||||
}
|
||||
if r.gdriveStorage != nil {
|
||||
r.gdriveStorage.RefreshClient()
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshAll 刷新所有后端客户端(用作配置变更回调)。
|
||||
func (r *SoraStorageRouter) RefreshAll() {
|
||||
r.RefreshClient()
|
||||
}
|
||||
|
||||
func (r *SoraStorageRouter) StorageType() string {
|
||||
// 不带 context 的方法,返回默认值
|
||||
// 真实的 StorageType 在 activeBackend 中动态确定
|
||||
return SoraStorageTypeS3
|
||||
}
|
||||
|
||||
// StorageTypeWithContext 返回当前激活后端的存储类型。
|
||||
func (r *SoraStorageRouter) StorageTypeWithContext(ctx context.Context) string {
|
||||
backend := r.activeBackend(ctx)
|
||||
if backend == nil {
|
||||
return SoraStorageTypeS3
|
||||
}
|
||||
return backend.StorageType()
|
||||
}
|
||||
@@ -413,9 +413,7 @@ export interface SoraS3Profile {
|
||||
profile_id: string
|
||||
name: string
|
||||
is_active: boolean
|
||||
provider: string // "s3" | "gdrive"
|
||||
enabled: boolean
|
||||
access_mode: string // "direct" | "proxy"
|
||||
endpoint: string
|
||||
region: string
|
||||
bucket: string
|
||||
@@ -426,13 +424,6 @@ export interface SoraS3Profile {
|
||||
cdn_url: string
|
||||
default_storage_quota_bytes: number
|
||||
updated_at: string
|
||||
// Google Drive fields
|
||||
auth_type: string // "oauth2" | "service_account"
|
||||
client_id: string
|
||||
client_secret_configured: boolean
|
||||
refresh_token_configured: boolean
|
||||
service_account_configured: boolean
|
||||
folder_id: string
|
||||
}
|
||||
|
||||
export interface ListSoraS3ProfilesResponse {
|
||||
@@ -458,9 +449,7 @@ export interface CreateSoraS3ProfileRequest {
|
||||
profile_id: string
|
||||
name: string
|
||||
set_active?: boolean
|
||||
provider?: string
|
||||
enabled: boolean
|
||||
access_mode?: string
|
||||
endpoint?: string
|
||||
region?: string
|
||||
bucket?: string
|
||||
@@ -470,19 +459,11 @@ export interface CreateSoraS3ProfileRequest {
|
||||
force_path_style?: boolean
|
||||
cdn_url?: string
|
||||
default_storage_quota_bytes?: number
|
||||
// Google Drive fields
|
||||
auth_type?: string
|
||||
client_id?: string
|
||||
client_secret?: string
|
||||
refresh_token?: string
|
||||
service_account_json?: string
|
||||
folder_id?: string
|
||||
}
|
||||
|
||||
export interface UpdateSoraS3ProfileRequest {
|
||||
name: string
|
||||
enabled: boolean
|
||||
access_mode?: string
|
||||
endpoint?: string
|
||||
region?: string
|
||||
bucket?: string
|
||||
@@ -492,13 +473,6 @@ export interface UpdateSoraS3ProfileRequest {
|
||||
force_path_style?: boolean
|
||||
cdn_url?: string
|
||||
default_storage_quota_bytes?: number
|
||||
// Google Drive fields
|
||||
auth_type?: string
|
||||
client_id?: string
|
||||
client_secret?: string
|
||||
refresh_token?: string
|
||||
service_account_json?: string
|
||||
folder_id?: string
|
||||
}
|
||||
|
||||
export interface TestSoraS3ConnectionRequest {
|
||||
@@ -515,30 +489,6 @@ export interface TestSoraS3ConnectionRequest {
|
||||
default_storage_quota_bytes?: number
|
||||
}
|
||||
|
||||
export interface StartGDriveOAuthRequest {
|
||||
client_id: string
|
||||
client_secret: string
|
||||
redirect_uri: string
|
||||
}
|
||||
|
||||
export interface StartGDriveOAuthResponse {
|
||||
auth_url: string
|
||||
state: string
|
||||
}
|
||||
|
||||
export interface ExchangeGDriveOAuthCodeRequest {
|
||||
client_id: string
|
||||
client_secret: string
|
||||
redirect_uri: string
|
||||
code: string
|
||||
profile_id?: string
|
||||
}
|
||||
|
||||
export interface ExchangeGDriveOAuthCodeResponse {
|
||||
refresh_token: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function getSoraS3Settings(): Promise<SoraS3Settings> {
|
||||
const { data } = await apiClient.get<SoraS3Settings>('/admin/settings/sora-storage')
|
||||
return data
|
||||
@@ -580,53 +530,6 @@ export async function setActiveSoraS3Profile(profileID: string): Promise<SoraS3P
|
||||
return data
|
||||
}
|
||||
|
||||
export async function startGDriveOAuth(request: StartGDriveOAuthRequest): Promise<StartGDriveOAuthResponse> {
|
||||
const { data } = await apiClient.post<StartGDriveOAuthResponse>('/admin/settings/sora-storage/gdrive-oauth/start', request)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function exchangeGDriveOAuthCode(request: ExchangeGDriveOAuthCodeRequest): Promise<ExchangeGDriveOAuthCodeResponse> {
|
||||
const { data } = await apiClient.post<ExchangeGDriveOAuthCodeResponse>('/admin/settings/sora-storage/gdrive-oauth/callback', request)
|
||||
return data
|
||||
}
|
||||
|
||||
export interface TestGDriveStorageResponse {
|
||||
status: string
|
||||
quota_limit_bytes?: number
|
||||
quota_used_bytes?: number
|
||||
uploaded_file_id?: string
|
||||
uploaded_file_name?: string
|
||||
uploaded_file_size?: number
|
||||
access_url?: string
|
||||
web_view_link?: string
|
||||
deleted?: boolean
|
||||
delete_warning?: string
|
||||
}
|
||||
|
||||
export async function testGDriveStorage(): Promise<TestGDriveStorageResponse> {
|
||||
const { data } = await apiClient.post<TestGDriveStorageResponse>('/admin/settings/sora-storage/gdrive-test')
|
||||
return data
|
||||
}
|
||||
|
||||
export interface GDriveQuotaInfo {
|
||||
limit_bytes: number
|
||||
used_bytes: number
|
||||
}
|
||||
|
||||
export interface StorageVideoStats {
|
||||
[type: string]: { completed: number; in_progress: number }
|
||||
}
|
||||
|
||||
export async function getGDriveQuota(): Promise<GDriveQuotaInfo> {
|
||||
const { data } = await apiClient.get<GDriveQuotaInfo>('/admin/settings/sora-storage/gdrive-quota')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getStorageVideoStats(): Promise<StorageVideoStats> {
|
||||
const { data } = await apiClient.get<StorageVideoStats>('/admin/settings/sora-storage/video-stats')
|
||||
return data
|
||||
}
|
||||
|
||||
export const settingsAPI = {
|
||||
getSettings,
|
||||
updateSettings,
|
||||
@@ -650,12 +553,7 @@ export const settingsAPI = {
|
||||
createSoraS3Profile,
|
||||
updateSoraS3Profile,
|
||||
deleteSoraS3Profile,
|
||||
setActiveSoraS3Profile,
|
||||
startGDriveOAuth,
|
||||
exchangeGDriveOAuthCode,
|
||||
testGDriveStorage,
|
||||
getGDriveQuota,
|
||||
getStorageVideoStats
|
||||
setActiveSoraS3Profile
|
||||
}
|
||||
|
||||
export default settingsAPI
|
||||
|
||||
@@ -4483,7 +4483,7 @@ export default {
|
||||
},
|
||||
soraS3: {
|
||||
title: 'Sora Storage',
|
||||
description: 'Manage Sora media storage profiles with S3 and Google Drive support',
|
||||
description: 'Manage Sora media storage profiles with S3 support',
|
||||
newProfile: 'New Profile',
|
||||
reloadProfiles: 'Reload Profiles',
|
||||
empty: 'No storage profiles yet, create one first',
|
||||
@@ -4491,7 +4491,6 @@ export default {
|
||||
editTitle: 'Edit Storage Profile',
|
||||
selectProvider: 'Select Storage Type',
|
||||
providerS3Desc: 'S3-compatible object storage',
|
||||
providerGDriveDesc: 'Google Drive cloud storage',
|
||||
profileID: 'Profile ID',
|
||||
profileName: 'Profile Name',
|
||||
setActive: 'Set as active after creation',
|
||||
@@ -4549,29 +4548,7 @@ export default {
|
||||
testSuccess: 'Connection test successful',
|
||||
testFailed: 'Connection test failed',
|
||||
saved: 'Storage settings saved successfully',
|
||||
saveFailed: 'Failed to save storage settings',
|
||||
gdrive: {
|
||||
authType: 'Authentication Method',
|
||||
serviceAccount: 'Service Account',
|
||||
clientId: 'Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
clientSecretConfigured: '(Configured, leave blank to keep)',
|
||||
refreshToken: 'Refresh Token',
|
||||
refreshTokenConfigured: '(Configured, leave blank to keep)',
|
||||
serviceAccountJson: 'Service Account JSON',
|
||||
serviceAccountConfigured: '(Configured, leave blank to keep)',
|
||||
folderId: 'Folder ID (optional)',
|
||||
authorize: 'Authorize Google Drive',
|
||||
authorizeHint: 'Get Refresh Token via OAuth2',
|
||||
oauthFieldsRequired: 'Please fill in Client ID and Client Secret first',
|
||||
oauthSuccess: 'Google Drive authorization successful',
|
||||
oauthFailed: 'Google Drive authorization failed',
|
||||
closeWindow: 'This window will close automatically',
|
||||
processing: 'Processing authorization...',
|
||||
testStorage: 'Test Storage',
|
||||
testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)',
|
||||
testFailed: 'Google Drive storage test failed'
|
||||
}
|
||||
saveFailed: 'Failed to save storage settings'
|
||||
},
|
||||
overloadCooldown: {
|
||||
title: '529 Overload Cooldown',
|
||||
|
||||
@@ -4647,7 +4647,7 @@ export default {
|
||||
},
|
||||
soraS3: {
|
||||
title: 'Sora 存储配置',
|
||||
description: '以多配置列表管理 Sora 媒体存储,支持 S3 和 Google Drive',
|
||||
description: '以多配置列表管理 Sora 媒体存储,支持 S3',
|
||||
newProfile: '新建配置',
|
||||
reloadProfiles: '刷新列表',
|
||||
empty: '暂无存储配置,请先创建',
|
||||
@@ -4655,7 +4655,6 @@ export default {
|
||||
editTitle: '编辑存储配置',
|
||||
selectProvider: '选择存储类型',
|
||||
providerS3Desc: 'S3 兼容对象存储',
|
||||
providerGDriveDesc: 'Google Drive 云盘',
|
||||
profileID: '配置 ID',
|
||||
profileName: '配置名称',
|
||||
setActive: '创建后设为生效',
|
||||
@@ -4713,29 +4712,7 @@ export default {
|
||||
testSuccess: '连接测试成功',
|
||||
testFailed: '连接测试失败',
|
||||
saved: '存储设置保存成功',
|
||||
saveFailed: '保存存储设置失败',
|
||||
gdrive: {
|
||||
authType: '认证方式',
|
||||
serviceAccount: '服务账号',
|
||||
clientId: 'Client ID',
|
||||
clientSecret: 'Client Secret',
|
||||
clientSecretConfigured: '(已配置,留空保持不变)',
|
||||
refreshToken: 'Refresh Token',
|
||||
refreshTokenConfigured: '(已配置,留空保持不变)',
|
||||
serviceAccountJson: '服务账号 JSON',
|
||||
serviceAccountConfigured: '(已配置,留空保持不变)',
|
||||
folderId: 'Folder ID(可选)',
|
||||
authorize: '授权 Google Drive',
|
||||
authorizeHint: '通过 OAuth2 获取 Refresh Token',
|
||||
oauthFieldsRequired: '请先填写 Client ID 和 Client Secret',
|
||||
oauthSuccess: 'Google Drive 授权成功',
|
||||
oauthFailed: 'Google Drive 授权失败',
|
||||
closeWindow: '此窗口将自动关闭',
|
||||
processing: '正在处理授权...',
|
||||
testStorage: '测试存储',
|
||||
testSuccess: 'Google Drive 存储测试成功(上传、访问、删除均正常)',
|
||||
testFailed: 'Google Drive 存储测试失败'
|
||||
}
|
||||
saveFailed: '保存存储设置失败'
|
||||
},
|
||||
overloadCooldown: {
|
||||
title: '529 过载冷却',
|
||||
|
||||
@@ -225,17 +225,6 @@ const routes: RouteRecordRaw[] = [
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Admin OAuth Callback Routes ====================
|
||||
{
|
||||
path: '/admin/gdrive-oauth-callback',
|
||||
name: 'GDriveOAuthCallback',
|
||||
component: () => import('@/views/admin/GDriveOAuthCallbackView.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Google Drive OAuth'
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Admin Routes ====================
|
||||
{
|
||||
path: '/admin',
|
||||
|
||||
@@ -21,16 +21,13 @@
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full min-w-[1300px] text-sm">
|
||||
<table class="w-full min-w-[1000px] text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500 dark:border-dark-700 dark:text-gray-400">
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.profileId') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.name') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.provider') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.active') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.storagePath') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.capacityUsage') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.videoCount') }}</th>
|
||||
<th class="py-2 pr-4">{{ t('admin.settings.soraS3.columns.updatedAt') }}</th>
|
||||
<th class="py-2">{{ t('admin.settings.soraS3.columns.actions') }}</th>
|
||||
</tr>
|
||||
@@ -41,14 +38,6 @@
|
||||
<div class="font-mono text-xs">{{ profile.profile_id }}</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-xs">{{ profile.name }}</td>
|
||||
<td class="py-3 pr-4">
|
||||
<span
|
||||
class="rounded px-2 py-0.5 text-xs"
|
||||
:class="getProviderBadgeClass(profile.provider)"
|
||||
>
|
||||
{{ getProviderLabel(profile.provider) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 pr-4">
|
||||
<span
|
||||
class="rounded px-2 py-0.5 text-xs"
|
||||
@@ -58,43 +47,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-xs">
|
||||
<template v-if="(profile.provider || 's3') === 's3'">
|
||||
<div>{{ profile.endpoint || '-' }}</div>
|
||||
<div class="mt-1 text-gray-500 dark:text-gray-400">{{ [profile.bucket, profile.prefix].filter(Boolean).join('/') || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>{{ profile.folder_id ? `folder: ${profile.folder_id}` : t('admin.settings.soraS3.columns.rootFolder') }}</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-xs">
|
||||
<template v-if="(profile.provider || 's3') === 'gdrive'">
|
||||
<template v-if="gdriveQuota">
|
||||
<div>{{ formatBytes(gdriveQuota.used_bytes) }} / {{ formatBytes(gdriveQuota.limit_bytes) }}</div>
|
||||
</template>
|
||||
<template v-else-if="loadingGDriveQuota">
|
||||
<div class="text-gray-400">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-gray-400">-</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-gray-400">{{ t('admin.settings.soraS3.columns.capacityUnlimited') }}</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-xs">
|
||||
<template v-if="videoStats && videoStats[profile.provider || 's3']">
|
||||
<div>{{ videoStats[profile.provider || 's3'].completed }} {{ t('admin.settings.soraS3.columns.videoCompleted') }}</div>
|
||||
<div v-if="videoStats[profile.provider || 's3'].in_progress > 0" class="mt-0.5 text-gray-500 dark:text-gray-400">
|
||||
{{ videoStats[profile.provider || 's3'].in_progress }} {{ t('admin.settings.soraS3.columns.videoInProgress') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="loadingVideoStats">
|
||||
<div class="text-gray-400">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-gray-400">-</div>
|
||||
</template>
|
||||
<div>{{ profile.endpoint || '-' }}</div>
|
||||
<div class="mt-1 text-gray-500 dark:text-gray-400">{{ [profile.bucket, profile.prefix].filter(Boolean).join('/') || '-' }}</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-xs">{{ formatDate(profile.updated_at) }}</td>
|
||||
<td class="py-3 text-xs">
|
||||
@@ -102,8 +56,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-xs"
|
||||
:disabled="testingProfiles[profile.profile_id]
|
||||
|| ((profile.provider || 's3') === 'gdrive' && !profile.is_active)"
|
||||
:disabled="testingProfiles[profile.profile_id]"
|
||||
@click="testProfileInTable(profile)"
|
||||
>
|
||||
{{ testingProfiles[profile.profile_id] ? t('admin.settings.soraS3.columns.testingInTable') : t('admin.settings.soraS3.columns.testInTable') }}
|
||||
@@ -132,7 +85,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="soraS3Profiles.length === 0">
|
||||
<td colspan="9" class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<td colspan="6" class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.soraS3.empty') }}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -160,9 +113,6 @@
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-dark-700">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ creatingSoraProfile ? t('admin.settings.soraS3.createTitle') : t('admin.settings.soraS3.editTitle') }}
|
||||
<span v-if="soraProfileForm.provider" class="ml-2 rounded bg-gray-100 px-2 py-0.5 text-xs font-normal text-gray-600 dark:bg-dark-800 dark:text-gray-400">
|
||||
{{ getProviderLabel(soraProfileForm.provider) }}
|
||||
</span>
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
@@ -174,33 +124,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<!-- Provider selection (only when creating and not yet chosen) -->
|
||||
<div v-if="creatingSoraProfile && !soraProfileForm.provider" class="flex flex-col items-center justify-center gap-4 py-12">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.soraS3.selectProvider') }}
|
||||
</h4>
|
||||
<div class="grid w-full max-w-sm grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-2 rounded-lg border-2 border-gray-200 p-4 transition hover:border-blue-400 hover:bg-blue-50 dark:border-dark-600 dark:hover:border-blue-500 dark:hover:bg-dark-800"
|
||||
@click="selectProvider('s3')"
|
||||
>
|
||||
<span class="text-2xl">S3</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('admin.settings.soraS3.providerS3Desc') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-2 rounded-lg border-2 border-gray-200 p-4 transition hover:border-blue-400 hover:bg-blue-50 dark:border-dark-600 dark:hover:border-blue-500 dark:hover:bg-dark-800"
|
||||
@click="selectProvider('gdrive')"
|
||||
>
|
||||
<span class="text-2xl">GDrive</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('admin.settings.soraS3.providerGDriveDesc') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile form (shown after provider is selected) -->
|
||||
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<!-- Common fields -->
|
||||
<input
|
||||
v-model="soraProfileForm.profile_id"
|
||||
@@ -218,79 +142,22 @@
|
||||
<span>{{ t('admin.settings.soraS3.enabled') }}</span>
|
||||
</label>
|
||||
|
||||
<!-- S3-specific fields -->
|
||||
<template v-if="soraProfileForm.provider === 's3'">
|
||||
<input v-model="soraProfileForm.endpoint" class="input w-full" :placeholder="t('admin.settings.soraS3.endpoint')" />
|
||||
<input v-model="soraProfileForm.region" class="input w-full" :placeholder="t('admin.settings.soraS3.region')" />
|
||||
<input v-model="soraProfileForm.bucket" class="input w-full" :placeholder="t('admin.settings.soraS3.bucket')" />
|
||||
<input v-model="soraProfileForm.prefix" class="input w-full" :placeholder="t('admin.settings.soraS3.prefix')" />
|
||||
<input v-model="soraProfileForm.access_key_id" class="input w-full" :placeholder="t('admin.settings.soraS3.accessKeyId')" />
|
||||
<input
|
||||
v-model="soraProfileForm.secret_access_key"
|
||||
type="password"
|
||||
class="input w-full"
|
||||
:placeholder="soraProfileForm.secret_access_key_configured ? t('admin.settings.soraS3.secretConfigured') : t('admin.settings.soraS3.secretAccessKey')"
|
||||
/>
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input v-model="soraProfileForm.force_path_style" type="checkbox" />
|
||||
<span>{{ t('admin.settings.soraS3.forcePathStyle') }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<!-- GDrive-specific fields -->
|
||||
<template v-if="soraProfileForm.provider === 'gdrive'">
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.soraS3.gdrive.authType') }}
|
||||
</label>
|
||||
<select v-model="soraProfileForm.auth_type" class="input w-full">
|
||||
<option value="oauth2">OAuth2</option>
|
||||
<option value="service_account">{{ t('admin.settings.soraS3.gdrive.serviceAccount') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template v-if="soraProfileForm.auth_type === 'oauth2'">
|
||||
<input v-model="soraProfileForm.client_id" class="input w-full" :placeholder="t('admin.settings.soraS3.gdrive.clientId')" />
|
||||
<input
|
||||
v-model="soraProfileForm.client_secret"
|
||||
type="password"
|
||||
class="input w-full"
|
||||
:placeholder="soraProfileForm.client_secret_configured ? t('admin.settings.soraS3.gdrive.clientSecretConfigured') : t('admin.settings.soraS3.gdrive.clientSecret')"
|
||||
/>
|
||||
<div class="md:col-span-2">
|
||||
<input
|
||||
v-model="soraProfileForm.refresh_token"
|
||||
type="password"
|
||||
class="input w-full"
|
||||
:placeholder="soraProfileForm.refresh_token_configured ? t('admin.settings.soraS3.gdrive.refreshTokenConfigured') : t('admin.settings.soraS3.gdrive.refreshToken')"
|
||||
/>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-xs"
|
||||
:disabled="!soraProfileForm.client_id || !soraProfileForm.client_secret || startingOAuth"
|
||||
@click="startGDriveOAuth"
|
||||
>
|
||||
{{ startingOAuth ? t('common.loading') : t('admin.settings.soraS3.gdrive.authorize') }}
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.soraS3.gdrive.authorizeHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="soraProfileForm.auth_type === 'service_account'">
|
||||
<div class="md:col-span-2">
|
||||
<textarea
|
||||
v-model="soraProfileForm.service_account_json"
|
||||
class="input w-full"
|
||||
rows="6"
|
||||
:placeholder="soraProfileForm.service_account_configured ? t('admin.settings.soraS3.gdrive.serviceAccountConfigured') : t('admin.settings.soraS3.gdrive.serviceAccountJson')"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<input v-model="soraProfileForm.folder_id" class="input w-full md:col-span-2" :placeholder="t('admin.settings.soraS3.gdrive.folderId')" />
|
||||
</template>
|
||||
<!-- S3 fields -->
|
||||
<input v-model="soraProfileForm.endpoint" class="input w-full" :placeholder="t('admin.settings.soraS3.endpoint')" />
|
||||
<input v-model="soraProfileForm.region" class="input w-full" :placeholder="t('admin.settings.soraS3.region')" />
|
||||
<input v-model="soraProfileForm.bucket" class="input w-full" :placeholder="t('admin.settings.soraS3.bucket')" />
|
||||
<input v-model="soraProfileForm.prefix" class="input w-full" :placeholder="t('admin.settings.soraS3.prefix')" />
|
||||
<input v-model="soraProfileForm.access_key_id" class="input w-full" :placeholder="t('admin.settings.soraS3.accessKeyId')" />
|
||||
<input
|
||||
v-model="soraProfileForm.secret_access_key"
|
||||
type="password"
|
||||
class="input w-full"
|
||||
:placeholder="soraProfileForm.secret_access_key_configured ? t('admin.settings.soraS3.secretConfigured') : t('admin.settings.soraS3.secretAccessKey')"
|
||||
/>
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input v-model="soraProfileForm.force_path_style" type="checkbox" />
|
||||
<span>{{ t('admin.settings.soraS3.forcePathStyle') }}</span>
|
||||
</label>
|
||||
|
||||
<!-- Common bottom fields -->
|
||||
<input v-model="soraProfileForm.cdn_url" class="input w-full" :placeholder="t('admin.settings.soraS3.cdnUrl')" />
|
||||
@@ -301,12 +168,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="soraProfileForm.provider" class="flex flex-wrap justify-end gap-2 border-t border-gray-200 p-4 dark:border-dark-700">
|
||||
<div class="flex flex-wrap justify-end gap-2 border-t border-gray-200 p-4 dark:border-dark-700">
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="closeSoraProfileDrawer">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="soraProfileForm.provider === 's3'"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
:disabled="testingSoraProfile || !soraProfileForm.enabled"
|
||||
@@ -314,15 +180,6 @@
|
||||
>
|
||||
{{ testingSoraProfile ? t('common.loading') : t('admin.settings.soraS3.testConnection') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="soraProfileForm.provider === 'gdrive' && !creatingSoraProfile"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
:disabled="testingSoraProfile || !soraProfileForm.enabled"
|
||||
@click="testGDriveStorageConnection"
|
||||
>
|
||||
{{ testingSoraProfile ? t('common.loading') : t('admin.settings.soraS3.gdrive.testStorage') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" :disabled="savingSoraProfile" @click="saveSoraProfile">
|
||||
{{ savingSoraProfile ? t('common.loading') : t('admin.settings.soraS3.saveProfile') }}
|
||||
</button>
|
||||
@@ -335,7 +192,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SoraS3Profile, GDriveQuotaInfo, StorageVideoStats } from '@/api/admin/settings'
|
||||
import type { SoraS3Profile } from '@/api/admin/settings'
|
||||
import { adminAPI } from '@/api'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
@@ -349,12 +206,7 @@ const activatingSoraProfile = ref(false)
|
||||
const deletingSoraProfile = ref(false)
|
||||
const creatingSoraProfile = ref(false)
|
||||
const soraProfileDrawerOpen = ref(false)
|
||||
const startingOAuth = ref(false)
|
||||
const loadingGDriveQuota = ref(false)
|
||||
const loadingVideoStats = ref(false)
|
||||
const testingProfiles: Record<string, boolean> = reactive({})
|
||||
const gdriveQuota = ref<GDriveQuotaInfo | null>(null)
|
||||
const videoStats = ref<StorageVideoStats | null>(null)
|
||||
|
||||
const soraS3Profiles = ref<SoraS3Profile[]>([])
|
||||
const selectedSoraProfileID = ref('')
|
||||
@@ -363,9 +215,7 @@ type SoraStorageProfileForm = {
|
||||
profile_id: string
|
||||
name: string
|
||||
set_active: boolean
|
||||
provider: string
|
||||
enabled: boolean
|
||||
// S3 fields
|
||||
endpoint: string
|
||||
region: string
|
||||
bucket: string
|
||||
@@ -374,31 +224,10 @@ type SoraStorageProfileForm = {
|
||||
secret_access_key_configured: boolean
|
||||
prefix: string
|
||||
force_path_style: boolean
|
||||
// GDrive fields
|
||||
auth_type: string
|
||||
client_id: string
|
||||
client_secret: string
|
||||
client_secret_configured: boolean
|
||||
refresh_token: string
|
||||
refresh_token_configured: boolean
|
||||
service_account_json: string
|
||||
service_account_configured: boolean
|
||||
folder_id: string
|
||||
// Common
|
||||
cdn_url: string
|
||||
}
|
||||
|
||||
const soraProfileForm = ref<SoraStorageProfileForm>(newDefaultProfileForm('s3'))
|
||||
|
||||
function getProviderLabel(provider?: string): string {
|
||||
return (provider || 's3') === 'gdrive' ? 'Google Drive' : 'S3'
|
||||
}
|
||||
|
||||
function getProviderBadgeClass(provider?: string): string {
|
||||
return (provider || 's3') === 'gdrive'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
}
|
||||
const soraProfileForm = ref<SoraStorageProfileForm>(newDefaultProfileForm())
|
||||
|
||||
async function loadSoraS3Profiles() {
|
||||
loadingSoraProfiles.value = true
|
||||
@@ -414,9 +243,6 @@ async function loadSoraS3Profiles() {
|
||||
}
|
||||
syncSoraProfileFormWithSelection()
|
||||
}
|
||||
// Async load quota and video stats (non-blocking)
|
||||
loadGDriveQuotaIfNeeded()
|
||||
loadVideoStats()
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
} finally {
|
||||
@@ -431,36 +257,24 @@ async function testProfileInTable(profile: SoraS3Profile) {
|
||||
if (testingProfiles[pid]) return
|
||||
|
||||
testingProfiles[pid] = true
|
||||
const provider = profile.provider || 's3'
|
||||
|
||||
try {
|
||||
if (provider === 'gdrive') {
|
||||
const result = await Promise.race([
|
||||
adminAPI.settings.testGDriveStorage(),
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error(t('admin.settings.soraS3.columns.testTimeout'))), testProfileTimeoutMs))
|
||||
])
|
||||
const msg = result.status === 'ok'
|
||||
? t('admin.settings.soraS3.gdrive.testSuccess')
|
||||
: t('admin.settings.soraS3.gdrive.testFailed')
|
||||
appStore.showSuccess(msg)
|
||||
} else {
|
||||
await Promise.race([
|
||||
adminAPI.settings.testSoraS3Connection({
|
||||
profile_id: pid,
|
||||
enabled: profile.enabled,
|
||||
endpoint: profile.endpoint || '',
|
||||
region: profile.region || '',
|
||||
bucket: profile.bucket || '',
|
||||
access_key_id: profile.access_key_id || '',
|
||||
prefix: profile.prefix || '',
|
||||
force_path_style: Boolean(profile.force_path_style),
|
||||
cdn_url: profile.cdn_url || '',
|
||||
default_storage_quota_bytes: profile.default_storage_quota_bytes || 0
|
||||
}),
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error(t('admin.settings.soraS3.columns.testTimeout'))), testProfileTimeoutMs))
|
||||
])
|
||||
appStore.showSuccess(t('admin.settings.soraS3.testSuccess'))
|
||||
}
|
||||
await Promise.race([
|
||||
adminAPI.settings.testSoraS3Connection({
|
||||
profile_id: pid,
|
||||
enabled: profile.enabled,
|
||||
endpoint: profile.endpoint || '',
|
||||
region: profile.region || '',
|
||||
bucket: profile.bucket || '',
|
||||
access_key_id: profile.access_key_id || '',
|
||||
prefix: profile.prefix || '',
|
||||
force_path_style: Boolean(profile.force_path_style),
|
||||
cdn_url: profile.cdn_url || '',
|
||||
default_storage_quota_bytes: profile.default_storage_quota_bytes || 0
|
||||
}),
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error(t('admin.settings.soraS3.columns.testTimeout'))), testProfileTimeoutMs))
|
||||
])
|
||||
appStore.showSuccess(t('admin.settings.soraS3.testSuccess'))
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
} finally {
|
||||
@@ -468,46 +282,13 @@ async function testProfileInTable(profile: SoraS3Profile) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGDriveQuotaIfNeeded() {
|
||||
const hasGDrive = soraS3Profiles.value.some(
|
||||
(p) => (p.provider || 's3') === 'gdrive' && p.is_active
|
||||
)
|
||||
if (!hasGDrive) {
|
||||
gdriveQuota.value = null
|
||||
return
|
||||
}
|
||||
loadingGDriveQuota.value = true
|
||||
try {
|
||||
gdriveQuota.value = await adminAPI.settings.getGDriveQuota()
|
||||
} catch {
|
||||
gdriveQuota.value = null
|
||||
} finally {
|
||||
loadingGDriveQuota.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVideoStats() {
|
||||
loadingVideoStats.value = true
|
||||
try {
|
||||
videoStats.value = await adminAPI.settings.getStorageVideoStats()
|
||||
} catch {
|
||||
videoStats.value = null
|
||||
} finally {
|
||||
loadingVideoStats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startCreateSoraProfile() {
|
||||
creatingSoraProfile.value = true
|
||||
selectedSoraProfileID.value = ''
|
||||
soraProfileForm.value = newDefaultProfileForm('')
|
||||
soraProfileForm.value = newDefaultProfileForm()
|
||||
soraProfileDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function selectProvider(provider: string) {
|
||||
soraProfileForm.value = newDefaultProfileForm(provider)
|
||||
}
|
||||
|
||||
function editSoraProfile(profileID: string) {
|
||||
selectedSoraProfileID.value = profileID
|
||||
creatingSoraProfile.value = false
|
||||
@@ -538,8 +319,8 @@ async function saveSoraProfile() {
|
||||
appStore.showError(t('admin.settings.soraS3.profileSelectRequired'))
|
||||
return
|
||||
}
|
||||
// S3-specific validation
|
||||
if (form.provider === 's3' && form.enabled) {
|
||||
// S3 validation
|
||||
if (form.enabled) {
|
||||
if (!form.endpoint.trim()) {
|
||||
appStore.showError(t('admin.settings.soraS3.endpointRequired'))
|
||||
return
|
||||
@@ -557,66 +338,37 @@ async function saveSoraProfile() {
|
||||
savingSoraProfile.value = true
|
||||
try {
|
||||
if (creatingSoraProfile.value) {
|
||||
const request: Record<string, unknown> = {
|
||||
const created = await adminAPI.settings.createSoraS3Profile({
|
||||
profile_id: form.profile_id.trim(),
|
||||
name: form.name.trim(),
|
||||
set_active: form.set_active,
|
||||
provider: form.provider,
|
||||
enabled: form.enabled,
|
||||
endpoint: form.endpoint,
|
||||
region: form.region,
|
||||
bucket: form.bucket,
|
||||
access_key_id: form.access_key_id,
|
||||
secret_access_key: form.secret_access_key || undefined,
|
||||
prefix: form.prefix,
|
||||
force_path_style: form.force_path_style,
|
||||
cdn_url: form.cdn_url
|
||||
}
|
||||
if (form.provider === 's3') {
|
||||
Object.assign(request, {
|
||||
endpoint: form.endpoint,
|
||||
region: form.region,
|
||||
bucket: form.bucket,
|
||||
access_key_id: form.access_key_id,
|
||||
secret_access_key: form.secret_access_key || undefined,
|
||||
prefix: form.prefix,
|
||||
force_path_style: form.force_path_style
|
||||
})
|
||||
} else {
|
||||
Object.assign(request, {
|
||||
auth_type: form.auth_type,
|
||||
client_id: form.client_id,
|
||||
client_secret: form.client_secret || undefined,
|
||||
refresh_token: form.refresh_token || undefined,
|
||||
service_account_json: form.service_account_json || undefined,
|
||||
folder_id: form.folder_id
|
||||
})
|
||||
}
|
||||
const created = await adminAPI.settings.createSoraS3Profile(request as never)
|
||||
})
|
||||
selectedSoraProfileID.value = created.profile_id
|
||||
creatingSoraProfile.value = false
|
||||
soraProfileDrawerOpen.value = false
|
||||
appStore.showSuccess(t('admin.settings.soraS3.profileCreated'))
|
||||
} else {
|
||||
const request: Record<string, unknown> = {
|
||||
await adminAPI.settings.updateSoraS3Profile(selectedSoraProfileID.value, {
|
||||
name: form.name.trim(),
|
||||
enabled: form.enabled,
|
||||
endpoint: form.endpoint,
|
||||
region: form.region,
|
||||
bucket: form.bucket,
|
||||
access_key_id: form.access_key_id,
|
||||
secret_access_key: form.secret_access_key || undefined,
|
||||
prefix: form.prefix,
|
||||
force_path_style: form.force_path_style,
|
||||
cdn_url: form.cdn_url
|
||||
}
|
||||
if (form.provider === 's3') {
|
||||
Object.assign(request, {
|
||||
endpoint: form.endpoint,
|
||||
region: form.region,
|
||||
bucket: form.bucket,
|
||||
access_key_id: form.access_key_id,
|
||||
secret_access_key: form.secret_access_key || undefined,
|
||||
prefix: form.prefix,
|
||||
force_path_style: form.force_path_style
|
||||
})
|
||||
} else {
|
||||
Object.assign(request, {
|
||||
auth_type: form.auth_type,
|
||||
client_id: form.client_id,
|
||||
client_secret: form.client_secret || undefined,
|
||||
refresh_token: form.refresh_token || undefined,
|
||||
service_account_json: form.service_account_json || undefined,
|
||||
folder_id: form.folder_id
|
||||
})
|
||||
}
|
||||
await adminAPI.settings.updateSoraS3Profile(selectedSoraProfileID.value, request as never)
|
||||
})
|
||||
soraProfileDrawerOpen.value = false
|
||||
appStore.showSuccess(t('admin.settings.soraS3.profileSaved'))
|
||||
}
|
||||
@@ -651,82 +403,6 @@ async function testSoraProfileConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testGDriveStorageConnection() {
|
||||
testingSoraProfile.value = true
|
||||
try {
|
||||
const result = await adminAPI.settings.testGDriveStorage()
|
||||
const msg = result.status === 'ok'
|
||||
? t('admin.settings.soraS3.gdrive.testSuccess')
|
||||
: t('admin.settings.soraS3.gdrive.testFailed')
|
||||
appStore.showSuccess(msg)
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('admin.settings.soraS3.gdrive.testFailed'))
|
||||
} finally {
|
||||
testingSoraProfile.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startGDriveOAuth() {
|
||||
const form = soraProfileForm.value
|
||||
if (!form.client_id || !form.client_secret) {
|
||||
appStore.showError(t('admin.settings.soraS3.gdrive.oauthFieldsRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
startingOAuth.value = true
|
||||
try {
|
||||
const redirectUri = `${window.location.origin}/admin/gdrive-oauth-callback`
|
||||
const result = await adminAPI.settings.startGDriveOAuth({
|
||||
client_id: form.client_id,
|
||||
client_secret: form.client_secret,
|
||||
redirect_uri: redirectUri
|
||||
})
|
||||
|
||||
// Store state for callback verification
|
||||
sessionStorage.setItem('gdrive_oauth_state', result.state)
|
||||
sessionStorage.setItem('gdrive_oauth_client_id', form.client_id)
|
||||
sessionStorage.setItem('gdrive_oauth_client_secret', form.client_secret)
|
||||
sessionStorage.setItem('gdrive_oauth_redirect_uri', redirectUri)
|
||||
sessionStorage.setItem('gdrive_oauth_profile_id', creatingSoraProfile.value ? '' : selectedSoraProfileID.value)
|
||||
|
||||
// Open Google auth in new window
|
||||
const authWindow = window.open(result.auth_url, 'gdrive_oauth', 'width=600,height=700')
|
||||
|
||||
// Listen for callback message
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
if (event.data?.type !== 'gdrive_oauth_callback') return
|
||||
window.removeEventListener('message', handleMessage)
|
||||
if (authWindow) authWindow.close()
|
||||
|
||||
const code = event.data.code
|
||||
if (!code) {
|
||||
appStore.showError(t('admin.settings.soraS3.gdrive.oauthFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const exchangeResult = await adminAPI.settings.exchangeGDriveOAuthCode({
|
||||
client_id: form.client_id,
|
||||
client_secret: form.client_secret,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
profile_id: creatingSoraProfile.value ? undefined : selectedSoraProfileID.value
|
||||
})
|
||||
form.refresh_token = exchangeResult.refresh_token
|
||||
form.refresh_token_configured = true
|
||||
appStore.showSuccess(exchangeResult.message || t('admin.settings.soraS3.gdrive.oauthSuccess'))
|
||||
} catch (err) {
|
||||
appStore.showError((err as { message?: string })?.message || t('admin.settings.soraS3.gdrive.oauthFailed'))
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', handleMessage)
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
} finally {
|
||||
startingOAuth.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function activateSoraProfile(profileID: string) {
|
||||
activatingSoraProfile.value = true
|
||||
try {
|
||||
@@ -766,18 +442,6 @@ function formatDate(value?: string): string {
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!bytes || bytes <= 0) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
let idx = 0
|
||||
let val = bytes
|
||||
while (val >= 1024 && idx < units.length - 1) {
|
||||
val /= 1024
|
||||
idx++
|
||||
}
|
||||
return `${val.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`
|
||||
}
|
||||
|
||||
function pickPreferredSoraProfileID(): string {
|
||||
const active = soraS3Profiles.value.find((item) => item.is_active)
|
||||
if (active) return active.profile_id
|
||||
@@ -786,16 +450,15 @@ function pickPreferredSoraProfileID(): string {
|
||||
|
||||
function syncSoraProfileFormWithSelection() {
|
||||
const profile = soraS3Profiles.value.find((item) => item.profile_id === selectedSoraProfileID.value)
|
||||
soraProfileForm.value = newDefaultProfileForm(profile?.provider || 's3', profile)
|
||||
soraProfileForm.value = newDefaultProfileForm(profile)
|
||||
}
|
||||
|
||||
function newDefaultProfileForm(provider: string, profile?: SoraS3Profile): SoraStorageProfileForm {
|
||||
function newDefaultProfileForm(profile?: SoraS3Profile): SoraStorageProfileForm {
|
||||
if (!profile) {
|
||||
return {
|
||||
profile_id: '',
|
||||
name: '',
|
||||
set_active: false,
|
||||
provider,
|
||||
enabled: false,
|
||||
endpoint: '',
|
||||
region: '',
|
||||
@@ -803,17 +466,8 @@ function newDefaultProfileForm(provider: string, profile?: SoraS3Profile): SoraS
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
secret_access_key_configured: false,
|
||||
prefix: provider === 's3' ? 'sora/' : '',
|
||||
prefix: 'sora/',
|
||||
force_path_style: false,
|
||||
auth_type: 'oauth2',
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
client_secret_configured: false,
|
||||
refresh_token: '',
|
||||
refresh_token_configured: false,
|
||||
service_account_json: '',
|
||||
service_account_configured: false,
|
||||
folder_id: '',
|
||||
cdn_url: ''
|
||||
}
|
||||
}
|
||||
@@ -822,7 +476,6 @@ function newDefaultProfileForm(provider: string, profile?: SoraS3Profile): SoraS
|
||||
profile_id: profile.profile_id,
|
||||
name: profile.name,
|
||||
set_active: false,
|
||||
provider: profile.provider || 's3',
|
||||
enabled: profile.enabled,
|
||||
endpoint: profile.endpoint || '',
|
||||
region: profile.region || '',
|
||||
@@ -832,15 +485,6 @@ function newDefaultProfileForm(provider: string, profile?: SoraS3Profile): SoraS
|
||||
secret_access_key_configured: Boolean(profile.secret_access_key_configured),
|
||||
prefix: profile.prefix || '',
|
||||
force_path_style: Boolean(profile.force_path_style),
|
||||
auth_type: profile.auth_type || 'oauth2',
|
||||
client_id: profile.client_id || '',
|
||||
client_secret: '',
|
||||
client_secret_configured: Boolean(profile.client_secret_configured),
|
||||
refresh_token: '',
|
||||
refresh_token_configured: Boolean(profile.refresh_token_configured),
|
||||
service_account_json: '',
|
||||
service_account_configured: Boolean(profile.service_account_configured),
|
||||
folder_id: profile.folder_id || '',
|
||||
cdn_url: profile.cdn_url || ''
|
||||
}
|
||||
}
|
||||
@@ -870,16 +514,7 @@ onMounted(async () => {
|
||||
|
||||
.dm-drawer-panel-enter-from,
|
||||
.dm-drawer-panel-leave-to {
|
||||
opacity: 0.96;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dm-drawer-mask-enter-active,
|
||||
.dm-drawer-mask-leave-active,
|
||||
.dm-drawer-panel-enter-active,
|
||||
.dm-drawer-panel-leave-active {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-dark-900">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<div v-if="error" class="card p-8">
|
||||
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
|
||||
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.settings.soraS3.gdrive.oauthFailed') }}</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ error }}</p>
|
||||
<p class="mt-4 text-xs text-gray-400 dark:text-gray-500">{{ t('admin.settings.soraS3.gdrive.closeWindow') }}</p>
|
||||
</div>
|
||||
<div v-else-if="sent" class="card p-8">
|
||||
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<svg class="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.settings.soraS3.gdrive.oauthSuccess') }}</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('admin.settings.soraS3.gdrive.closeWindow') }}</p>
|
||||
</div>
|
||||
<div v-else class="card p-8">
|
||||
<div class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"></div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('admin.settings.soraS3.gdrive.processing') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const error = ref('')
|
||||
const sent = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
const code = route.query.code as string
|
||||
const state = route.query.state as string
|
||||
const errorParam = route.query.error as string
|
||||
const errorDesc = route.query.error_description as string
|
||||
|
||||
if (errorParam) {
|
||||
error.value = errorDesc || errorParam
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
error.value = 'No authorization code received'
|
||||
return
|
||||
}
|
||||
|
||||
// Send code back to opener window via postMessage
|
||||
if (window.opener) {
|
||||
window.opener.postMessage(
|
||||
{ type: 'gdrive_oauth_callback', code, state },
|
||||
window.location.origin
|
||||
)
|
||||
sent.value = true
|
||||
// Auto-close after a short delay
|
||||
setTimeout(() => window.close(), 1500)
|
||||
} else {
|
||||
error.value = 'Cannot communicate with the parent window. Please close this window and try again.'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user