Skip to content

Commit

Permalink
Backup restore (#29)
Browse files Browse the repository at this point in the history
* support for backup & restore
  • Loading branch information
KircMax authored Jan 3, 2023
1 parent 804e2d8 commit 72b2428
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 79 deletions.
4 changes: 4 additions & 0 deletions src/Webserver.API/Enums/ApiErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ public enum ApiErrorCode
/// </summary>
ResourceContentHasBeenCorrupted = 514,
/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
PLCNotInStop = 1004,
/// <summary>
/// The method has not been found by the plc - check the spelling and fw-version (and according methods) of plc
/// </summary>
MethodNotFound = -32601,
Expand Down
7 changes: 6 additions & 1 deletion src/Webserver.API/Enums/ApiTicketProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ public enum ApiTicketProvider
[JsonProperty("Files.Create")]
[EnumMember(Value = "Files.Create")]
Files_Create = 4,

/// <summary>
/// For Api: Plc.CreateBackup
/// </summary>
[JsonProperty("Plc.CreateBackup")]
[EnumMember(Value = "Plc.CreateBackup")]
Plc_CreateBackup = 5,
}


Expand Down
42 changes: 42 additions & 0 deletions src/Webserver.API/Exceptions/ApiPLCNotInStopException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2023, Siemens AG
//
// SPDX-License-Identifier: MIT
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Siemens.Simatic.S7.Webserver.API.Exceptions
{
/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
public class ApiPLCNotInStopException : Exception
{
private static string message = "The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.";
/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference
/// (Nothing in Visual Basic) if no inner exception is specified.</param>
public ApiPLCNotInStopException(Exception innerException) : base(message, innerException) { }
/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
public ApiPLCNotInStopException() : base(message) { }

/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
/// <param name="userMessage">Further information about the error message that explains the reason for the exception.</param>
public ApiPLCNotInStopException(string userMessage) : base(message + Environment.NewLine + userMessage) { }
/// <summary>
/// The PLC is not in operating mode stop. The method cannot be executed while the plc is not in stop mode.
/// </summary>
/// <param name="userMessage">Further information about the error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference
/// (Nothing in Visual Basic) if no inner exception is specified.</param>
public ApiPLCNotInStopException(string userMessage, Exception innerException) : base(message + Environment.NewLine + userMessage, innerException) { }
}
}
2 changes: 2 additions & 0 deletions src/Webserver.API/Models/Responses/ApiErrorModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public void ThrowAccordingException(string apiRequestString, string responseStri
throw new ApiInvalidETagException(new ApiException(this, apiRequestString));
case ApiErrorCode.ResourceContentHasBeenCorrupted:
throw new ApiResourceContentHasBeenCorruptedException(new ApiException(this, apiRequestString));
case ApiErrorCode.PLCNotInStop:
throw new ApiPLCNotInStopException(new ApiException(this, apiRequestString));
case ApiErrorCode.MethodNotFound:
throw new ApiMethodNotFoundException(new ApiException(this, apiRequestString));
case ApiErrorCode.InvalidParams:
Expand Down
28 changes: 16 additions & 12 deletions src/Webserver.API/Services/Backup/ApiBackupHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,36 @@ public ApiBackupHandler(IApiRequestHandler apiRequestHandler, IApiTicketHandler
}

/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// Will send a DownloadbackupName, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="pathToDownloadDirectory">will default to Downloads but will determine path from -DESKTOP-, replaced "Desktop" by "Downloads"</param>
/// <param name="resource">will default to "resource.name</param>
/// <param name="backupName">will default to the backup name suggested by the plc</param>
/// <param name="overwriteExistingFile">choose wether you want to replace an existing file or add another file with that name to you download directory in case one already exists</param>
/// <returns>FileInfo</returns>
/// <exception cref="DirectoryNotFoundException"></exception>
public async Task<FileInfo> DownloadBackupAsync(string pathToDownloadDirectory = null, string resource = null, bool overwriteExistingFile = false)
public async Task<FileInfo> DownloadBackupAsync(string pathToDownloadDirectory = null, string backupName = null, bool overwriteExistingFile = false)
{
if (pathToDownloadDirectory != null && !Directory.Exists(pathToDownloadDirectory))
{
throw new DirectoryNotFoundException($"the given directory at {Environment.NewLine}{pathToDownloadDirectory}{Environment.NewLine} has not been found!");
}
var ticket = (await ApiRequestHandler.PlcCreateBackupAsync()).Result;
return (await ApiTicketHandler.HandleDownloadAsync(ticket, pathToDownloadDirectory)).File_Downloaded;
return (await ApiTicketHandler.HandleDownloadAsync(ticket, pathToDownloadDirectory, backupName, null, overwriteExistingFile)).File_Downloaded;
}

/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// Will send a DownloadbackupName, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="pathToDownloadDirectory">will default to Downloads but will determine path from -DESKTOP-, replaced "Desktop" by "Downloads"</param>
/// <param name="resource">will default to "resource.name</param>
/// <param name="backupName">will default to the backup name suggested by the plc</param>
/// <param name="overwriteExistingFile">choose wether you want to replace an existing file or add another file with that name to you download directory in case one already exists</param>
/// <returns>FileInfo</returns>
/// <exception cref="DirectoryNotFoundException"></exception>
public FileInfo DownloadBackup(string pathToDownloadDirectory = null, string resource = null, bool overwriteExistingFile = false)
=> DownloadBackupAsync(pathToDownloadDirectory, resource).GetAwaiter().GetResult();
public FileInfo DownloadBackup(string pathToDownloadDirectory = null, string backupName = null, bool overwriteExistingFile = false)
=> DownloadBackupAsync(pathToDownloadDirectory, backupName).GetAwaiter().GetResult();

/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// Will send a DownloadbackupName, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="userName">Username for re-login</param>
/// <param name="password">Password for re-login</param>
Expand All @@ -81,27 +81,31 @@ public async Task RestoreBackupAsync(string restoreFilePath, string userName, st
{
throw new FileNotFoundException($"the given file at {Environment.NewLine}{restoreFilePath}{Environment.NewLine} has not been found!");
}
string ticketResponse = (await ApiRequestHandler.PlcRestoreBackupAsync(password)).Result;
string ticketResponse = (await ApiRequestHandler.PlcRestoreBackupAsync(password)).Result;
try
{
await ApiRequestHandler.UploadTicketAsync(ticketResponse, restoreFilePath);
await ApiTicketHandler.HandleUploadAsync(ticketResponse, restoreFilePath);
}
// HttpRequestException is okay since during the upload the plc will power cycle and not "successfully answer" the request
catch (ApiTicketingEndpointUploadException e)
{
if (e.InnerException != null || !(e.InnerException is HttpRequestException))
throw;
}
//var ticket = await ApiRequestHandler.ApiBrowseTicketsAsync();

var waitHandler = new WaitHandler(timeToWait);
Console.WriteLine($"{DateTime.Now}: Wait for plc to not be pingable anymore (reboot).");
WaitForPlcReboot(waitHandler);
await ApiRequestHandler.ReLoginAsync(userName, password);
ticketResponse = (await ApiRequestHandler.PlcRestoreBackupAsync(password)).Result;
await ApiTicketHandler.HandleUploadAsync(ticketResponse, restoreFilePath);
WaitForPlcReboot(waitHandler);
await ApiRequestHandler.ReLoginAsync(userName, password);
}

/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// Will send a DownloadbackupName, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="userName">Username for re-login</param>
/// <param name="password">Password for re-login</param>
Expand Down
8 changes: 4 additions & 4 deletions src/Webserver.API/Services/Backup/IApiBackupHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ public interface IApiBackupHandler
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="pathToDownloadDirectory">will default to Downloads but will determine path from -DESKTOP-, replaced "Desktop" by "Downloads"</param>
/// <param name="resource">will default to "resource.name</param>
/// <param name="backupName">will default to the backup name suggested by the plc</param>
/// <param name="overwriteExistingFile">choose wether you want to replace an existing file or add another file with that name to you download directory in case one already exists</param>
/// <returns>FileInfo</returns>
FileInfo DownloadBackup(string pathToDownloadDirectory = null, string resource = null, bool overwriteExistingFile = false);
FileInfo DownloadBackup(string pathToDownloadDirectory = null, string backupName = null, bool overwriteExistingFile = false);
/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// </summary>
/// <param name="pathToDownloadDirectory">will default to Downloads but will determine path from -DESKTOP-, replaced "Desktop" by "Downloads"</param>
/// <param name="resource">will default to "resource.name</param>
/// <param name="backupName">will default to the backup name suggested by the plc</param>
/// <param name="overwriteExistingFile">choose wether you want to replace an existing file or add another file with that name to you download directory in case one already exists</param>
/// <returns>FileInfo</returns>
Task<FileInfo> DownloadBackupAsync(string pathToDownloadDirectory = null, string resource = null, bool overwriteExistingFile = false);
Task<FileInfo> DownloadBackupAsync(string pathToDownloadDirectory = null, string backupName = null, bool overwriteExistingFile = false);
/// <summary>
/// Will send a Downloadresource, Downloadticket and Closeticket request to the API
/// </summary>
Expand Down
74 changes: 44 additions & 30 deletions src/Webserver.API/Services/Ticketing/ApiTicketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,42 +297,56 @@ public ApiTicket HandleDownload(ApiTicket ticket, FileInfo filePath, bool overwr
/// <exception cref="Exception">File has no content</exception>
public async Task<ApiTicket> HandleDownloadAsync(string ticketId, string pathToDownloadDirectory = null, string fileName = null, string fileExtension = null, bool overwriteExistingFile = false)
{
if (pathToDownloadDirectory != null && !Directory.Exists(pathToDownloadDirectory))
{
throw new DirectoryNotFoundException($"the given directory at {Environment.NewLine}{pathToDownloadDirectory}{Environment.NewLine} has not been found!");
}
var response = await ApiRequestHandler.DownloadTicketAndGetResponseAsync(ticketId);
//Downloads: 374DE290-123F-4565-9164-39C4925E467B
string usedPathToDownloadDirectory = pathToDownloadDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.Desktop).Replace("Desktop", "Downloads");
string usedFilename = fileName ?? response.Content.Headers.ContentDisposition.FileName;
string usedFileExtension = fileExtension ?? "";
var content = await response.Content.ReadAsByteArrayAsync();
string path = Path.Combine(usedPathToDownloadDirectory, usedFilename + usedFileExtension);
uint counter = 0;
var firstPath = path;
while (File.Exists(path) && !overwriteExistingFile)
{
FileInfo fileInfo = new FileInfo(path);
DirectoryInfo dir = fileInfo.Directory;
path = Path.Combine(dir.FullName, (Path.GetFileNameWithoutExtension(firstPath) + "(" + counter + ")" + fileInfo.Extension));
counter++;
}
if (usedFilename.Contains("/"))
var success = false;
try
{
var split = usedFilename.Split('/');
var paths = "";
foreach (var s in split)
if (pathToDownloadDirectory != null && !Directory.Exists(pathToDownloadDirectory))
{
throw new DirectoryNotFoundException($"the given directory at {Environment.NewLine}{pathToDownloadDirectory}{Environment.NewLine} has not been found!");
}
var response = await ApiRequestHandler.DownloadTicketAndGetResponseAsync(ticketId);
//Downloads: 374DE290-123F-4565-9164-39C4925E467B
string usedPathToDownloadDirectory = pathToDownloadDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.Desktop).Replace("Desktop", "Downloads");
var suggestedFileName = response.Content.Headers.ContentDisposition.FileName.Replace("\"", "").Replace("-", "_").Replace(":", "_").Replace(" ", "_");
string usedFilename = fileName ?? suggestedFileName;
string usedFileExtension = fileExtension ?? (Path.HasExtension(usedFilename) ? "" : Path.GetExtension(suggestedFileName));
var content = await response.Content.ReadAsByteArrayAsync();
string path = Path.Combine(usedPathToDownloadDirectory, usedFilename + usedFileExtension);
uint counter = 0;
var firstPath = path;
while (File.Exists(path) && !overwriteExistingFile)
{
FileInfo fileInfo = new FileInfo(path);
DirectoryInfo dir = fileInfo.Directory;
var determinedFileName = $"{Path.GetFileNameWithoutExtension(firstPath)}({counter}){fileInfo.Extension}";
path = Path.Combine(dir.FullName, determinedFileName);
counter++;
}
if (usedFilename.Contains("/"))
{
if (s == split.Last())
continue;
paths += $"\\{s}";
if (!Directory.Exists(usedPathToDownloadDirectory + paths))
var split = usedFilename.Split('/');
var paths = "";
foreach (var s in split)
{
Directory.CreateDirectory(usedPathToDownloadDirectory + paths);
if (s == split.Last())
continue;
paths += $"\\{s}";
if (!Directory.Exists(usedPathToDownloadDirectory + paths))
{
Directory.CreateDirectory(usedPathToDownloadDirectory + paths);
}
}
}
success = true;
return await HandleWriteFileAndCheckAsync(ticketId, content, path, overwriteExistingFile);
}
finally
{
if(!success)
{
await ApiRequestHandler.ApiCloseTicketAsync(ticketId);
}
}
return await HandleWriteFileAndCheckAsync(ticketId, content, path, overwriteExistingFile);
}

/// <summary>
Expand Down
Loading

0 comments on commit 72b2428

Please sign in to comment.