Files
web/server/cmd/promotion-import/main.go

230 lines
6.6 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.
// promotion-import将 web/promotion/视频发布 下中文路径素材导入到 uploads + site_assets与后台上传到 promotion/social 一致)
//
// 用法(在项目 server 目录,已配置 server/.env 中 MONGODB_URI / MONGODB_DB
//
// go run -mod=vendor ./cmd/promotion-import/ -site=站点MongoID
//
// 或指定路径:
//
// go run -mod=vendor ./cmd/promotion-import/ -site=xxx -src=/www/yh_web/web/promotion/视频发布 -upload=/www/yh_web/data/uploads
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
"yh_web/server/config"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
func loadEnv() {
wd, _ := os.Getwd()
serverDir := wd
if !strings.HasSuffix(filepath.Clean(wd), "server") {
serverDir = filepath.Join(wd, "server")
}
envPath := filepath.Clean(filepath.Join(serverDir, ".env"))
if _, err := os.Stat(envPath); err == nil {
_ = godotenv.Load(envPath)
log.Printf("已加载: %s", envPath)
}
}
func mimeForExt(ext string) string {
switch strings.ToLower(ext) {
case ".mov":
return "video/quicktime"
case ".mp4":
return "video/mp4"
case ".webm":
return "video/webm"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
default:
return "application/octet-stream"
}
}
// 与 scripts/sync-video-assets-to-social.sh 一致:源相对「视频发布」目录,目标为 promotion/social 下英文名
var mappings = []struct {
SrcRel string // 相对 视频发布/
Dst string // 仅文件名,落在 promotion/social/
}{
{"宇恒一号操作计算软件实例(一)/宣传片-封面.jpg", "video-calc-demo-1-cover.jpg"},
{"宇恒一号操作计算软件实例(一)/宣传片.mov", "video-calc-demo-1.mov"},
{"宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg", "video-calc-demo-2-cover.jpg"},
{"宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov", "video-calc-demo-2.mov"},
{"宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg", "video-aiword-cover.jpg"},
{"宇恒一号AIWord简介/宇恒一号AIWord简介.mov", "video-aiword.mov"},
{"宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg", "video-voice-office-cover.jpg"},
{"宇恒一号语音办公实例/宇恒一号语音办公实例.mov", "video-voice-office.mov"},
{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票-封面.jpg", "video-invoice-ai-cover.jpg"},
{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票.mov", "video-invoice-ai.mov"},
}
func main() {
loadEnv()
siteID := flag.String("site", "", "站点 MongoDB ObjectID必填与 /web/routes 的 site_id 一致)")
srcRoot := flag.String("src", "", "「视频发布」目录绝对路径;默认尝试项目 web/promotion/视频发布")
uploadRoot := flag.String("upload", "", "上传根目录(内含 sites/);默认 data/uploads 或环境变量 UPLOAD_DIR")
dryRun := flag.Bool("dry-run", false, "只打印计划,不写盘、不写库")
flag.Parse()
if strings.TrimSpace(*siteID) == "" {
log.Fatal("请指定 -site=站点ID")
}
wd, _ := os.Getwd()
projectRoot := wd
if strings.HasSuffix(filepath.Clean(wd), "server") {
projectRoot = filepath.Join(wd, "..")
}
projectRoot = filepath.Clean(projectRoot)
videoPublish := *srcRoot
if videoPublish == "" {
videoPublish = filepath.Join(projectRoot, "web", "promotion", "视频发布")
}
videoPublish = filepath.Clean(videoPublish)
uploadDir := *uploadRoot
if uploadDir == "" {
uploadDir = os.Getenv("UPLOAD_DIR")
}
if uploadDir == "" {
uploadDir = filepath.Join(projectRoot, "data", "uploads")
}
uploadDir = filepath.Clean(uploadDir)
mongoURI := os.Getenv("MONGODB_URI")
if mongoURI == "" {
mongoURI = "mongodb://localhost:27017"
}
if dbName := os.Getenv("MONGODB_DB"); dbName != "" {
config.DBName = dbName
}
if *dryRun {
log.Printf("[dry-run] 视频发布源: %s", videoPublish)
log.Printf("[dry-run] 上传根: %s", uploadDir)
log.Printf("[dry-run] site_id: %s", *siteID)
}
if !*dryRun {
if err := config.ConnectMongoDB(mongoURI); err != nil {
log.Fatalf("MongoDB: %v", err)
}
defer config.CloseMongoDB()
}
db := config.GetDB(config.DBName)
if db == nil && !*dryRun {
log.Fatal("数据库未连接")
}
var coll *mongo.Collection
if db != nil {
coll = db.Collection("site_assets")
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
ok, skip, fail := 0, 0, 0
for _, m := range mappings {
from := filepath.Join(videoPublish, filepath.FromSlash(m.SrcRel))
if _, err := os.Stat(from); err != nil {
log.Printf("SKIP 源文件不存在: %s", from)
skip++
continue
}
destDir := filepath.Join(uploadDir, "sites", *siteID, "promotion", "social")
destPath := filepath.Join(destDir, m.Dst)
relPath := filepath.ToSlash(filepath.Join("sites", *siteID, "promotion", "social", m.Dst))
if *dryRun {
log.Printf("COPY %s -> %s | DB file_path=%s", from, destPath, relPath)
ok++
continue
}
if err := os.MkdirAll(destDir, 0755); err != nil {
log.Printf("FAIL 创建目录 %s: %v", destDir, err)
fail++
continue
}
if err := copyFile(from, destPath); err != nil {
log.Printf("FAIL 复制 %s: %v", m.SrcRel, err)
fail++
continue
}
_ = os.Chmod(destPath, 0644)
fi, _ := os.Stat(destPath)
size := int64(0)
if fi != nil {
size = fi.Size()
}
ext := strings.ToLower(filepath.Ext(m.Dst))
ct := mimeForExt(ext)
_, _ = coll.DeleteMany(ctx, bson.M{"site_id": *siteID, "file_path": relPath})
doc := bson.M{
"site_id": *siteID,
"name": m.Dst,
"file_path": relPath,
"size": size,
"content_type": ct,
"downloadable": false,
"created_at": time.Now().Format(time.RFC3339),
"import_source": "video_publish_legacy",
"source_relpath": m.SrcRel,
"promotion_alias": filepath.ToSlash(filepath.Join("promotion", "social", m.Dst)),
}
if _, err := coll.InsertOne(ctx, doc); err != nil {
log.Printf("FAIL 写库 %s: %v", relPath, err)
fail++
continue
}
log.Printf("OK %s -> %s", m.SrcRel, relPath)
ok++
}
fmt.Printf("\n完成: 成功=%d 跳过=%d 失败=%d\n", ok, skip, fail)
if fail > 0 {
os.Exit(1)
}
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() { _ = out.Close() }()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}