Settings are now saved
Tutorial is only displayed on first start (when no settings file is found)
This commit is contained in:
454
Assets/RothenburgAR/Scripts/Updater/UpdateManager.cs
Normal file
454
Assets/RothenburgAR/Scripts/Updater/UpdateManager.cs
Normal file
@@ -0,0 +1,454 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using RothenburgAR.Common;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace RothenburgAR.Updater
|
||||
{
|
||||
public enum UpdateState
|
||||
{
|
||||
UpdatesFound,
|
||||
ConfirmationPending,
|
||||
Downloading,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class UpdateManager : Singleton<UpdateManager>
|
||||
{
|
||||
public UpdateState UpdateState { get; private set; }
|
||||
|
||||
public int CurrentProgress { get; private set; }
|
||||
public int MaxProgress { get; private set; }
|
||||
|
||||
private float afterDownloadWaitTime = 10f;
|
||||
private readonly int retriesUntilFailure = 4;
|
||||
private readonly string trackerMainFile = "tracker.dat";
|
||||
|
||||
public ApiVersioncheckAnswer VersionAnswer { get; set; }
|
||||
|
||||
Dictionary<string, List<ApiExhibit>> ExhibitMetas = new Dictionary<string, List<ApiExhibit>>();
|
||||
private LogFileHandler logFileHandler;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (Application.internetReachability == NetworkReachability.NotReachable)
|
||||
{
|
||||
// just continue to app
|
||||
UpdatesCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
CheckForUpdates();
|
||||
}
|
||||
|
||||
private void CheckForUpdates()
|
||||
{
|
||||
// check for updates and ask for permission to download if there are any
|
||||
string versionMap = GenerateVersionMap();
|
||||
CheckForUpdates(versionMap, 0);
|
||||
}
|
||||
|
||||
private void CheckForUpdates(string versionMap, int retries)
|
||||
{
|
||||
HttpHandler http = new HttpRequest(ApiInfo.VersionCheckEndpoint, HttpVerb.POST, versionMap).Send();
|
||||
http.Operation.completed += ar =>
|
||||
{
|
||||
Debug.Log("VersionAnswer: " + http.Download.text);
|
||||
var answer = JsonConvert.DeserializeObject<ApiVersioncheckAnswer>(http.Download.text);
|
||||
if (http.IsError)
|
||||
{
|
||||
if (retries < retriesUntilFailure)
|
||||
{
|
||||
CheckForUpdates(versionMap, retries + 1);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateFailed(http);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log(string.Format("{1}-DONE with {0}", ApiInfo.VersionCheckEndpoint, Time.realtimeSinceStartup));
|
||||
VersionAnswer = answer;
|
||||
|
||||
if (VersionAnswer.Data.TrueForAll(d => d.Meta.Status == VersionStatus.ok && d.Tracker.Status == VersionStatus.ok))
|
||||
{
|
||||
afterDownloadWaitTime = 0f;
|
||||
StartCoroutine(CheckFiles());
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateState = UpdateState.UpdatesFound;
|
||||
|
||||
if (Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
|
||||
{
|
||||
TriggerUpdate();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateState = UpdateState.ConfirmationPending;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private string GenerateVersionMap()
|
||||
{
|
||||
string result = "{ ";
|
||||
|
||||
var rootDir = new DirectoryInfo(PathHelper.ExhibitionPath);
|
||||
if (!rootDir.Exists)
|
||||
{
|
||||
rootDir.Create();
|
||||
}
|
||||
|
||||
var exhibitionDirs = rootDir.GetDirectories().ToList();
|
||||
|
||||
foreach (var dir in exhibitionDirs)
|
||||
{
|
||||
var versionFilePath = Path.Combine(dir.FullName, "version.txt");
|
||||
if (!File.Exists(versionFilePath)) continue;
|
||||
|
||||
var version = long.Parse(File.ReadAllText(versionFilePath));
|
||||
result += @"""{id}"":""{version}"",".Replace("{id}", dir.Name).Replace("{version}", version.ToString());
|
||||
};
|
||||
|
||||
result = result.Substring(0, result.Length - 1) + "}";
|
||||
return result;
|
||||
}
|
||||
|
||||
public void TriggerUpdate()
|
||||
{
|
||||
/*
|
||||
* dir structure:
|
||||
* data
|
||||
* exhibition
|
||||
* id
|
||||
* tracker.dat
|
||||
* tracker.xml
|
||||
* de
|
||||
* meta.json
|
||||
* en
|
||||
* meta.json
|
||||
* id
|
||||
* ...
|
||||
* media
|
||||
* ...
|
||||
*/
|
||||
|
||||
UpdateState = UpdateState.Downloading;
|
||||
|
||||
if (VersionAnswer == null)
|
||||
{
|
||||
// try to get VersionAnswer first if that's what failed
|
||||
CheckForUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO write languages to file the app can read (so that the languagemanager can decide which languages the user can choose from)
|
||||
|
||||
var updatedExhibitions = VersionAnswer.Data.Where(d => d.Meta.Status == VersionStatus.updated).ToList();
|
||||
var updatedTracker = VersionAnswer.Data.Where(d => d.Tracker.Status == VersionStatus.updated).ToList();
|
||||
updatedExhibitions.Union(updatedTracker).Distinct().ToList().ForEach(updatedExhibition =>
|
||||
{
|
||||
// create exhibition directories
|
||||
var path = Path.Combine(PathHelper.ExhibitionPath, updatedExhibition.Id);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
});
|
||||
|
||||
updatedExhibitions.ForEach(d => UpdateExhibition(d, updatedExhibitions));
|
||||
StartCoroutine(UpdateExhibitionsCoroutine(updatedExhibitions));
|
||||
}
|
||||
|
||||
private IEnumerator UpdateExhibitionsCoroutine(List<ApiExhibitionVersion> updatedExhibitions)
|
||||
{
|
||||
yield return new WaitUntil(() => updatedExhibitions.Count == 0);
|
||||
StartCoroutine(CheckFiles());
|
||||
}
|
||||
|
||||
private void UpdateExhibition(ApiExhibitionVersion exhibition, List<ApiExhibitionVersion> updatedExhibitions)
|
||||
{
|
||||
var exhibitionDownloads = new List<HttpHandler>();
|
||||
|
||||
foreach (var lang in VersionAnswer.Languages)
|
||||
{
|
||||
var path = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id, lang);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
UpdateMetaFile(exhibition, path, lang, exhibitionDownloads);
|
||||
}
|
||||
|
||||
var versionPath = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id, "version.txt");
|
||||
var version = exhibition.Meta.UpdateVersion.ToString();
|
||||
StartCoroutine(UpdateVersionFileCoroutine(exhibitionDownloads, versionPath, version, exhibition, updatedExhibitions));
|
||||
}
|
||||
|
||||
private IEnumerator UpdateVersionFileCoroutine(List<HttpHandler> exhibitionDownloads, string versionPath, string version, ApiExhibitionVersion exhibition, List<ApiExhibitionVersion> updatedExhibitions)
|
||||
{
|
||||
// updates version file only if all sub files were downloaded successfully
|
||||
yield return new WaitUntil(() => exhibitionDownloads.All(h => h.IsDone));
|
||||
if (exhibitionDownloads.Any(http => http.IsError)) yield break;
|
||||
|
||||
File.WriteAllText(versionPath, version);
|
||||
updatedExhibitions.Remove(exhibition);
|
||||
}
|
||||
|
||||
private void UpdateMetaFile(ApiExhibitionVersion exhibition, string path, string lang, List<HttpHandler> exhibitionDownloads, int retries = 0)
|
||||
{
|
||||
var url = exhibition.Meta.UpdateUrl.Replace("{lang}", lang);
|
||||
var http = new HttpRequest(url, HttpVerb.GET).Send();
|
||||
exhibitionDownloads.Add(http);
|
||||
|
||||
http.Operation.completed += ar =>
|
||||
{
|
||||
if (http.IsError)
|
||||
{
|
||||
if (retries < retriesUntilFailure)
|
||||
{
|
||||
exhibitionDownloads.Remove(http);
|
||||
UpdateMetaFile(exhibition, path, lang, exhibitionDownloads, retries + 1);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateFailed(http);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(Path.Combine(path, "meta.json"), http.Download.text, System.Text.Encoding.UTF8);
|
||||
Debug.Log(string.Format("{1}-DONE with {0}", url, Time.realtimeSinceStartup));
|
||||
|
||||
var exhibits = JsonConvert.DeserializeObject<List<ApiExhibit>>(http.Download.text);
|
||||
if (exhibits == null)
|
||||
{
|
||||
Debug.LogError(string.Format("Exhibit could not be parsed from Json:\nurl: {0}\njson: {1}", url, http.Download.text));
|
||||
http.IsError = true;
|
||||
UpdateFailed(http);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerator CheckFiles()
|
||||
{
|
||||
var usedFileList = new Dictionary<string, FileDownloadInfo>();
|
||||
GenerateUsedFileList(usedFileList);
|
||||
|
||||
MaxProgress = usedFileList.Count;
|
||||
foreach (var file in usedFileList)
|
||||
{
|
||||
var fileInfo = file.Value;
|
||||
Dictionary<string, byte[]> data = new Dictionary<string, byte[]>();
|
||||
yield return DownloadFile(fileInfo, data);
|
||||
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (!Directory.Exists(fileInfo.directory)) Directory.CreateDirectory(fileInfo.directory);
|
||||
File.WriteAllBytes(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var deletedData = VersionAnswer.Data.Where(d =>
|
||||
d.Meta.Status == VersionStatus.deleted
|
||||
|| d.Tracker.Status == VersionStatus.deleted).ToList();
|
||||
deletedData.ForEach(d => DeleteExhibition(d));
|
||||
|
||||
CleanupMediaFiles();
|
||||
UpdatesCompleted();
|
||||
}
|
||||
|
||||
private void GenerateUsedFileList(Dictionary<string, FileDownloadInfo> downloadList)
|
||||
{
|
||||
ExhibitMetas.Clear();
|
||||
var rootDir = new DirectoryInfo(PathHelper.ExhibitionPath);
|
||||
var exhibitionDirs = rootDir.GetDirectories().ToList();
|
||||
|
||||
foreach (var dir in exhibitionDirs)
|
||||
{
|
||||
var versionFilePath = Path.Combine(dir.FullName, "version.txt");
|
||||
if (!File.Exists(versionFilePath)) continue;
|
||||
|
||||
var exhibitionId = dir.Name;
|
||||
foreach (var lang in VersionAnswer.Languages)
|
||||
{
|
||||
var metaFilePath = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibitionId, lang, "meta.json");
|
||||
var metaFile = File.ReadAllText(metaFilePath);
|
||||
var exhibits = JsonConvert.DeserializeObject<List<ApiExhibit>>(metaFile);
|
||||
ExhibitMetas.Add(exhibitionId + "/" + lang, exhibits);
|
||||
}
|
||||
}
|
||||
|
||||
// would check for || d.Tracker.Status == VersionStatus.ok as well but we don't get an updateUrl for those so it's no use
|
||||
var updatedTracker = VersionAnswer.Data.Where(d => d.Tracker.Status == VersionStatus.updated).ToList();
|
||||
foreach (var exhibition in updatedTracker)
|
||||
{
|
||||
var url = exhibition.Tracker.UpdateUrl;
|
||||
var directory = PathHelper.CombinePaths(PathHelper.ExhibitionPath, exhibition.Id);
|
||||
var filepath = PathHelper.CombinePaths(directory, trackerMainFile);
|
||||
|
||||
downloadList.Add(url, new FileDownloadInfo(url, filepath, directory));
|
||||
}
|
||||
|
||||
var mediaList = ExhibitMetas.Values.ToList().SelectMany(m => m.SelectMany(i => i.Pois.Select(p => p.MediaId))).Where(m => m != null).Distinct().ToList();
|
||||
foreach (var mediaId in mediaList)
|
||||
{
|
||||
var url = ApiInfo.FileEndpoint.Replace("{id}", mediaId);
|
||||
var directory = PathHelper.CombinePaths(PathHelper.MediaPath, mediaId);
|
||||
var filepath = PathHelper.CombinePaths(directory, mediaId + ".obj");
|
||||
|
||||
downloadList.Add(url, new FileDownloadInfo(url, filepath, directory));
|
||||
}
|
||||
}
|
||||
|
||||
private class FileDownloadInfo
|
||||
{
|
||||
public string url;
|
||||
public string filepath;
|
||||
public string directory;
|
||||
|
||||
public FileDownloadInfo(string url, string filepath, string directory)
|
||||
{
|
||||
this.url = url;
|
||||
this.filepath = filepath;
|
||||
this.directory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DownloadFile(FileDownloadInfo info, Dictionary<string, byte[]> data)
|
||||
{
|
||||
if (!Directory.Exists(info.directory)) Directory.CreateDirectory(info.directory);
|
||||
|
||||
var eTag = GenerateETag(info.filepath);
|
||||
yield return DownloadFile(info, eTag, data);
|
||||
}
|
||||
|
||||
private IEnumerator DownloadFile(FileDownloadInfo info, string eTag, Dictionary<string, byte[]> data, int retries = 0)
|
||||
{
|
||||
var url = info.url;
|
||||
var http = new HttpRequest(url, HttpVerb.GET)
|
||||
.WithHeader("If-None-Match", eTag)
|
||||
.WithHeader("Accept-Encoding", "gzip,deflate")
|
||||
.Send();
|
||||
|
||||
yield return new WaitUntil(() => http.IsDone);
|
||||
|
||||
if (http.IsError)
|
||||
{
|
||||
if (retries < retriesUntilFailure)
|
||||
{
|
||||
yield return DownloadFile(info, eTag, data, retries + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateFailed(http);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (http.Request.responseCode != 304)
|
||||
{
|
||||
if (!data.ContainsKey(info.filepath))
|
||||
{
|
||||
// gzipped files are un-gzipped automagically
|
||||
data.Add(info.filepath, http.Download.data);
|
||||
}
|
||||
}
|
||||
|
||||
var subfilesHeader = http.Request.GetResponseHeader("subfiles");
|
||||
var test = http.Request.GetResponseHeaders();
|
||||
if (subfilesHeader != null)
|
||||
{
|
||||
var subfiles = subfilesHeader.Split(new string[1] { ";;" }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
MaxProgress += subfiles.Count(subfileName => !data.Keys.Contains(Path.Combine(info.directory, subfileName)));
|
||||
|
||||
foreach (var subfileName in subfiles)
|
||||
{
|
||||
var subfileInfo = new FileDownloadInfo(
|
||||
info.url + "/" + subfileName,
|
||||
Path.Combine(info.directory, subfileName),
|
||||
info.directory);
|
||||
|
||||
var subfileETag = GenerateETag(info.filepath);
|
||||
// HACK to prevent infinite subfile loop
|
||||
if (!data.ContainsKey(subfileInfo.filepath) && subfileName != "brick.png")
|
||||
{
|
||||
yield return DownloadFile(subfileInfo, subfileETag, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log(string.Format("{1}-DONE with {0}", url, Time.realtimeSinceStartup));
|
||||
CurrentProgress += 1;
|
||||
}
|
||||
|
||||
private string GenerateETag(string filepath)
|
||||
{
|
||||
string eTag = "";
|
||||
if (File.Exists(filepath))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(filepath);
|
||||
SHA256Managed hasher = new SHA256Managed();
|
||||
byte[] hash = hasher.ComputeHash(bytes);
|
||||
eTag = "sha256-";
|
||||
foreach (byte x in hash)
|
||||
{
|
||||
eTag += String.Format("{0:x2}", x);
|
||||
}
|
||||
}
|
||||
return eTag;
|
||||
}
|
||||
|
||||
private void DeleteExhibition(ApiExhibitionVersion exhibition)
|
||||
{
|
||||
var path = Path.Combine(PathHelper.ExhibitionPath, exhibition.Id);
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
|
||||
private void CleanupMediaFiles()
|
||||
{
|
||||
var requiredMedia = ExhibitMetas.Values.ToList().SelectMany(m => m.SelectMany(i => i.Pois.Select(p => p.MediaId))).Where(m => m != null).Distinct().ToList();
|
||||
|
||||
var mediaDir = new DirectoryInfo(PathHelper.MediaPath);
|
||||
var mediaDirs = mediaDir.GetDirectories().ToList();
|
||||
foreach (var dir in mediaDirs)
|
||||
{
|
||||
// HACK to preserve passion media files
|
||||
if (!requiredMedia.Contains(dir.Name) && Regex.IsMatch(dir.Name, "[0-9]"))
|
||||
{
|
||||
Directory.Delete(dir.FullName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFailed(HttpHandler http)
|
||||
{
|
||||
Debug.LogError(String.Format("Error while downloading\nurl: {0}\nNetwork Error: {1}\nHttp Error: {2}\nHttp Response Code: {3}",
|
||||
http.Request.url,
|
||||
http.Request.isNetworkError,
|
||||
http.Request.isHttpError,
|
||||
http.Request.responseCode));
|
||||
|
||||
UpdateState = UpdateState.Failed;
|
||||
}
|
||||
|
||||
public void UpdatesCompleted()
|
||||
{
|
||||
//TODO force switch to UpdaterView when update needs to be applied
|
||||
UpdateState = UpdateState.Completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user