package main import ( "context" "database/sql" "encoding/json" "log" "strconv" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // migrateFiles copies file *metadata* only. // // Blobs strategy (cutover): // - Do NOT stream MySQL/local blob bytes through the migrator. // - Preserve legacy url/path in files.path and legacy id in metadata._legacy_file_id. // - Operators re-attach object storage / local volumes under the same relative paths, // or run a separate rsync/S3 sync keyed by legacy id after DNS freeze. // - raw_products.file_id is left unset until a follow-up remapper exists. func migrateFiles( ctx context.Context, mysqlDB *sql.DB, pg *pgxpool.Pool, companyMap, userMap, fileMap map[string]string, allow map[string]bool, report map[string]int, dryRun bool, ) { if !mysqlTableExists(ctx, mysqlDB, "files") { log.Printf("files skipped: table missing (blobs strategy: metadata-only when present)") return } clause, cargs := mysqlCompanyFilter("company_id", allow) rows, err := mysqlDB.QueryContext(ctx, ` SELECT id, company_id, COALESCE(user_id, ''), file_name, COALESCE(file_type, ''), COALESCE(file_size, 0), COALESCE(status, 'uploaded'), url, metadata FROM files WHERE 1=1`+clause, cargs...) if err != nil { // Older dumps may lack metadata/url/status. rows, err = mysqlDB.QueryContext(ctx, ` SELECT id, company_id, COALESCE(user_id, ''), file_name, COALESCE(file_type, ''), COALESCE(file_size, 0), 'uploaded', NULL, NULL FROM files WHERE 1=1`+clause, cargs...) } if err != nil { log.Printf("files skipped: %v", err) return } defer rows.Close() for rows.Next() { var legacyID int64 var companyLegacy, userLegacy, name, fileType, status string var size int64 var url sql.NullString var metadata []byte if err := rows.Scan(&legacyID, &companyLegacy, &userLegacy, &name, &fileType, &size, &status, &url, &metadata); err != nil { report["files_skipped"]++ continue } cid, ok := companyMap[companyLegacy] if !ok { report["files_skipped"]++ continue } meta := map[string]any{} if len(metadata) > 0 { _ = json.Unmarshal(metadata, &meta) } meta["_legacy_file_id"] = legacyID meta["_blob_strategy"] = "metadata_only_resync_paths" if fileType != "" { meta["file_type"] = fileType } metaBytes, _ := json.Marshal(meta) newID := uuid.New() legacyKey := strconv.FormatInt(legacyID, 10) fileMap[legacyKey] = newID.String() var uid *string if userLegacy != "" { if mapped, ok := userMap[userLegacy]; ok { uid = &mapped } } if dryRun { report["files"]++ continue } _, err = pg.Exec(ctx, ` INSERT INTO files (id, company_id, user_id, name, path, content_type, size_bytes, status, metadata) VALUES ($1, $2, $3::uuid, $4, $5, $6, $7, $8, $9::jsonb)`, newID, cid, uid, name, nullString(url), nullStr(fileType), size, status, string(metaBytes)) if err != nil { log.Printf("files insert %d: %v", legacyID, err) delete(fileMap, legacyKey) report["files_skipped"]++ continue } report["files"]++ } }