GoWindGoWind
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
Home
Framework
GoWind Toolkit
  • Open Source

    • GoWind Admin (Admin Platform)
    • GoWind CMS (Content Management)
    • GoWind UBA (Behavior Analytics)
    • GoWind Shop (E-commerce)
    • GoWind Kanban (Kanban Boards)
    • GoWind Ledger (Personal Finance)
    • GoWind ShortPlay (Short Drama)
    • GoWind Ops (Operations Platform)
    • RushWind (Rust Ecosystem)
  • Open Core (Community + Enterprise)

    • GoWind OA (Office Collaboration)
    • GoWind ERP (Procure-to-Pay)
    • GoWind WMS (Warehousing)
  • Commercial

    • GoWind IM (Instant Messaging)
    • GoWind IoT (IoT Platform)
    • GoWind Edu (AI Education)
    • GoWind MES (Manufacturing Execution)
    • GoWind CDC (Data Pipeline)
    • GoWind SDP (Zero-Trust Gateway)
    • GoWind Quant (Quant Research)
    • GoWind Trade Radar (Export Sourcing)
    • GoWind Game Radar (Game Greenlight)
  • 简体中文
  • English
GitHub
  • Introduction

    • GoWind CMS Product Introduction
    • GoWind CMS Installation Guide
  • Backend

    • CMS Backend Architecture Overview
    • CMS Backend Modules Overview
    • CMS Protobuf API Definitions
    • CMS Configuration and Deployment Guide
    • Backend Extension Mechanisms Guide
  • Frontend

    • CMS Frontend Architecture
    • CMS Frontend Modules Overview
  • Getting Started

    • Adding a Content Type, Full Stack in Practice
    • Multi-Client API Client Code Generation Tutorial
  • Core Tutorials

    • Content Multilingual Translation Tutorial
    • Content Publishing Workflow Tutorial
    • Section Editor in Practice
    • Headless API Multi-Client Integration in Practice
    • Frontend Application Development in Practice
  • Intermediate Tutorials

    • Multi-Site Management in Practice
    • Media Asset Management in Practice
    • Comment System Tutorial
    • Event Bus Architecture in Practice
    • Lua Script Extensions in Practice
  • Advanced Topics

    • Permission System in Practice
    • Dual-Client Login Security in Practice
    • Encryption and Security Toolkit Tutorial
    • Full-Text Search in Practice
    • Task Scheduling in Practice
    • Real-Time Message Push (SSE) in Practice
    • Performance Monitoring in Practice
    • Dictionary Management System Tutorial
  • Full-Stack Guides

    • Full-Stack Integration in Practice
    • Three-Service Deployment Tutorial

Media Asset Management in Practice

GoWind CMS provides complete media asset management capabilities, supporting unified storage and management of images, documents, and videos, with MinIO object storage (S3-compatible) integrated underneath. This tutorial explains the architecture design of media assets, uploading and downloading, and the implementation of content association.

Prerequisites

  • You have read CMS Backend Architecture Overview
  • The MinIO service is running locally (default ports 9000/9001)

1. Media Asset Architecture

1.1 Overall Architecture

1.2 Media Asset Types

TypeSupported FormatsUse Case
ImageJPG, PNG, GIF, WebP, SVGArticle images, thumbnails, logos
DocumentPDF, DOC, DOCX, XLS, PPTAttachment downloads
VideoMP4, WebM, MOVVideo content
AudioMP3, WAV, AACPodcasts, audio content

1.3 Storage Solution Comparison

SolutionImplementationUse Case
Local storageFile systemDevelopment and testing
MinIOObject storage (S3-compatible)Production environments
Cloud storageAWS S3 / Alibaba Cloud OSSLarge-scale production

2. Data Model

2.1 The MediaAsset Entity

// media/service/v1/media_asset.proto

