@@ -10,7 +10,8 @@ import (
|
||||
|
||||
// ---- 实例 IP ----
|
||||
|
||||
// @Summary ---- 实例 IP ----
|
||||
// @Summary 更换实例主网卡临时公网 IP
|
||||
// @Description 旧临时 IP 删除、旧保留 IP 自动解绑(资源保留在账户),随后分配新临时 IP
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param instanceId path string true "instanceId"
|
||||
@@ -35,6 +36,32 @@ func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"publicIp": ip})
|
||||
}
|
||||
|
||||
// @Summary 更换 VNIC 临时公网 IP
|
||||
// @Description 旧临时 IP 删除、旧保留 IP 自动解绑(资源保留在账户),随后分配新临时 IP
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param vnicId path string true "VNIC OCID"
|
||||
// @Param body body object true "请求体:region"
|
||||
// @Success 200 {object} publicIpResponse
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/vnics/{vnicId}/change-public-ip [post]
|
||||
func (h *ociConfigHandler) changeVnicPublicIP(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Region string `json:"region"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
ip, err := h.svc.ChangeVnicPublicIP(c.Request.Context(), id, req.Region, c.Param("vnicId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"publicIp": ip})
|
||||
}
|
||||
|
||||
// @Summary 实例添加 IPv6 地址
|
||||
// @Tags 计算
|
||||
// @Param id path int true "配置 ID"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
_ "oci-portal/internal/oci" // swagger 注解引用
|
||||
)
|
||||
|
||||
// payInvoiceRequest 是付款请求体;email 接收 OSP 付款回执。
|
||||
type payInvoiceRequest struct {
|
||||
Email string `json:"email" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary 发票列表
|
||||
// @Tags 账单
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param year query int false "自然年过滤(按开票时间);缺省全量"
|
||||
// @Success 200 {array} oci.Invoice
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/invoices [get]
|
||||
func (h *ociConfigHandler) invoices(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
year, _ := strconv.Atoi(c.Query("year"))
|
||||
list, err := h.svc.Invoices(c.Request.Context(), id, year)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// @Summary 发票费用明细
|
||||
// @Tags 账单
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param internalId path string true "发票内部 ID"
|
||||
// @Success 200 {array} oci.InvoiceLine
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/invoices/{internalId}/lines [get]
|
||||
func (h *ociConfigHandler) invoiceLines(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
lines, err := h.svc.InvoiceLines(c.Request.Context(), id, c.Param("internalId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, lines)
|
||||
}
|
||||
|
||||
// @Summary 发票 PDF 下载
|
||||
// @Tags 账单
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param internalId path string true "发票内部 ID"
|
||||
// @Param number query string false "发票号(用作下载文件名)"
|
||||
// @Success 200 {file} binary "PDF 原文"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/invoices/{internalId}/pdf [get]
|
||||
func (h *ociConfigHandler) invoicePdf(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := h.svc.InvoicePdf(c.Request.Context(), id, c.Param("internalId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", pdfFileName(c.Query("number"), c.Param("internalId"))))
|
||||
c.Data(http.StatusOK, "application/pdf", data)
|
||||
}
|
||||
|
||||
// pdfFileName 生成下载文件名:优先发票号,剔除引号/路径分隔等不安全字符。
|
||||
func pdfFileName(number, internalID string) string {
|
||||
name := strings.TrimSpace(number)
|
||||
if name == "" {
|
||||
name = internalID
|
||||
}
|
||||
name = strings.Map(func(r rune) rune {
|
||||
if r == '"' || r == '/' || r == '\\' || r < 0x20 {
|
||||
return '_'
|
||||
}
|
||||
return r
|
||||
}, name)
|
||||
return name + ".pdf"
|
||||
}
|
||||
|
||||
// @Summary 支付发票
|
||||
// @Tags 账单
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param internalId path string true "发票内部 ID"
|
||||
// @Param body body payInvoiceRequest true "回执邮箱"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/invoices/{internalId}/pay [post]
|
||||
func (h *ociConfigHandler) payInvoice(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req payInvoiceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.svc.PayInvoice(c.Request.Context(), id, c.Param("internalId"), req.Email); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "submitted"})
|
||||
}
|
||||
|
||||
// @Summary 付款方式列表
|
||||
// @Tags 账单
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {array} oci.PaymentMethod
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/payment-methods [get]
|
||||
func (h *ociConfigHandler) paymentMethods(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
methods, err := h.svc.PaymentMethods(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, methods)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ type createInstanceRequest struct {
|
||||
BootVolumeVpusPerGB int64 `json:"bootVolumeVpusPerGB"`
|
||||
SubnetID string `json:"subnetId"` // 为空时自动创建 VCN 与子网
|
||||
AssignPublicIP bool `json:"assignPublicIp"`
|
||||
ReservedPublicIPID string `json:"reservedPublicIpId"` // 非空时不分配临时 IP,实例就绪后自动绑定该保留 IP
|
||||
AssignIpv6 bool `json:"assignIpv6"`
|
||||
SSHPublicKey string `json:"sshPublicKey"`
|
||||
RootPassword string `json:"rootPassword"`
|
||||
@@ -118,6 +119,7 @@ func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
||||
BootVolumeVpusPerGB: req.BootVolumeVpusPerGB,
|
||||
SubnetID: req.SubnetID,
|
||||
AssignPublicIP: req.AssignPublicIP,
|
||||
ReservedPublicIPID: req.ReservedPublicIPID,
|
||||
AssignIpv6: req.AssignIpv6,
|
||||
SSHPublicKey: req.SSHPublicKey,
|
||||
RootPassword: req.RootPassword,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/oci"
|
||||
)
|
||||
|
||||
// swag 解析 @Success 里的 oci.ReservedIP 需要本文件持有该包导入
|
||||
var _ = oci.ReservedIP{}
|
||||
|
||||
// ---- 保留公网 IP ----
|
||||
|
||||
// @Summary 保留 IP 列表
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param region query string false "区域,空为主区域"
|
||||
// @Param compartmentId query string false "区间 OCID,空为生效区间"
|
||||
// @Success 200 {array} oci.ReservedIP
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/reserved-ips [get]
|
||||
func (h *ociConfigHandler) listReservedIPs(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ips, err := h.svc.ReservedIPs(c.Request.Context(), id, c.Query("region"), c.Query("compartmentId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, ips)
|
||||
}
|
||||
|
||||
// @Summary 创建保留 IP
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param body body object true "请求体:region、compartmentId、displayName"
|
||||
// @Success 201 {object} oci.ReservedIP
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/reserved-ips [post]
|
||||
func (h *ociConfigHandler) createReservedIP(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Region string `json:"region"`
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
ip, err := h.svc.CreateReservedIP(c.Request.Context(), id, req.Region, req.CompartmentID, req.DisplayName)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, ip)
|
||||
}
|
||||
|
||||
// @Summary 绑定/解绑保留 IP
|
||||
// @Description vnicId 非空时绑到该网卡,否则绑 instanceId 主网卡(两者都空为解绑);目标已有公网 IP 时自动释放/解绑
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param publicIpId path string true "保留 IP OCID"
|
||||
// @Param body body object true "请求体:region、instanceId、vnicId"
|
||||
// @Success 204 "绑定成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/reserved-ips/{publicIpId} [put]
|
||||
func (h *ociConfigHandler) assignReservedIP(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Region string `json:"region"`
|
||||
InstanceID string `json:"instanceId"`
|
||||
VnicID string `json:"vnicId"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
err := h.svc.AssignReservedIP(c.Request.Context(), id, req.Region, c.Param("publicIpId"), req.InstanceID, req.VnicID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary 删除保留 IP
|
||||
// @Tags 网络
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param publicIpId path string true "保留 IP OCID"
|
||||
// @Param region query string false "区域"
|
||||
// @Success 204 "删除成功"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/reserved-ips/{publicIpId} [delete]
|
||||
func (h *ociConfigHandler) deleteReservedIP(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := h.svc.DeleteReservedIP(c.Request.Context(), id, c.Query("region"), c.Param("publicIpId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ func registerOci(secured *gin.RouterGroup, ociConfigs *service.OciConfigService)
|
||||
registerOciCompute(secured, h)
|
||||
registerOciNetwork(secured, h)
|
||||
registerOciStorage(secured, h)
|
||||
registerOciObjectStorage(secured, h)
|
||||
registerOciCost(secured, h)
|
||||
registerOciTenantIAM(secured, h)
|
||||
}
|
||||
@@ -37,6 +38,13 @@ func registerOciConfig(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/limits/services", h.limitServices)
|
||||
g.GET("/oci-configs/:id/subscriptions", h.subscriptions)
|
||||
g.GET("/oci-configs/:id/subscriptions/:subscriptionId", h.subscriptionDetail)
|
||||
|
||||
// 账单:发票与付款方式(OSP Gateway,仅主区域)
|
||||
g.GET("/oci-configs/:id/invoices", h.invoices)
|
||||
g.GET("/oci-configs/:id/invoices/:internalId/lines", h.invoiceLines)
|
||||
g.GET("/oci-configs/:id/invoices/:internalId/pdf", h.invoicePdf)
|
||||
g.POST("/oci-configs/:id/invoices/:internalId/pay", h.payInvoice)
|
||||
g.GET("/oci-configs/:id/payment-methods", h.paymentMethods)
|
||||
g.GET("/oci-configs/:id/shapes", h.shapes)
|
||||
g.GET("/oci-configs/:id/images", h.images)
|
||||
g.GET("/oci-configs/:id/images/:imageId", h.getImage)
|
||||
@@ -61,6 +69,7 @@ func registerOciCompute(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.POST("/oci-configs/:id/instances/:instanceId/vnics", h.attachVnic)
|
||||
g.DELETE("/oci-configs/:id/vnic-attachments/:attachmentId", h.detachVnic)
|
||||
g.POST("/oci-configs/:id/vnics/:vnicId/ipv6-addresses", h.addVnicIpv6)
|
||||
g.POST("/oci-configs/:id/vnics/:vnicId/change-public-ip", h.changeVnicPublicIP)
|
||||
g.GET("/oci-configs/:id/instances/:instanceId/traffic", h.instanceTraffic)
|
||||
}
|
||||
|
||||
@@ -77,6 +86,10 @@ func registerOciNetwork(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/subnets/:subnetId", h.getSubnet)
|
||||
g.PUT("/oci-configs/:id/subnets/:subnetId", h.updateSubnet)
|
||||
g.DELETE("/oci-configs/:id/subnets/:subnetId", h.deleteSubnet)
|
||||
g.GET("/oci-configs/:id/reserved-ips", h.listReservedIPs)
|
||||
g.POST("/oci-configs/:id/reserved-ips", h.createReservedIP)
|
||||
g.PUT("/oci-configs/:id/reserved-ips/:publicIpId", h.assignReservedIP)
|
||||
g.DELETE("/oci-configs/:id/reserved-ips/:publicIpId", h.deleteReservedIP)
|
||||
g.GET("/oci-configs/:id/security-lists", h.listSecurityLists)
|
||||
g.POST("/oci-configs/:id/security-lists", h.createSecurityList)
|
||||
g.GET("/oci-configs/:id/security-lists/:securityListId", h.getSecurityList)
|
||||
@@ -100,6 +113,27 @@ func registerOciStorage(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.DELETE("/oci-configs/:id/volume-attachments/:attachmentId", h.detachVolume)
|
||||
}
|
||||
|
||||
// registerOciObjectStorage 对象存储:namespace、桶、对象、临时链接。
|
||||
func registerOciObjectStorage(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/object-storage/namespace", h.osNamespace)
|
||||
g.GET("/oci-configs/:id/buckets", h.listBuckets)
|
||||
g.POST("/oci-configs/:id/buckets", h.createBucket)
|
||||
g.PUT("/oci-configs/:id/buckets/:bucket", h.updateBucket)
|
||||
g.DELETE("/oci-configs/:id/buckets/:bucket", h.deleteBucket)
|
||||
g.GET("/oci-configs/:id/buckets/:bucket/objects", h.listObjects)
|
||||
g.DELETE("/oci-configs/:id/buckets/:bucket/objects", h.deleteObject)
|
||||
g.POST("/oci-configs/:id/buckets/:bucket/objects/rename", h.renameObject)
|
||||
g.POST("/oci-configs/:id/buckets/:bucket/objects/restore", h.restoreObject)
|
||||
g.GET("/oci-configs/:id/buckets/:bucket/objects/detail", h.objectDetail)
|
||||
// 小文件中转:预览/编辑直读直写,不签发 PAR
|
||||
g.GET("/oci-configs/:id/buckets/:bucket/objects/content", h.getObjectContent)
|
||||
g.PUT("/oci-configs/:id/buckets/:bucket/objects/content", h.putObjectContent)
|
||||
g.GET("/oci-configs/:id/buckets/:bucket/pars", h.listPARs)
|
||||
g.POST("/oci-configs/:id/buckets/:bucket/pars", h.createPAR)
|
||||
// parId 经 query 传递:OCI PAR id 含 "/" 等字符,路径参数无法匹配
|
||||
g.DELETE("/oci-configs/:id/buckets/:bucket/pars", h.deletePAR)
|
||||
}
|
||||
|
||||
// registerOciCost 成本快照。
|
||||
func registerOciCost(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/costs", h.costs)
|
||||
|
||||
@@ -38,6 +38,11 @@ func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
||||
// @Summary 配置成本快照
|
||||
// @Tags 成本
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param startTime query string false "起始时间 RFC3339,缺省为 endTime-30 天"
|
||||
// @Param endTime query string false "结束时间 RFC3339,缺省为当前时刻"
|
||||
// @Param granularity query string false "HOURLY / DAILY / MONTHLY,缺省 DAILY"
|
||||
// @Param queryType query string false "COST / USAGE,缺省 COST"
|
||||
// @Param groupBy query string false "分组维度,单维或复合(如 service,skuName),缺省 service"
|
||||
// @Success 200 {array} oci.CostItem
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/costs [get]
|
||||
|
||||
Reference in New Issue
Block a user