Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(BackupAndRestore): add backup and restore UI #1044

Merged
merged 3 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions TeslaSolarCharger/Client/Components/BackupComponent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
@page "/backupAndRestore"
@using System.Net.Http.Headers

@inject IJSRuntime JsRuntime
@inject ISnackbar Snackbar
@inject HttpClient HttpClient

<h1>Backup and Restore</h1>
<div>During the backup or restore process all TSC actions will be stopped and started again after the Backup</div>

<h2>Backup</h2>

<MudButton Disabled="@(_processingBackup || _processingRestore)" OnClick="StartBackup" Variant="Variant.Filled" Color="Color.Primary">
@if (_processingBackup)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<MudText Class="ms-2">Processing</MudText>
}
else
{
<MudText>Start Backup</MudText>
}
</MudButton>

<hr />
<h2>Restore</h2>

<div class="mb-2">
<MudFileUpload T="IBrowserFile" FilesChanged="SelectFile" Accept=".zip" MaximumFileCount="1">
<ButtonTemplate Context="fileUploadContext">
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.AttachFile"
for="@fileUploadContext">
Select Backup File
</MudButton>
</ButtonTemplate>
</MudFileUpload>
</div>

@if (_file != default)
{
<div class="mb-2">
@_file.Name <code>@((_file.Size * 0.000001).ToString("0.00")) MB</code>
</div>
}

<MudButton Disabled="@(_processingBackup || _processingRestore || _file == default)" OnClick="StartRestore" Variant="Variant.Filled" Color="Color.Primary">
@if (_processingRestore)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<MudText Class="ms-2">Processing</MudText>
}
else
{
<MudText>Start restore</MudText>
}
</MudButton>


@code {
private bool _processingBackup;
private bool _processingRestore;
private readonly long _maxFileSize = 1024 * 1024 * 1024; // 1024 MB

private IBrowserFile? _file;


private async Task StartBackup()
{
_processingBackup = true;
StateHasChanged();
var fileName = "TSCBackup.zip";
var url = "api/BaseConfiguration/DownloadBackup";
// ReSharper disable once UseConfigureAwaitFalse
await JsRuntime.InvokeVoidAsync("triggerFileDownload", fileName, url);
_processingBackup = false;
StateHasChanged();
}

private void SelectFile(IBrowserFile file)
{
if (file.Size > _maxFileSize)
{
Snackbar.Add($"{file.Name} is greater than {_maxFileSize / 1024 / 1024} and won't be uploaded."
, Severity.Error);
}
_file = file;
}

private async Task StartRestore()
{
_processingRestore = true;
var upload = false;
if (_file == default)
{
Snackbar.Add("No file selected", Severity.Error);
return;
}
using var content = new MultipartFormDataContent();
try
{
var fileContent = new StreamContent(_file.OpenReadStream(_maxFileSize));
fileContent.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data");
content.Add(
content: fileContent,
name: "\"file\"",
fileName: _file.Name);
upload = true;
}
catch (Exception e)
{
Snackbar.Add($"Error while uploading file: {e.Message}", Severity.Error);
_processingRestore = false;
return;
}

if (!upload)
{
_processingRestore = false;
return;
}

// ReSharper disable once UseConfigureAwaitFalse
var response = await HttpClient.PostAsync("api/BaseConfiguration/RestoreBackup", content);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Restore complete", Severity.Success);
}
else
{
Snackbar.Add($"Error while restoring backup: {response.ReasonPhrase}", Severity.Error);
}

_processingRestore = false;
}

}
5 changes: 5 additions & 0 deletions TeslaSolarCharger/Client/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<span class="oi oi-cog" aria-hidden="true"></span> Base Configuration
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="backupAndRestore">
<span class="oi oi-data-transfer-download" aria-hidden="true"></span> Backup and Restore
</NavLink>
</div>
</nav>
</div>

Expand Down
2 changes: 1 addition & 1 deletion TeslaSolarCharger/Client/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
</head>
Expand All @@ -32,6 +31,7 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="js/fileDownload.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
Expand Down
7 changes: 7 additions & 0 deletions TeslaSolarCharger/Client/wwwroot/js/fileDownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
window.triggerFileDownload = (fileName, url) => {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public interface IBaseConfigurationService
Task UpdateMaxCombinedCurrent(int? maxCombinedCurrent);
void UpdatePowerBuffer(int powerBuffer);
Task<byte[]> DownloadBackup();
Task RestoreBackup(IFormFile file);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,11 @@ public async Task<FileContentResult> DownloadBackup()
var bytes = await _service.DownloadBackup().ConfigureAwait(false);
return File(bytes, "application/zip", "TSCBackup.zip");
}