message MediaAsset {
  optional uint32 id = 1;
  optional string name = 2 [json_name = "name"];           // 原始文件名
  optional string file_name = 3 [json_name = "fileName"];   // 存储文件名(UUID)
  optional string url = 4 [json_name = "url"];              // 访问 URL
  optional string mime_type = 5 [json_name = "mimeType"];   // MIME 类型
  optional uint64 file_size = 6 [json_name = "fileSize"];   // 文件大小(字节)
  optional string storage_path = 7 [json_name = "storagePath"]; // 存储路径
  optional MediaType type = 8;                               // 媒体类型
  enum MediaType {
    MEDIA_TYPE_IMAGE = 0;
    MEDIA_TYPE_VIDEO = 1;
    MEDIA_TYPE_AUDIO = 2;
    MEDIA_TYPE_DOCUMENT = 3;
    MEDIA_TYPE_OTHER = 4;
  }

  // 图片特有属性
  optional uint32 width = 10 [json_name = "width"];
  optional uint32 height = 11 [json_name = "height"];

  // 视频特有属性
  optional uint32 duration = 12 [json_name = "duration"];  // 时长(秒)

  // 缩略图
  optional string thumbnail_url = 20 [json_name = "thumbnailUrl"];

  optional uint32 uploaded_by = 100 [json_name = "uploadedBy"];
  optional google.protobuf.Timestamp created_at = 200;
}

2.2 Ent Schema

// app/core/service/internal/data/ent/schema/media_asset.go
type MediaAsset struct{ ent.Schema }

func (MediaAsset) Mixin() []ent.Mixin {
    return []ent.Mixin{mixin.Time{}}
}

func (MediaAsset) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.String("file_name").Unique(),
        field.String("url"),
        field.String("mime_type"),
        field.Uint64("file_size").Default(0),
        field.String("storage_path"),
        field.Int("type").Default(0),
        field.Uint32("width").Optional(),
        field.Uint32("height").Optional(),
        field.Uint32("duration").Optional(),
        field.String("thumbnail_url").Optional(),
        field.Uint32("uploaded_by").Optional(),
    }
}

3. File Upload and Download

3.1 The FileTransfer Service

The CMS provides a unified file transfer service (FileTransferService), registered in both the Admin API and the App API:

// admin/service/v1/i_file.proto
service FileService {
  rpc UploadFile(UploadFileRequest) returns (FileInfo) {
    option (google.api.http) = {
      post: "/admin/v1/files/upload"
      body: "*"
    };
  }
  rpc DownloadFile(DownloadFileRequest) returns (stream FileChunk) {
    option (google.api.http) = {
      get: "/admin/v1/files/{path}"
    };
  }
  rpc DeleteFile(DeleteFileRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      delete: "/admin/v1/files/{path}"
    };
  }
}

message UploadFileRequest {
  string file_name = 1 [json_name = "fileName"];
  bytes chunk_data = 2 [json_name = "chunkData"];  // 分块数据
  uint32 chunk_index = 3 [json_name = "chunkIndex"];
  uint32 total_chunks = 4 [json_name = "totalChunks"];
  string mime_type = 5 [json_name = "mimeType"];
}

3.2 Chunked Upload

Chunked upload of large files is supported:

// app/core/service/internal/service/file_transfer_service.go
func (s *FileTransferService) UploadFile(
    ctx context.Context, req *storageV1.UploadFileRequest,
) (*storageV1.FileInfo, error) {
    // 1. 将分块写入临时文件
    tempPath := fmt.Sprintf("/tmp/%s_%d", req.GetFileName(), req.GetChunkIndex())
    if err := os.WriteFile(tempPath, req.GetChunkData(), 0644); err != nil {
        return nil, err
    }

    // 2. 所有分块上传完毕后合并
    if req.GetChunkIndex() == req.GetTotalChunks()-1 {
        finalPath, err := s.mergeChunks(req.GetFileName(), req.GetTotalChunks())
        if err != nil {
            return nil, err
        }

        // 3. 上传到 MinIO
        url, err := s.ossClient.Upload(ctx, finalPath, req.GetFileName())
        if err != nil {
            return nil, err
        }

        // 4. 创建媒体资源记录
        mediaAsset := s.createMediaAsset(ctx, finalPath, url, req)

        // 5. 清理临时文件
        s.cleanupTempFiles(req.GetFileName(), req.GetTotalChunks())

        return mediaAsset, nil
    }

    return &storageV1.FileInfo{FileName: req.GetFileName()}, nil
}

