Skip to content

How to Create a new Destination

James edited this page Sep 28, 2025 · 3 revisions

A destination is what is used to take a contents from the sources and upload it to a local path or online service. A destination executes after all files have been saved to a temporary location (cache) and modified using modifiers.

The Build Uploader uses reflection to find all classes inheriting from AUploadDestination after compilation to show them in the dropdown. UploadDestinationAttribute is used to define static information about the destination such as what to display in dropdowns

using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

/// <summary>
/// Move the modified build to a local path on the users computer
/// 
/// NOTE: This classes name path is saved in the JSON file so avoid renaming
/// </summary>
[UploadDestinationAttribute("Local Path")]
public partial class LocalPathDestination : AUploadDestination
{
    private string m_localPath = "";
    private Utils.FileExistHandling m_duplicateFileHandling = Utils.FileExistHandling.Overwrite;
    private bool m_zipContent = false;
    private string m_zippedFilesName = "";

    public LocalPathDestination() : base()
    {
        // Required for reflection
    }
    
    public LocalPathDestination(string localPath, Utils.FileExistHandling duplicateFileHandling = Utils.FileExistHandling.Overwrite) : base()
    {
        m_localPath = localPath;
        m_duplicateFileHandling = duplicateFileHandling;
    }

    public void ZipContents(string zippedFileName)
    {
        m_zipContent = true;
        m_zippedFilesName = zippedFileName;
    }

    private string FullPath(StringFormatter.Context ctx)
    {
        string path = StringFormatter.FormatString(m_localPath, ctx);
        if (m_zipContent)
        {
            path += StringFormatter.FormatString(m_zippedFilesName, ctx) + ".zip";
        }
        
        return path;
    }

    private bool SetNewPath(string newPath)
    {
        if (newPath == m_localPath || string.IsNullOrEmpty(newPath))
        {
            return false;
        }
        
        m_localPath = newPath;
        return true;
    }

    public override async Task<bool> Upload(UploadTaskReport.StepResult result, StringFormatter.Context ctx)
    {
        string fullPath = FullPath(ctx);
        string directory = m_zipContent ? Path.GetDirectoryName(fullPath) : fullPath;

        // Delete existing content
        if (Directory.Exists(fullPath))
        {
            result.AddLog($"Deleting existing directory: {fullPath}");
            Directory.Delete(fullPath, true);
        }
        else if (File.Exists(fullPath))
        {
            result.AddLog($"Deleting existing file: {fullPath}");
            File.Delete(fullPath);
        }
        
        // Create directory
        if (!Directory.Exists(directory))
        {
            result.AddLog($"Creating directory: {directory}");
            Directory.CreateDirectory(directory);
        }
        
        // Copy contents
        if (m_zipContent)
        {
            result.AddLog($"Zipping context to: {fullPath}");
            if (!await ZipUtils.Zip(m_cachedFolderPath, fullPath, result))
            {
                return false;
            }
        }
        else if (Utils.IsPathADirectory(m_cachedFolderPath))
        {
            result.AddLog($"Copying directory to: {fullPath}");
            if (!await Utils.CopyDirectoryAsync(m_cachedFolderPath, fullPath, m_duplicateFileHandling, result))
            {
                return false;
            }
        }
        else
        {
            result.AddLog($"Copying file to: {fullPath}");
            if (!await Utils.CopyFileAsync(m_cachedFolderPath, fullPath, m_duplicateFileHandling, result))
            {
                return false;
            }
        }
        
        return true;
    }

    public override void TryGetErrors(List<string> errors, StringFormatter.Context ctx)
    {
        base.TryGetErrors(errors, ctx);
        
        if (string.IsNullOrEmpty(m_localPath))
        {
            errors.Add("No local path selected");
        }
        else if (Utils.PathContainsInvalidCharacters(FullPath(ctx)))
        {
            errors.Add("Path contains invalid characters");
        }

        if (m_zipContent)
        {
            if (string.IsNullOrEmpty(m_zippedFilesName))
            {
                errors.Add("No Zipped Name specified");
            }
        }
    }

    public override void TryGetWarnings(List<string> warnings, StringFormatter.Context ctx)
    {
        base.TryGetWarnings(warnings, ctx);

        if (!Utils.PathExists(FullPath(ctx)))
        {
            warnings.Add("Path does not exist but may be created during upload.");
        }
    }

    public override Dictionary<string, object> Serialize()
    {
        Dictionary<string, object> data = new Dictionary<string, object>();
        data["m_localPath"] = m_localPath;
        data["m_fileName"] = m_zippedFilesName;
        data["m_zipContent"] = m_zipContent;
        data["m_duplicateFileHandling"] = (int)m_duplicateFileHandling;
        return data;
    }

    public override void Deserialize(Dictionary<string, object> data)
    {
        m_localPath = (string)data["m_localPath"];
        m_zippedFilesName = (string)data["m_fileName"];
        m_zipContent = (bool)data["m_zipContent"];

        if (data.TryGetValue("m_duplicateFileHandling", out object handling) && handling is long)
        {
            m_duplicateFileHandling = (Utils.FileExistHandling)(long)handling;
        }
        else
        {
            m_duplicateFileHandling = Utils.FileExistHandling.Error;
        }
    }
}
using UnityEditor;
using UnityEngine;

public partial class LocalPathDestination
{
    private string ButtonText => "Choose Local Path...";
    
    private bool m_showFormattedLocalPath = false;
    private bool m_showFormattedZippedFilesName = false;

    protected internal override void OnGUIExpanded(ref bool isDirty, StringFormatter.Context ctx)
    {
        isDirty |= CustomPathTextField.OnGUI(ref m_localPath, ref m_showFormattedLocalPath, ctx);

        using (new EditorGUILayout.HorizontalScope())
        {
            GUILayout.Label("Zip Contents:", GUILayout.Width(120));

            bool newZip = EditorGUILayout.Toggle(m_zipContent, GUILayout.Width(20));
            if (m_zipContent != newZip)
            {
                m_zipContent = newZip;
                isDirty = true;
            }

            using (new EditorGUI.DisabledScope(!m_zipContent))
            {
                GUIContent label = new GUIContent("Name (No extension):", "Name of the zipped file that will be created." +
                                                                          "\nSee docs for format options such as {buildNumber} and {date}.");
                GUILayout.Label(label, GUILayout.Width(125));
                
                if (EditorUtils.FormatStringTextField(ref m_zippedFilesName, ref m_showFormattedZippedFilesName, ctx))
                {
                    isDirty = true;
                }
            }
        }
        
        using (new GUILayout.HorizontalScope())
        {
            GUILayout.Label("Duplicate Files: ", GUILayout.Width(120));
            var newHandler = (Utils.FileExistHandling)EditorGUILayout.EnumPopup(m_duplicateFileHandling);
            if (m_duplicateFileHandling != newHandler)
            {
                m_duplicateFileHandling = newHandler;
                isDirty = true;
            }
        }
    }

    protected internal override void OnGUICollapsed(ref bool isDirty, float maxWidth, StringFormatter.Context ctx)
    {
        string displayedPath = FullPath(ctx);
        if (CustomPathButton.OnGUI(ref displayedPath, ButtonText, maxWidth))
        {
            m_localPath = displayedPath; // If this is changed we are given a non-formatted version
            isDirty = true;
        }
    }
}

Clone this wiki locally