名称: 媒体资产管理 描述: 当设计数字资产管理系统、媒体库、上传管道或资产元数据模式时使用。涵盖媒体存储模式、文件组织、元数据提取以及用于无头CMS的媒体API。 允许的工具: 阅读, Glob, Grep, 任务, 技能
媒体资产管理
为无头CMS设计数字资产管理系统、媒体库和上传管道的指南。
何时使用此技能
- 设计媒体库架构
- 实现文件上传管道
- 规划资产元数据模式
- 配置存储提供商
- 构建媒体搜索和筛选
媒体资产模型
核心实体
public class MediaItem
{
public Guid Id { get; set; }
// 文件信息
public string FileName { get; set; } = string.Empty;
public string Extension { get; set; } = string.Empty;
public string MimeType { get; set; } = string.Empty;
public long SizeBytes { get; set; }
// 存储
public string StorageProvider { get; set; } = string.Empty;
public string StoragePath { get; set; } = string.Empty;
public string PublicUrl { get; set; } = string.Empty;
// 组织
public Guid? FolderId { get; set; }
public MediaFolder? Folder { get; set; }
public List<string> Tags { get; set; } = new();
// 元数据
public MediaMetadata Metadata { get; set; } = new();
// 审计
public string UploadedBy { get; set; } = string.Empty;
public DateTime UploadedUtc { get; set; }
public DateTime? ModifiedUtc { get; set; }
}
public class MediaMetadata
{
// 通用
public string? Title { get; set; }
public string? Description { get; set; }
public string? Alt { get; set; }
public string? Caption { get; set; }
public string? Credit { get; set; }
// 图像特定
public int? Width { get; set; }
public int? Height { get; set; }
public string? ColorSpace { get; set; }
// 文档特定
public int? PageCount { get; set; }
public string? Author { get; set; }
// 视频特定
public TimeSpan? Duration { get; set; }
public string? Codec { get; set; }
public int? Bitrate { get; set; }
// EXIF/XMP
public Dictionary<string, string> ExifData { get; set; } = new();
}
public class MediaFolder
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
public Guid? ParentId { get; set; }
public List<MediaFolder> Children { get; set; } = new();
}
存储架构
存储提供商抽象
public interface IMediaStorageProvider
{
string ProviderName { get; }
Task<string> UploadAsync(Stream stream, string path, string contentType);
Task<Stream> DownloadAsync(string path);
Task DeleteAsync(string path);
Task<bool> ExistsAsync(string path);
string GetPublicUrl(string path);
}
// Azure Blob 存储
public class AzureBlobStorageProvider : IMediaStorageProvider
{
public string ProviderName => "AzureBlob";
public async Task<string> UploadAsync(
Stream stream, string path, string contentType)
{
var blobClient = _containerClient.GetBlobClient(path);
await blobClient.UploadAsync(stream, new BlobHttpHeaders
{
ContentType = contentType,
CacheControl = "public, max-age=31536000"
});
return path;
}
public string GetPublicUrl(string path)
{
return $"{_containerClient.Uri}/{path}";
}
}
// AWS S3
public class S3StorageProvider : IMediaStorageProvider
{
public string ProviderName => "S3";
public async Task<string> UploadAsync(
Stream stream, string path, string contentType)
{
var request = new PutObjectRequest
{
BucketName = _bucketName,
Key = path,
InputStream = stream,
ContentType = contentType,
CannedACL = S3CannedACL.PublicRead
};
await _s3Client.PutObjectAsync(request);
return path;
}
}
// 本地文件系统
public class LocalStorageProvider : IMediaStorageProvider
{
public string ProviderName => "Local";
public async Task<string> UploadAsync(
Stream stream, string path, string contentType)
{
var fullPath = Path.Combine(_basePath, path);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await using var fileStream = File.Create(fullPath);
await stream.CopyToAsync(fileStream);
return path;
}
}
路径生成
public class MediaPathGenerator
{
public string GeneratePath(string fileName, PathStrategy strategy)
{
var ext = Path.GetExtension(fileName);
var name = Path.GetFileNameWithoutExtension(fileName);
var safeName = Slugify(name);
return strategy switch
{
PathStrategy.DateBased => $"{DateTime.UtcNow:yyyy/MM/dd}/{safeName}-{Guid.NewGuid():N}{ext}",
PathStrategy.HashBased => $"{ComputeHash(fileName)[..2]}/{ComputeHash(fileName)[2..4]}/{Guid.NewGuid():N}{ext}",
PathStrategy.Flat => $"{Guid.NewGuid():N}{ext}",
PathStrategy.OriginalName => $"{safeName}-{DateTime.UtcNow:yyyyMMddHHmmss}{ext}",
_ => throw new ArgumentOutOfRangeException()
};
}
}
public enum PathStrategy
{
DateBased, // 2025/01/15/image-abc123.jpg
HashBased, // ab/cd/abc123.jpg
Flat, // abc123.jpg
OriginalName // my-image-20250115103045.jpg
}
上传管道
上传服务
public class MediaUploadService
{
public async Task<MediaItem> UploadAsync(
Stream stream,
string fileName,
string contentType,
UploadOptions? options = null)
{
options ??= new UploadOptions();
// 验证
ValidateFile(fileName, contentType, stream.Length, options);
// 生成路径
var path = _pathGenerator.GeneratePath(fileName, options.PathStrategy);
// 处理(调整大小、优化)
var processedStream = await ProcessMediaAsync(stream, contentType, options);
// 上传到存储
var storagePath = await _storageProvider.UploadAsync(
processedStream, path, contentType);
// 提取元数据
var metadata = await ExtractMetadataAsync(processedStream, contentType);
// 创建记录
var mediaItem = new MediaItem
{
Id = Guid.NewGuid(),
FileName = fileName,
Extension = Path.GetExtension(fileName),
MimeType = contentType,
SizeBytes = processedStream.Length,
StorageProvider = _storageProvider.ProviderName,
StoragePath = storagePath,
PublicUrl = _storageProvider.GetPublicUrl(storagePath),
FolderId = options.FolderId,
Tags = options.Tags ?? new List<string>(),
Metadata = metadata,
UploadedBy = _currentUser.UserId,
UploadedUtc = DateTime.UtcNow
};
await _repository.AddAsync(mediaItem);
// 触发事件
await _mediator.Publish(new MediaUploadedEvent(mediaItem));
return mediaItem;
}
private void ValidateFile(
string fileName, string contentType, long size, UploadOptions options)
{
// 检查文件大小
if (size > options.MaxFileSizeBytes)
throw new MediaValidationException($"文件大小超过最大限制 {options.MaxFileSizeBytes} 字节");
// 检查允许的类型
if (options.AllowedMimeTypes?.Any() == true &&
!options.AllowedMimeTypes.Contains(contentType))
throw new MediaValidationException($"文件类型 {contentType} 不被允许");
// 检查扩展名
var ext = Path.GetExtension(fileName).ToLowerInvariant();
if (options.BlockedExtensions?.Contains(ext) == true)
throw new MediaValidationException($"文件扩展名 {ext} 被阻止");
}
}
public class UploadOptions
{
public Guid? FolderId { get; set; }
public List<string>? Tags { get; set; }
public PathStrategy PathStrategy { get; set; } = PathStrategy.DateBased;
public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB
public List<string>? AllowedMimeTypes { get; set; }
public List<string>? BlockedExtensions { get; set; }
public bool ExtractMetadata { get; set; } = true;
public ImageProcessingOptions? ImageOptions { get; set; }
}
元数据提取
public class MetadataExtractor
{
public async Task<MediaMetadata> ExtractAsync(Stream stream, string contentType)
{
var metadata = new MediaMetadata();
if (contentType.StartsWith("image/"))
{
await ExtractImageMetadataAsync(stream, metadata);
}
else if (contentType.StartsWith("video/"))
{
await ExtractVideoMetadataAsync(stream, metadata);
}
else if (contentType == "application/pdf")
{
await ExtractPdfMetadataAsync(stream, metadata);
}
return metadata;
}
private async Task ExtractImageMetadataAsync(Stream stream, MediaMetadata metadata)
{
using var image = await Image.LoadAsync(stream);
metadata.Width = image.Width;
metadata.Height = image.Height;
// 提取 EXIF
if (image.Metadata.ExifProfile != null)
{
foreach (var value in image.Metadata.ExifProfile.Values)
{
metadata.ExifData[value.Tag.ToString()] = value.GetValue()?.ToString() ?? "";
}
}
}
}
媒体库功能
文件夹管理
public class MediaFolderService
{
public async Task<MediaFolder> CreateFolderAsync(string name, Guid? parentId = null)
{
var folder = new MediaFolder
{
Id = Guid.NewGuid(),
Name = name,
ParentId = parentId,
Path = await BuildPathAsync(name, parentId)
};
await _repository.AddAsync(folder);
return folder;
}
public async Task<List<MediaFolder>> GetFolderTreeAsync()
{
var folders = await _repository.GetAllAsync();
return BuildTree(folders.Where(f => f.ParentId == null));
}
}
媒体搜索
public class MediaSearchService
{
public async Task<PagedResult<MediaItem>> SearchAsync(MediaSearchQuery query)
{
var queryable = _context.MediaItems.AsQueryable();
// 按文件夹筛选
if (query.FolderId.HasValue)
{
queryable = queryable.Where(m => m.FolderId == query.FolderId);
}
// 按类型筛选
if (!string.IsNullOrEmpty(query.MediaType))
{
queryable = query.MediaType switch
{
"image" => queryable.Where(m => m.MimeType.StartsWith("image/")),
"video" => queryable.Where(m => m.MimeType.StartsWith("video/")),
"document" => queryable.Where(m =>
m.MimeType == "application/pdf" ||
m.MimeType.Contains("document")),
_ => queryable
};
}
// 按标签筛选
if (query.Tags?.Any() == true)
{
queryable = queryable.Where(m =>
query.Tags.All(t => m.Tags.Contains(t)));
}
// 搜索文本
if (!string.IsNullOrEmpty(query.SearchText))
{
var search = query.SearchText.ToLower();
queryable = queryable.Where(m =>
m.FileName.ToLower().Contains(search) ||
m.Metadata.Title!.ToLower().Contains(search) ||
m.Metadata.Description!.ToLower().Contains(search));
}
// 应用排序
queryable = query.SortBy switch
{
"name" => queryable.OrderBy(m => m.FileName),
"date" => queryable.OrderByDescending(m => m.UploadedUtc),
"size" => queryable.OrderByDescending(m => m.SizeBytes),
_ => queryable.OrderByDescending(m => m.UploadedUtc)
};
return await queryable.ToPagedResultAsync(query.Page, query.PageSize);
}
}
public class MediaSearchQuery
{
public Guid? FolderId { get; set; }
public string? MediaType { get; set; }
public List<string>? Tags { get; set; }
public string? SearchText { get; set; }
public string? SortBy { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
媒体 API
端点
POST /api/media/upload # 上传单个文件
POST /api/media/upload/bulk # 批量上传
GET /api/media # 列表/搜索媒体
GET /api/media/{id} # 获取媒体项
DELETE /api/media/{id} # 删除媒体
PATCH /api/media/{id} # 更新元数据
# 文件夹
GET /api/media/folders # 获取文件夹树
POST /api/media/folders # 创建文件夹
DELETE /api/media/folders/{id} # 删除文件夹
媒体响应
{
"data": {
"id": "media-123",
"fileName": "hero-image.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 245678,
"url": "https://cdn.example.com/media/2025/01/15/hero-image-abc123.jpg",
"metadata": {
"title": "主页英雄图",
"alt": "团队合作",
"width": 1920,
"height": 1080
},
"folder": {
"id": "folder-456",
"name": "主页",
"path": "/营销/主页"
},
"tags": ["英雄", "主页", "团队"],
"uploadedBy": "用户-789",
"uploadedUtc": "2025-01-15T10:30:00Z"
}
}
相关技能
image-optimization- 图像处理和优化cdn-media-delivery- CDN配置和交付content-type-modeling- 内容类型中的媒体字段