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;
}
}