Files
soul-yongping/soul-api/cmd/migrate-base64-images/main.go
2026-03-14 23:27:22 +08:00

147 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// migrate-base64-images 将 chapters 表中 content 内的 base64 图片提取为文件并替换为 URL
// 用法cd soul-api && go run ./cmd/migrate-base64-images [--dry-run]
// 测试环境APP_ENV=development 时加载 .env.development请先在测试库验证
package main
import (
"encoding/base64"
"flag"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
)
// data:image/png;base64,iVBORw0KG... 或 data:image/jpeg;base64,/9j/4AAQ...
var base64ImgRe = regexp.MustCompile(`(?i)src=["'](data:image/([^;"']+);base64,([A-Za-z0-9+/=]+))["']`)
func main() {
dryRun := flag.Bool("dry-run", false, "仅统计和预览,不写入文件与数据库")
flag.Parse()
cfg, err := config.Load()
if err != nil {
log.Fatal("load config: ", err)
}
config.SetCurrent(cfg)
if err := database.Init(cfg.DBDSN); err != nil {
log.Fatal("database: ", err)
}
uploadDir := cfg.UploadDir
if uploadDir == "" {
uploadDir = "uploads"
}
bookImagesDir := filepath.Join(uploadDir, "book-images")
if !*dryRun {
if err := os.MkdirAll(bookImagesDir, 0755); err != nil {
log.Fatal("mkdir book-images: ", err)
}
}
db := database.DB()
var chapters []model.Chapter
if err := db.Select("id", "mid", "section_title", "content").Where("content LIKE ?", "%data:image%").Find(&chapters).Error; err != nil {
log.Fatal("query chapters: ", err)
}
log.Printf("找到 %d 篇含 base64 图片的章节", len(chapters))
if len(chapters) == 0 {
return
}
rand.Seed(time.Now().UnixNano())
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
randomStr := func(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
mimeToExt := map[string]string{
"png": ".png",
"jpeg": ".jpg",
"jpg": ".jpg",
"gif": ".gif",
"webp": ".webp",
}
totalReplaced := 0
totalFiles := 0
for _, ch := range chapters {
matches := base64ImgRe.FindAllStringSubmatch(ch.Content, -1)
if len(matches) == 0 {
continue
}
newContent := ch.Content
for _, m := range matches {
fullDataURL := m[1]
mime := strings.ToLower(strings.TrimSpace(m[2]))
b64 := m[3]
ext := mimeToExt[mime]
if ext == "" {
ext = ".png"
}
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
log.Printf(" [%s] base64 解码失败: %v", ch.ID, err)
continue
}
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStr(6), ext)
dst := filepath.Join(bookImagesDir, name)
url := "/uploads/" + filepath.ToSlash(filepath.Join("book-images", name))
if !*dryRun {
if err := os.WriteFile(dst, decoded, 0644); err != nil {
log.Printf(" [%s] 写入文件失败 %s: %v", ch.ID, name, err)
continue
}
}
oldSrc := `src="` + fullDataURL + `"`
newSrc := `src="` + url + `"`
if strings.Contains(newContent, oldSrc) {
newContent = strings.Replace(newContent, oldSrc, newSrc, 1)
} else {
oldSrc2 := `src='` + fullDataURL + `'`
newSrc2 := `src="` + url + `"`
newContent = strings.Replace(newContent, oldSrc2, newSrc2, 1)
}
totalFiles++
log.Printf(" [%s] %s -> %s (%d bytes)", ch.ID, mime, name, len(decoded))
}
if newContent != ch.Content {
totalReplaced++
oldLen := len(ch.Content)
newLen := len(newContent)
if !*dryRun {
if err := db.Model(&model.Chapter{}).Where("id = ?", ch.ID).Update("content", newContent).Error; err != nil {
log.Printf(" [%s] 更新数据库失败: %v", ch.ID, err)
continue
}
}
log.Printf(" [%s] 已更新content 长度 %d -> %d (减少 %d)", ch.ID, oldLen, newLen, oldLen-newLen)
}
}
if *dryRun {
log.Printf("[dry-run] 将处理 %d 篇章节,共 %d 张 base64 图片", totalReplaced, totalFiles)
log.Printf("[dry-run] 去掉 --dry-run 后执行以实际写入")
} else {
log.Printf("完成:更新 %d 篇章节,提取 %d 张图片到 uploads/book-images/", totalReplaced, totalFiles)
}
}