|
package main |
|
|
|
import ( |
|
"archive/zip" |
|
"bytes" |
|
"encoding/csv" |
|
"fmt" |
|
"html" |
|
"io" |
|
"log" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"strings" |
|
"time" |
|
) |
|
|
|
func main() { |
|
includeManagedPackages := false |
|
if len(os.Args) > 1 && os.Args[1] == "--include-managed" { |
|
includeManagedPackages = true |
|
} |
|
|
|
// Check if sf is installed |
|
if _, err := exec.LookPath("sf"); err != nil { |
|
log.Fatal("sf could not be found. Please install the Salesforce CLI.") |
|
} |
|
|
|
// Authenticate to Salesforce org |
|
fmt.Printf("Authenticating to Salesforce org...\n") |
|
cmd := exec.Command("sf", "org", "login", "web") |
|
cmd.Stdout = os.Stdout |
|
cmd.Stderr = os.Stderr |
|
if err := cmd.Run(); err != nil { |
|
log.Fatalf("Failed to authenticate: %v", err) |
|
} |
|
|
|
// Query all reports |
|
fmt.Printf("Querying all reports in the org...\n") |
|
query := "SELECT DeveloperName, FolderName, NamespacePrefix FROM Report" |
|
if !includeManagedPackages { |
|
query += " WHERE NamespacePrefix = ''" |
|
} |
|
cmd = exec.Command("sf", "data", "query", "--query", query, "-r", "csv") |
|
var out bytes.Buffer |
|
cmd.Stdout = &out |
|
cmd.Stderr = os.Stderr |
|
if err := cmd.Run(); err != nil { |
|
log.Fatalf("Failed to query reports: %v", err) |
|
} |
|
|
|
// Create package.xml |
|
fmt.Println("Creating package.xml...") |
|
packageXML := `<?xml version="1.0" encoding="UTF-8"?> |
|
<Package xmlns="http://soap.sforce.com/2006/04/metadata"> |
|
<types> |
|
` |
|
// Read the CSV data from the buffer and add each report to the package.xml |
|
reader := csv.NewReader(bytes.NewReader(out.Bytes())) |
|
records, err := reader.ReadAll() |
|
if err != nil { |
|
log.Fatalf("Failed to read CSV data: %v", err) |
|
} |
|
|
|
if len(records) < 2 { |
|
log.Fatal("CSV file does not contain the expected format (header and at least one data row).") |
|
} |
|
|
|
for _, record := range records[1:] { |
|
escapedFolderName := html.EscapeString(record[1]) |
|
escapedDeveloperName := html.EscapeString(record[0]) |
|
namespacePrefix := record[2] |
|
if namespacePrefix != "" { |
|
escapedDeveloperName = namespacePrefix + "__" + escapedDeveloperName |
|
} |
|
packageXML += fmt.Sprintf(" <members>%s/%s</members>\n", escapedFolderName, escapedDeveloperName) |
|
} |
|
|
|
packageXML += ` <name>Report</name> |
|
</types> |
|
<version>57.0</version> |
|
</Package> |
|
` |
|
|
|
var fileName = "reports" + time.Now().Format("20060102150405") + "package.xml" |
|
if err := os.WriteFile(fileName, []byte(packageXML), 0644); err != nil { |
|
log.Fatalf("Failed to write package.xml: %v", err) |
|
} |
|
|
|
// Ensure the temporary file is deleted after execution |
|
defer os.Remove(fileName) |
|
|
|
waitTime := "10" |
|
cmd = exec.Command("sfdx", "force:mdapi:retrieve", "-r", "./", "-k", fileName, "-w", waitTime) |
|
cmd.Stdout = os.Stdout |
|
cmd.Stderr = os.Stderr |
|
for { |
|
if err := cmd.Run(); err != nil { |
|
if waitTime == "10" { |
|
fmt.Println("Initial wait time exceeded, increasing wait time to 30 minutes...") |
|
waitTime = "30" |
|
cmd = exec.Command("sfdx", "force:mdapi:retrieve", "-r", "./", "-k", fileName, "-w", waitTime) |
|
cmd.Stdout = os.Stdout |
|
cmd.Stderr = os.Stderr |
|
continue |
|
} else { |
|
log.Fatalf("Failed to retrieve reports after increasing wait time: %v", err) |
|
} |
|
} else { |
|
break |
|
} |
|
} |
|
|
|
// Check if a zip file was created |
|
zipFileName := "unpackaged.zip" |
|
if _, err := os.Stat(zipFileName); err == nil { |
|
fmt.Println("Unzipping retrieved reports...") |
|
if err := unzip(zipFileName, "./force-app/main/default/reports/"); err != nil { |
|
log.Fatalf("Failed to unzip reports: %v", err) |
|
} |
|
os.Remove(zipFileName) |
|
} |
|
|
|
fmt.Println("Reports have been retrieved.") |
|
} |
|
|
|
// Unzip extracts a zip archive to a specified destination. |
|
func unzip(src, dest string) error { |
|
r, err := zip.OpenReader(src) |
|
if err != nil { |
|
return err |
|
} |
|
defer r.Close() |
|
|
|
for _, f := range r.File { |
|
// Only include report files and their folders |
|
if !strings.HasPrefix(f.Name, "unpackaged/reports/") { |
|
continue |
|
} |
|
// Remove the top-level "unpackaged/reports" folder from the path |
|
fpath := filepath.Join(dest, strings.TrimPrefix(f.Name, "unpackaged/reports/")) |
|
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { |
|
return fmt.Errorf("%s: illegal file path", fpath) |
|
} |
|
if f.FileInfo().IsDir() { |
|
if _, err := os.Stat(fpath); os.IsNotExist(err) { |
|
os.MkdirAll(fpath, os.ModePerm) |
|
} |
|
continue |
|
} |
|
// Ensure .report suffix is converted to .report-meta.xml |
|
if strings.HasSuffix(fpath, ".report") { |
|
fpath = strings.TrimSuffix(fpath, ".report") + ".report-meta.xml" |
|
} |
|
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { |
|
return err |
|
} |
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) |
|
if err != nil { |
|
return err |
|
} |
|
rc, err := f.Open() |
|
if err != nil { |
|
return err |
|
} |
|
_, err = io.Copy(outFile, rc) |
|
outFile.Close() |
|
rc.Close() |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
} |