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
+187
View File
@@ -0,0 +1,187 @@
@using hassio_onedrive_backup;
@using hassio_onedrive_backup.Contracts;
@using onedrive_backup.Extensions;
@using onedrive_backup.Hass;
@using onedrive_backup.Models;
@inject Orchestrator Orchestrator;
@inject AddonOptions AddonOptions;
@inject IJSRuntime JS
<div class="card text-white bg-light mb-3 w-100 h-100" style="min-width: 16rem">
<div class="card-header text-dark">
<div class="row">
<div class="col-8">
@**************Add Badges*****************@
@if (Backup.Type != null)
{
<span class="badge rounded-pill m-1 @BackupTypeColor" data-bs-toggle="tooltip" data-bs-title="Backup is @Backup.Type">@Backup.Type</span>
}
@if (Backup.Location == BackupModel.BackupLocation.OneDrive || Backup.Location == BackupModel.BackupLocation.Both)
{
<span class="badge rounded-pill m-1 bg-primary" data-bs-toggle="tooltip" data-bs-title="Backup is in OneDrive">OneDrive</span>
}
@if (Backup.Location == BackupModel.BackupLocation.Local || Backup.Location == BackupModel.BackupLocation.Both)
{
<span class="badge rounded-pill m-1 bg-dark" data-bs-toggle="tooltip" data-bs-title="Backup is in Home Assistant">Local</span>
}
@if (Backup.IsProtected)
{
<span class="badge rounded-pill m-1 bg-dark" data-bs-toggle="tooltip" data-bs-title="Backup is password protected">Protected</span>
}
@***************@
</div>
<div class="col text-end">
<span class="small">@timeAgoText</span>
</div>
</div>
</div>
<div class="card-body bg-light text-body">
<h6 class="card-title">@Backup.DisplayName</h6>
<h7 class="card-subtitle text-muted">@(Backup.Date)</h7>
@if (Backup.Size > 0)
{
<div class="card-text">@Backup.Size MB</div>
}
<div class="collapse" id="details-@Backup.Slug">
@if (Backup.Folders != null && Backup.Folders.Any())
{
<div class="card-text">
<p><strong>Folders:</strong>@string.Join(", ", Backup.Folders)</p>
</div>
}
</div>
</div>
<div class="card-footer d-flex">
@*Upload Button*@
@if (@Backup.Location == BackupModel.BackupLocation.Local)
{
<button type="button" @onclick="UploadBackup" class="btn btn-sm m-1 btn-outline-primary" data-bs-toggle="tooltip" data-bs-title="Upload backup to OneDrive" disabled="@uploading">
@if (uploading)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="oi oi-cloud-upload">Uploading @uploadPercent%</span>
}
else
{
<span class="oi oi-cloud-upload">Upload</span>
}
</button>
}
@*Download Button*@
@if (@Backup.Location == BackupModel.BackupLocation.OneDrive)
{
<button type="button" @onclick="DownloadBackup" class="btn btn-sm m-1 btn-outline-primary" data-bs-toggle="tooltip" data-bs-title="Download Backup to Home Assistant" disabled="@downloading">
@if (downloading)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="oi oi-cloud-download">Downloading @downloadPercent%</span>
}
else
{
<span class="oi oi-cloud-download">Download</span>
}
</button>
}
@*Details Button*@
<button type="button" class="btn btn-sm m-1 btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#details-@Backup.Slug" disabled="@detailsDisabled"><span class="oi oi-expand-down" title="Backup details">Details</span></button>
@if (transferSpeed > 0)
{
<span class="text-end ms-auto my-auto text-dark">@TransferSpeedText</span>
}
</div>
</div>
@code {
private bool uploading = false;
private bool downloading = false;
private int uploadPercent = 0;
private int downloadPercent = 0;
private int transferSpeed = 0;
[Parameter]
public BackupModel Backup { get; set; }
public string BackupTypeColor => Backup.Type.Equals("full", StringComparison.OrdinalIgnoreCase) ? "bg-info" : "bg-secondary";
private bool detailsDisabled => (Backup.Addons?.Count() > 0 || Backup.Folders?.Count() > 0) ? false : true;
private string timeAgoText => CalculateTimeAgoText();
private string TransferSpeedText => ViewHelpers.GetSpeedDisplayText(transferSpeed / 1000);
private string CalculateTimeAgoText()
{
TimeSpan timeAgo = DateTimeHelper.Instance.Now - Backup.Date;
if (timeAgo.TotalDays > 1)
{
return $"{timeAgo.Days} days ago";
}
else if (timeAgo.TotalHours > 1)
{
return $"{timeAgo.Hours} hours ago";
}
else if (timeAgo.TotalMinutes > 1)
{
return $"{timeAgo.Minutes} minutes ago";
}
else
{
return "Now";
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("addTooltips");
}
}
private async void UploadBackup()
{
uploading = true;
uploadPercent = 0;
var backup = Backup.ToBackup();
await Orchestrator.BackupManager.UploadLocalBackupToOneDrive(backup, updateHassEntityState: false, progressCallback: (prog, speed) =>
{
uploadPercent = prog ?? 0;
transferSpeed = speed ?? 0;
TriggerStateChanged();
});
uploading = false;
transferSpeed = 0;
await Orchestrator.BackupManager.GetOnlineBackupsAsync(AddonOptions.InstanceName);
}
private async void DownloadBackup()
{
downloading = true;
downloadPercent = 0;
var onedriveBackup = Backup.ToOneDriveBackup();
await Orchestrator.BackupManager.DownloadBackupFromOneDrive(onedriveBackup, updateHassEntityState: false, progressCallback: (prog) =>
{
downloadPercent = prog ?? 0;
TriggerStateChanged();
});
downloading = false;
await Orchestrator.BackupManager.GetLocalBackups();
}
private async void TriggerStateChanged()
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
}
@@ -0,0 +1,11 @@
.transition-visible {
overflow: hidden;
transition: transform 0.3s ease-out;
height: auto;
transform: scaleY(1);
transform-origin: top;
}
.transition-visible.transition-collapsed {
transform: scaleY(0);
}
+62
View File
@@ -0,0 +1,62 @@
@using hassio_onedrive_backup.Graph;
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
@inject IGraphHelper GraphHelper
@inject IJSRuntime JS
<div class="container-fluid p-1 pb-5">
@if (GraphHelper.IsAuthenticated == false)
{
<div class="alert alert-danger" role="alert" style="padding:1px;margin-bottom:1px;">
<strong>Not logged in!</strong> To sign in to OneDrive, navigate to <a href="@GraphHelper.AuthUrl" class="alert-link" target="_blank">@GraphHelper.AuthUrl</a> and enter the code <i>
@GraphHelper.AuthCode
</i>
<button type="button" class="btn btn-sm m-1 btn-light" @onclick="CopyCodeToClipboard"><span class="oi oi-clipboard" title="Copy code to clipboard"></span></button> to authenticate.
</div>
}
<nav class="navbar navbar-expand-lg">
<span class="navbar-brand" href="#">OneDrive Backup</span>
<div class="nav-item">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
Dashboard
</NavLink>
</div>
<div class="nav-item">
<NavLink class="nav-link" href="about" Match="NavLinkMatch.All">
About
</NavLink>
</div>
</nav>
</div>
<div class="container" style="padding-bottom: 2rem">
@Body
</div>
<footer>
<a href="about"><img class="mx-1" src="images/Bitcoin.svg" alt="Donate Bitcoin" data-bs-toggle="tooltip" data-bs-title="Donate Bitcoin" style="width: 50px; height: auto; transform: scale(0.5);" /></a>
<a href="https://www.buymeacoffee.com/snirlavis" target="_blank"><img class="mx-1" src="bmc/bmc-button.svg" alt="Buy Me A Coffee" data-bs-toggle="tooltip" data-bs-title="Buy me a coffee" style="height: 1.8rem !important;"></a>
<p>&copy; Nir Lavi</p>
</footer>
@code{
protected override void OnInitialized()
{
base.OnInitialized();
GraphHelper.AuthStatusChangedEventHandler += OnStateChanged;
}
private void CopyCodeToClipboard()
{
JS.InvokeVoidAsync("copyToClipboard", GraphHelper.AuthCode);
}
private async void OnStateChanged()
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
}
@@ -0,0 +1,49 @@
/* Set the height of the footer and give it a dark background */
footer {
position: fixed;
bottom: 0;
width: 100%;
height: 2rem;
background-color: #333;
color: #fff;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 20px;
}
/* Style the copyright text in the footer */
footer p {
font-size: 14px;
margin: 0;
}
/* Set the min-height of the content area to fill the viewport */
.container {
min-height: calc(100vh - 140px);
}
::deep nav.navbar {
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.05);
}
::deep nav.navbar .nav-link {
color: #333;
font-size: 16px;
font-weight: 500;
text-transform: uppercase;
padding: 16px;
border-bottom: 2px solid transparent;
transition: border-color 0.3s ease;
}
::deep nav.navbar .nav-link:hover {
color: #0066CC;
}
::deep nav.navbar .nav-link.active {
color: #0066CC;
border-bottom-color: #0066CC;
}
@@ -0,0 +1,258 @@
@using hassio_onedrive_backup.Contracts;
@using hassio_onedrive_backup.Graph;
@using hassio_onedrive_backup.Hass;
@using hassio_onedrive_backup;
@inject IJSRuntime JS
@inject AddonOptions AddonOptions
@inject HassOnedriveEntityState HassOnedriveEntityState
@inject HassOnedriveFreeSpaceEntityState HassOnedriveFreeSpaceEntityState
@inject HassOnedriveFileSyncEntityState HassOnedriveFileSyncEntityState
@inject Orchestrator Orchestrator
@inject IGraphHelper GraphHelper
<div class="card text-white bg-light mb-3 w-100" style="min-width: 16rem">
<div class="card-header text-dark">
<div class="row">
<div class="col d-flex align-items-center">
Summary <span class="badge rounded-pill m-1 @SummaryBadgeColor">@SummaryBadgeText</span>
@if (HassOnedriveEntityState.State == HassOnedriveEntityState.BackupState.Syncing)
{
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
}
</div>
@if (GraphHelper.IsAuthenticated == true)
{
<div class="col text-end">
@* <span class="oi oi-data-transfer-upload"></span>
<span class="card-text">@UploadSpeed</span>
*@ <button type="button" @onclick="RefreshData" class="btn btn-sm btn-primary" data-bs-toggle="tooltip" data-bs-title="Refresh Backups Data" disabled="@refreshingData">
<span class="oi oi-reload @(refreshingData ? "refreshing" : string.Empty)" aria-hidden="true"></span>
</button>
<button type="button" @onclick="TriggerBackup" class="btn btn-sm btn-primary" data-bs-toggle="tooltip" data-bs-title="Trigger a backup now" disabled="@onDemandBackup">
<span class="card-text">Trigger Backup</span>
@if (onDemandBackup)
{
<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>
}
</button>
</div>
}
</div>
</div>
<div class="card-body bg-light text-body">
<div class="row">
<div class="col">
<span class="card-text">OneDrive Storage</span>
</div>
<div class="col">
@if (HassOnedriveFreeSpaceEntityState.FreeSpaceGB == null)
{
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-secondary" role="progressbar" style="width: 100%">Initializing</div>
</div>
}
else
{
<div class="progress">
<div class="progress-bar @FreeSpaceColor" role="progressbar" style="width: @(OneDriveUsedSpacePercentage)%" aria-valuenow="@OneDriveUsedSpacePercentage" data-bs-toggle="tooltip" title=@(OneDriveUsedSpace.ToString("N2") + " GB / " + (HassOnedriveFreeSpaceEntityState.FreeSpaceGB ?? 0).ToString("N2") + " GB" ) aria-valuemin="0" aria-valuemax="100">Used: @(OneDriveUsedSpacePercentage)%</div>
</div>
}
</div>
</div>
<div class="row">
<div class="col">
<span class="card-text">Local Backups</span>
</div>
<div class="col">
<div class="progress">
<div class="progress-bar bg-primary" role="progressbar" style="width: @(LocalBackupsPercentage)%" aria-valuenow="@LocalBackupsPercentage" aria-valuemin="0" aria-valuemax="@AddonOptions.MaxLocalBackups">@HassOnedriveEntityState.BackupsInHomeAssistant / @AddonOptions.MaxLocalBackups</div>
@*<div class="progress-bar bg-secondary" role="progressbar" style="width: @(100 - LocalBackupsPercentage)%" aria-valuenow="@(100 - LocalBackupsPercentage)" aria-valuemin="0" aria-valuemax="@AddonOptions.MaxLocalBackups"></div>*@
</div>
</div>
</div>
<div class="row">
<div class="col">
<span class="card-text">OneDrive Backups</span>
</div>
<div class="col">
<div class="progress">
<div class="progress-bar bg-primary" role="progressbar" style="width: @(OneDriveBackupsPercentage)%" aria-valuenow="@OneDriveBackupsPercentage" aria-valuemin="0" aria-valuemax="@AddonOptions.MaxOnedriveBackups">@HassOnedriveEntityState.BackupsInOnedrive / @AddonOptions.MaxOnedriveBackups</div>
@*<div class="progress-bar bg-secondary" role="progressbar" style="width: @(100 - OneDriveBackupsPercentage)%" aria-valuenow="@(100 - OneDriveBackupsPercentage)" aria-valuemin="0" aria-valuemax="@AddonOptions.MaxOnedriveBackups"></div>*@
</div>
</div>
</div>
<div class="row">
<div class="col">
<span class="card-text">Last Backup Date</span>
</div>
<div class="col">
@if (LastBackupDate == null)
{
<span class="card-text">None</span>
}
else
{
<span class="card-text">@LastBackupDate</span>
}
</div>
</div>
<div class="row">
<div class="col">
<span class="card-text">Backup Upload Progress</span>
</div>
<div class="col">
@if (HassOnedriveEntityState.UploadPercentage == null)
{
<span class="card-text">N/A</span>
}
else
{
<div class="progress">
<div class="progress-bar progress-bar-animated bg-primary" role="progressbar" style="width: @(HassOnedriveEntityState.UploadPercentage)%">@BackupUploadSpeed</div>
</div>
}
</div>
</div>
@if (AddonOptions.FileSyncEnabled)
{
<div class="row">
<div class="col">
<span class="card-text">File Sync Upload Progress</span>
</div>
<div class="col">
@if (HassOnedriveFileSyncEntityState.State != HassOnedriveFileSyncEntityState.FileState.Syncing)
{
<span class="card-text">N/A</span>
}
else
{
<div class="progress">
<div class="progress-bar progress-bar-animated bg-primary" role="progressbar" style="width: @(HassOnedriveFileSyncEntityState.UploadPercentage)%">@(FileUploadSpeed)</div>
</div>
}
</div>
</div>
}
</div>
</div>
@code {
private bool onDemandBackup = false;
private bool refreshingData = false;
private int OneDriveUsedSpacePercentage => (int)((OneDriveUsedSpace / (HassOnedriveFreeSpaceEntityState.TotalSpaceGB ?? 1)) * 100);
private int LocalBackupsPercentage => (int)((HassOnedriveEntityState.BackupsInHomeAssistant / (double)AddonOptions.MaxLocalBackups) * 100);
private int OneDriveBackupsPercentage => (int)((HassOnedriveEntityState.BackupsInOnedrive / (double)AddonOptions.MaxOnedriveBackups) * 100);
private string BackupUploadSpeed => ViewHelpers.GetSpeedDisplayText(HassOnedriveEntityState.UploadSpeed);
private string FileUploadSpeed => ViewHelpers.GetSpeedDisplayText(HassOnedriveFileSyncEntityState.UploadSpeed);
private double OneDriveUsedSpace => ((HassOnedriveFreeSpaceEntityState.TotalSpaceGB ?? 0) - (HassOnedriveFreeSpaceEntityState.FreeSpaceGB ?? 0));
private DateTime? LastBackupDate => HassOnedriveEntityState.LastLocalBackupDate > HassOnedriveEntityState.LastOnedriveBackupDate ? HassOnedriveEntityState.LastLocalBackupDate : HassOnedriveEntityState.LastOnedriveBackupDate;
private string FreeSpaceColor => OneDriveUsedSpacePercentage > 90 ? "bg-danger" : "bg-primary";
private string SummaryBadgeColor
{
get
{
switch (HassOnedriveEntityState.State)
{
case HassOnedriveEntityState.BackupState.Stale:
return "bg-warning";
case HassOnedriveEntityState.BackupState.Backed_Up:
return "bg-success";
case HassOnedriveEntityState.BackupState.Backed_Up_Local:
return "bg-secondary";
case HassOnedriveEntityState.BackupState.Backed_Up_Onedrive:
return "bg-primary";
case HassOnedriveEntityState.BackupState.Syncing:
return "bg-dark";
default:
return "bg-dark";
}
}
}
private string SummaryBadgeText
{
get
{
switch (HassOnedriveEntityState.State)
{
case HassOnedriveEntityState.BackupState.Stale:
return "Stale";
case HassOnedriveEntityState.BackupState.Backed_Up:
return "Backed Up";
case HassOnedriveEntityState.BackupState.Backed_Up_Local:
return "Backed Up Local";
case HassOnedriveEntityState.BackupState.Backed_Up_Onedrive:
return "Backed Up OneDrive";
case HassOnedriveEntityState.BackupState.Syncing:
return "Syncing";
default:
return "";
}
}
}
protected override void OnInitialized()
{
base.OnInitialized();
HassOnedriveFreeSpaceEntityState.PropertyChanged += TriggerStateChanged;
HassOnedriveEntityState.PropertyChanged += TriggerStateChanged;
HassOnedriveFileSyncEntityState.PropertyChanged += TriggerStateChanged;
Orchestrator.BackupManager.OneDriveBackupsUpdated += TriggerStateChanged;
Orchestrator.BackupManager.LocalBackupsUpdated += TriggerStateChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// await JS.InvokeVoidAsync("addTooltips");
}
}
private async void TriggerBackup()
{
onDemandBackup = true;
bool ret = await Orchestrator.BackupManager.CreateLocalBackup();
onDemandBackup = false;
TriggerStateChanged();
}
private async void RefreshData()
{
refreshingData = true;
TriggerStateChanged();
await Orchestrator.BackupManager.RefreshBackupData();
refreshingData = false;
TriggerStateChanged();
}
private async void TriggerStateChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
private async void TriggerStateChanged()
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
}
@@ -0,0 +1,14 @@
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
span.oi-reload.refreshing {
display: inline-block;
animation: rotate 2s linear infinite;
}
+26
View File
@@ -0,0 +1,26 @@
namespace onedrive_backup.Shared
{
public static class ViewHelpers
{
public static string GetSpeedDisplayText(int? speedKbSec)
{
if (speedKbSec == null)
{
return "0 KB/s";
}
if (speedKbSec < 1024)
{
return $"{speedKbSec} KB/s";
}
else if (speedKbSec < 1024 * 1024)
{
return $"{speedKbSec / 1024} MB/s";
}
else
{
return $"{speedKbSec / (1024 * 1024)} GB/s";
}
}
}
}