3.3 MinIO Integration

// pkg/oss/minio_client.go
type MinIOClient struct {
    client *minio.Client
    bucket string
}

func NewMinIOClient(endpoint, accessKey, secretKey, bucket string) (*MinIOClient, error) {
    client, err := minio.New(endpoint, &minio.Options{
        Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
        Secure: false,
    })
    if err != nil {
        return nil, err
    }
    // 自动创建 bucket
    ctx := context.Background()
    exists, err := client.BucketExists(ctx, bucket)
    if err != nil {
        return nil, err
    }
    if !exists {
        if err := client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}); err != nil {
            return nil, err
        }
    }
    return &MinIOClient{client: client, bucket: bucket}, nil
}

func (c *MinIOClient) Upload(ctx context.Context, localPath, objectName string) (string, error) {
    _, err := c.client.FPutObject(ctx, c.bucket, objectName, localPath, minio.PutObjectOptions{
        ContentType: "application/octet-stream",
    })
    if err != nil {
        return "", err
    }
    return fmt.Sprintf("/%s/%s", c.bucket, objectName), nil
}

4. Admin Console Media Library

4.1 Media Library Page

<!-- views/media/index.vue -->
<script setup lang="ts">
import { useListMediaAssets, useDeleteMediaAsset } from '#/api/composables/media';
import { Upload } from 'ant-design-vue';

const pagination = reactive(new PaginationQuery());
const { data, isLoading } = useListMediaAssets(pagination);
const deleteMutation = useDeleteMediaAsset();
const previewVisible = ref(false);
const previewUrl = ref('');

const handleUpload = (info) => {
  // 上传成功后刷新列表
  if (info.file.status === 'done') {
    queryClient.invalidateQueries({ queryKey: ['listMediaAssets'] });
  }
};

const handleDelete = (id: number) => {
  Modal.confirm({
    title: '确认删除?',
    onOk: () => deleteMutation.mutate(id),
  });
};
</script>

<template>
  <Page title="媒体资源">
    <!-- 上传区域 -->
    <Upload
      action="/api/admin/v1/files/upload"
      :headers="{ Authorization: `Bearer ${token}` }"
      multiple
      @change="handleUpload"
    >
      <Button type="primary">上传文件</Button>
    </Upload>

    <!-- 媒体网格 -->
    <div class="grid grid-cols-6 gap-4 mt-4">
      <div v-for="asset in data?.items" :key="asset.id" class="media-item">
        <div class="thumbnail" @click="previewUrl = asset.url; previewVisible = true">
          <img v-if="asset.type === 0" :src="asset.url" :alt="asset.name" />
          <video v-else-if="asset.type === 1" :src="asset.url" />
          <div v-else class="file-icon">{{ asset.mimeType }}</div>
        </div>
        <div class="info">
          <span class="name">{{ asset.name }}</span>
          <span class="size">{{ formatSize(asset.fileSize) }}</span>
        </div>
        <Button danger size="small" @click="handleDelete(asset.id)">删除</Button>
      </div>
    </div>
  </Page>
</template>

4.2 Image Picker Component

An image picker is integrated into the content editor:

<!-- components/MediaPicker.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();

const open = ref(false);
const { data } = useListMediaAssets({ pageSize: 24 });

const handleSelect = (url: string) => {
  emit('update:modelValue', url);
  open.value = false;
};
</script>

