初始化
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// 本文件负责将剧集视频按清单写入 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
|
||||
}
|
||||
Reference in New Issue
Block a user