162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
// Package mimeutil 提供 MIME 类型检测工具函数。
|
||
//
|
||
// 该文件实现了基于文件扩展名的 MIME 类型检测,
|
||
// 补充 Go 标准库 mime 包中缺失或错误的映射。
|
||
//
|
||
// 主要功能:
|
||
// - 本地 MIME 映射:避免 mime.AddExtensionType 的全局副作用
|
||
// - 自动回退:未覆盖的扩展名回退到标准库
|
||
// - 大小写处理:自动将扩展名转为小写再查找
|
||
// - LRU 缓存:缓存检测结果,减少重复计算
|
||
//
|
||
// 注意事项:
|
||
// - 使用包本地映射而非全局修改,确保多线程安全
|
||
// - 部分扩展名(如 .otf、.webm)Go 标准库返回错误类型,已在此纠正
|
||
//
|
||
// 作者:xfy
|
||
package mimeutil
|
||
|
||
import (
|
||
"container/list"
|
||
"mime"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
const mimeCacheSize = 64 // 常见扩展名约 50 个
|
||
|
||
// mimeCacheEntry MIME 缓存条目
|
||
type mimeCacheEntry struct {
|
||
ext string
|
||
mimeType string
|
||
element *list.Element
|
||
}
|
||
|
||
// mimeOverrides 补充 Go 标准库缺失或错误的 MIME 类型映射。
|
||
// 使用包本地映射而非 mime.AddExtensionType,避免全局副作用。
|
||
//
|
||
// 注意: 部分扩展名 Go 返回错误类型而非缺失:
|
||
// - .otf: Go 映射到 OpenDocument 公式模板,应为字体格式
|
||
// - .webm: Go 返回 audio/webm,但 webm 可包含视频
|
||
var (
|
||
mimeOverrides = map[string]string{
|
||
".eot": "application/vnd.ms-fontobject", // 缺失
|
||
".otf": "font/otf", // Go 返回错误类型
|
||
".webmanifest": "application/manifest+json", // 缺失
|
||
".map": "application/json", // 缺失
|
||
".webm": "video/webm", // Go 返回 audio/webm
|
||
// 注意: Go 1.26.2+ 已正确支持 .mjs, .avif, .woff, .woff2
|
||
}
|
||
mimeMutex sync.RWMutex
|
||
|
||
// MIME 检测结果缓存(O(1) LRU)
|
||
mimeCache = make(map[string]*mimeCacheEntry, mimeCacheSize)
|
||
mimeLRU = list.New()
|
||
mimeCacheMu sync.Mutex
|
||
|
||
defaultMIME = "application/octet-stream"
|
||
defaultMutex sync.RWMutex
|
||
)
|
||
|
||
// AddTypes 添加自定义 MIME 类型映射(线程安全)。
|
||
//
|
||
// 参数:
|
||
// - types: 扩展名到 MIME 类型的映射,扩展名会自动转为小写
|
||
func AddTypes(types map[string]string) {
|
||
mimeMutex.Lock()
|
||
for ext, mimeType := range types {
|
||
mimeOverrides[strings.ToLower(ext)] = mimeType
|
||
}
|
||
mimeMutex.Unlock()
|
||
|
||
// 清除缓存中受影响的条目
|
||
mimeCacheMu.Lock()
|
||
for ext := range types {
|
||
ext = strings.ToLower(ext)
|
||
if entry, ok := mimeCache[ext]; ok {
|
||
mimeLRU.Remove(entry.element)
|
||
delete(mimeCache, ext)
|
||
}
|
||
}
|
||
mimeCacheMu.Unlock()
|
||
}
|
||
|
||
// SetDefaultType 设置默认 MIME 类型(线程安全)。
|
||
//
|
||
// 参数:
|
||
// - defaultType: 默认 MIME 类型
|
||
func SetDefaultType(defaultType string) {
|
||
defaultMutex.Lock()
|
||
defer defaultMutex.Unlock()
|
||
defaultMIME = defaultType
|
||
}
|
||
|
||
// DetectContentType 检测文件的 MIME 类型。
|
||
//
|
||
// 优先使用包本地映射,回退到 Go 标准库 mime.TypeByExtension。
|
||
// 自动处理扩展名大小写问题。
|
||
// 使用 LRU 缓存减少重复计算。
|
||
//
|
||
// 参数:
|
||
// - filePath: 文件路径
|
||
//
|
||
// 返回值:
|
||
// - string: MIME 类型,未知类型返回 defaultMIME(默认为 application/octet-stream)
|
||
func DetectContentType(filePath string) string {
|
||
ext := strings.ToLower(filepath.Ext(filePath))
|
||
|
||
// 先查缓存
|
||
mimeCacheMu.Lock()
|
||
if entry, ok := mimeCache[ext]; ok {
|
||
// 命中,移动到 LRU 头部
|
||
mimeLRU.MoveToFront(entry.element)
|
||
mimeType := entry.mimeType
|
||
mimeCacheMu.Unlock()
|
||
return mimeType
|
||
}
|
||
mimeCacheMu.Unlock()
|
||
|
||
// 未命中,计算
|
||
mimeMutex.RLock()
|
||
mimeType, ok := mimeOverrides[ext]
|
||
mimeMutex.RUnlock()
|
||
|
||
if !ok {
|
||
mimeType = mime.TypeByExtension(ext)
|
||
}
|
||
|
||
// 写入缓存
|
||
mimeCacheMu.Lock()
|
||
defer mimeCacheMu.Unlock()
|
||
|
||
// 双重检查(可能其他 goroutine 已写入)
|
||
if entry, ok := mimeCache[ext]; ok {
|
||
mimeLRU.MoveToFront(entry.element)
|
||
return entry.mimeType
|
||
}
|
||
|
||
// 淘汰最久未用的
|
||
if mimeLRU.Len() >= mimeCacheSize {
|
||
if oldest := mimeLRU.Back(); oldest != nil {
|
||
if entry, ok := oldest.Value.(*mimeCacheEntry); ok {
|
||
delete(mimeCache, entry.ext)
|
||
}
|
||
mimeLRU.Remove(oldest)
|
||
}
|
||
}
|
||
|
||
// 插入新条目
|
||
entry := &mimeCacheEntry{ext: ext, mimeType: mimeType}
|
||
entry.element = mimeLRU.PushFront(entry)
|
||
mimeCache[ext] = entry
|
||
|
||
if mimeType == "" {
|
||
defaultMutex.RLock()
|
||
mimeType = defaultMIME
|
||
defaultMutex.RUnlock()
|
||
}
|
||
|
||
return mimeType
|
||
}
|