[HttpPost]
public async Task RestoreBackup(IFormFile file)
{
await _service.RestoreBackup(file).ConfigureAwait(false);
}
}
}
76 changes: 71 additions & 5 deletions TeslaSolarCharger/Server/Services/BaseConfigurationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
using System.IO;
using System.IO.Compression;
using TeslaSolarCharger.Model.Contracts;
using TeslaSolarCharger.Server.Contracts;
Expand Down Expand Up @@ -82,11 +83,7 @@ public async Task<byte[]> DownloadBackup()
await _jobManager.StopJobs().ConfigureAwait(false);

var backupCopyDestinationDirectory = _configurationWrapper.BackupCopyDestinationDirectory();
if (Directory.Exists(backupCopyDestinationDirectory))
{
Directory.Delete(backupCopyDestinationDirectory, true);
}
Directory.CreateDirectory(backupCopyDestinationDirectory);
CreateEmptyDirectory(backupCopyDestinationDirectory);

//Backup Sqlite database
using (var source = new SqliteConnection(_dbConnectionStringHelper.GetTeslaSolarChargerDbPath()))
Expand Down Expand Up @@ -127,4 +124,73 @@ public async Task<byte[]> DownloadBackup()


}


public async Task RestoreBackup(IFormFile file)
{
_logger.LogTrace("{method}({file})", nameof(RestoreBackup), file.FileName);
try
{
await _jobManager.StopJobs().ConfigureAwait(false);

var restoreTempDirectory = _configurationWrapper.RestoreTempDirectory();
CreateEmptyDirectory(restoreTempDirectory);
var restoreFileName = "TSC-Restore.zip";
var path = Path.Combine(restoreTempDirectory, restoreFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs).ConfigureAwait(false);
fs.Close();
var extractedFilesDirectory = Path.Combine(restoreTempDirectory, "RestoredFiles");
CreateEmptyDirectory(extractedFilesDirectory);
ZipFile.ExtractToDirectory(path, extractedFilesDirectory);
var configFileDirectoryPath = _configurationWrapper.ConfigFileDirectory();
var directoryInfo = new DirectoryInfo(configFileDirectoryPath);
foreach (var fileInfo in directoryInfo.GetFiles())
{
fileInfo.Delete();
}
CopyFiles(extractedFilesDirectory, configFileDirectoryPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Couldn't restore backup");
throw;
}
finally
{
await _jobManager.StartJobs().ConfigureAwait(false);
}
}

private static void CreateEmptyDirectory(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}

Directory.CreateDirectory(path);
}

public void CopyFiles(string sourceDir, string targetDir)
{
// Create the target directory if it doesn't already exist
Directory.CreateDirectory(targetDir);

// Get the files in the source directory
var files = Directory.GetFiles(sourceDir);

foreach (var file in files)
{
// Extract the file name
var fileName = Path.GetFileName(file);

// Combine the target directory with the file name
var targetFilePath = Path.Combine(targetDir, fileName);

// Copy the file
File.Copy(file, targetFilePath, true); // true to overwrite if the file already exists
}
}

}
1 change: 1 addition & 0 deletions TeslaSolarCharger/Server/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"ConfigFileLocation": "configs",
"BackupCopyDestinationDirectory": "backups",
"BackupZipDirectory": "backupZips",
"RestoreTempDirectory": "restores",
"CarConfigFilename": "carConfig.json",
"BaseConfigFileName": "baseConfig.json",
"SqliteFileName": "TeslaSolarCharger.db",
Expand Down
2 changes: 2 additions & 0 deletions TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,6 @@ public interface IConfigurationWrapper
string GetAwattarBaseUrl();
string? GetFleetApiBaseUrl();
bool UseFleetApiProxy();
string RestoreTempDirectory();
string ConfigFileDirectory();
}
11 changes: 10 additions & 1 deletion TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
return Path.Combine(configFileDirectory, value);
}

public string RestoreTempDirectory()
{
var configFileDirectory = ConfigFileDirectory();
var environmentVariableName = "RestoreTempDirectory";
var value = GetNotNullableConfigurationValue<string>(environmentVariableName);
logger.LogTrace("Config value extracted: [{key}]: {value}", environmentVariableName, value);
return Path.Combine(configFileDirectory, value);
}

public string SqliteFileFullName()
{
var configFileDirectory = ConfigFileDirectory();
Expand All @@ -73,7 +82,7 @@
return Path.Combine(configFileDirectory, value);
}

internal string ConfigFileDirectory()
public string ConfigFileDirectory()
{
var environmentVariableName = "ConfigFileLocation";
var value = GetNotNullableConfigurationValue<string>(environmentVariableName);
Expand Down Expand Up @@ -122,7 +131,7 @@
{
var environmentVariableName = "BackendApiBaseUrl";
var value = configuration.GetValue<string>(environmentVariableName);
return value;

Check warning on line 134 in TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs

View workflow job for this annotation

GitHub Actions / Building TeslaSolarCharger image

Possible null reference return.
}

public bool IsDevelopmentEnvironment()
Expand Down
Loading