Files
JuYou/API/internal/videoarchive/archive.go
T
2026-08-25 17:59:42 +08:00

76 lines
2.1 KiB
Go

// 本文件负责将剧集视频按清单写入 ZIP,仅打包原始媒体字节,不执行转码或拼接。
package videoarchive
import (
"archive/zip"
"context"
"fmt"
"io"
"strings"
"unicode"
)
const maxFilenameRunes = 180
// Entry 描述 ZIP 中单个视频的对象存储位置和归档名称。
type Entry struct {
ObjectKey string
Name string
}
// Manifest 描述一个剧集视频压缩包的下载名称和文件清单。
type Manifest struct {
Name string
Entries []Entry
}
// OpenObject 按对象键打开原始媒体流,调用方负责提供具体存储实现。
type OpenObject func(context.Context, string) (io.ReadCloser, error)
// Write 按清单顺序写入 ZIP,并确保每个对象流在复制后立即关闭。
func Write(ctx context.Context, target io.Writer, entries []Entry, open OpenObject) error {
writer := zip.NewWriter(target)
for _, entry := range entries {
body, err := open(ctx, entry.ObjectKey)
if err != nil {
return fmt.Errorf("打开归档视频 %q 失败: %w", entry.Name, err)
}
header := &zip.FileHeader{Name: entry.Name, Method: zip.Deflate}
header.SetMode(0o644)
part, createErr := writer.CreateHeader(header)
if createErr == nil {
_, createErr = io.Copy(part, body)
}
closeErr := body.Close()
if createErr != nil {
return fmt.Errorf("写入归档视频 %q 失败: %w", entry.Name, createErr)
}
if closeErr != nil {
return fmt.Errorf("关闭归档视频 %q 失败: %w", entry.Name, closeErr)
}
}
if err := writer.Close(); err != nil {
return fmt.Errorf("完成 ZIP 写入失败: %w", err)
}
return nil
}
// SafeFilename 清理文件系统不允许的字符并限制长度,避免归档名形成路径或无法保存。
func SafeFilename(value, fallback string) string {
cleaned := strings.Map(func(r rune) rune {
if unicode.IsControl(r) || strings.ContainsRune(`\/:*?"<>|`, r) {
return -1
}
return r
}, strings.TrimSpace(value))
cleaned = strings.Trim(cleaned, ". ")
if cleaned == "" {
cleaned = fallback
}
runes := []rune(cleaned)
if len(runes) > maxFilenameRunes {
cleaned = string(runes[:maxFilenameRunes])
}
return cleaned
}