using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.IO.Compression; using Unity.EditorCoroutines.Editor; using UnityEditor; using UnityEngine; using UnityEngine.Networking; namespace Unity.Play.Publisher.Editor { /// /// Provides methods for performing operations according to the state of the application /// public class PublisherMiddleware { const string WebglSharingFile = "webgl_sharing"; const string ZipName = "connectwebgl.zip"; const string UploadEndpoint = "/api/webgl/upload"; const string QueryProgressEndpoint = "/api/webgl/progress"; const string UndefinedGUID = "UNDEFINED_GUID"; const int ZipFileLimitBytes = 200 * 1024 * 1024; static EditorCoroutine waitUntilUserLogsInRoutine; static UnityWebRequest uploadRequest; /// /// Creates a new middleware according to the state of the application /// /// public static Middleware Create() { return (store) => (next) => (action) => { var result = next(action); switch (action) { case PublishStartAction published: ZipAndPublish(published.title, published.buildPath, store); break; case UploadStartAction upload: Upload(store, upload.buildGUID); break; case QueryProgressAction query: CheckProgress(store, query.key); break; case StopUploadAction stopUpload: StopUploadAction(); break; case NotLoginAction login: CheckLoginStatus(store); break; } return result; }; } static void ZipAndPublish(string title, string buildPath, Store store) { store.Dispatch(new TitleChangeAction { title = title }); if (!PublisherUtils.BuildIsValid(buildPath)) { store.Dispatch(new OnErrorAction { errorMsg = Localization.Tr("ERROR_BUILD_ABSENT") }); return; } if (!Zip(store, buildPath)) { return; } string GUIDPath = Path.Combine(buildPath, "GUID.txt"); if (File.Exists(GUIDPath)) { store.Dispatch(new UploadStartAction() { buildGUID = File.ReadAllText(GUIDPath) }); return; } Debug.LogWarningFormat("Missing GUID file for {0}, consider deleting the build and making a new one through the WebGL Publisher", buildPath); store.Dispatch(new UploadStartAction() { buildGUID = UndefinedGUID }); } static bool Zip(Store store, string buildOutputDir) { var projectDir = Directory.GetParent(Application.dataPath).FullName; var destPath = Path.Combine(projectDir, ZipName); File.Delete(destPath); ZipFile.CreateFromDirectory(buildOutputDir, destPath); FileInfo fileInfo = new FileInfo(destPath); if (fileInfo.Length > ZipFileLimitBytes) { store.Dispatch(new OnErrorAction { errorMsg = string.Format(Localization.Tr("ERROR_MAX_SIZE"), PublisherUtils.FormatBytes(ZipFileLimitBytes)) }); return false; } store.Dispatch(new ZipPathChangeAction { zipPath = destPath }); return true; } static void Upload(Store store, string buildGUID) { var token = UnityConnectSession.instance.GetAccessToken(); if (token.Length == 0) { CheckLoginStatus(store); return; } string path = store.state.zipPath; string title = string.IsNullOrEmpty(store.state.title) ? PublisherUtils.DefaultGameName : store.state.title; string baseUrl = GetAPIBaseUrl(); string projectId = GetProjectId(); var formSections = new List(); formSections.Add(new MultipartFormDataSection("title", title)); if (buildGUID.Length > 0) { formSections.Add(new MultipartFormDataSection("buildGUID", buildGUID)); } if (projectId.Length > 0) { formSections.Add(new MultipartFormDataSection("projectId", projectId)); } formSections.Add(new MultipartFormFileSection("file", File.ReadAllBytes(path), Path.GetFileName(path), "application/zip")); uploadRequest = UnityWebRequest.Post(baseUrl + UploadEndpoint, formSections); uploadRequest.SetRequestHeader("Authorization", $"Bearer {token}"); uploadRequest.SetRequestHeader("X-Requested-With", "XMLHTTPREQUEST"); var op = uploadRequest.SendWebRequest(); EditorCoroutineUtility.StartCoroutineOwnerless(UpdateProgress(store, uploadRequest)); op.completed += operation => { #if UNITY_2020 if ((uploadRequest.result == UnityWebRequest.Result.ConnectionError) || (uploadRequest.result == UnityWebRequest.Result.ProtocolError)) #else if (uploadRequest.isNetworkError || uploadRequest.isHttpError) #endif { if (uploadRequest.error != "Request aborted") { store.Dispatch(new OnErrorAction { errorMsg = uploadRequest.error }); } } else { var response = JsonUtility.FromJson(op.webRequest.downloadHandler.text); if (!string.IsNullOrEmpty(response.key)) { store.Dispatch(new QueryProgressAction { key = response.key }); } } }; } static void StopUploadAction() { if (uploadRequest == null) { return; } uploadRequest.Abort(); } static void CheckProgress(Store store, string key) { var token = UnityConnectSession.instance.GetAccessToken(); if (token.Length == 0) { CheckLoginStatus(store); return; } key = key ?? store.state.key; string baseUrl = GetAPIBaseUrl(); var uploadRequest = UnityWebRequest.Get($"{baseUrl + QueryProgressEndpoint}?key={key}"); uploadRequest.SetRequestHeader("Authorization", $"Bearer {token}"); uploadRequest.SetRequestHeader("X-Requested-With", "XMLHTTPREQUEST"); var op = uploadRequest.SendWebRequest(); op.completed += operation => { #if UNITY_2020 if ((uploadRequest.result == UnityWebRequest.Result.ConnectionError) || (uploadRequest.result == UnityWebRequest.Result.ProtocolError)) #else if (uploadRequest.isNetworkError || uploadRequest.isHttpError) #endif { AnalyticsHelper.UploadCompleted(UploadResult.Failed); Debug.LogError(uploadRequest.error); StopUploadAction(); return; } var response = JsonUtility.FromJson(op.webRequest.downloadHandler.text); store.Dispatch(new QueryProgressResponseAction { response = response }); if (response.progress == 100 || !string.IsNullOrEmpty(response.error)) { SaveProjectID(response.projectId); return; } EditorCoroutineUtility.StartCoroutineOwnerless(RefreshProcessingProgress(1.5f, store)); }; } static void SaveProjectID(string projectId) { if (projectId.Length == 0) { return; } StreamWriter writer = new StreamWriter(WebglSharingFile, false); writer.Write(projectId); writer.Close(); } static string GetProjectId() { if (!File.Exists(WebglSharingFile)) { return string.Empty; } var reader = new StreamReader(WebglSharingFile); var projectId = reader.ReadLine(); reader.Close(); return projectId; } static IEnumerator UpdateProgress(Store store, UnityWebRequest request) { EditorWaitForSeconds waitForSeconds = new EditorWaitForSeconds(0.5f); while (true) { if (request.isDone) { break; } int progress = (int)(Mathf.Clamp(request.uploadProgress, 0, 1) * 100); store.Dispatch(new UploadProgressAction { progress = progress }); yield return waitForSeconds; } yield return null; } static void CheckLoginStatus(Store store) { var token = UnityConnectSession.instance.GetAccessToken(); if (token.Length != 0) { store.Dispatch(new LoginAction()); return; } if (waitUntilUserLogsInRoutine != null) { return; } waitUntilUserLogsInRoutine = EditorCoroutineUtility.StartCoroutineOwnerless(WaitUntilUserLogsIn(2f, store)); } static IEnumerator WaitUntilUserLogsIn(float refreshDelay, Store store) { EditorWaitForSeconds waitAmount = new EditorWaitForSeconds(refreshDelay); while (EditorWindow.HasOpenInstances()) { yield return waitAmount; //Debug.LogError("Rechecking login in " + refreshDelay); if (UnityConnectSession.instance.GetAccessToken().Length != 0) { store.Dispatch(new LoginAction()); //Debug.LogError("Connected!"); waitUntilUserLogsInRoutine = null; yield break; } } waitUntilUserLogsInRoutine = null; //Debug.LogError("Window closed"); } static IEnumerator RefreshProcessingProgress(float refreshDelay, Store store) { EditorWaitForSeconds waitAmount = new EditorWaitForSeconds(refreshDelay); yield return waitAmount; store.Dispatch(new QueryProgressAction()); } static string GetAPIBaseUrl() { string env = UnityConnectSession.instance.GetEnvironment(); if (env == "staging") { return "https://connect-staging.unity.com"; } else if (env == "dev") { return "https://connect-dev.unity.com"; } return "https://play.unity.com"; } } /// /// Represents the response received on an upload request /// [Serializable] public class UploadResponse { /// /// The key that identifies the uploaded project /// public string key; } /// /// Represents a response that contains data about upload progress /// [Serializable] public class GetProgressResponse { /// /// ID of the project /// public string projectId; /// /// URL of the project /// public string url; /// /// Upload progress /// public int progress; /// /// Error which occured /// public string error; } }