package installer import ( "errors" "fmt" "os" "path/filepath" "gitgud.io/mike/mpv-manager/internal/assets" ) type renameFileFunc func(oldPath, newPath string) error // installEmbeddedThumbfast installs the reviewed Thumbfast source carried by // this binary. It deliberately performs no runtime upstream lookup or download. func installEmbeddedThumbfast(installDir string) error { content, err := assets.ReadThumbfast() if err != nil { return fmt.Errorf("read embedded thumbfast: %w", err) } scriptsDir := filepath.Join(installDir, "scripts") if err := os.MkdirAll(scriptsDir, 0755); err != nil { return fmt.Errorf("create scripts directory: %w", err) } targetPath := filepath.Join(scriptsDir, "thumbfast.lua") if err := writeFileAtomicallyPreserving(targetPath, content, 0644); err != nil { return fmt.Errorf("install embedded thumbfast: %w", err) } return nil } // writeFileAtomicallyPreserving stages data beside targetPath, then replaces // the target. An existing target is moved to a unique backup first and restored // if the final rename fails. func writeFileAtomicallyPreserving(targetPath string, data []byte, perm os.FileMode) error { return writeFileAtomicallyPreservingWithRename(targetPath, data, perm, os.Rename) } func writeFileAtomicallyPreservingWithRename( targetPath string, data []byte, perm os.FileMode, rename renameFileFunc, ) error { dir := filepath.Dir(targetPath) base := filepath.Base(targetPath) tempFile, err := os.CreateTemp(dir, "."+base+".download-*") if err != nil { return fmt.Errorf("create temporary file: %w", err) } tempPath := tempFile.Name() tempExists := true defer func() { _ = tempFile.Close() if tempExists { _ = os.Remove(tempPath) } }() if _, err := tempFile.Write(data); err != nil { return fmt.Errorf("write temporary file: %w", err) } if err := tempFile.Chmod(perm); err != nil { return fmt.Errorf("set temporary file permissions: %w", err) } if err := tempFile.Sync(); err != nil { return fmt.Errorf("sync temporary file: %w", err) } if err := tempFile.Close(); err != nil { return fmt.Errorf("close temporary file: %w", err) } info, err := os.Lstat(targetPath) if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("inspect existing target: %w", err) } if err := rename(tempPath, targetPath); err != nil { return fmt.Errorf("install new file: %w", err) } tempExists = false return nil } if info.IsDir() { return fmt.Errorf("target is a directory: %s", targetPath) } backupFile, err := os.CreateTemp(dir, "."+base+".backup-*") if err != nil { return fmt.Errorf("reserve backup path: %w", err) } backupPath := backupFile.Name() if err := backupFile.Close(); err != nil { _ = os.Remove(backupPath) return fmt.Errorf("close backup placeholder: %w", err) } if err := os.Remove(backupPath); err != nil { return fmt.Errorf("prepare backup path: %w", err) } if err := rename(targetPath, backupPath); err != nil { return fmt.Errorf("back up existing file: %w", err) } if err := rename(tempPath, targetPath); err != nil { replaceErr := fmt.Errorf("replace existing file: %w", err) if restoreErr := rename(backupPath, targetPath); restoreErr != nil { return errors.Join( replaceErr, fmt.Errorf("restore previous file from %s: %w", backupPath, restoreErr), ) } return replaceErr } tempExists = false if err := os.Remove(backupPath); err != nil { return fmt.Errorf("remove previous-file backup %s: %w", backupPath, err) } return nil }