Preserve uploaded file extensions

This commit is contained in:
richjohnrambo
2026-09-07 16:13:20 +08:00
parent 35da3025a3
commit 8a9e202b56
8 changed files with 139 additions and 14 deletions
+1 -2
View File
@@ -139,8 +139,7 @@ func (fh *fshandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64,
return "", 0, err
}
fname := fdef.Id
fname += media.ExtensionByMimeType(fdef.MimeType)
fname := media.FileURLName(fdef)
fdef.Location = location
// Use file path to create ETag. File paths are unique so will be the ETag.
+52
View File
@@ -69,6 +69,58 @@ type AllowedOrigin struct {
var fileNamePattern = regexp.MustCompile(`^[-_A-Za-z0-9]+`)
var uploadExtensionMimeTypes = map[string]string{
".aac": "audio/aac",
".m4a": "audio/x-m4a",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
// MimeTypeForUpload picks the MIME type to persist for an uploaded file. Some common formats are
// containers and are misdetected by http.DetectContentType, for example m4a as video/mp4 and Office
// documents as zip, so the original file extension wins for those cases.
func MimeTypeForUpload(detected, clientContentType, originalName string) string {
ext := ExtensionByFilename(originalName)
if extMime := uploadExtensionMimeTypes[ext]; extMime != "" {
return extMime
}
if clientContentType != "" {
return clientContentType
}
return strings.TrimSpace(detected)
}
// FileURLName returns the public URL file name for a stored file.
func FileURLName(fdef *types.FileDef) string {
if fdef == nil {
return ""
}
fname := fdef.Id
if ext := ExtensionByFilename(fdef.OriginalName); ext != "" {
return fname + ext
}
return fname + ExtensionByMimeType(fdef.MimeType)
}
// ExtensionByFilename returns a safe lower-case extension from a client-provided file name.
func ExtensionByFilename(name string) string {
_, fname := path.Split(strings.TrimSpace(name))
ext := strings.ToLower(path.Ext(fname))
if len(ext) < 2 || len(ext) > 16 {
return ""
}
for _, r := range ext[1:] {
if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
return ""
}
}
return ext
}
// ExtensionByMimeType returns a stable URL extension for uploaded media.
func ExtensionByMimeType(mimeType string) string {
switch strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0])) {
+70
View File
@@ -3,6 +3,8 @@ package media
import (
"strings"
"testing"
"github.com/tinode/chat/server/store/types"
)
func TestMatchCORSOrigin(t *testing.T) {
@@ -237,3 +239,71 @@ func TestExtensionByMimeType(t *testing.T) {
}
}
}
func TestMimeTypeForUploadPrefersContainerFilename(t *testing.T) {
cases := []struct {
name string
detect string
client string
filename string
want string
}{
{
name: "m4a misdetected as mp4",
detect: "video/mp4",
filename: "voice.m4a",
want: "audio/x-m4a",
},
{
name: "xlsx misdetected as zip",
detect: "application/zip",
filename: "iOS任务拆解.xlsx",
want: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
{
name: "client content type fallback",
detect: "application/octet-stream",
client: "image/jfif",
filename: "thumb",
want: "image/jfif",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := MimeTypeForUpload(tc.detect, tc.client, tc.filename); got != tc.want {
t.Fatalf("MimeTypeForUpload()=%q, want %q", got, tc.want)
}
})
}
}
func TestFileURLNamePrefersOriginalExtension(t *testing.T) {
cases := []struct {
name string
def *types.FileDef
want string
}{
{
name: "voice keeps m4a extension",
def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "file123"}, MimeType: "video/mp4", OriginalName: "voice.m4a"},
want: "file123.m4a",
},
{
name: "xlsx keeps xlsx extension",
def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "sheet123"}, MimeType: "application/zip", OriginalName: "iOS任务拆解.xlsx"},
want: "sheet123.xlsx",
},
{
name: "falls back to mime extension",
def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "image123"}, MimeType: "image/jpeg"},
want: "image123.jpg",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := FileURLName(tc.def); got != tc.want {
t.Fatalf("FileURLName()=%q, want %q", got, tc.want)
}
})
}
}
+1 -2
View File
@@ -245,8 +245,7 @@ func (oh *ossHandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64
return "", 0, err
}
fname := fdef.Id
fname += media.ExtensionByMimeType(fdef.MimeType)
fname := media.FileURLName(fdef)
fdef.Location = key
fdef.ETag = etagFromLocation(key)
+1 -2
View File
@@ -294,8 +294,7 @@ func (ah *awshandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64
return "", 0, err
}
fname := fdef.Id
fname += media.ExtensionByMimeType(fdef.MimeType)
fname := media.FileURLName(fdef)
fdef.Location = key
if result.ETag != nil {