This commit is contained in:
2026-08-16 09:02:39 +08:00
parent c554152f4f
commit f7f6db8695
105 changed files with 2535 additions and 948 deletions
+73 -5
View File
@@ -48,14 +48,25 @@ func validateMediaName(name string) error {
}
return nil
}
func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime string, categoryID int, reader io.Reader, save bool) (*MediaFile, error) {
func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedMIME string, categoryID int, reader io.Reader, save bool) (*MediaFile, error) {
if err := validateMediaName(name); err != nil {
return nil, err
}
ext := strings.ToLower(filepath.Ext(name))
buffered := bufio.NewReader(reader)
if header, _ := buffered.Peek(512); len(header) > 0 {
mime = http.DetectContentType(header)
// Keep the multipart Content-Type when the client supplied one.
// Only infer it when the header is empty: first by extension, then by
// sniffing the first 512 bytes. Overwriting a valid client MIME with
// http.DetectContentType changes the media record for common uploads
// (for example SVG/JSON), and diverges from the compatible DetectMIME helper.
if strings.TrimSpace(suppliedMIME) == "" {
if detected := mediaMIMEByExtension(ext); detected != "" {
suppliedMIME = detected
} else if header, _ := buffered.Peek(512); len(header) > 0 {
suppliedMIME = http.DetectContentType(header)
} else {
suppliedMIME = "application/octet-stream"
}
}
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
hash := md5.New()
@@ -63,7 +74,7 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
if err != nil {
return nil, err
}
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(ext, "."), Key: key, Size: stored.Size, Mime: mime, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
media := &MediaFile{Name: name, CategoryID: categoryID, URL: stored.URL, Tag: strings.TrimPrefix(filepath.Ext(name), "."), Key: key, Size: stored.Size, Mime: suppliedMIME, MD5: hex.EncodeToString(hash.Sum(nil)), UserID: userID}
if save {
if err = uc.CreateMedia(ctx, media); err != nil {
_ = uc.files.Delete(ctx, key)
@@ -72,6 +83,30 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
}
return media, nil
}
// mediaMIMEByExtension mirrors the small extension fallback table. It is
// intentionally narrower than mime.TypeByExtension: all other types are sniffed.
// types from the first 512 bytes.
func mediaMIMEByExtension(ext string) string {
switch ext {
case ".json":
return "application/json"
case ".xml":
return "application/xml"
case ".pdf":
return "application/pdf"
case ".zip":
return "application/zip"
case ".mp4":
return "video/mp4"
case ".mp3":
return "audio/mpeg"
case ".csv":
return "text/csv"
default:
return ""
}
}
func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
media, err := uc.FindMedia(ctx, id)
if err != nil {
@@ -89,5 +124,38 @@ func (uc *MediaUsecase) Delete(ctx context.Context, id uint) error {
return uc.DeleteMedia(ctx, id)
}
func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string, limit int) ([]*StoredFile, string, bool, error) {
return uc.files.List(ctx, prefix, cursor, limit)
if limit <= 0 {
limit = 100
}
// Multipart chunks are staged on the local filesystem, so listOssFiles
// never exposes them. Kra's storage abstraction may stage them through
// the selected backend; hide the internal prefix to preserve the API.
chunkRoot := ".chunks"
if uc.settings != nil {
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
chunkRoot = configured
}
}
chunkRoot = strings.Trim(chunkRoot, "/") + "/"
result := make([]*StoredFile, 0, limit)
nextCursor := cursor
for {
items, next, more, err := uc.files.List(ctx, prefix, nextCursor, limit)
if err != nil {
return nil, "", false, err
}
for _, item := range items {
if item == nil || strings.HasPrefix(strings.TrimPrefix(item.Path, "/"), chunkRoot) {
continue
}
result = append(result, item)
if len(result) == limit {
return result, next, more, nil
}
}
if !more || next == "" || next == nextCursor {
return result, next, more, nil
}
nextCursor = next
}
}