OneDrive Backup

OneDrive Backup
This commit is contained in:
2023-07-28 22:36:06 +07:00
parent e1c6e91a0a
commit d70d81ebdf
244 changed files with 75229 additions and 0 deletions
+546
View File
@@ -0,0 +1,546 @@
using hassio_onedrive_backup.Contracts;
using hassio_onedrive_backup.Graph;
using Microsoft.Graph;
using Newtonsoft.Json;
using onedrive_backup.Graph;
using onedrive_backup.Hass;
using System.Collections;
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
namespace hassio_onedrive_backup.Hass
{
public class BackupManager
{
private const int InstanceNameMaxLength = 20;
private readonly HassOnedriveEntityState _hassEntityState;
private readonly TransferSpeedHelper? _transferSpeedHelper;
private readonly HassContext _hassContext;
private AddonOptions _addonOptions;
private IGraphHelper _graphHelper;
private IHassioClient _hassIoClient;
private BitArray _allowedHours;
private bool _isExecuting = false;
public List<Backup> LocalBackups { get; private set; }
public List<OnedriveBackup> OnlineBackups { get; private set; }
public BackupManager(IServiceProvider serviceProvider, BitArray allowedHours, TransferSpeedHelper? transferSpeedHelper)
{
_addonOptions = serviceProvider.GetService<AddonOptions>();
_graphHelper = serviceProvider.GetService<IGraphHelper>();
_hassIoClient = serviceProvider.GetService<IHassioClient>();
_hassEntityState = serviceProvider.GetService<HassOnedriveEntityState>();
_transferSpeedHelper = transferSpeedHelper;
_hassContext = serviceProvider.GetService<HassContext>();
_allowedHours = allowedHours;
}
public event Action? LocalBackupsUpdated;
public event Action? OneDriveBackupsUpdated;
public async Task PerformBackupsAsync()
{
if (_isExecuting)
{
ConsoleLogger.LogVerbose("Previous backup iteration still executing. Skipping...");
return;
}
try
{
_isExecuting = true;
await UpdateHassEntity();
var now = DateTimeHelper.Instance!.Now;
// Get existing local backups
ConsoleLogger.LogVerbose("Retrieving existing local backups...");
var localBackups = await GetLocalBackups();
// Get existing online backups
ConsoleLogger.LogVerbose("Retrieving existing online backups...");
var onlineBackups = await GetOnlineBackupsAsync(_addonOptions.InstanceName);
DateTime lastLocalBackupTime = localBackups.Any() ? localBackups.Max(backup => backup.Date) : DateTime.MinValue;
ConsoleLogger.LogVerbose($"Last local backup Date: {(lastLocalBackupTime == DateTime.MinValue ? "None" : lastLocalBackupTime)}");
DateTime lastOnlineBackupTime = onlineBackups.Any() ? onlineBackups.Max(backup => backup.BackupDate) : DateTime.MinValue;
ConsoleLogger.LogVerbose($"Last online backup Date: {(lastOnlineBackupTime == DateTime.MinValue ? "None" : lastOnlineBackupTime)}");
// Create local backups if needed
if ((now - lastLocalBackupTime).TotalHours >= _addonOptions.BackupIntervalHours && (now - lastOnlineBackupTime).TotalHours >= _addonOptions.BackupIntervalHours)
{
if (_allowedHours[now.Hour] == false)
{
ConsoleLogger.LogWarning("Not performing backup outside allowed times");
}
else
{
_hassEntityState.State = HassOnedriveEntityState.BackupState.Syncing;
await _hassEntityState.UpdateBackupEntityInHass();
await CreateLocalBackup();
}
}
// Get Online backup candidates
var onlineBackupCandiates = await GetOnlineBackupCandidatesAsync(localBackups);
var uploadCandidates = onlineBackupCandiates
.Select(bc => new { Slug = bc.Slug, Date = bc.Date }).Union(onlineBackups.Select(ob => new { Slug = ob.Slug, Date = ob.BackupDate }))
.OrderByDescending(backup => backup.Date)
.Take(_addonOptions.MaxOnedriveBackups).ToList();
// Get Online Backups Candidates that have not yet been uploaded
var backupsToUpload = new List<Backup>();
foreach (var backupId in uploadCandidates)
{
if (onlineBackups.Any(ob => ob.Slug == backupId.Slug))
{
continue;
}
backupsToUpload.Add(onlineBackupCandiates.Single(bc => bc.Slug == backupId.Slug));
}
// Upload backups
if (backupsToUpload.Any())
{
ConsoleLogger.LogInfo($"Found {backupsToUpload.Count()} backups to upload.");
// Set Home Assistant Entity state to Syncing
_hassEntityState.State = HassOnedriveEntityState.BackupState.Syncing;
await _hassEntityState.UpdateBackupEntityInHass();
foreach (var backup in backupsToUpload)
{
await UploadLocalBackupToOneDrive(backup);
}
}
else
{
ConsoleLogger.LogVerbose("Online backups synced. No upload required");
}
// Refresh Online Backups
onlineBackups = await GetOnlineBackupsAsync(_addonOptions.InstanceName);
int numOfOnlineBackupsToDelete = Math.Max(0, onlineBackups.Count - _addonOptions.MaxOnedriveBackups);
// Delete Old Online Backups
var backupsToDelete = onlineBackups
.OrderBy(onlineBackup => onlineBackup.BackupDate)
.Take(numOfOnlineBackupsToDelete)
.ToList();
if (backupsToDelete.Any())
{
_hassEntityState.State = HassOnedriveEntityState.BackupState.Syncing;
await _hassEntityState.UpdateBackupEntityInHass();
ConsoleLogger.LogInfo($"Found {backupsToDelete.Count()} backups to delete from OneDrive.");
foreach (var backupToDelete in backupsToDelete)
{
bool deleteSuccessfull = await _graphHelper.DeleteItemFromAppFolderAsync(backupToDelete.FileName);
if (deleteSuccessfull == false)
{
await _hassIoClient.PublishEventAsync(Events.OneDriveEvents.OneDriveBackupDeleteFailed);
if (_addonOptions.NotifyOnError)
{
await _hassIoClient.SendPersistentNotificationAsync("Failed deleting old backup from OneDrive. Check Addon logs for more details");
}
}
}
}
// Delete Old Local Backups
if (localBackups.Count > _addonOptions.MaxLocalBackups)
{
_hassEntityState.State = HassOnedriveEntityState.BackupState.Syncing;
await _hassEntityState.UpdateBackupEntityInHass();
int numOfLocalBackupsToRemove = localBackups.Count - _addonOptions.MaxLocalBackups;
var localBackupsToRemove = localBackups
.OrderBy(backup => backup.Date)
.Take(numOfLocalBackupsToRemove)
.ToList();
ConsoleLogger.LogInfo($"Removing {numOfLocalBackupsToRemove} local backups");
foreach (var localBackup in localBackupsToRemove)
{
bool deleteSuccess = await _hassIoClient.DeleteBackupAsync(localBackup);
if (deleteSuccess == false)
{
await _hassIoClient.PublishEventAsync(Events.OneDriveEvents.LocalBackupDeleteFailed);
if (_addonOptions.NotifyOnError)
{
await _hassIoClient.SendPersistentNotificationAsync("Error Deleting Local Backup. Check Addon logs for more details");
}
}
}
}
await UpdateHassEntity();
}
finally
{
_isExecuting = false;
}
}
public async Task<bool> CreateLocalBackup()
{
List<string>? addons = null;
List<string>? folders = null;
ConsoleLogger.LogInfo($"Creating new backup");
if (_addonOptions.IsPartialBackup)
{
addons = _hassContext.Addons
.Where(addon => _addonOptions.ExcludedAddons.Any(excludedAddon => excludedAddon.Equals(addon.Slug, StringComparison.OrdinalIgnoreCase)) == false)
.Select(addon => addon.Slug)
.ToList();
folders = _addonOptions.IncludedFolderList;
}
bool backupCreated = await _hassIoClient.CreateBackupAsync(
_addonOptions.BackupNameSafe,
compressed: true,
password: String.IsNullOrEmpty(_addonOptions.BackupPassword) ? null : _addonOptions.BackupPassword,
addons: addons,
folders: folders);
if (backupCreated == false)
{
await _hassIoClient.PublishEventAsync(Events.OneDriveEvents.BackupCreateFailed);
if (_addonOptions.NotifyOnError)
{
await _hassIoClient.SendPersistentNotificationAsync("Failed creating local backup. Check Addon logs for more details");
}
}
else
{
LocalBackups = await GetLocalBackups();
}
return backupCreated;
}
public async Task<bool> UploadLocalBackupToOneDrive(Backup backup, Action<int?, int?>? progressCallback = null, bool updateHassEntityState = true)
{
string? tempBackupFilePath = null;
try
{
ConsoleLogger.LogInfo($"Uploading {backup.Name} ({backup.Date})");
string? instanceSuffix = _addonOptions.InstanceName == null ? null : $".{_addonOptions.InstanceName.Substring(0, Math.Min(InstanceNameMaxLength, _addonOptions.InstanceName.Length))}";
string destinationFileName = $"{backup.Name}{instanceSuffix}.tar";
tempBackupFilePath = await _hassIoClient.DownloadBackupAsync(backup.Slug);
var uploadSuccessful = await _graphHelper.UploadFileAsync(tempBackupFilePath, backup.Date, _addonOptions.InstanceName, _transferSpeedHelper, destinationFileName,
async (prog, speed) =>
{
if (updateHassEntityState)
{
_hassEntityState.UploadPercentage = prog;
_hassEntityState.UploadSpeed = speed / 1024;
await _hassEntityState.UpdateBackupEntityInHass();
}
progressCallback?.Invoke(prog, speed);
},
description: SerializeBackupDescription(tempBackupFilePath, backup)
);
if (uploadSuccessful == false)
{
await _hassIoClient.PublishEventAsync(Events.OneDriveEvents.BackupUploadFailed);
if (_addonOptions.NotifyOnError)
{
await _hassIoClient.SendPersistentNotificationAsync("Failed uploading backup to onedrive. Check Addon logs for more details");
}
}
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Error uploading backup: {ex}");
return false;
}
finally
{
// Delete temporary backup file
if (tempBackupFilePath != null)
{
System.IO.File.Delete(tempBackupFilePath);
}
}
return true;
}
public async Task DownloadCloudBackupsAsync()
{
_hassEntityState.State = HassOnedriveEntityState.BackupState.RecoveryMode;
await _hassEntityState.UpdateBackupEntityInHass();
var onlineBackups = await GetOnlineBackupsAsync("*");
var onlineInstanceBackups = onlineBackups.Where(backup => string.Equals(backup.InstanceName, _addonOptions.InstanceName, StringComparison.OrdinalIgnoreCase)).ToList();
var localBackups = await GetLocalBackups();
if (onlineInstanceBackups.Count > 0)
{
ConsoleLogger.LogInfo($"Found {onlineInstanceBackups.Count} matching backups in OneDrive");
}
else if (onlineBackups.Count > 0)
{
var instanceNames = onlineBackups.Select(backup => backup.InstanceName ?? "*NoInstance*").Distinct();
ConsoleLogger.LogInfo($"Found backups belonging to other instances: {string.Join(',', instanceNames)}. If you would like to use another instance backup please update the addon configuration and set the appropriate instance name");
return;
}
else
{
ConsoleLogger.LogWarning($"No backups found in OneDrive");
return;
}
var localBackupNum = localBackups.Count;
int numberOfBackupsToDownload = Math.Max(0, _addonOptions.MaxLocalBackups - localBackupNum);
if (numberOfBackupsToDownload == 0)
{
ConsoleLogger.LogWarning(
$"Local backups at maximum configured number ({_addonOptions.MaxLocalBackups}). To sync additional backups from OneDrive either delete some local backups or increase the configured maximum");
return;
}
var backupsToDownload = onlineInstanceBackups
.OrderByDescending(backup => backup.BackupDate)
.Where(backup => localBackups.Any(local => local.Slug.Equals(backup.Slug, StringComparison.OrdinalIgnoreCase)) == false)
.Take(numberOfBackupsToDownload)
.ToList();
if (backupsToDownload.Count == 0)
{
ConsoleLogger.LogInfo($"All {Math.Min(numberOfBackupsToDownload, onlineInstanceBackups.Count)} latest backups already exist locally");
}
foreach (var onlineBackup in backupsToDownload)
{
await DownloadBackupFromOneDrive(onlineBackup);
}
}
public async Task<bool> DownloadBackupFromOneDrive(OnedriveBackup onlineBackup, Action<int?>? progressCallback = null, bool updateHassEntityState = true)
{
string? backupFile = null;
try
{
ConsoleLogger.LogInfo($"Downloading backup {onlineBackup.FileName}");
backupFile = await _graphHelper.DownloadFileAsync(onlineBackup.FileName, async (prog) =>
{
if (updateHassEntityState)
{
_hassEntityState.DownloadPercentage = prog;
await _hassEntityState.UpdateBackupEntityInHass();
}
progressCallback?.Invoke(prog);
});
if (backupFile == null)
{
ConsoleLogger.LogError($"Error downloading backup {onlineBackup.FileName}");
return false;
}
// Upload backup to Home Assistant
ConsoleLogger.LogInfo($"Loading backup {onlineBackup.FileName} to Home Assisant");
await _hassIoClient.UploadBackupAsync(backupFile);
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Error fetching backup {onlineBackup.FileName} from Onedrive to Home Assistant. {ex}");
return false;
}
finally
{
if (backupFile != null)
{
System.IO.File.Delete(backupFile);
}
}
return true;
}
private string SerializeBackupDescription(string originalFileName, Backup backup)
{
var description = new OnedriveItemDescription
{
Slug = Path.GetFileNameWithoutExtension(originalFileName),
BackupDate = backup.Date,
InstanceName = _addonOptions.InstanceName,
BackupType = backup.Type,
IsProtected = backup.Protected,
Size = backup.Size,
Addons = Enumerable.Empty<string>(), //Temporary workaround for size issue
Folders = backup.Content?.Folders ?? Enumerable.Empty<string>()
};
string serializedDesc = JsonConvert.SerializeObject(description);
ConsoleLogger.LogVerbose($"Backup Description: {serializedDesc}");
return serializedDesc;
}
private async Task UpdateHassEntity()
{
var now = DateTimeHelper.Instance!.Now;
var localBackups = await GetLocalBackups();
var onlineBackups = await GetOnlineBackupsAsync(_addonOptions.InstanceName);
_hassEntityState.BackupsInHomeAssistant = localBackups.Count;
_hassEntityState.BackupsInOnedrive = onlineBackups.Count;
_hassEntityState.LastLocalBackupDate = localBackups.Any() ? localBackups.Max(backup => backup.Date) : null;
_hassEntityState.LastOnedriveBackupDate = onlineBackups.Any() ? onlineBackups.Max(backup => backup.BackupDate) : null;
bool onedriveSynced = false;
bool localSynced = false;
if (_hassEntityState.LastOnedriveBackupDate != null && (now - _hassEntityState.LastOnedriveBackupDate.Value).TotalHours <= _addonOptions.BackupIntervalHours)
{
onedriveSynced = true;
}
if (_hassEntityState.LastLocalBackupDate != null && (now - _hassEntityState.LastLocalBackupDate.Value).TotalHours <= _addonOptions.BackupIntervalHours)
{
localSynced = true;
}
HassOnedriveEntityState.BackupState state = HassOnedriveEntityState.BackupState.Unknown;
if (onedriveSynced && localSynced)
{
state = HassOnedriveEntityState.BackupState.Backed_Up;
}
else if (onedriveSynced)
{
state = HassOnedriveEntityState.BackupState.Backed_Up_Onedrive;
}
else if (localSynced)
{
state = HassOnedriveEntityState.BackupState.Backed_Up_Local;
}
else
{
state = HassOnedriveEntityState.BackupState.Stale;
}
_hassEntityState.State = state;
await _hassEntityState.UpdateBackupEntityInHass();
}
public async Task RefreshBackupData()
{
var getLocalBackupsTask = GetLocalBackups();
var getOneDriveBackupTasks = GetOnlineBackupsAsync(_addonOptions.InstanceName);
await Task.WhenAll(getLocalBackupsTask, getOneDriveBackupTasks);
}
public async Task<List<OnedriveBackup>> GetOnlineBackupsAsync(string? instanceName)
{
var onlineBackups = (await _graphHelper.GetItemsInAppFolderAsync()).Select(CheckIfFileIsBackup).ToList();
onlineBackups.RemoveAll(item => item == null);
if (instanceName != "*")
{
onlineBackups = onlineBackups.Where(item => string.Equals(instanceName, item.InstanceName, StringComparison.OrdinalIgnoreCase)).ToList();
}
OnlineBackups = onlineBackups;
OneDriveBackupsUpdated?.Invoke();
return onlineBackups;
}
private OnedriveBackup? CheckIfFileIsBackup(DriveItem item)
{
OnedriveBackup ret = null;
if (item.Folder != null && item.Name.Equals("FileSync"))
{
return ret;
}
try
{
ret = new OnedriveBackup(item.Name, JsonConvert.DeserializeObject<OnedriveItemDescription>(item.Description)!);
}
catch (Exception ex)
{
ConsoleLogger.LogWarning($"Unrecognized file found in backup folder : {item.Name}");
}
return ret;
}
private Task<List<Backup>> GetOnlineBackupCandidatesAsync(IEnumerable<Backup> localBackups)
{
var filteredLocalBackups = localBackups
.Where(backup => backup.Compressed)
.OrderByDescending(backup => backup.Date);
return Task.FromResult(filteredLocalBackups.ToList());
}
public async Task<List<Backup>> GetLocalBackups()
{
var ret = await _hassIoClient.GetBackupsAsync(IsMonitoredBackup);
LocalBackups = ret;
LocalBackupsUpdated?.Invoke();
return ret;
}
private bool IsMonitoredBackup(Backup backup)
{
// Monitoring All Backups
if (_addonOptions.MonitorAllLocalBackups)
{
// If should ignore upgrade backups and backup seems like an upgrade backup skip it
if (_addonOptions.IgnoreUpgradeBackups && IsUpgradeBackup(backup))
{
ConsoleLogger.LogVerbose($"Ignoring Upgrade Backup: {backup.Name}");
return false;
}
// Otherwise back it up
return true;
}
// If addon backup always back up
if (backup.Name.StartsWith(_addonOptions.BackupNameSafe, StringComparison.OrdinalIgnoreCase))
{
return true;
}
else
{
ConsoleLogger.LogVerbose($"Ignoring 'External' backup: {backup.Name}");
return false;
}
}
private bool IsUpgradeBackup(Backup backup)
{
// Check if addon upgrade backup
if (backup.Name.StartsWith("addon_", StringComparison.OrdinalIgnoreCase))
{
if (backup.Content?.Homeassistant != true && backup.Content?.Addons?.Count() == 1)
{
ConsoleLogger.LogVerbose($"Backup {backup.Name} detected as Addon auto upgrade backup");
return true;
}
}
// check if core upgrade backup
if (backup.Name.StartsWith("core_", StringComparison.OrdinalIgnoreCase))
{
if (backup.Content?.Homeassistant == true && backup.Content?.Addons.Count() == 0)
{
ConsoleLogger.LogVerbose($"Backup {backup.Name} detected as Home Assistant auto upgrade backup");
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,10 @@
namespace hassio_onedrive_backup.Hass.Events
{
public enum OneDriveEvents
{
BackupCreateFailed,
BackupUploadFailed,
OneDriveBackupDeleteFailed,
LocalBackupDeleteFailed,
}
}
+14
View File
@@ -0,0 +1,14 @@
using Microsoft.Graph;
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
namespace onedrive_backup.Hass
{
public class HassContext
{
public string IngressUrl { get; set; }
public string HeaderIngressPath { get; set; }
public IEnumerable<Addon> Addons { get; set; }
}
}
@@ -0,0 +1,109 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.ComponentModel;
namespace hassio_onedrive_backup.Hass
{
internal class HassOnedriveEntityState : INotifyPropertyChanged
{
private const string OneDrive_Backup_Entity_ID = "sensor.onedrivebackup";
private IHassioClient? _hassioClient;
private BackupState state;
private List<JsonConverter> entityStateConverters = new List<JsonConverter>
{
new StringEnumConverter()
};
public event PropertyChangedEventHandler? PropertyChanged;
public HassOnedriveEntityState(IHassioClient hassioClient)
{
_hassioClient = hassioClient;
State = BackupState.Unknown;
}
[JsonConverter(typeof(StringEnumConverter))]
public BackupState State
{
get => state;
set
{
state = value;
if (state != BackupState.Syncing)
{
UploadPercentage = null;
UploadSpeed = null;
}
}
}
public DateTime? LastLocalBackupDate { get; set; }
public DateTime? LastOnedriveBackupDate { get; set; }
public int BackupsInHomeAssistant { get; set; }
public int BackupsInOnedrive { get; set; }
public int? UploadPercentage { get; set; }
public int? DownloadPercentage { get; set; }
// KB/s
public int? UploadSpeed { get; set; }
public async Task UpdateBackupEntityInHass()
{
var payload = new
{
state = State,
attributes = new Dictionary<string, string?>
{
{ BackupStateAttribute.LastLocalBackupDate, LastLocalBackupDate?.ToString(DateTimeHelper.DateTimeFormat) },
{ BackupStateAttribute.LastOnedriveBackupDate, LastOnedriveBackupDate?.ToString(DateTimeHelper.DateTimeFormat) },
{ BackupStateAttribute.BackupsInHomeAssistant, BackupsInHomeAssistant.ToString() },
{ BackupStateAttribute.BackupsInOnedrive, BackupsInOnedrive.ToString() },
{ BackupStateAttribute.UploadPercentage, UploadPercentage == null ? null : $"{UploadPercentage}%" },
{ BackupStateAttribute.UploadSpeed, UploadSpeed== null ? null : $"{UploadSpeed} KB/s" },
{ BackupStateAttribute.DownloadPercentage, DownloadPercentage == null ? null : $"{DownloadPercentage}%" }
}
};
string payloadStr = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Converters = entityStateConverters
});
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
await _hassioClient.UpdateHassEntityStateAsync(OneDrive_Backup_Entity_ID, payloadStr);
}
public class BackupStateAttribute
{
public const string LastLocalBackupDate = "Last Local backup date";
public const string LastOnedriveBackupDate = "Last OneDrive backup date";
public const string BackupsInHomeAssistant = "Backups in Home Assistant";
public const string BackupsInOnedrive = "Backups in OneDrive";
public const string UploadPercentage = "Current backup upload percentage";
public const string DownloadPercentage = "Backup download percentage";
public const string UploadSpeed = "Current backup upload speed (KB/s)";
}
public enum BackupState
{
Syncing,
Backed_Up_Local,
Backed_Up_Onedrive,
Backed_Up,
Stale,
RecoveryMode,
Unknown,
}
}
}
@@ -0,0 +1,82 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.ComponentModel;
namespace hassio_onedrive_backup.Hass
{
internal class HassOnedriveFileSyncEntityState : INotifyPropertyChanged
{
private const string OneDrive_FileSync_Entity_ID = "sensor.onedrivefilesync";
private IHassioClient? _hassioClient;
private static HassOnedriveFileSyncEntityState? _instance;
private FileState state;
private List<JsonConverter> entityStateConverters = new List<JsonConverter>
{
new StringEnumConverter()
};
public event PropertyChangedEventHandler? PropertyChanged;
public HassOnedriveFileSyncEntityState(IHassioClient hassioClient)
{
_hassioClient = hassioClient;
State = FileState.Unknown;
}
[JsonConverter(typeof(StringEnumConverter))]
public FileState State
{
get => state;
set
{
state = value;
if (state != FileState.Syncing)
{
UploadPercentage = null;
UploadSpeed = null;
}
}
}
public int? UploadPercentage { get; set; }
public int? UploadSpeed { get; set; }
public async Task UpdateBackupEntityInHass()
{
var payload = new
{
state = State,
attributes = new Dictionary<string, string?>
{
{ FileStateAttribute.UploadPercentage, UploadPercentage == null ? null : $"{UploadPercentage}%" },
{ FileStateAttribute.UploadSpeed, UploadSpeed == null ? null : $"{UploadSpeed} KB/s" },
}
};
string payloadStr = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Converters = entityStateConverters
});
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
await _hassioClient.UpdateHassEntityStateAsync(OneDrive_FileSync_Entity_ID, payloadStr);
}
public class FileStateAttribute
{
public const string UploadPercentage = "Current file upload percentage";
public const string UploadSpeed = "Current file upload speed (KB/s)";
}
public enum FileState
{
Syncing,
Synced,
Unknown,
}
}
}
@@ -0,0 +1,47 @@
using Newtonsoft.Json;
using onedrive_backup.Contracts;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hassio_onedrive_backup.Hass
{
internal class HassOnedriveFreeSpaceEntityState : INotifyPropertyChanged
{
private const string OneDrive_FreeSpace_Entity_ID = "sensor.onedrivefreespace";
private IHassioClient? _hassioClient;
public HassOnedriveFreeSpaceEntityState(IHassioClient hassioClient)
{
_hassioClient = hassioClient;
}
public double? FreeSpaceGB { get; private set; }
public double? TotalSpaceGB { get; private set; }
public event PropertyChangedEventHandler? PropertyChanged;
public async Task UpdateOneDriveFreespaceSensorInHass(OneDriveFreeSpaceData freeSpaceData)
{
FreeSpaceGB = freeSpaceData.FreeSpace;
TotalSpaceGB = freeSpaceData.TotalSpace;
var payload = new
{
state = FreeSpaceGB == null ? "-" : FreeSpaceGB.Value.ToString("0.00"),
attributes = new Dictionary<string, string>
{
{ "unit_of_measurement", "GB" }
}
};
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
string payloadStr = JsonConvert.SerializeObject(payload);
await _hassioClient.UpdateHassEntityStateAsync(OneDrive_FreeSpace_Entity_ID, payloadStr);
}
}
}
+225
View File
@@ -0,0 +1,225 @@
using hassio_onedrive_backup.Contracts;
using hassio_onedrive_backup.Hass.Events;
using hassio_onedrive_backup.Storage;
using Newtonsoft.Json;
using onedrive_backup.Contracts;
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
namespace hassio_onedrive_backup.Hass
{
internal class HassioClient : IHassioClient
{
private const string Supervisor_Base_Uri_Str = "http://supervisor";
private const string Hass_Base_Uri_Str = "http://supervisor/core/api";
private readonly HttpClient _httpClient;
public HassioClient(string token, int hassioTimeout)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
_httpClient.Timeout = TimeSpan.FromMinutes(hassioTimeout);
}
public async Task<bool> DeleteBackupAsync(Backup backup)
{
try
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + $"/backups/{backup.Slug}");
await _httpClient.DeleteAsync(uri);
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Error deleting backup {backup.Slug}. {ex}");
return false;
}
return true;
}
public async Task<List<Backup>> GetBackupsAsync(Predicate<Backup> filter)
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + "/backups");
var response = await GetJsonResponseAsync<HassBackupsResponse>(uri);
if (response.Result.Equals("ok", StringComparison.OrdinalIgnoreCase) == false)
{
throw new InvalidOperationException($"Failed getting Backups from Supervisor. Result: {response.Result}");
}
var backups = response.DataProperty.Backups;
return filter != null ? backups.Where(backup => filter(backup)).ToList() : backups.ToList();
}
public async Task<bool> CreateBackupAsync(string backupName, bool appendTimestamp = true, bool compressed = true, string? password = null, IEnumerable<string>? folders = null, IEnumerable<string>? addons = null)
{
DateTime timeStamp = DateTimeHelper.Instance!.Now;
const string dt_format = "yyyy-MM-dd-HH-mm";
string? payloadStr;
Uri? uri;
string finalBackupName = appendTimestamp ? $"{backupName}_{timeStamp.ToString(dt_format, CultureInfo.CurrentCulture)}" : backupName;
// Full Backup
if (folders == null && addons == null)
{
uri = new Uri(Supervisor_Base_Uri_Str + "/backups/new/full");
var fullPayload = new
{
name = finalBackupName,
compressed = compressed,
password = password
};
payloadStr = JsonConvert.SerializeObject(fullPayload, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
ConsoleLogger.LogInfo("Starting full local backup");
}
// Partial Backup
else
{
uri = new Uri(Supervisor_Base_Uri_Str + "/backups/new/partial");
var partialPayload = new
{
name = finalBackupName,
compressed = compressed,
password = password,
homeassistant = true,
addons = addons,
folders = folders
};
payloadStr = JsonConvert.SerializeObject(partialPayload, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
ConsoleLogger.LogInfo("Starting partial local backup");
}
try
{
await _httpClient.PostAsync(uri, new StringContent(payloadStr, Encoding.UTF8, "application/json"));
ConsoleLogger.LogInfo("Backup complete");
}
catch (TaskCanceledException tce)
{
if (tce.InnerException is TimeoutException)
{
ConsoleLogger.LogError($"Backup request timed out. {tce}");
ConsoleLogger.LogError($"Increase the timeout value in configuration");
return false;
}
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Failed creating new backup. {ex}");
return false;
}
return true;
}
public async Task<bool> UploadBackupAsync(string filePath)
{
try
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + "/backups/new/upload");
using var multiPartFormContent = new MultipartFormDataContent();
var fsContent = new StreamContent(System.IO.File.OpenRead(filePath));
multiPartFormContent.Add(fsContent, name: "file", fileName: filePath);
var response = await _httpClient.PostAsync(uri, multiPartFormContent);
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Error uploading backup to Home Assistant. {ex}");
return false;
}
return true;
}
public async Task SendPersistentNotificationAsync(string message)
{
try
{
Uri uri = new Uri(Hass_Base_Uri_Str + "/services/notify/persistent_notification");
var payload = new
{
message = message,
title = "hassio-onedrive-backup"
};
string payloadStr = JsonConvert.SerializeObject(payload);
await _httpClient.PostAsync(uri, new StringContent(payloadStr, Encoding.UTF8, "application/json"));
}
catch (Exception ex)
{
ConsoleLogger.LogError($"Failed sending persistent notification. {ex}");
}
}
public async Task<List<Addon>> GetAddonsAsync()
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + "/addons");
var response = await GetJsonResponseAsync<HassAddonsResponse>(uri);
var ret = response.DataProperty.Addons.ToList();
return ret;
}
public async Task UpdateHassEntityStateAsync(string entityId, string payload)
{
Uri uri = new Uri(Hass_Base_Uri_Str + $"/states/{entityId}");
await _httpClient.PostAsync(uri, new StringContent(payload, Encoding.UTF8, "application/json"));
}
public async Task PublishEventAsync(OneDriveEvents eventType, string payload = "")
{
Uri uri = new Uri(Hass_Base_Uri_Str + $"/events/onedrive.{eventType}");
await _httpClient.PostAsync(uri, new StringContent(payload, Encoding.UTF8, "application/json"));
}
public async Task<string> DownloadBackupAsync(string backupSlug)
{
ConsoleLogger.LogInfo($"Fetching Local Backup (Slug:{backupSlug})");
Uri uri = new Uri(Supervisor_Base_Uri_Str + $"/backups/{backupSlug}/download");
var fileInfo = new FileInfo($"{LocalStorage.TempFolder}/{backupSlug}.tar");
await using var memStream = await _httpClient.GetStreamAsync(uri);
using var fileStream = System.IO.File.Create(fileInfo.FullName);
await memStream.CopyToAsync(fileStream);
ConsoleLogger.LogInfo($"Backup ({backupSlug}) fetched successfully");
return fileInfo.FullName;
}
public async Task<string> GetTimeZoneAsync()
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + "/supervisor/info");
var response = await GetJsonResponseAsync<HassSupervisorInfoResponse>(uri);
var ret = response.DataProperty.Timezone;
return ret;
}
private async Task<T> GetJsonResponseAsync<T>(Uri uri)
{
string response = await _httpClient.GetStringAsync(uri);
T ret = JsonConvert.DeserializeObject<T>(response)!;
return ret;
}
public async Task<HassAddonInfoResponse> GetAddonInfo(string slug)
{
Uri uri = new Uri(Supervisor_Base_Uri_Str + $"/addons/{slug}/info");
var response = await GetJsonResponseAsync<HassAddonInfoResponse>(uri);
return response;
}
}
}
+164
View File
@@ -0,0 +1,164 @@
using hassio_onedrive_backup.Contracts;
using hassio_onedrive_backup.Hass.Events;
using Newtonsoft.Json;
using onedrive_backup.Contracts;
using System.Diagnostics;
using System.Net.Http;
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
namespace hassio_onedrive_backup.Hass
{
internal class HassioClientMock : IHassioClient
{
private List<Backup> _backups = new List<Backup>
{
new Backup
{
Compressed = true,
Date = DateTime.Now.AddDays(-10),
Name = "Mock1",
Protected = false,
Size = 1,
Slug = "Mock1",
Type = "partial",
Content = new Content
{
Addons = new string[]
{
"Addon1", "Addon2"
}
}
},
new Backup
{
Compressed = true,
Date = DateTime.Now.AddDays(-1),
Name = "Mock2",
Protected = false,
Size = 2,
Slug = "Mock2",
Type = "full",
Content = new Content
{
Addons = new string[]
{
"Addon1", "Addon2"
},
Folders = new string[]
{
"Folder1"
}
}
}
};
public Task<bool> CreateBackupAsync(string backupName, bool appendTimestamp = true, bool compressed = true, string? password = null, IEnumerable<string>? folders = null, IEnumerable<string>? addons = null)
{
DateTime timeStamp = DateTimeHelper.Instance!.Now;
string finalBackupName = appendTimestamp ? $"{backupName}_{timeStamp.ToString("yyyy-MM-dd-HH-mm")}" : backupName;
var backup = new Backup
{
Compressed = compressed,
Name = finalBackupName,
Protected = !string.IsNullOrEmpty(password),
Type = folders == null && addons == null ? "full" : "partial",
Date = DateTime.Now,
Slug = Guid.NewGuid().ToString()
};
_backups.Add(backup);
return Task.FromResult(true);
}
public Task<bool> DeleteBackupAsync(Backup backup)
{
var backupToRemove = _backups.SingleOrDefault(bck => bck.Slug.Equals(backup.Slug, StringComparison.OrdinalIgnoreCase));
if (backupToRemove != null)
{
_backups.Remove(backupToRemove);
return Task.FromResult(true);
}
return Task.FromResult(false);
}
public Task<string> DownloadBackupAsync(string backupSlug)
{
string backupFile = $"./{backupSlug}.tar";
// 15MB File
byte[] data = new byte[100 * 1024 * 1024];
new Random().NextBytes(data);
File.WriteAllBytes(backupFile, data);
return Task.FromResult(backupFile);
}
public Task<HassAddonInfoResponse> GetAddonInfo(string slug)
{
return Task.FromResult(new HassAddonInfoResponse
{
DataProperty = new HassAddonInfoResponse.Data
{
IngressEntry = "/",
IngressUrl = "/"
}
});
}
public Task<List<HassAddonsResponse.Addon>> GetAddonsAsync()
{
return Task.FromResult(new List<HassAddonsResponse.Addon>()
{
new HassAddonsResponse.Addon{ Slug = "Addon1", Name = "First Addon" },
new HassAddonsResponse.Addon { Slug = "Addon2", Name = "Second Addon" }
});
}
public Task<List<Backup>> GetBackupsAsync(Predicate<Backup> filter)
{
return Task.FromResult(_backups);
}
public Task<string> GetTimeZoneAsync()
{
return Task.FromResult("Local");
}
public Task PublishEventAsync(OneDriveEvents eventType, string payload = "")
{
Debug.WriteLine($"EventType: {eventType}. Payload: {payload}");
return Task.CompletedTask;
}
public Task SendPersistentNotificationAsync(string message)
{
var payload = new
{
message = message,
title = "hassio-onedrive-backup"
};
string payloadStr = JsonConvert.SerializeObject(payload);
Debug.WriteLine(payloadStr);
return Task.CompletedTask;
}
public Task UpdateHassEntityStateAsync(string entityId, string payload)
{
Debug.WriteLine($"EntityId: {entityId}. State: {payload}");
return Task.CompletedTask;
}
public Task<bool> UploadBackupAsync(string filePath)
{
_backups.Add(new Backup
{
Name = Path.GetFileNameWithoutExtension(filePath),
Slug = Guid.NewGuid().ToString()
});
return Task.FromResult(true);
}
}
}
+32
View File
@@ -0,0 +1,32 @@
using hassio_onedrive_backup.Hass.Events;
using onedrive_backup.Contracts;
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
namespace hassio_onedrive_backup.Hass
{
public interface IHassioClient
{
Task<List<Backup>> GetBackupsAsync(Predicate<Backup> filter);
Task SendPersistentNotificationAsync(string message);
Task<bool> CreateBackupAsync(string backupName, bool appendTimestamp = true, bool compressed = true, string? password = null, IEnumerable<string>? folders = null, IEnumerable<string>? addons = null);
Task<bool> DeleteBackupAsync(Backup backup);
Task UpdateHassEntityStateAsync(string entityId, string payload);
Task<string> DownloadBackupAsync(string backupSlug);
Task<bool> UploadBackupAsync(string filePath);
Task<List<Addon>> GetAddonsAsync();
Task<HassAddonInfoResponse> GetAddonInfo(string slug);
Task<string> GetTimeZoneAsync();
Task PublishEventAsync(OneDriveEvents eventType, string payload = "");
}
}