using System.IO; using System.Threading; using System.Threading.Tasks; using Needle.Engine.Core; using Needle.Engine.Utils; using UnityEditor; using UnityEngine; namespace Needle.Engine.Deployment { [CustomEditor(typeof(DeployToFTP))] public class DeployToFTPEditor : BaseDeployToEditor { private IProjectInfo project; private SerializedProperty settings, path, useGzipCompression, overrideGzipCompression; private void OnEnable() { project = ObjectUtils.FindObjectOfType(); path = serializedObject.FindProperty(nameof(DeployToFTP.Path)); settings = serializedObject.FindProperty(nameof(DeployToFTP.FTPServer)); useGzipCompression = serializedObject.FindProperty(nameof(DeployToFTP.UseGzipCompression)); overrideGzipCompression = serializedObject.FindProperty(nameof(DeployToFTP.OverrideGzipCompression)); } public override void OnInspectorGUI() { if (!OnBeforeInspectorGUI()) return; if (project == null) { EditorGUILayout.HelpBox("No project found - please add a " + nameof(ExportInfo) + " component to you scene", MessageType.Warning); return; } var ftp = target as DeployToFTP; if (!ftp) { base.OnInspectorGUI(); return; } ftp.Path = ftp.Path?.TrimStart('.'); if (string.IsNullOrWhiteSpace(ftp.Path)) ftp.Path = "/"; var change = new EditorGUI.ChangeCheckScope(); EditorGUILayout.LabelField("FTP", EditorStyles.boldLabel); using (new GUILayout.HorizontalScope()) { EditorGUILayout.PropertyField(settings, new GUIContent("Server")); if (!settings.objectReferenceValue) { if (GUILayout.Button("Create", GUILayout.Width(50))) { var instance = CreateInstance(); AssetDatabase.CreateAsset(instance, "Assets/FTPServer.asset"); settings.objectReferenceValue = instance; serializedObject.ApplyModifiedProperties(); } } } var server = ftp.FTPServer; string key = default; if (server) server.TryGetKey(out key); var password = SecretsHelper.GetSecret(key); var hasInvalidDeploymentPath = (server && !server.AllowTopLevelDeployment) && (string.IsNullOrWhiteSpace(path.stringValue) || path.stringValue.Trim() == "/"); if (server) { EditorGUILayout.PropertyField(path, new GUIContent("Path", "The path on the ftp server where you want to deploy your website to.")); if (hasInvalidDeploymentPath) { EditorGUILayout.HelpBox("Deployment to the top level directory is not allowed. Please specify a subfolder path.", MessageType.Warning); } using (new EditorGUILayout.HorizontalScope()) { UseGizp.Enabled = EditorGUILayout.Toggle(new GUIContent("Use Gzip", "Enable gzip compression for the files. Make sure your server supports gzip compression."), UseGizp.Enabled); } } if (change.changed) serializedObject.ApplyModifiedProperties(); NeedleProjectConfig.TryGetBuildDirectory(out var directory); if (!server) { EditorGUILayout.Space(2); EditorGUILayout.HelpBox("Instructions:\n1) Create FTP Server settings\n2) Enter your server details there\n3) Assign the FTP Server settings here", MessageType.None); } EditorGUILayout.Space(5); var hasPassword = !string.IsNullOrWhiteSpace(password); var hasServerName = !string.IsNullOrWhiteSpace(server?.Servername); var hasUsername = !string.IsNullOrWhiteSpace(server?.Username); var canDeploy = server && hasPassword && hasServerName && hasUsername; if (!canDeploy) { if (server) { var missing = hasServerName ? "" : "\n- server name"; if (!hasUsername) missing += "\n- username"; if (!hasPassword) missing += "\n- password"; EditorGUILayout.HelpBox($"FTP Server configuration is missing:{missing}", MessageType.None); } } var isDeploying = currentTask != null && !currentTask.IsCompleted; using (new EditorGUI.DisabledScope(!canDeploy || hasInvalidDeploymentPath)) { using(new EditorGUI.DisabledScope(isDeploying)) using (new GUILayout.HorizontalScope()) { var devBuild = NeedleEngineBuildOptions.DevelopmentBuild; if ((Event.current.modifiers & EventModifiers.Alt) != 0) devBuild = !devBuild; if (GUILayout.Button("Build & Deploy: " + (devBuild ? "Dev" : "Prod"), GUILayout.Height(30))) { RunDeployment(target as DeployToFTP, true, devBuild); } using (new EditorGUI.DisabledScope(!Directory.Exists(directory))) { if (GUILayout.Button("Deploy Only", GUILayout.Height(30))) { RunDeployment(target as DeployToFTP,false, devBuild); } } } } var hasRemoteUrl = server && server.RemoteUrlIsValid; if (hasRemoteUrl) { var fullUrl = server.GetUrl(ftp.Path); // using (new EditorGUI.DisabledScope(!hasRemoteUrl)) if (GUILayout.Button(new GUIContent("Open in Browser " + Constants.ExternalLinkChar, fullUrl), GUILayout.Height(30))) { Application.OpenURL(fullUrl); } } if (isDeploying) { EditorGUILayout.HelpBox("Deployment to FTP is in progress...", MessageType.None); } } private static Task currentTask; private static CancellationTokenSource cancel; public static async void RunDeployment(DeployToFTP comp, bool exportScene, bool devBuild) { var projectInfo = ObjectUtils.FindObjectOfType(); var server = comp.FTPServer; var remotePath = comp.Path; var username = server.Username; server.TryGetKey(out var key); var password = SecretsHelper.GetSecret(key); var sftp = server.SFTP; var port = server.Port; Debug.Log("Begin uploading..."); cancel?.Cancel(); if (currentTask != null && currentTask.IsCompleted == false) { await currentTask; return; } const int maxUploadDurationInMilliseconds = 10 * 60 * 1000; cancel = new CancellationTokenSource(maxUploadDurationInMilliseconds); var progId = Progress.Start("FTP Upload", "", Progress.Options.Managed); Progress.RegisterCancelCallback(progId, () => { if (!cancel.IsCancellationRequested) { Debug.Log("Cancelling FTP upload..."); cancel.Cancel(); } return true; }); BuildContext buildContext; if (exportScene) buildContext = BuildContext.Distribution(!devBuild); else buildContext = BuildContext.PrepareDeploy; if (server.RemoteUrlIsValid) { var baseUrl = server.RemoteUrl; // ensure we don't have double slashes (while keeping https:// intact) while(baseUrl.EndsWith("/")) baseUrl = baseUrl.Substring(0, baseUrl.Length - 1); while (remotePath.StartsWith("/")) remotePath = remotePath.Substring(1); while(remotePath.Contains("//")) remotePath = remotePath.Replace("//", "/"); buildContext.LiveUrl = baseUrl + "/" + remotePath; } var distDirectory = projectInfo.ProjectDirectory + "/dist"; if (NeedleProjectConfig.TryGetBuildDirectory(out var dir)) { distDirectory = dir; } var buildResult = false; var postBuildMessage = default(string); if (exportScene) { Progress.SetDescription(progId, "Export and Build"); var dev = NeedleEngineBuildOptions.DevelopmentBuild; Debug.Log("Begin building distribution"); currentTask = Actions.ExportAndBuild(buildContext); buildResult = await currentTask; postBuildMessage = "Successfully built distribution"; } else { currentTask = Actions.ExportAndBuild(buildContext); buildResult = await currentTask; } if (cancel.IsCancellationRequested) { Debug.LogWarning("Upload cancelled"); return; } if (!buildResult) { Debug.LogError("Build failed, aborting FTP upload - see console for errors"); return; } if (postBuildMessage != null) Debug.Log(postBuildMessage); Debug.Log("Begin uploading " + distDirectory); Progress.SetDescription(progId, "Upload " + Path.GetDirectoryName(projectInfo.ProjectDirectory) + " to FTP"); if(SceneView.lastActiveSceneView) SceneView.lastActiveSceneView.ShowNotification(new GUIContent("Begin Upload..."), 5); currentTask = Tools.UploadToFTP(server.Servername, username, password, distDirectory, remotePath, sftp, false, port, cancel.Token); // currentTask = UploadDirectory(distDirectory, opts); var uploadResult = await currentTask; if (cancel.Token.IsCancellationRequested) Debug.LogWarning("FTP upload was cancelled"); else if (uploadResult) { if(SceneView.lastActiveSceneView) SceneView.lastActiveSceneView.ShowNotification(new GUIContent("Upload succeeded"), 5); Debug.Log($"FTP upload {"succeeded".AsSuccess()} " + distDirectory); if (!string.IsNullOrWhiteSpace(buildContext.LiveUrl)) { Application.OpenURL(buildContext.LiveUrl); } } else Debug.LogError("Uploading failed. Please see console for errors.\n" + distDirectory); if (Progress.Exists(progId)) Progress.Finish(progId); } } }