370 lines
19 KiB
C#
370 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
using JD.LocalizationExport.Service.API;
|
|
using JD.LocalizationExport.Service.Implementation.Exporters;
|
|
using JD.LocalizationExport.Service.Implementation.IntermediaryFile;
|
|
using JD.LocalizationExport.Service.Implementation.Secrets;
|
|
using JD.LocalizationExport.Service.Implementation.Serialization;
|
|
using JD.LocalizationExport.Service.Implementation.Server;
|
|
using JD.LocalizationExport.Service.Model;
|
|
using JD.LocalizationExport.Service.Model.Options;
|
|
using Newtonsoft.Json;
|
|
using Plume.Core.Logging;
|
|
using Plume.Interfaces.Services;
|
|
|
|
namespace JD.LocalizationExport.Service.Implementation
|
|
{
|
|
public class LocalizationExportService : ILocalizationExportService
|
|
{
|
|
private const string URLToCallOnOnlineLocalizationUploadFormat = @"http://backoffice-prod.tools.just-dance.com/Tools/loc/{0}";
|
|
|
|
private static readonly Dictionary<ServiceContentEnvironment, string> EnvironmentServerNames = new Dictionary<ServiceContentEnvironment, string>()
|
|
{
|
|
{ ServiceContentEnvironment.Dev, "dev" },
|
|
{ ServiceContentEnvironment.Prod, "prod" },
|
|
};
|
|
|
|
private ILogger _logger;
|
|
|
|
|
|
// The private part can seem useless, but only way to make the property readonly while initializing it outside of the constructor
|
|
private Dictionary<ExportType, ExportConfig> _exportConfigs;
|
|
public Dictionary<ExportType, ExportConfig> ExportConfigs => _exportConfigs;
|
|
|
|
public ExportEventsWrapper ExportEvents { get; } = new ExportEventsWrapper();
|
|
|
|
public string ConfigPath { get; } = @"Resources/ExportConfig.json";
|
|
|
|
|
|
public void Dispose()
|
|
{
|
|
// Nothing to dispose
|
|
}
|
|
|
|
public void Initialize(IServiceContext serviceContext)
|
|
{
|
|
_logger = serviceContext.Logger ?? throw new ArgumentNullException(nameof(serviceContext.Logger));
|
|
|
|
_logger.Info("Initializing export service...");
|
|
|
|
string fileConfig = File.ReadAllText(ConfigPath);
|
|
FullExportConfiguration config = JsonConvert.DeserializeObject<FullExportConfiguration>(fileConfig);
|
|
|
|
_exportConfigs = config.ExportConfigs;
|
|
|
|
_logger.Info("Export service initialized");
|
|
}
|
|
|
|
|
|
#region ILocalizationExportService implementation
|
|
|
|
public async Task<bool> ExportConsoleLocalizationAsync(DataToExport dataToExport, ConsoleExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
|
|
{
|
|
bool hasExportSucceeded = true;
|
|
|
|
try
|
|
{
|
|
string fullExportFolderPath = Path.GetFullPath(Path.Combine(repositoryRoot, ExportConfigs[ExportType.Console].ExportFolderPath));
|
|
|
|
LocalizationExporter exporter = new ConsoleLocalizationExporter(fullExportFolderPath, repositoryRoot, _logger, ExportEvents);
|
|
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, null).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error(e.Message);
|
|
await Task.FromException(e);
|
|
}
|
|
|
|
return hasExportSucceeded;
|
|
}
|
|
|
|
public async Task<bool> ExportOnlineLocalizationAsync(DataToExport dataToExport, OnlineExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
|
|
{
|
|
bool hasExportSucceeded = true;
|
|
bool isTestExport = false;
|
|
string intermediaryFileAddressFormat = ExportConfigs[ExportType.Online].SaveFilePath;
|
|
|
|
var outputPath = Path.Combine(Path.GetTempPath(), "localisation.js"); // TODO: return exported files from ExportAsync to get them in the onLocalisationExportSuccess callback
|
|
|
|
try
|
|
{
|
|
LocalizationExporter exporter = new OnlineLocalizationExporter(Path.GetTempPath(), _logger, ExportEvents);
|
|
bool uploadToDevelopmentServers = exportOptions.UploadToDevServers;
|
|
bool uploadToProductionServers = exportOptions.UploadToProdServers;
|
|
bool forceUpload = exportOptions.ForceUpload;
|
|
|
|
List<ServiceContentEnvironment> serversToExportTo = new List<ServiceContentEnvironment>(Enum.GetNames(typeof(ServiceContentEnvironment)).Length);
|
|
if (uploadToDevelopmentServers)
|
|
{
|
|
serversToExportTo.Add(ServiceContentEnvironment.Dev);
|
|
}
|
|
if (uploadToProductionServers)
|
|
{
|
|
// Ask if the user is certain to export to avoid accidental exports on prod
|
|
System.Windows.MessageBoxResult answer = System.Windows.MessageBox.Show($"Are you sure you want to export on prod? {Environment.NewLine}" +
|
|
$"{Environment.NewLine}" +
|
|
$"This will change the texts seen by current players.",
|
|
"Export on production server",
|
|
System.Windows.MessageBoxButton.OKCancel,
|
|
System.Windows.MessageBoxImage.Warning);
|
|
if (answer != System.Windows.MessageBoxResult.OK)
|
|
{
|
|
string exportCanceledMessage = "Online localisation export canceled";
|
|
System.Windows.MessageBox.Show(exportCanceledMessage,
|
|
"Export canceled",
|
|
System.Windows.MessageBoxButton.OK,
|
|
System.Windows.MessageBoxImage.Error);
|
|
throw new Exception(exportCanceledMessage);
|
|
}
|
|
|
|
serversToExportTo.Add(ServiceContentEnvironment.Prod);
|
|
}
|
|
|
|
if (serversToExportTo.Count == 0)
|
|
{
|
|
_logger.Warn("No upload type selected: making a test export without uploading it");
|
|
isTestExport = true;
|
|
serversToExportTo.Add(ServiceContentEnvironment.Dev);
|
|
}
|
|
|
|
foreach (ServiceContentEnvironment serverType in serversToExportTo)
|
|
{
|
|
// Updating intermediary file address
|
|
ExportConfigs[ExportType.Online].SaveFilePath = string.Format(intermediaryFileAddressFormat, EnvironmentServerNames[serverType]);
|
|
|
|
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, async () =>
|
|
{
|
|
bool hasUploadSucceeded = true;
|
|
|
|
if (!isTestExport && (uploadToDevelopmentServers || uploadToProductionServers)) // Double-check here so that we can never export by mistake
|
|
{
|
|
void LogInfo(string message)
|
|
{
|
|
_logger.Info(message);
|
|
}
|
|
|
|
_logger.Info("Getting credentials to connect to online server");
|
|
var secretsManager = await SecretsManagement.GetSecretsManager(_logger);
|
|
var configuration = await secretsManager.GetOnlineConfiguration();
|
|
_logger.Info("Successfully retrieved credentials to connect to online server");
|
|
|
|
var client = new GameServerClient();
|
|
ServerName serverName = serverType == ServiceContentEnvironment.Prod ? ServerName.Prod : ServerName.Uat;
|
|
await client.InitializeAsync(serverName, LogInfo, configuration);
|
|
|
|
async Task OnLocalizationUploadAsync(ServiceContentEnvironment s)
|
|
{
|
|
string server = EnvironmentServerNames[s];
|
|
string urlToCallOnLocalizationUpload = string.Format(URLToCallOnOnlineLocalizationUploadFormat, server);
|
|
|
|
try
|
|
{
|
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlToCallOnLocalizationUpload);
|
|
HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
|
|
if (response.StatusCode == HttpStatusCode.OK)
|
|
{
|
|
_logger.Info("Successfully updated localization history on GitLab");
|
|
}
|
|
else
|
|
{
|
|
_logger.Error($"Attempt to update localization history failed with status code {response.StatusCode}");
|
|
hasUploadSucceeded = false;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error("Exception during request on localization upload, please contact a backoffice or tools developer");
|
|
_logger.Error(e.Message);
|
|
|
|
hasUploadSucceeded = false;
|
|
}
|
|
}
|
|
|
|
IProgress<double> onlineUploadProgress = new Progress<double>(d =>
|
|
{
|
|
int progressValue = (int)(100d * d);
|
|
progress.Report(Tuple.Create($"Online upload: {progressValue}%", progressValue));
|
|
});
|
|
|
|
// Upload
|
|
_logger.Info($"Uploading localization on UAT-{EnvironmentServerNames[serverType]}");
|
|
await client.UploadLocalizationAsync(outputPath,
|
|
serverType,
|
|
forceUpload,
|
|
LogInfo,
|
|
onlineUploadProgress,
|
|
async () => await OnLocalizationUploadAsync(serverType).ConfigureAwait(false))
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
return hasUploadSucceeded;
|
|
}, isTestExport).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error(e.Message);
|
|
await Task.FromException(e);
|
|
}
|
|
|
|
return hasExportSucceeded;
|
|
}
|
|
|
|
public async Task<bool> ExportMCALocalizationAsync(DataToExport dataToExport, MCAExportOptions exportOptions, string repositoryRoot, IProgress<Tuple<string, int>> progress)
|
|
{
|
|
bool hasExportSucceeded = true;
|
|
|
|
try
|
|
{
|
|
string fullExportFolderPath = Path.GetFullPath(Path.Combine(repositoryRoot, ExportConfigs[ExportType.MCA].ExportFolderPath));
|
|
|
|
LocalizationExporter exporter = new MCALocalizationExporter(fullExportFolderPath, _logger, ExportEvents);
|
|
hasExportSucceeded &= await ExportLocalizationAsync(exporter, dataToExport, repositoryRoot, progress, null).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error(e.Message);
|
|
await Task.FromException(e);
|
|
}
|
|
|
|
return hasExportSucceeded;
|
|
}
|
|
|
|
#endregion
|
|
|
|
private async Task<bool> ExportLocalizationAsync(LocalizationExporter exporter,
|
|
DataToExport dataToExport,
|
|
string repositoryRoot,
|
|
IProgress<Tuple<string, int>> progress,
|
|
Func<Task<bool>> onLocalisationExportSuccess,
|
|
bool isTestExport = false) // TODO: Would be better with a debug option for the export
|
|
{
|
|
bool hasExportSucceeded = true;
|
|
|
|
if (dataToExport == null)
|
|
throw new ArgumentNullException(nameof(dataToExport));
|
|
if (progress == null)
|
|
throw new ArgumentNullException(nameof(progress));
|
|
|
|
try
|
|
{
|
|
Line[] fullLinesToExport = { };
|
|
Language[] fullLanguagesToExport = { };
|
|
Language[] minLanguagesToExport = dataToExport.LanguagesToExport.ToArray();
|
|
|
|
ExportType currentExportType = exporter.ExportType;
|
|
ExportConfig currentConfig = ExportConfigs[currentExportType];
|
|
|
|
|
|
await exporter.ExportEvents.InvokeExportStartedAsync(currentExportType);
|
|
|
|
string locIntermediaryFile;
|
|
if (Uri.TryCreate(currentConfig.SaveFilePath, UriKind.Absolute, out Uri locIntermediaryFileUri))
|
|
{
|
|
if (locIntermediaryFileUri.Scheme == Uri.UriSchemeHttp || locIntermediaryFileUri.Scheme == Uri.UriSchemeHttps)
|
|
{
|
|
locIntermediaryFile = Path.Combine(Path.GetTempPath(), $"{currentExportType}Save.json");
|
|
await exporter.ExportEvents.InvokeExportFileStartedAsync(currentExportType, locIntermediaryFile, locIntermediaryFileUri);
|
|
}
|
|
else
|
|
{
|
|
throw new UriFormatException($"The path '{currentConfig.SaveFilePath}' is not a relative file path or an URL. Fix it in the configuration {ConfigPath}.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
locIntermediaryFile = Path.Combine(repositoryRoot, currentConfig.ExportFolderPath, currentConfig.SaveFilePath);
|
|
await exporter.ExportEvents.InvokeExportFileStartedAsync(currentExportType, locIntermediaryFile);
|
|
}
|
|
|
|
IIntermediaryFileManager intermediaryFileManager = new IntermediaryFileManager(locIntermediaryFile);
|
|
if (File.Exists(locIntermediaryFile))
|
|
{
|
|
fullLinesToExport = dataToExport.LinesToExport.ToArray();
|
|
fullLanguagesToExport = minLanguagesToExport;
|
|
|
|
_logger.Info("Loading previous export info...");
|
|
|
|
Tuple<Line[], Language[]> updatedExportData = await intermediaryFileManager.LoadFileAndMergeAsync(fullLinesToExport, fullLanguagesToExport, dataToExport.AllLanguages)
|
|
.ConfigureAwait(false);
|
|
fullLinesToExport = updatedExportData.Item1;
|
|
fullLanguagesToExport = updatedExportData.Item2;
|
|
}
|
|
else
|
|
{
|
|
// Need to export all lines in all languages to get a starting point for the intermediary file
|
|
System.Windows.MessageBoxResult answer = System.Windows.MessageBox.Show($"The {currentExportType.ToString()} localisation save file is missing. {Environment.NewLine}" +
|
|
$"Expected file: {Path.GetFullPath(locIntermediaryFile)} {Environment.NewLine}" +
|
|
$"{Environment.NewLine}" +
|
|
$"Without it, impossible to export only some lines or some languages. {Environment.NewLine}" +
|
|
$"{Environment.NewLine}" +
|
|
$"Export all lines in all languages?",
|
|
"Export all lines",
|
|
System.Windows.MessageBoxButton.OKCancel,
|
|
System.Windows.MessageBoxImage.Question);
|
|
// TODO: Fix freeze just after this popup
|
|
|
|
if (answer == System.Windows.MessageBoxResult.OK)
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(locIntermediaryFile)); // Creating folder hierarchy if it doesn't exist
|
|
|
|
fullLinesToExport = dataToExport.AllLines.ToArray();
|
|
fullLanguagesToExport = dataToExport.AllLanguages.ToArray();
|
|
}
|
|
else
|
|
{
|
|
_logger.Error("No save file available, export aborted.");
|
|
hasExportSucceeded = false;
|
|
}
|
|
}
|
|
|
|
if (hasExportSucceeded)
|
|
{
|
|
_logger.Info("Beginning export...");
|
|
|
|
await exporter.ExportAsync(fullLinesToExport, fullLanguagesToExport, minLanguagesToExport).ConfigureAwait(false);
|
|
|
|
if (onLocalisationExportSuccess != null)
|
|
{
|
|
hasExportSucceeded &= await onLocalisationExportSuccess().ConfigureAwait(false);
|
|
}
|
|
|
|
if (hasExportSucceeded)
|
|
{
|
|
_logger.Info("Saving export info...");
|
|
|
|
await intermediaryFileManager.SaveFileAsync(fullLinesToExport, fullLanguagesToExport).ConfigureAwait(false);
|
|
|
|
_logger.Info("Saved");
|
|
|
|
if (locIntermediaryFileUri != null)
|
|
{
|
|
// Avoid permanent modification of remote data just for testing purposes
|
|
if (!isTestExport)
|
|
{
|
|
await exporter.ExportEvents.InvokeExportFileEndedAsync(currentExportType, locIntermediaryFile, locIntermediaryFileUri);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await exporter.ExportEvents.InvokeExportFileEndedAsync(currentExportType, locIntermediaryFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
await exporter.ExportEvents.InvokeExportEndedAsync(currentExportType);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.Error(e.Message);
|
|
await Task.FromException(e);
|
|
}
|
|
|
|
return hasExportSucceeded;
|
|
}
|
|
}
|
|
}
|