<template>
  <div>
    <div v-if="modelValue" class="preview">
      <img :src="modelValue" class="max-w-xs" />
    </div>
    <Button @click="open = true">选择图片</Button>
    <Modal v-model:open="open" title="选择媒体" width="800px">
      <div class="grid grid-cols-6 gap-2">
        <div
          v-for="asset in data?.items?.filter(a => a.type === 0)"
          :key="asset.id"
          @click="handleSelect(asset.url)"
          class="cursor-pointer border-2 hover:border-blue-500"
        >
          <img :src="asset.url" :alt="asset.name" />
        </div>
      </div>
    </Modal>
  </div>
</template>

5. Frontend File Upload

5.1 Frontend Upload (Login Required)

// React 前台:用户头像上传
async function uploadAvatar(file: File): Promise<string> {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch(`${API_BASE}/files/upload`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
    body: formData,
  });

  const data = await response.json();
  return data.url;
}

5.2 Flutter Frontend Upload

Future<String> uploadFile(File file, {String? mimeType}) async {
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      file.path,
      contentType: MediaType.parse(mimeType ?? 'application/octet-stream'),
    ),
  });

  final response = await dio.post('/files/upload', data: formData);
  return response.data['url'];
}

6. Associating Content with Media

6.1 Post Thumbnails

POST /admin/v1/posts
{
  "title": "新产品发布",
  "thumbnail": "https://oss.example.com/images/product-launch.jpg",
  ...
}

6.2 Images in the Rich Text Editor

The TipTap editor embeds image URLs directly:

// 编辑器中插入图片
editor.commands.setImage({
  src: selectedImageUrl,
  alt: '图片描述',
  title: '图片标题',
});

7. Image Processing

7.1 Thumbnail Generation

// pkg/oss/thumbnail.go
func GenerateThumbnail(srcPath, dstPath string, maxWidth, maxHeight int) error {
    src, err := imaging.Open(srcPath)
    if err != nil {
        return err
    }
    thumb := imaging.Fit(src, maxWidth, maxHeight, imaging.Lanczos)
    return imaging.Save(thumb, dstPath)
}

7.2 Image Format Conversion

The WebP format is automatically generated on upload to optimize loading:

func OptimizeAndUpload(ctx context.Context, ossClient *MinIOClient, localPath string) (string, error) {
    // 生成 WebP 版本
    webpPath := strings.Replace(localPath, filepath.Ext(localPath), ".webp", 1)
    if err := ConvertToWebP(localPath, webpPath); err == nil {
        // 上传 WebP 版本
        ossClient.Upload(ctx, webpPath, generateObjectName(webpPath))
    }

    // 上传原始版本
    url, err := ossClient.Upload(ctx, localPath, generateObjectName(localPath))
    return url, err
}

8. Security and Permissions

8.1 Upload Security

Security CheckImplementation
File type whitelistServer-side MIME type validation
File size limitSite setting maxUploadSize
File name safetyUUID renaming to prevent path traversal
Virus scanningOptional ClamAV integration

8.2 Access Control

# MinIO 桶策略:公开读取
# 公开 bucket 用于 CDN 加速
# 私有 bucket 用于需要授权的文件
oss:
  public_bucket: "cms-public"
  private_bucket: "cms-private"

9. Checklist

Check ItemDescription
MinIO service startupStart dependencies with Docker Compose
OSS client configurationendpoint / bucket / credentials
MediaAsset SchemaComplete fields
FileTransfer serviceUpload / download / delete
Admin console media libraryGrid display + upload + picker
Frontend uploadToken authentication + FormData
Content associationThumbnails / rich text images
Security checksType whitelist + size limits

Related Documents

  • CMS Backend Architecture Overview
  • Frontend Application Development in Practice
  • Multi-Site Management in Practice
  • GoWind Admin File Storage Tutorial
Edit this page
Last Updated:: 11/3/26, 2:14 PM
Contributors: Bobo
Prev
Multi-Site Management in Practice
Next
Comment System Tutorial