@@ -0,0 +1,450 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/oci"
|
||||
)
|
||||
|
||||
// ---- 对象存储 ----
|
||||
|
||||
// @Summary 对象存储 namespace
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param region query string false "区域"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/object-storage/namespace [get]
|
||||
func (h *ociConfigHandler) osNamespace(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ns, err := h.svc.ObjectStorageNamespace(c.Request.Context(), id, c.Query("region"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"namespace": ns})
|
||||
}
|
||||
|
||||
// @Summary 存储桶列表
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param region query string false "区域"
|
||||
// @Param compartmentId query string false "区间 OCID,空为生效区间"
|
||||
// @Success 200 {array} oci.Bucket
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets [get]
|
||||
func (h *ociConfigHandler) listBuckets(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
buckets, err := h.svc.Buckets(c.Request.Context(), id, c.Query("region"), c.Query("compartmentId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, buckets)
|
||||
}
|
||||
|
||||
type createBucketRequest struct {
|
||||
Region string `json:"region"`
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
PublicRead bool `json:"publicRead"`
|
||||
StorageTier string `json:"storageTier"` // Standard / Archive
|
||||
VersioningOn bool `json:"versioningOn"`
|
||||
}
|
||||
|
||||
// @Summary 创建存储桶
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body createBucketRequest true "请求体"
|
||||
// @Success 201 {object} oci.Bucket
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets [post]
|
||||
func (h *ociConfigHandler) createBucket(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req createBucketRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
bucket, err := h.svc.CreateBucket(c.Request.Context(), id, req.Region, oci.CreateBucketInput{
|
||||
Name: req.Name, CompartmentID: req.CompartmentID,
|
||||
PublicRead: req.PublicRead, StorageTier: req.StorageTier, VersioningOn: req.VersioningOn,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, bucket)
|
||||
}
|
||||
|
||||
type updateBucketRequest struct {
|
||||
Region string `json:"region"`
|
||||
PublicRead *bool `json:"publicRead"`
|
||||
VersioningOn *bool `json:"versioningOn"`
|
||||
}
|
||||
|
||||
// @Summary 更新存储桶(可见性/版本控制)
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param body body updateBucketRequest true "请求体"
|
||||
// @Success 200 {object} oci.Bucket
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket} [put]
|
||||
func (h *ociConfigHandler) updateBucket(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req updateBucketRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
bucket, err := h.svc.UpdateBucket(c.Request.Context(), id, req.Region, c.Param("bucket"), oci.UpdateBucketInput{
|
||||
PublicRead: req.PublicRead, VersioningOn: req.VersioningOn,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, bucket)
|
||||
}
|
||||
|
||||
// @Summary 删除存储桶(非空桶后台清空后删除)
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Success 200 {object} map[string]bool "queued=true 表示已转后台清空删除"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket} [delete]
|
||||
func (h *ociConfigHandler) deleteBucket(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queued, err := h.svc.DeleteBucket(c.Request.Context(), id, c.Query("region"), c.Param("bucket"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"queued": queued})
|
||||
}
|
||||
|
||||
// @Summary 对象列表(前缀模式分页)
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param prefix query string false "前缀(虚拟目录)"
|
||||
// @Param startWith query string false "上页 nextStartWith 游标"
|
||||
// @Param limit query int false "每页数量,默认 100"
|
||||
// @Success 200 {object} oci.ListObjectsResult
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects [get]
|
||||
func (h *ociConfigHandler) listObjects(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
result, err := h.svc.Objects(c.Request.Context(), id, c.Query("region"),
|
||||
c.Param("bucket"), c.Query("prefix"), c.Query("startWith"), limit)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// @Summary 删除对象
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param object query string true "对象名(完整 key)"
|
||||
// @Success 204 "删除成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects [delete]
|
||||
func (h *ociConfigHandler) deleteObject(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := h.svc.DeleteObject(c.Request.Context(), id, c.Query("region"), c.Param("bucket"), c.Query("object"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 重命名对象
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param body body object true "请求体:region、source、newName"
|
||||
// @Success 204 "重命名成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects/rename [post]
|
||||
func (h *ociConfigHandler) renameObject(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Region string `json:"region"`
|
||||
Source string `json:"source" binding:"required"`
|
||||
NewName string `json:"newName" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.svc.RenameObject(c.Request.Context(), id, req.Region, c.Param("bucket"), req.Source, req.NewName)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 取回 Archive 对象
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param body body object true "请求体:region、object、hours(可下载时长,默认 24)"
|
||||
// @Success 202 "取回已提交,约 1 小时后可下载"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects/restore [post]
|
||||
func (h *ociConfigHandler) restoreObject(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Region string `json:"region"`
|
||||
Object string `json:"object" binding:"required"`
|
||||
Hours int `json:"hours"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.svc.RestoreObject(c.Request.Context(), id, req.Region, c.Param("bucket"), req.Object, req.Hours)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// @Summary 对象元数据
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param object query string true "对象名(完整 key)"
|
||||
// @Success 200 {object} oci.ObjectDetail
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects/detail [get]
|
||||
func (h *ociConfigHandler) objectDetail(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
detail, err := h.svc.ObjectDetail(c.Request.Context(), id, c.Query("region"), c.Param("bucket"), c.Query("object"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, detail)
|
||||
}
|
||||
|
||||
// maxPutContentBody 是保存对象内容的请求体上限(与 service 层 5MB 限制一致)。
|
||||
const maxPutContentBody = 5 << 20
|
||||
|
||||
// respondContentError 对象内容中转专属错误:超限映射 413,其余走通用处理。
|
||||
func respondContentError(c *gin.Context, err error) {
|
||||
if errors.Is(err, oci.ErrObjectTooLarge) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "对象超出面板中转大小上限"})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
}
|
||||
|
||||
// @Summary 读取对象内容(面板中转)
|
||||
// @Description 预览/编辑用小文件直读(上限 20MB),不签发 PAR;响应体为原始字节,ETag 经响应头返回
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param object query string true "对象名(完整 key)"
|
||||
// @Success 200 {string} string "对象原始内容"
|
||||
// @Failure 413 {object} map[string]string "对象超出中转上限"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects/content [get]
|
||||
func (h *ociConfigHandler) getObjectContent(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
content, err := h.svc.ObjectContent(c.Request.Context(), id, c.Query("region"), c.Param("bucket"), c.Query("object"))
|
||||
if err != nil {
|
||||
respondContentError(c, err)
|
||||
return
|
||||
}
|
||||
ct := content.ContentType
|
||||
if ct == "" {
|
||||
ct = "application/octet-stream"
|
||||
}
|
||||
c.Header("ETag", content.Etag)
|
||||
c.Data(http.StatusOK, ct, content.Data)
|
||||
}
|
||||
|
||||
// @Summary 保存对象内容(面板中转)
|
||||
// @Description 文本编辑保存(上限 5MB),请求体为原始字节;If-Match 请求头做并发保护,冲突返回 412
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param object query string true "对象名(完整 key)"
|
||||
// @Success 200 {object} map[string]string "etag=新 ETag"
|
||||
// @Failure 412 {object} map[string]string "If-Match 不匹配,对象已被并发修改"
|
||||
// @Failure 413 {object} map[string]string "请求体超出上限"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/objects/content [put]
|
||||
func (h *ociConfigHandler) putObjectContent(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxPutContentBody)
|
||||
data, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "请求体超出 5MB 上限"})
|
||||
return
|
||||
}
|
||||
etag, err := h.svc.PutObjectContent(c.Request.Context(), id, c.Query("region"), c.Param("bucket"),
|
||||
c.Query("object"), data, c.ContentType(), c.GetHeader("If-Match"))
|
||||
if err != nil {
|
||||
respondContentError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"etag": etag})
|
||||
}
|
||||
|
||||
type createPARRequest struct {
|
||||
Region string `json:"region"`
|
||||
Name string `json:"name"`
|
||||
ObjectName string `json:"objectName"` // 空 = 桶级
|
||||
AccessType string `json:"accessType" binding:"required"`
|
||||
ExpiresHours int `json:"expiresHours"`
|
||||
}
|
||||
|
||||
// @Summary 签发临时链接(PAR)
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param body body createPARRequest true "请求体"
|
||||
// @Success 201 {object} oci.PAR "fullUrl 仅创建响应返回,请立即保存"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/pars [post]
|
||||
func (h *ociConfigHandler) createPAR(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req createPARRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
par, err := h.svc.CreatePAR(c.Request.Context(), id, req.Region, c.Param("bucket"), oci.CreatePARInput{
|
||||
Name: req.Name, ObjectName: req.ObjectName, AccessType: req.AccessType, ExpiresHours: req.ExpiresHours,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, par)
|
||||
}
|
||||
|
||||
// parPage 是 PAR 游标分页响应;nextPage 空串表示已到末页。
|
||||
type parPage struct {
|
||||
Items []oci.PAR `json:"items"`
|
||||
NextPage string `json:"nextPage"`
|
||||
}
|
||||
|
||||
// @Summary 临时链接列表
|
||||
// @Description 游标分页:page 传上次响应的 nextPage,nextPage 为空表示已到末页
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param region query string false "区域"
|
||||
// @Param page query string false "分页游标(上次响应的 nextPage,空取首页)"
|
||||
// @Param limit query int false "每页条数,默认 100,上限 1000"
|
||||
// @Success 200 {object} parPage
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/pars [get]
|
||||
func (h *ociConfigHandler) listPARs(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
items, next, err := h.svc.PARsPage(c.Request.Context(), id, c.Query("region"), c.Param("bucket"), c.Query("page"), limit)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, parPage{Items: items, NextPage: next})
|
||||
}
|
||||
|
||||
// @Summary 删除临时链接
|
||||
// @Description all=1 时删除桶内全部 PAR 并返回 {"deleted": n};否则按 parId 删单条
|
||||
// @Tags 对象存储
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param bucket path string true "桶名"
|
||||
// @Param parId query string false "PAR ID(可含 / 等字符,故经 query 传递)"
|
||||
// @Param all query string false "为 1 时删除全部"
|
||||
// @Param region query string false "区域"
|
||||
// @Success 204 "删除成功,链接立即失效"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/buckets/{bucket}/pars [delete]
|
||||
func (h *ociConfigHandler) deletePAR(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if c.Query("all") == "1" {
|
||||
n, err := h.svc.DeleteAllPARs(c.Request.Context(), id, c.Query("region"), c.Param("bucket"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": n})
|
||||
return
|
||||
}
|
||||
err := h.svc.DeletePAR(c.Request.Context(), id, c.Query("region"), c.Param("bucket"), c.Query("parId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user