OneDrive Backup
OneDrive Backup
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
bin/
|
||||||
|
Bin/
|
||||||
|
obj/
|
||||||
|
Obj/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
/wwwroot/dist/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/packages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/packages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/packages/repositories.config
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Microsoft Azure ApplicationInsights config file
|
||||||
|
ApplicationInsights.config
|
||||||
|
|
||||||
|
# Windows Store app package directory
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
record.auth*
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using onedrive_backup.Shared;
|
||||||
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>Not found</PageTitle>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
## v2.1.2 [May 18th 2023]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed permissions issue when running the add-on in a Supervised (Non HassOS) deployment
|
||||||
|
* Improved Exception Handling
|
||||||
|
|
||||||
|
## v2.1.1 [April 11th 2023]
|
||||||
|
### Fixed
|
||||||
|
* Fixed a bug where subsequent backup iterations would begin before the previous cycle has completed
|
||||||
|
|
||||||
|
## v2.1 [March 30th 2023]
|
||||||
|
### Added
|
||||||
|
* Option to [ignore Home Assistant Upgrade Backups](https://github.com/lavinir/hassio-onedrive-backup#ignore-home-assistant-upgrade-backups)
|
||||||
|
* Show Upload Transfer Speed in UI
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* UI bugfixes
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* More log verbosity updates (Info -> Verbose)
|
||||||
|
|
||||||
|
## v2.02 [Martch 28th 2023]
|
||||||
|
### Fixed
|
||||||
|
* Fixed a scenario where potentially temporary backup files would be uploaded as part of the addon's own backup
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* Log verbosity for INFO level has been decreased. Those logs will still be available at the VERBOSE level.
|
||||||
|
|
||||||
|
## v2.01 [March 21st 2023]
|
||||||
|
### Fixed
|
||||||
|
* Removed addon list from backup details in webui to workaround onedrive size limitation causing backup uplaods to fail when a lot of addons are backed up.
|
||||||
|
|
||||||
|
## v2.0 [March 21st 2023]
|
||||||
|
### Summary
|
||||||
|
This is a very big release 🎉 with many new features and some breaking changes, please see details below.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* **Web Interface!** - There is now a dedicated Web UI for the addon, where you can easily see an overview of all your backups, trigger manual backups, retrieve backups and more. **CloudFlared** users please see [relevant prerequisites](https://github.com/lavinir/hassio-onedrive-backup#html-content-filtering-error)
|
||||||
|
* **On Demand Backups** - Available through the new Web Interface
|
||||||
|
* **Log Verbosity Config** - You can now specify the [verbosity](https://github.com/lavinir/hassio-onedrive-backup#log-level) of the addon logs from the config.
|
||||||
|
* **Exclude Addons from Backup** - It is now possible to [exclude addons](https://github.com/lavinir/hassio-onedrive-backup#excluded-addons-optional) from your backups
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* <kbd>[Breaking change]</kbd> **File Sync Engine** - The File Syncing engine has been rewritten. Along with it the relevant configuration for specifying the [sync paths](https://github.com/lavinir/hassio-onedrive-backup#file-sync-paths-optional) has changed. Please see the [documentation]((https://github.com/lavinir/hassio-onedrive-backup#file-sync-paths-optional)) for proper configuration.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* **Sync Drift** - Updating backup interval logic to avoid an issue where backup times could slowly drift and if you were using a short **Allowed Hours** window you could reach an issue where an update would be skipped because a window was missed.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* <kbd>[Breaking change]</kbd> **Recovery Mode** - With the introduction of the dedicated Web UI, Recovery mode is no longer needed and has been removed. You can now load a Backup from OneDrive to Home Assistant with a single button from the Web UI. See [Restoring from Backup](https://github.com/lavinir/hassio-onedrive-backup#restoring-from-backup)
|
||||||
|
|
||||||
|
## v1.41 [January 26th 2023]
|
||||||
|
### Added
|
||||||
|
* [**Monitor *all* backups**](https://github.com/lavinir/hassio-onedrive-backup#monitor-all-local-backups) - You can now optionally choose to have the addon monitor **any** created local backup, treating it as any other and backing them up to OneDrive.
|
||||||
|
* [**FileSync 1**] - Added option to set paths as [recursive](https://github.com/lavinir/hassio-onedrive-backup#file-sync-paths-optional) enabling syncing all contained subfolders
|
||||||
|
* [**FileSync 2**] - Optional [deletions in OneDrive](https://github.com/lavinir/hassio-onedrive-backup#remove-deleted-files-during-file-sync) for Synced Files that no longer exist / no longer monitored.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [**Allowed Hours**] - Fixed an issue where backup uploads / deletions would still occur outside of allowed hours
|
||||||
|
* [**Redundant Uploads**] - Fixed an issue that occured when the configured local backup maximum exceeded the configured OneDrive backup maximum where remaining local uploads would be uploaded and immediately deleted from OneDrive
|
||||||
|
* [**Backup Sensor**] - DateTime attributes now in standard international format
|
||||||
|
## v1.4 [January 15th 2023]
|
||||||
|
### Added
|
||||||
|
* [**First release of File Syncs!** 🎉](https://github.com/lavinir/hassio-onedrive-backup#sync_paths-optional) This allows file syncing to OneDrive outside of Backups.
|
||||||
|
* [**New sensor added for File Syncs**](https://github.com/lavinir/hassio-onedrive-backup#home-assistant-file-sync-sensor)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* **Password protection for backups now optional** - Due to multiple requests you can now choose to not protect your backups with a password.
|
||||||
|
## v1.3 [December 13 2022]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* [**Multiple instance support!**](https://github.com/lavinir/hassio-onedrive-backup#backup_instance_name-optional) You can now have multiple installations of Home Assistant backing up to the same OneDrive account without overriding each other.
|
||||||
|
* [**Event support for backup failures.**](https://github.com/lavinir/hassio-onedrive-backup#events) Native Home Assistant events will now fire to notify you of backup failures.
|
||||||
|
* **Configuration Translations** added for English and German (Thank you [@Kreisverkehr](https://github.com/Kreisverkehr))
|
||||||
|
|
||||||
|
## v1.22 [December 6 2022]
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* Sensor will now only change to "Syncing" during actual OneDrive backup taking place and not whenever the add-on is checking whether a backup is needed. (This was done to remove state change noise)
|
||||||
|
|
||||||
|
## v1.21 [December 2 2022]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed ArmV7 image issue with timezone support
|
||||||
|
|
||||||
|
## v1.2 [December 1 2022]
|
||||||
|
See updates below. Also I've enabled **[Discussions](https://github.com/lavinir/hassio-onedrive-backup/discussions)** on the official repo to make it easier to get suggestions and feedback.
|
||||||
|
### Added
|
||||||
|
* **Allowed backup hours** - You can now specify specific hours during the day that backups will be perfomed. See [allowed_backup_hours](../README.md#backup_allowed_hours-optional)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Timestamps are now using the set TimeZone instead of UTC
|
||||||
|
### Removed
|
||||||
|
* **sync_interval_hours** setting has been removed. This is now handled internally by the add-on.
|
||||||
|
|
||||||
|
## v1.11 [November 1 2022]
|
||||||
|
### Fixed
|
||||||
|
* **Incorrect Hassio Role** - Role was not actually changed to manager (required for partial backup support)
|
||||||
|
* **Small fixes**
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1 [October 31 2022]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* **Recovery Mode** - Allow loading backups back from OneDrive to Home
|
||||||
|
Assistant
|
||||||
|
* **Partial backups** - Added support for partial folder backups
|
||||||
|
* **Added configuration** - Allow setting timeout on Home Assistant API calls (to accomodate large backup files)
|
||||||
|
* **Timestamp suffix** - Added Timestamp suffix to local backups
|
||||||
|
* **OneDrive free space sensor** - Added new sensor showing amount of free space left in OneDrive
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* **Large backup files** - Fixed failing backups when backup size is over 2GB
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Add-on role changed to *manager* to enumerate add-ons to support partial backups. This knocked 1 security point from the addon's previous security rating.
|
||||||
|
|
||||||
|
The only additional call being made now is enumerating the installed add-ons so they can be passed on to Home Assistant when creating a partial bakcup.
|
||||||
|
|
||||||
|
## v1.0 [October 27th 2022]
|
||||||
|
### Added
|
||||||
|
- First Release :)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup
|
||||||
|
{
|
||||||
|
public static class ConsoleLogger
|
||||||
|
{
|
||||||
|
private static LogLevel _logLevel;
|
||||||
|
|
||||||
|
public static void LogError(string msg)
|
||||||
|
{
|
||||||
|
WriteLog(LogLevel.Error, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogWarning(string msg)
|
||||||
|
{
|
||||||
|
WriteLog(LogLevel.Warning, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogInfo(string msg)
|
||||||
|
{
|
||||||
|
WriteLog(LogLevel.Info, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogVerbose(string msg)
|
||||||
|
{
|
||||||
|
WriteLog(LogLevel.Verbose, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetLogLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
_logLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteLog(LogLevel level, string msg)
|
||||||
|
{
|
||||||
|
if (level < _logLevel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestamp = DateTimeHelper.Instance?.Now;
|
||||||
|
string logMsg = $"{timestamp} [{Thread.CurrentThread.ManagedThreadId}] {level}: {msg}";
|
||||||
|
|
||||||
|
if (level == LogLevel.Error)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(logMsg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(logMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Verbose,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class AddonOptions
|
||||||
|
{
|
||||||
|
[JsonProperty("local_backup_num_to_keep")]
|
||||||
|
public int MaxLocalBackups { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("onedrive_backup_num_to_keep")]
|
||||||
|
public int MaxOnedriveBackups { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("backup_interval_days")]
|
||||||
|
public float BackupIntervalDays { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("backup_passwd")]
|
||||||
|
public string? BackupPassword { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("backup_name")]
|
||||||
|
public string? BackupName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("monitor_all_local_backups")]
|
||||||
|
public bool MonitorAllLocalBackups{ get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("notify_on_error")]
|
||||||
|
public bool NotifyOnError { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("hass_api_timeout_minutes")]
|
||||||
|
public int HassAPITimeoutMinutes { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("exclude_media_folder")]
|
||||||
|
public bool ExcludeMediaFolder { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("exclude_ssl_folder")]
|
||||||
|
public bool ExcludeSSLFolder { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("exclude_share_folder")]
|
||||||
|
public bool ExcludeShareFolder { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("exclude_local_addons_folder")]
|
||||||
|
public bool ExcludeLocalAddonsFolder { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("backup_allowed_hours")]
|
||||||
|
public string? BackupAllowedHours { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("backup_instance_name")]
|
||||||
|
public string? InstanceName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("sync_paths")]
|
||||||
|
public List<string>? SyncPaths { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("file_sync_remove_deleted")]
|
||||||
|
public bool FileSyncRemoveDeleted { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonProperty("excluded_addons")]
|
||||||
|
public List<string> ExcludedAddons { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[JsonProperty("log_level")]
|
||||||
|
public string LogLevelStr { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("ignore_upgrade_backups")]
|
||||||
|
public bool IgnoreUpgradeBackups { get; set; }
|
||||||
|
|
||||||
|
//[JsonProperty("upload_speed_cap")]
|
||||||
|
//public int? UploadSpeedCapKBPerSecond { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ConsoleLogger.LogLevel LogLevel => LogLevelStr switch
|
||||||
|
{
|
||||||
|
"verbose" => ConsoleLogger.LogLevel.Verbose,
|
||||||
|
"info" => ConsoleLogger.LogLevel.Info,
|
||||||
|
"warning" => ConsoleLogger.LogLevel.Warning,
|
||||||
|
"error" => ConsoleLogger.LogLevel.Error,
|
||||||
|
_ => ConsoleLogger.LogLevel.Info
|
||||||
|
};
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string BackupPasswordSafe => string.IsNullOrEmpty(BackupPassword) ? "backup" : BackupPassword;
|
||||||
|
[JsonIgnore]
|
||||||
|
public float BackupIntervalHours => BackupIntervalDays * 24;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string BackupNameSafe => string.IsNullOrEmpty(BackupName) ? "hass_backup" : BackupName;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsPartialBackup => ExcludeLocalAddonsFolder || ExcludeMediaFolder || ExcludeShareFolder || ExcludeSSLFolder || ExcludedAddons.Any();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool FileSyncEnabled => SyncPaths != null && SyncPaths.Count > 0;
|
||||||
|
|
||||||
|
//[JsonIgnore]
|
||||||
|
//public bool UploadSpeedCap => UploadSpeedCapKBPerSecond != null;
|
||||||
|
|
||||||
|
public List<string> IncludedFolderList
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
List<string> folders = new List<string>();
|
||||||
|
if (!ExcludeLocalAddonsFolder)
|
||||||
|
{
|
||||||
|
folders.Add("addons/local");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExcludeMediaFolder)
|
||||||
|
{
|
||||||
|
folders.Add("media");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExcludeShareFolder)
|
||||||
|
{
|
||||||
|
folders.Add("share");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExcludeSSLFolder)
|
||||||
|
{
|
||||||
|
folders.Add("ssl");
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class HassAddonInfoResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public Data DataProperty { get; set; }
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
[JsonProperty("ingress_entry")]
|
||||||
|
public string IngressEntry { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("ingress_url")]
|
||||||
|
public string IngressUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class HassAddonsResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("result")]
|
||||||
|
public string Result { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public Data DataProperty { get; set; }
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
[JsonProperty("addons")]
|
||||||
|
public Addon[] Addons { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Addon
|
||||||
|
{
|
||||||
|
[JsonProperty("slug")]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class HassBackupsResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("result")]
|
||||||
|
public string Result { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public Data DataProperty { get; set; }
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
[JsonProperty("backups")]
|
||||||
|
public Backup[] Backups { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Backup
|
||||||
|
{
|
||||||
|
[JsonProperty("slug")]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("date")]
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("size")]
|
||||||
|
public float Size { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("protected")]
|
||||||
|
public bool Protected { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("compressed")]
|
||||||
|
public bool Compressed { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("content")]
|
||||||
|
public Content Content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Content
|
||||||
|
{
|
||||||
|
[JsonProperty("homeassistant")]
|
||||||
|
public bool Homeassistant { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("addons")]
|
||||||
|
public string[] Addons { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("folders")]
|
||||||
|
public string[] Folders { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class HassSupervisorInfoResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("result")]
|
||||||
|
public string Result { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public Data DataProperty { get; set; }
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
[JsonProperty("timezone")]
|
||||||
|
public string Timezone{ get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class OneDriveFreeSpaceData
|
||||||
|
{
|
||||||
|
public double? FreeSpace { get; set; }
|
||||||
|
|
||||||
|
public double? TotalSpace { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class OnedriveBackup
|
||||||
|
{
|
||||||
|
public OnedriveBackup()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public OnedriveBackup(string fileName, OnedriveItemDescription itemDescription)
|
||||||
|
{
|
||||||
|
FileName = fileName;
|
||||||
|
Slug = itemDescription.Slug;
|
||||||
|
BackupDate = itemDescription.BackupDate;
|
||||||
|
InstanceName = itemDescription.InstanceName;
|
||||||
|
Type = itemDescription.BackupType;
|
||||||
|
IsProtected = itemDescription.IsProtected;
|
||||||
|
Size = itemDescription.Size;
|
||||||
|
Addons = itemDescription.Addons;
|
||||||
|
Folders = itemDescription.Folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
public DateTime BackupDate { get; set; }
|
||||||
|
|
||||||
|
public string? InstanceName { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public bool IsProtected { get; set; }
|
||||||
|
|
||||||
|
public float Size { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> Addons { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> Folders { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
public class OnedriveItemDescription
|
||||||
|
{
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
public DateTime BackupDate { get; set; }
|
||||||
|
|
||||||
|
public string? InstanceName { get; set; }
|
||||||
|
|
||||||
|
public bool IsProtected { get; set; }
|
||||||
|
|
||||||
|
public float Size { get; set; }
|
||||||
|
|
||||||
|
public string BackupType { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> Addons { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> Folders { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace hassio_onedrive_backup.Contracts
|
||||||
|
{
|
||||||
|
internal class SyncFileData
|
||||||
|
{
|
||||||
|
public SyncFileData(string path, string hash, long size)
|
||||||
|
{
|
||||||
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||||
|
Hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||||
|
Size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public string Hash { get; }
|
||||||
|
|
||||||
|
public long Size { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Home Assistant Addon: OneDrive Backup
|
||||||
|
|
||||||
|
See [README on GitHub](https://github.com/lavinir/hassio-onedrive-backup) for a full guide on using the addon.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace hassio_onedrive_backup
|
||||||
|
{
|
||||||
|
public class DateTimeHelper
|
||||||
|
{
|
||||||
|
public const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
private readonly TimeZoneInfo? _timeZoneInfo = null;
|
||||||
|
|
||||||
|
private DateTimeHelper(string timeZoneId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error finding timezone: {ex}. Falling back to UTC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTimeHelper? Instance { get; private set; }
|
||||||
|
|
||||||
|
public static DateTimeHelper Initialize(string timeZoneId)
|
||||||
|
{
|
||||||
|
Instance = new DateTimeHelper(timeZoneId);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime Now
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var ret = _timeZoneInfo != null ? TimeZoneInfo.ConvertTime(now, _timeZoneInfo) : now;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
ARG SDK_IMAGE_ARCH_TAG
|
||||||
|
ARG RUNTIME_IMAGE_ARCH_TAG
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:$RUNTIME_IMAGE_ARCH_TAG AS base
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8099
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:$SDK_IMAGE_ARCH_TAG AS build
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["onedrive-backup.csproj", "."]
|
||||||
|
RUN dotnet restore "./onedrive-backup.csproj" -r $BUILD_ARCH
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/."
|
||||||
|
RUN dotnet build "onedrive-backup.csproj" -c Release -o /app/build -r $BUILD_ARCH
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
RUN dotnet publish "onedrive-backup.csproj" -c Release -o /app/publish -r $BUILD_ARCH --no-self-contained /p:EnvironmentName=Production
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "onedrive-backup.dll"]
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
LABEL \
|
||||||
|
io.hass.version="$VERSION" \
|
||||||
|
io.hass.type="addon" \
|
||||||
|
io.hass.arch="armhf|aarch64|amd64|armv7"
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using hassio_onedrive_backup;
|
||||||
|
using hassio_onedrive_backup.Contracts;
|
||||||
|
using onedrive_backup.Hass;
|
||||||
|
using onedrive_backup.Models;
|
||||||
|
using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
|
||||||
|
using Addon = hassio_onedrive_backup.Contracts.HassAddonsResponse.Addon;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Extensions
|
||||||
|
{
|
||||||
|
public static class BackupExtensions
|
||||||
|
{
|
||||||
|
public static BackupModel ToBackupModel(this Backup backup, HassContext hassContext)
|
||||||
|
{
|
||||||
|
return new BackupModel
|
||||||
|
{
|
||||||
|
Slug = backup.Slug,
|
||||||
|
Date = backup.Date,
|
||||||
|
Name = backup.Name,
|
||||||
|
Type = backup.Type,
|
||||||
|
Size = backup.Size,
|
||||||
|
IsProtected = backup.Protected,
|
||||||
|
Location = BackupModel.BackupLocation.Local,
|
||||||
|
Addons = backup.Content?.Addons?.Select(slug => new Addon { Slug = slug, Name = GetAddonNameFromSlug(hassContext.Addons, slug) }).ToList() ?? Enumerable.Empty<Addon>(),
|
||||||
|
Folders = backup.Content?.Folders ?? Enumerable.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackupModel ToBackupModel(this OnedriveBackup onedriveBackup, HassContext hassContext)
|
||||||
|
{
|
||||||
|
return new BackupModel
|
||||||
|
{
|
||||||
|
Slug = onedriveBackup.Slug,
|
||||||
|
Date = onedriveBackup.BackupDate,
|
||||||
|
OneDriveFileName = onedriveBackup.FileName,
|
||||||
|
Type = onedriveBackup.Type,
|
||||||
|
Size = onedriveBackup.Size,
|
||||||
|
IsProtected = onedriveBackup.IsProtected,
|
||||||
|
Location = BackupModel.BackupLocation.OneDrive,
|
||||||
|
Addons = onedriveBackup.Addons?.Select(slug => new Addon { Slug = slug, Name = GetAddonNameFromSlug(hassContext.Addons, slug) }) ?? Enumerable.Empty<Addon>(),
|
||||||
|
Folders = onedriveBackup.Folders ?? Enumerable.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OnedriveBackup ToOneDriveBackup(this BackupModel backupModel)
|
||||||
|
{
|
||||||
|
return new OnedriveBackup
|
||||||
|
{
|
||||||
|
Addons = backupModel.Addons.Select(addon => addon.Slug),
|
||||||
|
BackupDate = backupModel.Date,
|
||||||
|
FileName = backupModel.OneDriveFileName,
|
||||||
|
Folders = backupModel.Folders,
|
||||||
|
IsProtected = backupModel.IsProtected,
|
||||||
|
Slug = backupModel.Slug,
|
||||||
|
Type = backupModel.Type,
|
||||||
|
Size = backupModel.Size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Backup ToBackup(this BackupModel backupModel)
|
||||||
|
{
|
||||||
|
return new Backup
|
||||||
|
{
|
||||||
|
Date = backupModel.Date,
|
||||||
|
Protected = backupModel.IsProtected,
|
||||||
|
Slug = backupModel.Slug,
|
||||||
|
Type = backupModel.Type,
|
||||||
|
Size = backupModel.Size,
|
||||||
|
Name = backupModel.DisplayName,
|
||||||
|
Content = new Content
|
||||||
|
{
|
||||||
|
Addons = backupModel.Addons.Select(addon => addon.Slug).ToArray() ?? Enumerable.Empty<string>().ToArray(),
|
||||||
|
Folders = backupModel.Folders?.ToArray() ?? Enumerable.Empty<string>().ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAddonNameFromSlug(IEnumerable<Addon> addons, string slug)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogVerbose($"Looking for Addon name matching slug: {slug}. Checking agaisnt {addons.Count()} Addons in cache ");
|
||||||
|
string name = addons.FirstOrDefault(addon => addon.Slug.Equals(slug, StringComparison.OrdinalIgnoreCase))?.Name;
|
||||||
|
return name ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using onedrive_backup.Middleware;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Extensions
|
||||||
|
{
|
||||||
|
public static class MiddlewareExtensions
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseHassUrlExtractor(
|
||||||
|
this IApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.UseMiddleware<ExtractHassUrlMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder UseIncomingHassFirewallMiddleware(this IApplicationBuilder builder )
|
||||||
|
{
|
||||||
|
return builder.UseMiddleware<IncomingHassFirewallMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
using Azure.Core;
|
||||||
|
using Azure.Identity;
|
||||||
|
using hassio_onedrive_backup.Storage;
|
||||||
|
using Microsoft.Graph;
|
||||||
|
using onedrive_backup.Contracts;
|
||||||
|
using onedrive_backup.Graph;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using File = System.IO.File;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Graph
|
||||||
|
{
|
||||||
|
public class GraphHelper : IGraphHelper
|
||||||
|
{
|
||||||
|
private const string AuthRecordFile = "record.auth";
|
||||||
|
private const int UploadRetryCount = 3;
|
||||||
|
private const int DownloadRetryCount = 3;
|
||||||
|
private const int GraphRequestTimeoutMinutes = 2;
|
||||||
|
private const int ChunkSize = (320 * 1024) * 10;
|
||||||
|
private DeviceCodeCredential? _deviceCodeCredential;
|
||||||
|
protected GraphServiceClient? _userClient;
|
||||||
|
private IEnumerable<string> _scopes;
|
||||||
|
private string _clientId;
|
||||||
|
private string _persistentDataPath;
|
||||||
|
private HttpClient _downloadHttpClient;
|
||||||
|
private bool? _isAuthenticated = null;
|
||||||
|
|
||||||
|
public event AuthStatusChanged? AuthStatusChangedEventHandler;
|
||||||
|
|
||||||
|
public GraphHelper(
|
||||||
|
IEnumerable<string> scopes,
|
||||||
|
string clientId,
|
||||||
|
string persistentDataPath = "")
|
||||||
|
{
|
||||||
|
_scopes = scopes;
|
||||||
|
_clientId = clientId;
|
||||||
|
_persistentDataPath = persistentDataPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool? IsAuthenticated
|
||||||
|
{
|
||||||
|
get => _isAuthenticated;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_isAuthenticated = value; AuthStatusChangedEventHandler?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AuthUrl { get; private set; }
|
||||||
|
|
||||||
|
public string AuthCode { get; private set; }
|
||||||
|
|
||||||
|
private string PersistentAuthRecordFullPath => Path.Combine(_persistentDataPath, AuthRecordFile);
|
||||||
|
|
||||||
|
public async Task<string> GetAndCacheUserTokenAsync()
|
||||||
|
{
|
||||||
|
if (_deviceCodeCredential == null)
|
||||||
|
{
|
||||||
|
await InitializeGraphForUserAuthAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = _deviceCodeCredential ??
|
||||||
|
throw new NullReferenceException("User Auth not Initialized");
|
||||||
|
|
||||||
|
_ = _scopes ?? throw new ArgumentNullException("'scopes' cannot be null");
|
||||||
|
|
||||||
|
var context = new TokenRequestContext(_scopes.ToArray());
|
||||||
|
var response = await _deviceCodeCredential.GetTokenAsync(context);
|
||||||
|
await PersistAuthenticationRecordAsync(GetAuthenticationRecordFromCredential());
|
||||||
|
IsAuthenticated = true;
|
||||||
|
return response.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DriveItem?> GetItemInAppFolderAsync(string subPath = "")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var item = await _userClient.Me.Drive.Special.AppRoot.ItemWithPath(subPath).Request().Expand("children").GetAsync();
|
||||||
|
return item;
|
||||||
|
// return item.Children.ToList();
|
||||||
|
}
|
||||||
|
catch (ServiceException se)
|
||||||
|
{
|
||||||
|
if (se.StatusCode != System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DriveItem>?> GetItemsInAppFolderAsync(string subPath = "")
|
||||||
|
{
|
||||||
|
var parent = await GetItemInAppFolderAsync(subPath);
|
||||||
|
return parent?.Children?.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteItemFromAppFolderAsync(string itemPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"Deleting item: {itemPath}");
|
||||||
|
await _userClient.Drive.Special.AppRoot.ItemWithPath(itemPath).Request().DeleteAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error deleting {itemPath}. {ex}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DriveItem> GetOrCreateFolder(string folderPath)
|
||||||
|
{
|
||||||
|
var folder = (await GetItemInAppFolderAsync(folderPath)) ??
|
||||||
|
await _userClient.Drive.Special.AppRoot.ItemWithPath(folderPath).Children.Request().AddAsync(new DriveItem
|
||||||
|
{
|
||||||
|
// Name = Path.GetFileName(folderPath),
|
||||||
|
// Folder = new Folder { }
|
||||||
|
File = new Microsoft.Graph.File { },
|
||||||
|
Name = "temp.txt",
|
||||||
|
Content = new MemoryStream(Encoding.UTF8.GetBytes("Here's your damn content"))
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UploadFileAsync(string filePath, DateTime date, string? instanceName, TransferSpeedHelper transferSpeedHelper, string? destinationFileName = null, Action<int, int>? progressCallback = null, bool flatten = true, string description = null)
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"File {filePath} not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var fileStream = File.OpenRead(filePath);
|
||||||
|
destinationFileName = destinationFileName ?? (flatten ? Path.GetFileName(filePath) : filePath);
|
||||||
|
// string originalFileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
var uploadSession = await _userClient.Drive.Special.AppRoot.ItemWithPath(destinationFileName).CreateUploadSession(new DriveItemUploadableProperties
|
||||||
|
{
|
||||||
|
Description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
).Request().PostAsync();
|
||||||
|
|
||||||
|
// todo: allow settings this in advanced configuration
|
||||||
|
int maxSlizeSize = ChunkSize;
|
||||||
|
long totalFileLength = fileStream.Length;
|
||||||
|
var fileUploadTask = new LargeFileUploadTask<DriveItem>(uploadSession, fileStream, maxSlizeSize);
|
||||||
|
var lastShownPercentageHolder = new UploadProgressHolder();
|
||||||
|
IProgress<long> progress = new Progress<long>(async prog =>
|
||||||
|
{
|
||||||
|
(double delayMS, double speed) = transferSpeedHelper.MarkAndCalcThrottle(prog);
|
||||||
|
double percentage = Math.Round((prog / (double)totalFileLength), 2) * 100;
|
||||||
|
if (percentage - lastShownPercentageHolder.Percentage >= 10 || percentage == 100)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogVerbose($"Uploaded {percentage}%");
|
||||||
|
lastShownPercentageHolder.Percentage = percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback?.Invoke((int)percentage, (int)speed);
|
||||||
|
//if (delayMS >= 1)
|
||||||
|
//{
|
||||||
|
// await Task.Delay((int)delayMS);
|
||||||
|
//}
|
||||||
|
});
|
||||||
|
|
||||||
|
int uploadAttempt = 0;
|
||||||
|
while (uploadAttempt++ < UploadRetryCount)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"Starting file upload. (Size:{totalFileLength} bytes. Attempt: {uploadAttempt}/{UploadRetryCount})");
|
||||||
|
UploadResult<DriveItem> uploadResult;
|
||||||
|
transferSpeedHelper.Start();
|
||||||
|
if (uploadAttempt > 1)
|
||||||
|
{
|
||||||
|
uploadResult = await fileUploadTask.ResumeAsync(progress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uploadResult = await fileUploadTask.UploadAsync(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferSpeedHelper.Reset();
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||||
|
if (uploadResult.UploadSucceeded)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo("Upload completed successfully");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError("Upload failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ServiceException ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error uploading: {ex}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OneDriveFreeSpaceData> GetFreeSpaceInGB()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var drive = await _userClient.Drive.Request().GetAsync();
|
||||||
|
double? totalSpace = drive.Quota.Total == null ? null : drive.Quota.Total.Value / (double)Math.Pow(1024, 3);
|
||||||
|
double? freeSpace = drive.Quota.Remaining == null ? null : drive.Quota.Remaining.Value / (double)Math.Pow(1024, 3);
|
||||||
|
return new OneDriveFreeSpaceData
|
||||||
|
{
|
||||||
|
FreeSpace = freeSpace,
|
||||||
|
TotalSpace = totalSpace
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error getting free space: {ex}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> DownloadFileAsync(string fileName, Action<int?>? progressCallback)
|
||||||
|
{
|
||||||
|
var item = await _userClient.Drive.Special.AppRoot.ItemWithPath(fileName).Request().GetAsync();
|
||||||
|
if (item.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Failed getting file download data. ${fileName}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo($"{LocalStorage.TempFolder}/{fileName}");
|
||||||
|
using var fileStream = File.Create(fileInfo.FullName);
|
||||||
|
|
||||||
|
_downloadHttpClient = _downloadHttpClient ?? new HttpClient();
|
||||||
|
long position = 0;
|
||||||
|
int attempt = 1;
|
||||||
|
while (position < item.Size)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long chunkSize = Math.Min(position + ChunkSize, item.Size.Value - 1);
|
||||||
|
_downloadHttpClient.DefaultRequestHeaders.Range = new System.Net.Http.Headers.RangeHeaderValue(position, chunkSize);
|
||||||
|
var contentStream = await _downloadHttpClient.GetStreamAsync(downloadUrl.ToString());
|
||||||
|
await contentStream.CopyToAsync(fileStream);
|
||||||
|
position = chunkSize + 1;
|
||||||
|
progressCallback?.Invoke((int)(position * 100 / item.Size.Value));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (attempt >= DownloadRetryCount)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Failed downloading file {fileName}. {ex}");
|
||||||
|
progressCallback?.Invoke(null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback?.Invoke(null);
|
||||||
|
ConsoleLogger.LogInfo($"{fileName} downloaded successfully");
|
||||||
|
return fileInfo.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task InitializeGraphForUserAuthAsync()
|
||||||
|
{
|
||||||
|
AuthenticationRecord? authRecord = await ReadPersistedAuthenticationRecordAsync();
|
||||||
|
var deviceCodeCredOptions = new DeviceCodeCredentialOptions
|
||||||
|
{
|
||||||
|
ClientId = _clientId,
|
||||||
|
DeviceCodeCallback = DeviceCodeBallBackPrompt,
|
||||||
|
TenantId = "common",
|
||||||
|
AuthenticationRecord = authRecord,
|
||||||
|
TokenCachePersistenceOptions = new TokenCachePersistenceOptions
|
||||||
|
{
|
||||||
|
Name = "hassio-onedrive-backup",
|
||||||
|
UnsafeAllowUnencryptedStorage = true
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceCodeCredential = new DeviceCodeCredential(deviceCodeCredOptions);
|
||||||
|
_userClient = new GraphServiceClient(_deviceCodeCredential, _scopes);
|
||||||
|
_userClient.HttpProvider.OverallTimeout = TimeSpan.FromMinutes(GraphRequestTimeoutMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationRecord GetAuthenticationRecordFromCredential()
|
||||||
|
{
|
||||||
|
var record = typeof(DeviceCodeCredential)
|
||||||
|
.GetProperty("Record", System.Reflection.BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
.GetValue(_deviceCodeCredential) as AuthenticationRecord;
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistAuthenticationRecordAsync(AuthenticationRecord record)
|
||||||
|
{
|
||||||
|
using var authRecordStream = new FileStream(PersistentAuthRecordFullPath, FileMode.Create, FileAccess.Write);
|
||||||
|
await record.SerializeAsync(authRecordStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AuthenticationRecord?> ReadPersistedAuthenticationRecordAsync()
|
||||||
|
{
|
||||||
|
if (File.Exists(PersistentAuthRecordFullPath) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogWarning("Token cache is empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var authRecordStream = new FileStream(PersistentAuthRecordFullPath, FileMode.Open, FileAccess.Read);
|
||||||
|
var record = await AuthenticationRecord.DeserializeAsync(authRecordStream);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DeviceCodeBallBackPrompt(DeviceCodeInfo info, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IsAuthenticated = false;
|
||||||
|
ConsoleLogger.LogInfo(info.Message);
|
||||||
|
(AuthUrl, AuthCode) = ExtractAuthParams(info.Message);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string url, string code) ExtractAuthParams(string message)
|
||||||
|
{
|
||||||
|
Match match = Regex.Match(message, "To sign in, use a web browser to open the page ([^ ]*) and enter the code ([\\w]*) to authenticate");
|
||||||
|
string url = match.Groups[1].Value;
|
||||||
|
string code = match.Groups[2].Value;
|
||||||
|
return (url, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UploadProgressHolder
|
||||||
|
{
|
||||||
|
public double Percentage { get; set; } = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Graph;
|
||||||
|
using onedrive_backup.Contracts;
|
||||||
|
using onedrive_backup.Graph;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Graph
|
||||||
|
{
|
||||||
|
public interface IGraphHelper
|
||||||
|
{
|
||||||
|
string AuthUrl { get; }
|
||||||
|
|
||||||
|
string AuthCode { get; }
|
||||||
|
|
||||||
|
bool? IsAuthenticated { get; }
|
||||||
|
|
||||||
|
event AuthStatusChanged AuthStatusChangedEventHandler;
|
||||||
|
|
||||||
|
Task<string> GetAndCacheUserTokenAsync();
|
||||||
|
|
||||||
|
Task<List<DriveItem>> GetItemsInAppFolderAsync(string subPath = "");
|
||||||
|
|
||||||
|
Task<DriveItem?> GetItemInAppFolderAsync(string subPath = "");
|
||||||
|
|
||||||
|
Task<bool> DeleteItemFromAppFolderAsync(string itemPath);
|
||||||
|
|
||||||
|
Task<bool> UploadFileAsync(string filePath, DateTime date, string? instanceName, TransferSpeedHelper transferSpeedHelper, string? destinationFileName = null, Action<int, int>? progressCallback = null, bool flatten = true, string description = null);
|
||||||
|
|
||||||
|
Task<string?> DownloadFileAsync(string fileName, Action<int?>? progressCallback);
|
||||||
|
|
||||||
|
Task<OneDriveFreeSpaceData> GetFreeSpaceInGB();
|
||||||
|
|
||||||
|
Task<DriveItem> GetOrCreateFolder(string path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void AuthStatusChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Graph
|
||||||
|
{
|
||||||
|
public class TransferSpeedHelper
|
||||||
|
{
|
||||||
|
private Stopwatch _stopWatch = new Stopwatch();
|
||||||
|
private double _prevProgress = 0;
|
||||||
|
private double _prevTimeStamp = 0;
|
||||||
|
private double _speedCap;
|
||||||
|
|
||||||
|
public TransferSpeedHelper(double? speedCapKBSec)
|
||||||
|
{
|
||||||
|
_speedCap = speedCapKBSec != null ? speedCapKBSec.Value * 1024 : double.MaxValue;
|
||||||
|
}
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
_stopWatch.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_stopWatch.Reset();
|
||||||
|
_prevTimeStamp = 0;
|
||||||
|
_prevProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (double delay, double ratePerSecond) MarkAndCalcThrottle(double progress)
|
||||||
|
{
|
||||||
|
double bytesTransfered = progress - _prevProgress;
|
||||||
|
var ellapsedMS = _stopWatch.ElapsedMilliseconds;
|
||||||
|
double timefromPrev = ellapsedMS - _prevTimeStamp;
|
||||||
|
double ratePerSecond = 1000 * bytesTransfered / timefromPrev;
|
||||||
|
|
||||||
|
_prevTimeStamp = ellapsedMS;
|
||||||
|
_prevProgress = progress;
|
||||||
|
|
||||||
|
if (ratePerSecond > _speedCap)
|
||||||
|
{
|
||||||
|
var targetTimeForSpeedMS = (bytesTransfered / _speedCap) * 1000;
|
||||||
|
var remainingTimeToWait = targetTimeForSpeedMS - _prevTimeStamp;
|
||||||
|
return (remainingTimeToWait, ratePerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, ratePerSecond);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
using hassio_onedrive_backup;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using onedrive_backup.Hass;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Middleware
|
||||||
|
{
|
||||||
|
public class ExtractHassUrlMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private HassContext _ingressSettings;
|
||||||
|
private IHostEnvironment _env;
|
||||||
|
|
||||||
|
public ExtractHassUrlMiddleware(RequestDelegate next, HassContext ingressSettings, IHostEnvironment env)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_ingressSettings = ingressSettings;
|
||||||
|
_env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
const string HeaderKeyName = "X-Ingress-Path";
|
||||||
|
if (context.Request.Headers.TryGetValue(HeaderKeyName, out StringValues headerValue))
|
||||||
|
{
|
||||||
|
_ingressSettings.HeaderIngressPath = headerValue.ToString();
|
||||||
|
ConsoleLogger.LogInfo($"X-Ingress-Path: {_ingressSettings.HeaderIngressPath}");
|
||||||
|
}
|
||||||
|
else if (_env.IsDevelopment())
|
||||||
|
{
|
||||||
|
_ingressSettings.HeaderIngressPath = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using hassio_onedrive_backup;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Middleware
|
||||||
|
{
|
||||||
|
public class IncomingHassFirewallMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IHostEnvironment _env;
|
||||||
|
private const string _allowedIP = "172.30.32.2";
|
||||||
|
|
||||||
|
public IncomingHassFirewallMiddleware(RequestDelegate next, IHostEnvironment env)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var ip = context.Connection.RemoteIpAddress.MapToIPv4();
|
||||||
|
ConsoleLogger.LogVerbose($"Source IP: {ip}");
|
||||||
|
if (_env.IsDevelopment() ==false && ip.ToString().Equals(_allowedIP, StringComparison.OrdinalIgnoreCase) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Blocking request from unauthorized source IP : {ip.ToString()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// await context.Response.WriteAsync($"Request origin ({ip}) blocked.");
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
using static hassio_onedrive_backup.Contracts.HassAddonsResponse;
|
||||||
|
|
||||||
|
namespace onedrive_backup.Models
|
||||||
|
{
|
||||||
|
public class BackupModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
public BackupLocation Location { get; set; }
|
||||||
|
|
||||||
|
public string OneDriveFileName { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public bool IsProtected { get; set; }
|
||||||
|
|
||||||
|
public float Size { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<Addon> Addons { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> Folders { get; set; }
|
||||||
|
|
||||||
|
public string DisplayName => string.IsNullOrEmpty(Name) ? Path.GetFileNameWithoutExtension(OneDriveFileName) : Name;
|
||||||
|
|
||||||
|
public enum BackupLocation
|
||||||
|
{
|
||||||
|
Local,
|
||||||
|
OneDrive,
|
||||||
|
Both
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using hassio_onedrive_backup.Contracts;
|
||||||
|
using hassio_onedrive_backup.Graph;
|
||||||
|
using hassio_onedrive_backup.Hass;
|
||||||
|
using hassio_onedrive_backup.Sync;
|
||||||
|
using onedrive_backup.Graph;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup
|
||||||
|
{
|
||||||
|
public class Orchestrator
|
||||||
|
{
|
||||||
|
private readonly IHassioClient _hassIoClient;
|
||||||
|
private readonly HassOnedriveFreeSpaceEntityState? _hassOnedriveFreeSpaceEntityState;
|
||||||
|
private readonly IGraphHelper _graphHelper;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly AddonOptions _addonOptions;
|
||||||
|
private readonly BitArray _allowedBackupHours;
|
||||||
|
|
||||||
|
private bool _enabled = false;
|
||||||
|
|
||||||
|
public Orchestrator(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_addonOptions = serviceProvider.GetService<AddonOptions>();
|
||||||
|
_graphHelper = serviceProvider.GetService<IGraphHelper>();
|
||||||
|
_hassIoClient = serviceProvider.GetService<IHassioClient>();
|
||||||
|
_hassOnedriveFreeSpaceEntityState = serviceProvider.GetService<HassOnedriveFreeSpaceEntityState>();
|
||||||
|
|
||||||
|
_allowedBackupHours = TimeRangeHelper.GetAllowedHours(_addonOptions.BackupAllowedHours);
|
||||||
|
BackupManager = new BackupManager(_serviceProvider, _allowedBackupHours, new TransferSpeedHelper(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupManager BackupManager { get; set; }
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
|
{
|
||||||
|
_enabled = true;
|
||||||
|
string timeZoneId = await _hassIoClient.GetTimeZoneAsync();
|
||||||
|
DateTimeHelper.Initialize(timeZoneId);
|
||||||
|
TimeSpan intervalDelay = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
ConsoleLogger.LogInfo($"Backup interval configured to every {_addonOptions.BackupIntervalHours} hours");
|
||||||
|
if (string.IsNullOrWhiteSpace(_addonOptions.BackupAllowedHours) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"Backups / Syncs will only run during these hours: {_allowedBackupHours.ToAllowedHoursText()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (_addonOptions.UploadSpeedCap)
|
||||||
|
//{
|
||||||
|
// ConsoleLogger.LogInfo($"Upload speed configured to cap at {_addonOptions.UploadSpeedCapKBPerSecond} KB/s");
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Initialize File Sync Manager
|
||||||
|
if (_addonOptions.FileSyncEnabled)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"File Sync Enabled");
|
||||||
|
var transferSpeedHelper = new TransferSpeedHelper(null);
|
||||||
|
var syncManager = new SyncManager(_serviceProvider, _allowedBackupHours, transferSpeedHelper);
|
||||||
|
var tokenSource = new CancellationTokenSource();
|
||||||
|
await _graphHelper.GetAndCacheUserTokenAsync();
|
||||||
|
var fileSyncTask = Task.Run(() => syncManager.SyncLoop(tokenSource.Token), tokenSource.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"File Sync Disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_enabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Refresh Graph Token
|
||||||
|
await _graphHelper.GetAndCacheUserTokenAsync();
|
||||||
|
|
||||||
|
// Update OneDrive Freespace Sensor
|
||||||
|
var oneDriveSpace = await _graphHelper.GetFreeSpaceInGB();
|
||||||
|
if (oneDriveSpace != null)
|
||||||
|
{
|
||||||
|
await _hassOnedriveFreeSpaceEntityState.UpdateOneDriveFreespaceSensorInHass(oneDriveSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleLogger.LogVerbose("Checking backups");
|
||||||
|
|
||||||
|
BackupManager.PerformBackupsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Unexpected error. {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleLogger.LogVerbose("Backup Interval Completed.");
|
||||||
|
await Task.Delay(intervalDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@page "/about"
|
||||||
|
|
||||||
|
<h1>About...</h1>
|
||||||
|
|
||||||
|
<h2>Official Repository</h2>
|
||||||
|
<p>You can find the code and documentation for the addon on the officail <a href="https://github.com/lavinir/hassio-onedrive-backup" target="_blank">GitHub Repo</a></p>
|
||||||
|
|
||||||
|
<h2>Open Issues</h2>
|
||||||
|
<p>If you encounter any bugs or issues with the add-on, please open an issue if needed <a href="https://github.com/lavinir/hassio-onedrive-backup/issues" target="_blank">here</a></p>
|
||||||
|
|
||||||
|
<h2>Suggest Features</h2>
|
||||||
|
<p>If you have any suggestions for new features or improvements to the add-on, please submit them on the offical <a href="https://github.com/lavinir/hassio-onedrive-backup/discussions/categories/ideas" target="_blank">GitHub repository discussions</a>.</p>
|
||||||
|
|
||||||
|
<h2>License</h2>
|
||||||
|
<p>I originally wanted a native cloud backup solution for Home Assistant with OneDrive which is why I created this. It has become a passion project and I am actively maintaining and supporting it. Completely free to use but if you
|
||||||
|
would like to show support for the project you could <a href="https://www.buymeacoffee.com/snirlavis" target="_blank"><img class="mx-1" src="bmc/bmc-button.svg" alt="Buy Me A Coffee" style="height: 1.8rem !important;"></a> :)
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<img src="images/Bitcoin-Logo.svg" alt="Bitcoin logo" style="width: 100px; height: auto; transform: scale(0.8);">
|
||||||
|
<img src="images/bc_address.png" alt="Bitcoin address" style="width: 100px; height: auto;">
|
||||||
|
<span class="text-center"><strong>Bitcoin Address</strong>: bc1q8htme48sx54gvctk59yxg3ts940ncf6p8377r6</span>
|
||||||
|
</div>
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@page
|
||||||
|
@model hassio_onedrive_backup.Pages.ErrorModel
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>Error</title>
|
||||||
|
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="content px-4">
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Pages
|
||||||
|
{
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
[IgnoreAntiforgeryToken]
|
||||||
|
public class ErrorModel : PageModel
|
||||||
|
{
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
private readonly ILogger<ErrorModel> _logger;
|
||||||
|
|
||||||
|
public ErrorModel(ILogger<ErrorModel> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@page "/"
|
||||||
|
@using hassio_onedrive_backup.Contracts;
|
||||||
|
@using hassio_onedrive_backup.Graph;
|
||||||
|
@using hassio_onedrive_backup;
|
||||||
|
@using onedrive_backup.Extensions;
|
||||||
|
@using onedrive_backup.Hass;
|
||||||
|
@using onedrive_backup.Models;
|
||||||
|
@using static hassio_onedrive_backup.Contracts.HassBackupsResponse;
|
||||||
|
@inject IGraphHelper GraphHelper
|
||||||
|
@inject Orchestrator Orchestrator
|
||||||
|
@inject HassContext HassContext;
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 d-flex justify-content-center">
|
||||||
|
<onedrive_backup.Shared.OneDriveSummaryCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||||
|
@foreach (var backup in Backups)
|
||||||
|
{
|
||||||
|
<div class="col">
|
||||||
|
<onedrive_backup.Shared.BackupCard Backup="@backup" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private List<BackupModel> Backups => GetBackupModels();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
GraphHelper.AuthStatusChangedEventHandler += OnStateChanged;
|
||||||
|
Orchestrator.BackupManager.OneDriveBackupsUpdated += OnStateChanged;
|
||||||
|
Orchestrator.BackupManager.LocalBackupsUpdated += OnStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BackupModel> GetBackupModels()
|
||||||
|
{
|
||||||
|
var ret = new List<BackupModel>();
|
||||||
|
var _localBackups = Orchestrator.BackupManager.LocalBackups?.Select(backup => backup.ToBackupModel(HassContext)) ?? Enumerable.Empty<BackupModel>();
|
||||||
|
var _onedriveBackups = Orchestrator.BackupManager.OnlineBackups?.Select(backup => backup.ToBackupModel(HassContext)) ?? Enumerable.Empty<BackupModel>();
|
||||||
|
|
||||||
|
ret.AddRange(_onedriveBackups);
|
||||||
|
foreach (var localBackup in _localBackups)
|
||||||
|
{
|
||||||
|
if (_onedriveBackups.Any(oneDriveBackup => oneDriveBackup.Slug == localBackup.Slug))
|
||||||
|
{
|
||||||
|
var syncedBackup = ret.Find(backup => backup.Slug == localBackup.Slug);
|
||||||
|
syncedBackup.Location = BackupModel.BackupLocation.Both;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Add(localBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = ret.OrderByDescending(backup => backup.Date).ToList();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
private async void OnStateChanged()
|
||||||
|
{
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@page "/"
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using onedrive_backup;
|
||||||
|
@using onedrive_backup.Hass;
|
||||||
|
@namespace hassio_onedrive_backup.Pages
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@inject HassContext IngressSettings
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="@IngressSettings.IngressUrl" />
|
||||||
|
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css" />
|
||||||
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
|
<link href="onedrive-backup.styles.css" rel="stylesheet" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
||||||
|
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
|
||||||
|
</head>
|
||||||
|
<body style="background: #f4f4f4; background-image: linear-gradient(to bottom, #f4f4f4, #e6e6e6);">
|
||||||
|
<div id="alertPlaceholder"></div>
|
||||||
|
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
<environment include="Staging,Production">
|
||||||
|
An error has occurred. This application may no longer respond until reloaded.
|
||||||
|
</environment>
|
||||||
|
<environment include="Development">
|
||||||
|
An unhandled exception has occurred. See browser dev tools for details.
|
||||||
|
</environment>
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="lib/jquery/jquery.slim.min.js"></script>
|
||||||
|
<script src="lib/popper.js/umd/popper.min.js"></script>
|
||||||
|
<script src="lib/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script src="scripts/BlazorHelpers.js"></script>
|
||||||
|
<script src="_framework/blazor.server.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using hassio_onedrive_backup.Contracts;
|
||||||
|
using hassio_onedrive_backup.Graph;
|
||||||
|
using hassio_onedrive_backup.Hass;
|
||||||
|
using hassio_onedrive_backup.Storage;
|
||||||
|
using hassio_onedrive_backup.Sync;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
using onedrive_backup;
|
||||||
|
using onedrive_backup.Extensions;
|
||||||
|
using onedrive_backup.Graph;
|
||||||
|
using onedrive_backup.Hass;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
private const string clientId = "b8a647cf-eccf-4c7f-a0a6-2cbec5d0b94d";
|
||||||
|
private const string addonDirectory = "/data";
|
||||||
|
private static readonly List<string> scopes = new() { "Files.ReadWrite.AppFolder" };
|
||||||
|
|
||||||
|
private static Orchestrator _orchestrator;
|
||||||
|
private static string _baseDirectory;
|
||||||
|
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
_baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
IHassioClient hassIoClient = new HassioClientMock();
|
||||||
|
var addonOptions = AddonOptionsReader.ReadOptions();
|
||||||
|
#else
|
||||||
|
|
||||||
|
Directory.SetCurrentDirectory(addonDirectory);
|
||||||
|
var addonOptions = AddonOptionsReader.ReadOptions();
|
||||||
|
string supervisorToken = Environment.GetEnvironmentVariable("SUPERVISOR_TOKEN")!;
|
||||||
|
IHassioClient hassIoClient = new HassioClient(supervisorToken, addonOptions.HassAPITimeoutMinutes);
|
||||||
|
#endif
|
||||||
|
ConsoleLogger.SetLogLevel(addonOptions.LogLevel);
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
LocalStorage.InitializeTempStorage();
|
||||||
|
IGraphHelper graphHelper = new GraphHelper(scopes, clientId);
|
||||||
|
var addons = hassIoClient.GetAddonsAsync().Result;
|
||||||
|
ConsoleLogger.LogVerbose($"Detected Addons: {string.Join(",", addons.Select(addon => addon.Slug))}");
|
||||||
|
HassContext hassContext = null;
|
||||||
|
var addonInfo = hassIoClient.GetAddonInfo("self").Result;
|
||||||
|
hassContext = new HassContext { IngressUrl = addonInfo.DataProperty.IngressUrl, Addons = addons };
|
||||||
|
ConsoleLogger.LogVerbose($"Ingress URL: {addonInfo.DataProperty.IngressUrl}");
|
||||||
|
builder.Services.AddSingleton(hassContext);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddRazorPages();
|
||||||
|
builder.Services.AddServerSideBlazor();
|
||||||
|
builder.Services.AddSingleton(addonOptions);
|
||||||
|
builder.Services.AddSingleton<IHassioClient>(hassIoClient);
|
||||||
|
builder.Services.AddSingleton<IGraphHelper>(graphHelper);
|
||||||
|
builder.Services.AddSingleton<HassOnedriveEntityState>();
|
||||||
|
builder.Services.AddSingleton<HassOnedriveFileSyncEntityState>();
|
||||||
|
builder.Services.AddSingleton<HassOnedriveFreeSpaceEntityState>();
|
||||||
|
builder.Services.AddSingleton<Orchestrator>();
|
||||||
|
builder.WebHost.UseUrls("http://*:8099");
|
||||||
|
|
||||||
|
if (!builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
// builder.Logging.AddConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
_orchestrator = app.Services.GetService<Orchestrator>();
|
||||||
|
_orchestrator.Start();
|
||||||
|
|
||||||
|
app.UseIncomingHassFirewallMiddleware();
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseWhen(ctx => !ctx.Request.Path
|
||||||
|
.StartsWithSegments("/_framework/blazor.server.js"),
|
||||||
|
subApp => subApp.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
FileProvider = new PhysicalFileProvider($"{_baseDirectory}/wwwroot")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo("Dev Mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app.UsePathBase($"{hassContext?.HeaderIngressPath ?? "/"}");
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// app.UseHassUrlExtractor();
|
||||||
|
|
||||||
|
app.MapBlazorHub();
|
||||||
|
app.MapFallbackToPage("/_Host");
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"onedrive_backup": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "http://localhost:8099;",
|
||||||
|
"hotReloadEnabled": false
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker": {
|
||||||
|
"commandName": "Docker",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||||
|
"publishAllPorts": true,
|
||||||
|
"useSSL": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:9765",
|
||||||
|
"sslPort": 44347
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Home Assistant Addon: OneDrive Backup
|
||||||
|
|
||||||
|
Simple OneDrive integration for seamless Home Assistant Backups to the cloud.
|
||||||
|
|
||||||
|
|
||||||
|
### Current Features
|
||||||
|
- Set backup creation schedule
|
||||||
|
- Syncs backups to OneDrive **(Personal Account only. OneDrive for business not currently supported)**
|
||||||
|
- Dedicated Web UI
|
||||||
|
- Additional File Sync to OneDrive
|
||||||
|
- Recovery mode for loading backups back from OneDrive to Home Assistant
|
||||||
|
- Supports backup retention for removing older backups
|
||||||
|
- Supports multiple Home Assistant instances
|
||||||
|
- Supports Home Assistant Persistent Notifications
|
||||||
|
- Supports Home Assistant Events
|
||||||
|
- Includes Sensor Entities for Dashboards / Automations
|
||||||
|
|
||||||
|
Full documentation available on project page at [GitHub](https://github.com/lavinir/hassio-onedrive-backup)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
This is a free addon but if you enjoy this addon and would like to support the development it would be much appreciated :)
|
||||||
|
|
||||||
|
[<img src ="https://raw.githubusercontent.com/lavinir/hassio-onedrive-backup/main/onedrive-backup/images/bmc.svg" width="150">](https://www.buymeacoffee.com/snirlavis)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>© 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;
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using hassio_onedrive_backup.Contracts;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Storage
|
||||||
|
{
|
||||||
|
internal class AddonOptionsReader
|
||||||
|
{
|
||||||
|
public static AddonOptions ReadOptions(string path = "options.json")
|
||||||
|
{
|
||||||
|
if (File.Exists(path) == false)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"{path} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
string optionContents = File.ReadAllText(path);
|
||||||
|
var options = JsonConvert.DeserializeObject<AddonOptions>(optionContents);
|
||||||
|
return options!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace hassio_onedrive_backup.Storage
|
||||||
|
{
|
||||||
|
internal class LocalStorage
|
||||||
|
{
|
||||||
|
public const string TempFolder = "../tmp";
|
||||||
|
private const string oldTempFolder = "./tmp";
|
||||||
|
|
||||||
|
public static void InitializeTempStorage()
|
||||||
|
{
|
||||||
|
// Legacy cleanup
|
||||||
|
if (Directory.Exists(oldTempFolder))
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogVerbose($"Deleting deprecated temp storage folder");
|
||||||
|
Directory.Delete(oldTempFolder, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear temporary storage
|
||||||
|
if (Directory.Exists(TempFolder))
|
||||||
|
{
|
||||||
|
if (Directory.EnumerateFiles($"{TempFolder}").Any())
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogVerbose("Cleaning up temporary artifcats");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.Delete(TempFolder, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)Create temporary directory
|
||||||
|
Directory.CreateDirectory(TempFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
using hassio_onedrive_backup.Contracts;
|
||||||
|
using hassio_onedrive_backup.Graph;
|
||||||
|
using hassio_onedrive_backup.Hass;
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
|
using Microsoft.Graph;
|
||||||
|
using onedrive_backup.Graph;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup.Sync
|
||||||
|
{
|
||||||
|
internal class SyncManager
|
||||||
|
{
|
||||||
|
private const string HashAlgo = "SHA256";
|
||||||
|
private const string OneDriveFileSyncRootDir = "FileSync";
|
||||||
|
private AddonOptions _addonOptions;
|
||||||
|
private IGraphHelper _graphHelper;
|
||||||
|
private IHassioClient _hassIoClient;
|
||||||
|
private BitArray _allowedHours;
|
||||||
|
private HassOnedriveFileSyncEntityState _hassEntityState;
|
||||||
|
private IWebHostEnvironment? _environment;
|
||||||
|
private TransferSpeedHelper _transferSpeedHelper;
|
||||||
|
private Matcher _fileMatcher;
|
||||||
|
|
||||||
|
public SyncManager(IServiceProvider serviceProvider, BitArray allowedHours, TransferSpeedHelper? transferSpeedHelper)
|
||||||
|
{
|
||||||
|
_addonOptions = serviceProvider.GetService<AddonOptions>();
|
||||||
|
_graphHelper = serviceProvider.GetService<IGraphHelper>();
|
||||||
|
_hassIoClient = serviceProvider.GetService<IHassioClient>();
|
||||||
|
_hassEntityState = serviceProvider.GetService<HassOnedriveFileSyncEntityState>();
|
||||||
|
_environment = serviceProvider.GetService<IWebHostEnvironment>();
|
||||||
|
_transferSpeedHelper = transferSpeedHelper;
|
||||||
|
_allowedHours = allowedHours;
|
||||||
|
_fileMatcher = new();
|
||||||
|
_fileMatcher.AddIncludePatterns(_addonOptions.SyncPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void SyncLoop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (true && ct.IsCancellationRequested == false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTimeHelper.Instance!.Now;
|
||||||
|
|
||||||
|
// Check if we are in the allowed hours
|
||||||
|
if (_allowedHours[now.Hour] == false)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hassEntityState.State = HassOnedriveFileSyncEntityState.FileState.Syncing;
|
||||||
|
await _hassEntityState.UpdateBackupEntityInHass();
|
||||||
|
|
||||||
|
var matchingFiles = _fileMatcher.GetResultsInFullPath("/");
|
||||||
|
foreach (var matchingFile in matchingFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SyncFile(matchingFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error syncing file {matchingFile}: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_addonOptions.FileSyncRemoveDeleted)
|
||||||
|
{
|
||||||
|
await DeleteRemovedFilesFromOneDrive($"{OneDriveFileSyncRootDir}", "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogError($"Error Syncing: {ex}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_hassEntityState.State = HassOnedriveFileSyncEntityState.FileState.Synced;
|
||||||
|
await _hassEntityState.UpdateBackupEntityInHass();
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SyncFile(string path)
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
if (fileInfo.Length == 0)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogWarning($"Skipping 0-byte file: {path}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTimeHelper.Instance!.Now;
|
||||||
|
string fileHash = CalculateFileHash(path);
|
||||||
|
var localFileSyncData = new SyncFileData(path, fileHash, fileInfo.Length);
|
||||||
|
|
||||||
|
string remotePath = $"/{OneDriveFileSyncRootDir}{path}".Replace("//", "/").Replace(@"\\", @"\");
|
||||||
|
if (_environment!.IsDevelopment())
|
||||||
|
{
|
||||||
|
remotePath = remotePath.Replace(@"c:\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveItem? remoteFile = await _graphHelper.GetItemInAppFolderAsync(remotePath);
|
||||||
|
bool requiresUpload =
|
||||||
|
remoteFile == null
|
||||||
|
|| remoteFile.Size != fileInfo.Length
|
||||||
|
|| remoteFile.File.Hashes.Sha256Hash.Equals(fileHash, StringComparison.OrdinalIgnoreCase) == false;
|
||||||
|
|
||||||
|
if (requiresUpload == false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleLogger.LogInfo($"File {path} out of sync. Starting Upload");
|
||||||
|
var uploadSuccessful = await _graphHelper.UploadFileAsync(
|
||||||
|
path,
|
||||||
|
now,
|
||||||
|
_addonOptions.InstanceName,
|
||||||
|
_transferSpeedHelper,
|
||||||
|
remotePath,
|
||||||
|
async (prog, speed) =>
|
||||||
|
{
|
||||||
|
_hassEntityState.UploadPercentage = prog;
|
||||||
|
_hassEntityState.UploadSpeed = speed / 1024;
|
||||||
|
await _hassEntityState.UpdateBackupEntityInHass();
|
||||||
|
Debug.WriteLine($"Progress: {prog}");
|
||||||
|
|
||||||
|
},
|
||||||
|
flatten: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CalculateFileHash(string path)
|
||||||
|
{
|
||||||
|
using (var hasher = HashAlgorithm.Create(HashAlgo))
|
||||||
|
{
|
||||||
|
using (var fileStream = System.IO.File.OpenRead(path))
|
||||||
|
{
|
||||||
|
byte[] hash = hasher!.ComputeHash(fileStream);
|
||||||
|
return BitConverter.ToString(hash).Replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteRemovedFilesFromOneDrive(string remotePath, string localPath)
|
||||||
|
{
|
||||||
|
var item = await _graphHelper.GetItemInAppFolderAsync(remotePath);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleLogger.LogVerbose($"Evaulating {remotePath}");
|
||||||
|
localPath = Path.Combine(localPath, item.Name);
|
||||||
|
if (localPath.StartsWith($"/{OneDriveFileSyncRootDir}"))
|
||||||
|
{
|
||||||
|
localPath = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Folder != null)
|
||||||
|
{
|
||||||
|
if (System.IO.Directory.Exists(localPath) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"{localPath} does not exist locally. Deleting from OneDrive");
|
||||||
|
await _graphHelper.DeleteItemFromAppFolderAsync(remotePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Recursive call for files in Folder
|
||||||
|
var folderItems = await _graphHelper.GetItemsInAppFolderAsync(remotePath);
|
||||||
|
foreach (var folderItem in folderItems)
|
||||||
|
{
|
||||||
|
await DeleteRemovedFilesFromOneDrive(Path.Combine(remotePath, folderItem.Name), localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no content left in folder then remove it
|
||||||
|
folderItems = await _graphHelper.GetItemsInAppFolderAsync(remotePath);
|
||||||
|
if (folderItems.Count == 0)
|
||||||
|
{
|
||||||
|
await _graphHelper.DeleteItemFromAppFolderAsync(remotePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (item.File != null)
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(localPath) == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"{localPath} does not exist locally. Deleting from OneDrive");
|
||||||
|
await _graphHelper.DeleteItemFromAppFolderAsync(remotePath);
|
||||||
|
}
|
||||||
|
else if (_fileMatcher.Match(localPath).HasMatches == false)
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogInfo($"{localPath} not included in Sync Paths. Deleting from OneDrive");
|
||||||
|
await _graphHelper.DeleteItemFromAppFolderAsync(remotePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConsoleLogger.LogVerbose($"{remotePath} in sync with {localPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace hassio_onedrive_backup
|
||||||
|
{
|
||||||
|
public static class TimeRangeHelper
|
||||||
|
{
|
||||||
|
public static BitArray GetAllowedHours(string? allowedHoursExpression)
|
||||||
|
{
|
||||||
|
var allowedHours = new BitArray(24);
|
||||||
|
if (string.IsNullOrWhiteSpace(allowedHoursExpression))
|
||||||
|
{
|
||||||
|
allowedHours.SetAll(true);
|
||||||
|
return allowedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sections = allowedHoursExpression.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (string section in sections)
|
||||||
|
{
|
||||||
|
string fromStr;
|
||||||
|
string toStr;
|
||||||
|
|
||||||
|
fromStr = section.StartsWith('-') ? "0" : section.Split('-').FirstOrDefault(s => string.IsNullOrWhiteSpace(s) == false, "0");
|
||||||
|
toStr = section.EndsWith('-') ? "23" : section.Split('-').LastOrDefault(s => string.IsNullOrWhiteSpace(s) == false, "23");
|
||||||
|
|
||||||
|
int from = int.Parse(fromStr);
|
||||||
|
int to = int.Parse(toStr);
|
||||||
|
|
||||||
|
for (int i = from; i <= to; i++)
|
||||||
|
{
|
||||||
|
allowedHours.Set(i, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToAllowedHoursText(this BitArray bitArray)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i=0; i< bitArray.Length; i++)
|
||||||
|
{
|
||||||
|
if (bitArray[i])
|
||||||
|
{
|
||||||
|
sb.Append($"{i},");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Remove(sb.Length - 1, 1);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile hassio_onedrive_backup flags=(attach_disconnected,mediate_deleted) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
file,
|
||||||
|
signal (send) set=(kill,term,int,hup,cont),
|
||||||
|
network,
|
||||||
|
deny network raw,
|
||||||
|
|
||||||
|
# S6-Overlay
|
||||||
|
/init ix,
|
||||||
|
/bin/** ix,
|
||||||
|
/usr/bin/** ix,
|
||||||
|
/run/{s6,s6-rc*,service}/** ix,
|
||||||
|
/package/** ix,
|
||||||
|
/command/** ix,
|
||||||
|
/etc/services.d/** rwix,
|
||||||
|
/etc/cont-init.d/** rwix,
|
||||||
|
/etc/cont-finish.d/** rwix,
|
||||||
|
/run/{,**} rwk,
|
||||||
|
/dev/tty rw,
|
||||||
|
|
||||||
|
# Addon Data
|
||||||
|
/data/** rw,
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"DetailedErrors": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Error",
|
||||||
|
"Microsoft.AspNetCore": "Error"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Error",
|
||||||
|
"Microsoft.AspNetCore": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Urls": "http://*:8099"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"DetailedErrors": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Error",
|
||||||
|
"Microsoft.AspNetCore": "Error"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Error",
|
||||||
|
"Microsoft.AspNetCore": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Urls": "http://*:8099"
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$version,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$signImages,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$casApiKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$env:CAS_API_KEY = $casApiKey
|
||||||
|
& cas-v1.0.3-windows-amd64.exe login
|
||||||
|
|
||||||
|
################## linux-x64
|
||||||
|
Write-Host "Building linux-x64"
|
||||||
|
docker build -t "ghcr.io/lavinir/amd64-hassonedrive:$($version)" --build-arg BUILD_ARCH=linux-x64 --build-arg SDK_IMAGE_ARCH_TAG=7.0-alpine --build-arg RUNTIME_IMAGE_ARCH_TAG=7.0.3-alpine3.17-amd64 .
|
||||||
|
|
||||||
|
if ($signImages.IsPresent) {
|
||||||
|
Write-Host "Signing linux-x64 Image"
|
||||||
|
& cas-v1.0.3-windows-amd64.exe notarize --bom "docker://ghcr.io/lavinir/amd64-hassonedrive:$($version)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Publishing linux-x64 Image"
|
||||||
|
docker push "ghcr.io/lavinir/amd64-hassonedrive:$($version)"
|
||||||
|
|
||||||
|
################## linux-arm
|
||||||
|
Write-Host "Building linux-arm"
|
||||||
|
docker build -t "ghcr.io/lavinir/armv7-hassonedrive:$($version)" --build-arg SDK_IMAGE_ARCH_TAG=7.0-alpine --build-arg RUNTIME_IMAGE_ARCH_TAG=7.0.3-alpine3.17-arm32v7 --build-arg BUILD_ARCH=linux-arm .
|
||||||
|
docker build -t "ghcr.io/lavinir/armhf-hassonedrive:$($version)" --build-arg SDK_IMAGE_ARCH_TAG=7.0-alpine --build-arg RUNTIME_IMAGE_ARCH_TAG=7.0.3-alpine3.17-arm32v7 --build-arg BUILD_ARCH=linux-arm .
|
||||||
|
|
||||||
|
if ($signImages.IsPresent) {
|
||||||
|
Write-Host "Signing linux-arm Images"
|
||||||
|
& cas-v1.0.3-windows-amd64.exe notarize --bom "docker://ghcr.io/lavinir/armv7-hassonedrive:$($version)"
|
||||||
|
& cas-v1.0.3-windows-amd64.exe notarize --bom "docker://ghcr.io/lavinir/armhf-hassonedrive:$($version)"
|
||||||
|
}
|
||||||
|
Write-Host "Publishing linux-arm Images"
|
||||||
|
docker push "ghcr.io/lavinir/armhf-hassonedrive:$($version)"
|
||||||
|
docker push "ghcr.io/lavinir/armv7-hassonedrive:$($version)"
|
||||||
|
|
||||||
|
################## linux-arm64
|
||||||
|
Write-Host "Building linux-arm64"
|
||||||
|
docker build -t "ghcr.io/lavinir/aarch64-hassonedrive:$($version)" --build-arg SDK_IMAGE_ARCH_TAG=7.0-alpine --build-arg RUNTIME_IMAGE_ARCH_TAG=7.0.3-alpine3.17-arm64v8 --build-arg BUILD_ARCH=linux-arm64 .
|
||||||
|
|
||||||
|
if ($signImages.IsPresent) {
|
||||||
|
Write-Host "Signing linux-arm64 Image"
|
||||||
|
& cas-v1.0.3-windows-amd64.exe notarize --bom "docker://ghcr.io/lavinir/aarch64-hassonedrive:$($version)"
|
||||||
|
}
|
||||||
|
Write-Host "Publishing linux-arm64 Image"
|
||||||
|
docker push "ghcr.io/lavinir/aarch64-hassonedrive:$($version)"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: "OneDrive Backup"
|
||||||
|
version: "2.1.2"
|
||||||
|
slug: hassio_onedrive_backup
|
||||||
|
description: >-
|
||||||
|
Automatic backups from Home Assistant to OneDrive
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- armhf
|
||||||
|
- armv7
|
||||||
|
- aarch64
|
||||||
|
url: "https://github.com/smarthomekh/hassio-onedrive-backup"
|
||||||
|
map:
|
||||||
|
- config:rw
|
||||||
|
- ssl:rw
|
||||||
|
- addons:rw
|
||||||
|
- media:rw
|
||||||
|
- share:rw
|
||||||
|
- backup:rw
|
||||||
|
options:
|
||||||
|
local_backup_num_to_keep : 2
|
||||||
|
onedrive_backup_num_to_keep : 2
|
||||||
|
backup_interval_days : 3
|
||||||
|
backup_name : "hass_backup"
|
||||||
|
monitor_all_local_backups: false
|
||||||
|
ignore_upgrade_backups: false
|
||||||
|
hass_api_timeout_minutes: 30
|
||||||
|
notify_on_error : true
|
||||||
|
exclude_media_folder: false
|
||||||
|
exclude_ssl_folder: false
|
||||||
|
exclude_share_folder: false
|
||||||
|
exclude_local_addons_folder: false
|
||||||
|
sync_paths: []
|
||||||
|
file_sync_remove_deleted: false
|
||||||
|
excluded_addons: []
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
schema:
|
||||||
|
local_backup_num_to_keep : "int(0,)"
|
||||||
|
onedrive_backup_num_to_keep : "int(1,100)"
|
||||||
|
backup_interval_days : "float(0.0417,)"
|
||||||
|
backup_name : "match([a-zA-Z_]*)"
|
||||||
|
monitor_all_local_backups: bool
|
||||||
|
ignore_upgrade_backups: bool
|
||||||
|
backup_passwd : "password?"
|
||||||
|
hass_api_timeout_minutes : "int(30,)"
|
||||||
|
notify_on_error : bool
|
||||||
|
exclude_media_folder: bool
|
||||||
|
exclude_ssl_folder: bool
|
||||||
|
exclude_share_folder: bool
|
||||||
|
exclude_local_addons_folder: bool
|
||||||
|
backup_allowed_hours: "str?"
|
||||||
|
backup_instance_name: "match([a-zA-Z_]*)?"
|
||||||
|
sync_paths:
|
||||||
|
- 'match(\/config[\/\w.*-]*|\/ssl[\/\w.*-]*|\/addons[\/\w.*-]*|\/media[\/\w.*-]*|\/share[\/\w.*-]*|)'
|
||||||
|
file_sync_remove_deleted: bool
|
||||||
|
excluded_addons:
|
||||||
|
- str
|
||||||
|
log_level: list(verbose|info|warning|error)
|
||||||
|
|
||||||
|
ingress: true
|
||||||
|
hassio_role : "manager"
|
||||||
|
codenotary: "tekdara@me.com"
|
||||||
|
apparmor: true
|
||||||
|
hassio_api : true
|
||||||
|
homeassistant_api: true
|
||||||
|
image: ghcr.io/lavinir/{arch}-hassonedrive
|
||||||
|
panel_icon: mdi:microsoft-onedrive
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.3.32929.385
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hassio-onedrive-backup", "hassio-onedrive-backup.csproj", "{93C9CA11-C946-496A-AF9C-A86667FE7BE4}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "onedrive-backup", "onedrive-backup.csproj", "{EAF98A54-3368-429C-A94E-B562AC6879B7}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{93C9CA11-C946-496A-AF9C-A86667FE7BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{93C9CA11-C946-496A-AF9C-A86667FE7BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{93C9CA11-C946-496A-AF9C-A86667FE7BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{93C9CA11-C946-496A-AF9C-A86667FE7BE4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EAF98A54-3368-429C-A94E-B562AC6879B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EAF98A54-3368-429C-A94E-B562AC6879B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EAF98A54-3368-429C-A94E-B562AC6879B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EAF98A54-3368-429C-A94E-B562AC6879B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {750FF813-82FB-41C8-B4E4-81DC2AF2EF67}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"defaultProvider": "cdnjs",
|
||||||
|
"libraries": [
|
||||||
|
{
|
||||||
|
"provider": "cdnjs",
|
||||||
|
"library": "jquery@3.6.4",
|
||||||
|
"destination": "wwwroot/lib/jquery/",
|
||||||
|
"files": [
|
||||||
|
"jquery.slim.min.js",
|
||||||
|
"jquery.slim.min.map"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "cdnjs",
|
||||||
|
"library": "popper.js@2.11.6",
|
||||||
|
"destination": "wwwroot/lib/popper.js/",
|
||||||
|
"files": [
|
||||||
|
"umd/popper.min.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "cdnjs",
|
||||||
|
"library": "bootstrap@5.3.0-alpha2",
|
||||||
|
"destination": "wwwroot/lib/bootstrap/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>onedrive_backup</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<DockerfileContext>.</DockerfileContext>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
|
||||||
|
<PackageReference Include="Azure.Identity" Version="1.8.1" />
|
||||||
|
<PackageReference Include="Microsoft.Graph" Version="4.52.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\bmc\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="wwwroot\images\Bitcoin-Logo.svg" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="wwwroot\bmc\bmc-button.svg">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="wwwroot\scripts\BlazorHelpers.js">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.4.33213.308
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "onedrive-backup", "onedrive-backup.csproj", "{F57B9442-408E-4DB1-ACDA-E8040B7812BA}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{F57B9442-408E-4DB1-ACDA-E8040B7812BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F57B9442-408E-4DB1-ACDA-E8040B7812BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F57B9442-408E-4DB1-ACDA-E8040B7812BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F57B9442-408E-4DB1-ACDA-E8040B7812BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {E330ED29-4C58-4586-97E4-29193B070AF3}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"local_backup_num_to_keep": 3,
|
||||||
|
"onedrive_backup_num_to_keep": 2,
|
||||||
|
"backup_interval_days": 1,
|
||||||
|
"backup_passwd": "alsdkfjslajfk",
|
||||||
|
"backup_name": "hass_backup",
|
||||||
|
"include_all_local_backups": false,
|
||||||
|
"upload_speed_cap": 100,
|
||||||
|
"notify_on_error": true,
|
||||||
|
"recovery_mode": false,
|
||||||
|
"hass_api_timeout_minutes": 30,
|
||||||
|
"exclude_media_folder": false,
|
||||||
|
"exclude_ssl_folder": false,
|
||||||
|
"exclude_share_folder": true,
|
||||||
|
"exclude_local_addons_folder": false,
|
||||||
|
"backup_allowed_hours": "0-23",
|
||||||
|
"sync_paths": [
|
||||||
|
],
|
||||||
|
"file_sync_remove_deleted": true,
|
||||||
|
"backup_instance_name": "LocalTest",
|
||||||
|
"log_level": "info",
|
||||||
|
"excluded_addons": [
|
||||||
|
"addon2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
configuration:
|
||||||
|
recovery_mode:
|
||||||
|
name: Wiederherstellungsmodus
|
||||||
|
description: Wenn diese Einstellung aktiviert ist, führt das Add-on keine Sicherungen durch und der Synchronisierungsmodus arbeitet umgekehrt, von OneDrive zu Home Assistant. Dies respektiert immer noch die eingestellte maximale Anzahl lokaler Sicherungen und versucht, die neuesten Sicherungen, die in OneDrive vorhanden sind, zu synchronisieren, während es unter dem eingestellten Limit bleibt.
|
||||||
|
local_backup_num_to_keep:
|
||||||
|
name: Anzahl der lokalen Sicherungen
|
||||||
|
description: Die maximale Anzahl von Sicherungen, die lokal im Home Assistant gespeichert werden sollen
|
||||||
|
onedrive_backup_num_to_keep:
|
||||||
|
name: Anzahl der Sicherungen in OneDrive
|
||||||
|
description: Die maximale Anzahl der in OneDrive zu speichernden Sicherungen
|
||||||
|
backup_interval_days:
|
||||||
|
name: Sicherungsintervall (Tage)
|
||||||
|
description: Die Häufigkeit der Sicherungserstellung in Tagen. Um eine untertägige Ausführung festzulegen, können Sie eine Zahl zwischen 0 und 1 verwenden. Wenn der Wert hier z. B. auf 0,5 gesetzt wird, erfolgt die Sicherung alle 12 Stunden.
|
||||||
|
backup_name:
|
||||||
|
name: Name der Sicherung
|
||||||
|
description: Name, der für die vom Add-on erstellten Sicherungen verwendet werden soll.
|
||||||
|
backup_passwd:
|
||||||
|
name: Sicherungskennwort
|
||||||
|
description: Das Kennwort, mit dem die erstellten und auf OneDrive hochgeladenen Sicherungen geschützt werden sollen.
|
||||||
|
notify_on_error:
|
||||||
|
name: Benachrichtigung im Fehlerfall
|
||||||
|
description: Aktiviert dauerhafte Benachrichtigungen im Home Assistant, um über Fehler bei der Datensicherung zu informieren.
|
||||||
|
hass_api_timeout_minutes:
|
||||||
|
name: Hass api timeout (Minuten)
|
||||||
|
description: Hier können Sie die beim Aufruf der Home Assistant-APIs verwendete Zeitüberschreitung einstellen.
|
||||||
|
exclude_media_folder:
|
||||||
|
name: Medienordner ausschließen
|
||||||
|
description: Wenn diese Option aktiviert ist, wird eine Teilsicherung ohne den Ordner "media" erstellt.
|
||||||
|
exclude_ssl_folder:
|
||||||
|
name: ssl-Ordner ausschließen
|
||||||
|
description: Wenn diese Option aktiviert ist, wird eine Teilsicherung ohne den Ordner "ssl" erstellt.
|
||||||
|
exclude_share_folder:
|
||||||
|
name: Freigabeordner ausschließen
|
||||||
|
description: Wenn diese Option aktiviert ist, wird eine Teilsicherung ohne den Ordner "share" erstellt.
|
||||||
|
exclude_local_addons_folder:
|
||||||
|
name: Lokalen Addons-Ordner ausschließen
|
||||||
|
description: Wenn diese Option aktiviert ist, wird eine Teilsicherung ohne den Ordner "addons/local" erstellt.
|
||||||
|
backup_allowed_hours:
|
||||||
|
name: Zulässige Stunden
|
||||||
|
description: Hier kann ein Stundenbereich von 0 bis 23 angegeben werden, in dem nur während dieser Stunden Backups durchgeführt werden. Wenn eine Sicherung erforderlich ist, wird sie im ersten zulässigen Zeitfenster innerhalb der festgelegten Stunden durchgeführt. Das Format ist ein oder mehrere Bereiche, die durch ein Komma getrennt sind. Ein Bereich wird durch einen Bindestrich angegeben.
|
||||||
|
backup_instance_name:
|
||||||
|
name: Name der Sicherungsinstanz
|
||||||
|
description: Dies wird verwendet, um zwischen mehreren Home Assistant-Installationen zu unterscheiden, die auf demselben OneDrive-Konto gesichert werden. OneDrive-Sicherungen werden für jeden „Instanznamen“ getrennt
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
configuration:
|
||||||
|
recovery_mode:
|
||||||
|
name: Recovery mode
|
||||||
|
description: When this settings is toggled on, the add-on will not perform any backups and sync mode will reverse from OneDrive back to Home Assistant. This will still respect the maximum local backups set and will try to sync back the latest backups that exist in OneDrive while remaining under the set limit.
|
||||||
|
local_backup_num_to_keep:
|
||||||
|
name: Number of local backups
|
||||||
|
description: The maximum amount of backups to keep locally in Home Assistant
|
||||||
|
onedrive_backup_num_to_keep:
|
||||||
|
name: Number of OneDrive backups
|
||||||
|
description: The maximum amount of backups to keep in OneDrive
|
||||||
|
backup_interval_days:
|
||||||
|
name: Backup interval (days)
|
||||||
|
description: The backup creation frequency in days. For setting a sub-day frequency you can use a number between 0 and 1, so for example if the value here is set to 0.5, the backup frequency will occur every 12 hours.
|
||||||
|
backup_name:
|
||||||
|
name: Backup name
|
||||||
|
description: Name to use for the backups created by the add-on.
|
||||||
|
monitor_all_local_backups:
|
||||||
|
name: Monitor all local backups
|
||||||
|
description: Use this to monitor all locally created backups not just ones created by the Addon. All backup quota rules defined will be applied here including backup deletion when this is enabled.
|
||||||
|
ignore_upgrade_backups:
|
||||||
|
name: Ignore upgrade backups
|
||||||
|
description: When enabled, the add-on will ignore backups that are automatically created during upgrades (of Home Assistant / Addons)
|
||||||
|
backup_passwd:
|
||||||
|
name: Backup password
|
||||||
|
description: The password to use to protect the backups created and uploaded to OneDrive.
|
||||||
|
notify_on_error:
|
||||||
|
name: Notify on error
|
||||||
|
description: Enables persistent notifications in Home Assistant to notify of backup failures.
|
||||||
|
hass_api_timeout_minutes:
|
||||||
|
name: Hass api timeout (minutes)
|
||||||
|
description: This allows you to set the timeout configured when calling the Home Assistant APIs.
|
||||||
|
exclude_media_folder:
|
||||||
|
name: Exclude media folder
|
||||||
|
description: When enabled, a partial backup will be created without the media folder
|
||||||
|
exclude_ssl_folder:
|
||||||
|
name: Exclude ssl folder
|
||||||
|
description: When enabled, a partial backup will be created without the ssl folder
|
||||||
|
exclude_share_folder:
|
||||||
|
name: Exclude share folder
|
||||||
|
description: When enabled, a partial backup will be created without the share folder
|
||||||
|
exclude_local_addons_folder:
|
||||||
|
name: Exclude local addons folder
|
||||||
|
description: When enabled, a partial backup will be created without the addons/local folder
|
||||||
|
backup_allowed_hours:
|
||||||
|
name: Allowed hours
|
||||||
|
description: This accepts a range of hours from 0 to 23 for which only during these hours will backups be performed. If a backup is required it will be performed at the first window allowed in the defined hours. The format of this is one or more ranges seperated by a comma. A range is specified by a dash.
|
||||||
|
backup_instance_name:
|
||||||
|
name: Backup instance name
|
||||||
|
description: This is used to distingiush between multiple Home Assistant installations backing up to the same OneDrive account. OneDrive backups will be seperated for each 'Instance Name'
|
||||||
|
sync_paths:
|
||||||
|
name: File Sync Paths
|
||||||
|
description: >
|
||||||
|
This allows you to specify a list of paths for the addon to sync to OneDrive so you can for example sync your **Media** folder to OneDrive and exclude it from the Backups allowing you to save storage space (as this will only be stored once). Files are stored under a new directory called "FileSync" under the App Folder root. Source folder structure is maintained under this folder.
|
||||||
|
Sync (sub)folders supported must be under one of the following roots: /config, /ssl, /share, /media, /addons
|
||||||
|
You can specify an array of items here containing "path" and "recursive".
|
||||||
|
See documentation for full usage details.
|
||||||
|
file_sync_remove_deleted:
|
||||||
|
name: Remove deleted files during File Sync
|
||||||
|
description: When enabled, the FileSync folder on OneDrive will mirror your included sync_paths meaning any 'extra' content that remains in OneDrive will be removed.
|
||||||
|
excluded_addons:
|
||||||
|
name: Excluded Addons
|
||||||
|
description: When enabled, partial backups will be created excluding the addons specified in this list. You need to specify the addon id (slug) in this list.
|
||||||
|
log_level:
|
||||||
|
name: Log Level
|
||||||
|
description: You can opt to see more / less logs by adjusting the verbosity of the addon logs.
|
||||||
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,86 @@
|
|||||||
|
SIL OPEN FONT LICENSE Version 1.1
|
||||||
|
|
||||||
|
Copyright (c) 2014 Waybury
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Waybury
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
[Open Iconic v1.1.1](https://github.com/iconic/open-iconic)
|
||||||
|
===========
|
||||||
|
|
||||||
|
### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## What's in Open Iconic?
|
||||||
|
|
||||||
|
* 223 icons designed to be legible down to 8 pixels
|
||||||
|
* Super-light SVG files - 61.8 for the entire set
|
||||||
|
* SVG sprite—the modern replacement for icon fonts
|
||||||
|
* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats
|
||||||
|
* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats
|
||||||
|
* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections.
|
||||||
|
|
||||||
|
### General Usage
|
||||||
|
|
||||||
|
#### Using Open Iconic's SVGs
|
||||||
|
|
||||||
|
We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute).
|
||||||
|
|
||||||
|
```
|
||||||
|
<img src="/open-iconic/svg/icon-name.svg" alt="icon name">
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Open Iconic's SVG Sprite
|
||||||
|
|
||||||
|
Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack.
|
||||||
|
|
||||||
|
Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `<svg>` *tag and a unique class name for each different icon in the* `<use>` *tag.*
|
||||||
|
|
||||||
|
```
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="open-iconic.svg#account-login" class="icon-account-login"></use>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `<svg>` tag with equal width and height dimensions.
|
||||||
|
|
||||||
|
```
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Coloring icons is even easier. All you need to do is set the `fill` rule on the `<use>` tag.
|
||||||
|
|
||||||
|
```
|
||||||
|
.icon-account-login {
|
||||||
|
fill: #f00;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/).
|
||||||
|
|
||||||
|
#### Using Open Iconic's Icon Font...
|
||||||
|
|
||||||
|
|
||||||
|
##### …with Bootstrap
|
||||||
|
|
||||||
|
You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
<link href="/open-iconic/font/css/open-iconic-bootstrap.css" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
<span class="oi oi-icon-name" title="icon name" aria-hidden="true"></span>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### …with Foundation
|
||||||
|
|
||||||
|
You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}`
|
||||||
|
|
||||||
|
```
|
||||||
|
<link href="/open-iconic/font/css/open-iconic-foundation.css" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
<span class="fi-icon-name" title="icon name" aria-hidden="true"></span>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### …on its own
|
||||||
|
|
||||||
|
You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}`
|
||||||
|
|
||||||
|
```
|
||||||
|
<link href="/open-iconic/font/css/open-iconic.css" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
<span class="oi" data-glyph="icon-name" title="icon name" aria-hidden="true"></span>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT).
|
||||||
|
|
||||||
|
### Fonts
|
||||||
|
|
||||||
|
All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web).
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<!--
|
||||||
|
2014-7-1: Created.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata>
|
||||||
|
Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014
|
||||||
|
By P.J. Onori
|
||||||
|
Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net)
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="open-iconic" horiz-adv-x="800" >
|
||||||
|
<font-face
|
||||||
|
font-family="Icons"
|
||||||
|
font-weight="400"
|
||||||
|
font-stretch="normal"
|
||||||
|
units-per-em="800"
|
||||||
|
panose-1="2 0 5 3 0 0 0 0 0 0"
|
||||||
|
ascent="800"
|
||||||
|
descent="0"
|
||||||
|
bbox="-0.5 -101 802 800.126"
|
||||||
|
underline-thickness="50"
|
||||||
|
underline-position="-100"
|
||||||
|
unicode-range="U+E000-E0DE"
|
||||||
|
/>
|
||||||
|
<missing-glyph />
|
||||||
|
<glyph glyph-name="" unicode=""
|
||||||
|
d="M300 700h500v-700h-500v100h400v500h-400v100zM400 500l200 -150l-200 -150v100h-400v100h400v100z" />
|
||||||
|
<glyph glyph-name="1" unicode=""
|
||||||
|
d="M300 700h500v-700h-500v100h400v500h-400v100zM200 500v-100h400v-100h-400v-100l-200 150z" />
|
||||||
|
<glyph glyph-name="2" unicode=""
|
||||||
|
d="M350 700c193 0 350 -157 350 -350v-50h100l-200 -200l-200 200h100v50c0 138 -112 250 -250 250s-250 -112 -250 -250c0 193 157 350 350 350z" />
|
||||||
|
<glyph glyph-name="3" unicode=""
|
||||||
|
d="M450 700c193 0 350 -157 350 -350c0 138 -112 250 -250 250s-250 -112 -250 -250v-50h100l-200 -200l-200 200h100v50c0 193 157 350 350 350z" />
|
||||||
|
<glyph glyph-name="4" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM100 500h600v-100h-600v100zM0 300h800v-100h-800v100zM100 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="5" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM0 500h600v-100h-600v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="6" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM200 500h600v-100h-600v100zM0 300h800v-100h-800v100zM200 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="7" unicode=""
|
||||||
|
d="M400 700c75 0 146 -23 206 -59l-75 -225l-322 234c57 31 122 50 191 50zM125 588l191 -138l-310 -222c-4 24 -6 47 -6 72c0 114 49 215 125 288zM688 575c69 -72 112 -168 112 -275c0 -35 -8 -68 -16 -100h-218zM216 253l112 -347c-128 23 -232 109 -287 222zM372 100
|
||||||
|
h372c-64 -109 -177 -185 -310 -197z" />
|
||||||
|
<glyph glyph-name="8" unicode="" horiz-adv-x="600"
|
||||||
|
d="M200 800h100v-500h200l-247 -300l-253 300h200v500z" />
|
||||||
|
<glyph glyph-name="9" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM300 700v-300h-200l300 -300l300 300h-200v300h-200z" />
|
||||||
|
<glyph glyph-name="a" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700l-300 -300l300 -300v200h300v200h-300v200z" />
|
||||||
|
<glyph glyph-name="b" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700v-200h-300v-200h300v-200l300 300z" />
|
||||||
|
<glyph glyph-name="c" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700l-300 -300h200v-300h200v300h200z" />
|
||||||
|
<glyph glyph-name="d" unicode=""
|
||||||
|
d="M300 600v-200h500v-100h-500v-200l-300 247z" />
|
||||||
|
<glyph glyph-name="e" unicode=""
|
||||||
|
d="M500 600l300 -247l-300 -253v200h-500v100h500v200z" />
|
||||||
|
<glyph glyph-name="f" unicode="" horiz-adv-x="600"
|
||||||
|
d="M200 800h200v-500h200l-297 -300l-303 300h200v500z" />
|
||||||
|
<glyph glyph-name="10" unicode=""
|
||||||
|
d="M300 700v-200h500v-200h-500v-200l-300 297z" />
|
||||||
|
<glyph glyph-name="11" unicode=""
|
||||||
|
d="M500 700l300 -297l-300 -303v200h-500v200h500v200z" />
|
||||||
|
<glyph glyph-name="12" unicode="" horiz-adv-x="600"
|
||||||
|
d="M297 800l303 -300h-200v-500h-200v500h-200z" />
|
||||||
|
<glyph glyph-name="13" unicode="" horiz-adv-x="600"
|
||||||
|
d="M247 800l253 -300h-200v-500h-100v500h-200z" />
|
||||||
|
<glyph glyph-name="14" unicode=""
|
||||||
|
d="M400 800h100v-800h-100v800zM200 700h100v-600h-100v600zM600 600h100v-400h-100v400zM0 500h100v-200h-100v200z" />
|
||||||
|
<glyph glyph-name="15" unicode=""
|
||||||
|
d="M116 600l72 -72c-54 -54 -88 -126 -88 -209s34 -159 88 -213l-72 -72c-72 72 -116 175 -116 285s44 209 116 281zM684 600c72 -72 116 -171 116 -281s-44 -213 -116 -285l-72 72c54 54 88 130 88 213s-34 155 -88 209zM259 460l69 -72c-18 -18 -28 -41 -28 -69
|
||||||
|
s10 -54 28 -72l-69 -72c-36 36 -59 89 -59 144s23 105 59 141zM541 459c36 -36 59 -85 59 -140s-23 -108 -59 -144l-69 72c18 18 28 44 28 72s-10 51 -28 69z" />
|
||||||
|
<glyph glyph-name="16" unicode="" horiz-adv-x="400"
|
||||||
|
d="M200 800c110 0 200 -90 200 -200s-90 -200 -200 -200s-200 90 -200 200s90 200 200 200zM100 319c31 -11 65 -19 100 -19s68 8 100 19v-319l-100 100l-100 -100v319z" />
|
||||||
|
<glyph glyph-name="17" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300c0 -66 21 -126 56 -175l419 419c-49 35 -109 56 -175 56zM644 575l-419 -419c49 -35 109 -56 175 -56c166 0 300 134 300 300
|
||||||
|
c0 66 -21 126 -56 175z" />
|
||||||
|
<glyph glyph-name="18" unicode=""
|
||||||
|
d="M0 700h100v-600h700v-100h-800v700zM500 700h200v-500h-200v500zM200 500h200v-300h-200v300z" />
|
||||||
|
<glyph glyph-name="19" unicode=""
|
||||||
|
d="M397 800c13 1 23 -4 34 -13c2 -2 214 -254 241 -287h128v-100h-100v-366c0 -18 -16 -34 -34 -34h-532c-18 0 -34 16 -34 34v366h-100v100h128l234 281c9 11 22 18 35 19zM400 672l-144 -172h288zM250 300c-28 0 -50 -22 -50 -50v-100c0 -28 22 -50 50 -50s50 22 50 50
|
||||||
|
v100c0 28 -22 50 -50 50zM550 300c-28 0 -50 -22 -50 -50v-100c0 -28 22 -50 50 -50s50 22 50 50v100c0 28 -22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="1a" unicode=""
|
||||||
|
d="M9 700h682c6 0 9 -4 9 -10v-190h100v-200h-100v-191c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v582c0 6 3 9 9 9zM100 600v-400h500v400h-500z" />
|
||||||
|
<glyph glyph-name="1b" unicode=""
|
||||||
|
d="M9 700h682c6 0 9 -4 9 -10v-190h100v-200h-100v-191c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v582c0 6 3 9 9 9z" />
|
||||||
|
<glyph glyph-name="1c" unicode=""
|
||||||
|
d="M92 650c0 23 19 50 45 50h3h5h5h500c28 0 50 -22 50 -50s-22 -50 -50 -50h-50v-141c9 -17 120 -231 166 -309c16 -26 34 -61 34 -106c0 -39 -15 -77 -41 -103h-3c-26 -25 -62 -41 -100 -41h-512c-39 0 -77 15 -103 41s-41 64 -41 103c0 46 18 80 34 106
|
||||||
|
c46 78 157 292 166 309v141h-50c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51zM500 600h-200v-162l-6 -10s-63 -123 -119 -228h450c-56 105 -119 228 -119 228l-6 10v162z" />
|
||||||
|
<glyph glyph-name="1d" unicode=""
|
||||||
|
d="M400 800c110 0 200 -90 200 -200c0 -104 52 -198 134 -266c41 -34 66 -82 66 -134h-800c0 52 25 100 66 134c82 68 134 162 134 266c0 110 90 200 200 200zM300 100h200c0 -55 -45 -100 -100 -100s-100 45 -100 100z" />
|
||||||
|
<glyph glyph-name="1e" unicode="" horiz-adv-x="600"
|
||||||
|
d="M150 800h50l350 -250l-225 -147l225 -153l-350 -250h-50v250l-75 -75l-75 75l150 150l-150 150l75 75l75 -75v250zM250 650v-200l150 100zM250 350v-200l150 100z" />
|
||||||
|
<glyph glyph-name="1f" unicode=""
|
||||||
|
d="M0 800h500c110 0 200 -90 200 -200c0 -47 -17 -91 -44 -125c85 -40 144 -125 144 -225c0 -138 -112 -250 -250 -250h-550v100c55 0 100 45 100 100v400c0 55 -45 100 -100 100v100zM300 700v-200h100c55 0 100 45 100 100s-45 100 -100 100h-100zM300 400v-300h150
|
||||||
|
c83 0 150 67 150 150s-67 150 -150 150h-150z" />
|
||||||
|
<glyph glyph-name="20" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800v-300h200l-300 -500v300h-200z" />
|
||||||
|
<glyph glyph-name="21" unicode=""
|
||||||
|
d="M100 800h300v-300l100 100l100 -100v300h50c28 0 50 -22 50 -50v-550h-550c-28 0 -50 -22 -50 -50s22 -50 50 -50h550v-100h-550c-83 0 -150 67 -150 150v550l3 19c8 39 39 70 78 78z" />
|
||||||
|
<glyph glyph-name="22" unicode="" horiz-adv-x="400"
|
||||||
|
d="M0 800h400v-800l-200 200l-200 -200v800z" />
|
||||||
|
<glyph glyph-name="23" unicode=""
|
||||||
|
d="M0 800h800v-100h-800v100zM0 600h300v-103h203v103h297v-591c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v591z" />
|
||||||
|
<glyph glyph-name="24" unicode=""
|
||||||
|
d="M300 800h200c55 0 100 -45 100 -100v-100h191c6 0 9 -3 9 -9v-241c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v241c0 6 3 9 9 9h191v100c0 55 45 100 100 100zM300 700v-100h200v100h-200zM0 209c16 -6 32 -9 50 -9h700c18 0 34 3 50 9v-200c0 -6 -3 -9 -9 -9h-782
|
||||||
|
c-6 0 -9 3 -9 9v200z" />
|
||||||
|
<glyph glyph-name="25" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800c58 0 110 -16 147 -53s53 -89 53 -147h-100c0 39 -11 61 -25 75s-36 25 -75 25c-35 0 -55 -10 -72 -31s-28 -55 -28 -94c0 -51 20 -107 28 -175h172v-100h-178c-14 -60 -49 -127 -113 -200h491v-100h-600v122l16 12c69 69 95 121 106 166h-122v100h125
|
||||||
|
c-8 50 -25 106 -25 175c0 58 16 114 50 156c34 43 88 69 150 69z" />
|
||||||
|
<glyph glyph-name="26" unicode=""
|
||||||
|
d="M34 700h4h3h4h5h700c28 0 50 -22 50 -50v-700c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v700v2c0 20 15 42 34 48zM150 600c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50zM350 600c-28 0 -50 -22 -50 -50s22 -50 50 -50h300c28 0 50 22 50 50
|
||||||
|
s-22 50 -50 50h-300zM100 400v-400h600v400h-600z" />
|
||||||
|
<glyph glyph-name="27" unicode=""
|
||||||
|
d="M744 797l6 -3l44 -44c4 -4 3 -8 0 -12l-266 -375l-15 -13l-25 -12c-23 72 -78 127 -150 150l12 25l13 15l375 266zM266 400c74 0 134 -60 134 -134c0 -147 -119 -266 -266 -266c-48 0 -95 12 -134 34c80 46 134 133 134 232c0 74 58 134 132 134z" />
|
||||||
|
<glyph glyph-name="28" unicode=""
|
||||||
|
d="M9 451c0 23 19 50 46 50c8 0 19 -3 26 -7l131 -66l29 22c-79 81 -1 250 118 250s197 -167 119 -250l28 -22l131 66c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-115 -56c9 -16 19 -33 25 -50h68c28 0 50 -22 50 -50s-22 -50 -50 -50h-50
|
||||||
|
c0 -23 -2 -45 -6 -66l78 -40c21 -5 37 -28 37 -49c0 -28 -22 -50 -50 -50c-10 0 -23 5 -31 11l-65 35c-24 -46 -62 -86 -103 -110c-35 19 -60 45 -60 72v135v4v5v6v5v5v87c0 28 -22 50 -50 50c-24 0 -45 -17 -50 -40c1 -3 1 -8 1 -11s0 -8 -1 -11v-82v-4v-5v-144
|
||||||
|
c0 -28 -24 -53 -59 -72c-41 25 -79 64 -103 110l-66 -35c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50c0 21 16 44 37 49l78 40c-4 21 -6 43 -6 66h-50h-5c-28 0 -50 22 -50 50c0 26 22 50 50 50h5h69c6 17 16 34 25 50l-116 56c-16 7 -28 27 -28 45z" />
|
||||||
|
<glyph glyph-name="29" unicode=""
|
||||||
|
d="M600 700h91c6 0 9 -3 9 -9v-582c0 -6 -3 -9 -9 -9h-91v600zM210 503l290 147v-500l-250 125v-3c-15 0 -25 -8 -28 -22l75 -178c11 -25 0 -58 -25 -69s-58 0 -69 25l-103 272h-91c-6 0 -9 3 -9 9v182c0 6 3 9 9 9h182z" />
|
||||||
|
<glyph glyph-name="2a" unicode=""
|
||||||
|
d="M9 800h682c6 0 9 -3 9 -9v-782c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v782c0 6 3 9 9 9zM100 700v-200h500v200h-500zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400v-300h100v300h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100z" />
|
||||||
|
<glyph glyph-name="2b" unicode=""
|
||||||
|
d="M0 800h700v-200h-700v200zM0 500h700v-491c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v491zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400v-100h100v100h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100z" />
|
||||||
|
<glyph glyph-name="2c" unicode=""
|
||||||
|
d="M409 800h182c6 0 10 -4 12 -9l94 -182c2 -5 6 -9 12 -9h82c6 0 9 -3 9 -9v-582c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v441c0 83 67 150 150 150h141c6 0 10 4 12 9l94 182c2 5 6 9 12 9zM150 500c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z
|
||||||
|
M500 500c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200zM500 400c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||||
|
<glyph glyph-name="2d" unicode=""
|
||||||
|
d="M0 600h800l-400 -400z" />
|
||||||
|
<glyph glyph-name="2e" unicode="" horiz-adv-x="400"
|
||||||
|
d="M400 800v-800l-400 400z" />
|
||||||
|
<glyph glyph-name="2f" unicode="" horiz-adv-x="400"
|
||||||
|
d="M0 800l400 -400l-400 -400v800z" />
|
||||||
|
<glyph glyph-name="30" unicode=""
|
||||||
|
d="M400 600l400 -400h-800z" />
|
||||||
|
<glyph glyph-name="31" unicode=""
|
||||||
|
d="M0 550c0 23 20 50 46 50h3h5h4h200c17 0 37 -13 44 -28l38 -72h444c14 0 19 -12 15 -25l-81 -250c-4 -13 -21 -25 -35 -25h-350c-14 0 -30 12 -34 25c-27 83 -54 167 -81 250l-10 25h-150c-2 0 -5 -1 -7 -1c-28 0 -51 23 -51 51zM358 100c28 0 50 -22 50 -50
|
||||||
|
s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM658 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||||
|
<glyph glyph-name="32" unicode=""
|
||||||
|
d="M0 700h500v-100h-300v-300h-100l-100 -100v500zM300 500h500v-500l-100 100h-400v400z" />
|
||||||
|
<glyph glyph-name="33" unicode=""
|
||||||
|
d="M641 700l143 -141l-493 -493c-71 76 -146 148 -219 222l-72 71l141 141c50 -51 101 -101 153 -150c116 117 234 231 347 350z" />
|
||||||
|
<glyph glyph-name="34" unicode=""
|
||||||
|
d="M150 600l250 -250l250 250l150 -150l-400 -400l-400 400z" />
|
||||||
|
<glyph glyph-name="35" unicode="" horiz-adv-x="600"
|
||||||
|
d="M400 800l150 -150l-250 -250l250 -250l-150 -150l-400 400z" />
|
||||||
|
<glyph glyph-name="36" unicode="" horiz-adv-x="600"
|
||||||
|
d="M150 800l400 -400l-400 -400l-150 150l250 250l-250 250z" />
|
||||||
|
<glyph glyph-name="37" unicode=""
|
||||||
|
d="M400 600l400 -400l-150 -150l-250 250l-250 -250l-150 150z" />
|
||||||
|
<glyph glyph-name="38" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM600 622l-250 -250l-100 100l-72 -72l172 -172l322 322z" />
|
||||||
|
<glyph glyph-name="39" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM250 622l-72 -72l150 -150l-150 -150l72 -72l150 150l150 -150l72 72l-150 150l150 150l-72 72l-150 -150z" />
|
||||||
|
<glyph glyph-name="3a" unicode=""
|
||||||
|
d="M350 800c28 0 50 -22 50 -50v-50h75c14 0 25 -11 25 -25v-75h-300v75c0 14 11 25 25 25h75v50c0 28 22 50 50 50zM25 700h75v-200h500v200h75c14 0 25 -11 25 -25v-650c0 -14 -11 -25 -25 -25h-650c-14 0 -25 11 -25 25v650c0 14 11 25 25 25z" />
|
||||||
|
<glyph glyph-name="3b" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM350 600h100v-181c23 -24 47 -47 72 -69l-72 -72c-27 30 -55 59 -84 88l-16 12
|
||||||
|
v222z" />
|
||||||
|
<glyph glyph-name="3c" unicode=""
|
||||||
|
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -18 -3 -34 -9 -50h-191v50c0 83 -67 150 -150 150s-150 -67 -150 -150v-50h-272c-17 30 -28 63 -28 100c0 110 90 200 200 200c23 114 129 200 250 200zM434 400h3h4c3 0 6 1 9 1c28 0 50 -22 50 -50v-1
|
||||||
|
v-150h150l-200 -200l-200 200h150v150v2c0 20 15 42 34 48z" />
|
||||||
|
<glyph glyph-name="3d" unicode=""
|
||||||
|
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -18 -3 -34 -9 -50h-141l-200 200l-200 -200h-222c-17 30 -28 63 -28 100c0 110 90 200 200 200c23 114 129 200 250 200zM450 350l250 -250h-200v-50c0 -28 -22 -50 -50 -50s-50 22 -50 50v50h-200z" />
|
||||||
|
<glyph glyph-name="3e" unicode=""
|
||||||
|
d="M450 700c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -83 -67 -150 -150 -150h-450c-110 0 -200 90 -200 200s90 200 200 200c23 114 129 200 250 200z" />
|
||||||
|
<glyph glyph-name="3f" unicode=""
|
||||||
|
d="M250 800c82 0 154 -40 200 -100c-143 0 -270 -85 -325 -209c-36 -10 -70 -25 -100 -47c-16 33 -25 67 -25 106c0 138 112 250 250 250zM450 600c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -83 -67 -150 -150 -150h-450c-110 0 -200 90 -200 200
|
||||||
|
s90 200 200 200c23 114 129 200 250 200z" />
|
||||||
|
<glyph glyph-name="40" unicode=""
|
||||||
|
d="M500 700h100l-300 -600h-100zM100 600h100l-100 -200l100 -200h-100l-100 200zM600 600h100l100 -200l-100 -200h-100l100 200z" />
|
||||||
|
<glyph glyph-name="41" unicode=""
|
||||||
|
d="M350 800h100l50 -119l28 -12l119 50l72 -72l-50 -119l12 -28l119 -50v-100l-119 -50l-12 -28l50 -119l-72 -72l-119 50l-28 -12l-50 -119h-100l-50 119l-28 12l-119 -50l-72 72l50 119l-12 28l-119 50v100l119 50l12 28l-50 119l72 72l119 -50l28 12zM400 550
|
||||||
|
c-83 0 -150 -67 -150 -150s67 -150 150 -150s150 67 150 150s-67 150 -150 150z" />
|
||||||
|
<glyph glyph-name="42" unicode=""
|
||||||
|
d="M0 800h800v-200h-800v200zM200 500h400l-200 -200zM0 100h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="43" unicode=""
|
||||||
|
d="M0 800h100v-800h-100v800zM600 800h200v-800h-200v800zM500 600v-400l-200 200z" />
|
||||||
|
<glyph glyph-name="44" unicode=""
|
||||||
|
d="M0 800h200v-800h-200v800zM700 800h100v-800h-100v800zM300 600l200 -200l-200 -200v400z" />
|
||||||
|
<glyph glyph-name="45" unicode=""
|
||||||
|
d="M0 800h800v-100h-800v100zM400 500l200 -200h-400zM0 200h800v-200h-800v200z" />
|
||||||
|
<glyph glyph-name="46" unicode=""
|
||||||
|
d="M150 700c83 0 150 -67 150 -150v-50h100v50c0 83 67 150 150 150s150 -67 150 -150s-67 -150 -150 -150h-50v-100h50c83 0 150 -67 150 -150s-67 -150 -150 -150s-150 67 -150 150v50h-100v-50c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150h50v100h-50
|
||||||
|
c-83 0 -150 67 -150 150s67 150 150 150zM150 600c-28 0 -50 -22 -50 -50s22 -50 50 -50h50v50c0 28 -22 50 -50 50zM550 600c-28 0 -50 -22 -50 -50v-50h50c28 0 50 22 50 50s-22 50 -50 50zM300 400v-100h100v100h-100zM150 200c-28 0 -50 -22 -50 -50s22 -50 50 -50
|
||||||
|
s50 22 50 50v50h-50zM500 200v-50c0 -28 22 -50 50 -50s50 22 50 50s-22 50 -50 50h-50z" />
|
||||||
|
<glyph glyph-name="47" unicode=""
|
||||||
|
d="M0 791c0 5 4 9 9 9h782c6 0 9 -4 9 -10v-790l-200 200h-591c-6 0 -9 3 -9 9v582z" />
|
||||||
|
<glyph glyph-name="48" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM600 600l-100 -300l-300 -100l100 300zM400 450c-28 0 -50 -22 -50 -50
|
||||||
|
s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="49" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700v-600c166 0 300 134 300 300s-134 300 -300 300z" />
|
||||||
|
<glyph glyph-name="4a" unicode=""
|
||||||
|
d="M0 800h800v-100h-800v100zM0 600h500v-100h-500v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100zM750 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||||
|
<glyph glyph-name="4b" unicode=""
|
||||||
|
d="M25 700h750c14 0 25 -11 25 -25v-75h-800v75c0 14 11 25 25 25zM0 500h800v-375c0 -14 -11 -25 -25 -25h-750c-14 0 -25 11 -25 25v375zM100 300v-100h100v100h-100zM300 300v-100h100v100h-100z" />
|
||||||
|
<glyph glyph-name="4c" unicode=""
|
||||||
|
d="M100 800h100v-100h450l100 100l50 -50l-100 -100v-450h100v-100h-100v-100h-100v100h-500v500h-100v100h100v100zM200 600v-350l350 350h-350zM600 550l-350 -350h350v350z" />
|
||||||
|
<glyph glyph-name="4d" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM400 600c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z
|
||||||
|
M200 452c0 20 15 42 34 48h3h3h8c12 0 28 -7 36 -16l91 -90l25 6c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100l6 25l-90 91c-9 8 -16 24 -16 36zM550 500c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||||
|
<glyph glyph-name="4e" unicode=""
|
||||||
|
d="M300 800h200v-300h200l-300 -300l-300 300h200v300zM0 100h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="4f" unicode=""
|
||||||
|
d="M0 800h800v-100h-800v100zM400 600l300 -300h-200v-300h-200v300h-200z" />
|
||||||
|
<glyph glyph-name="50" unicode=""
|
||||||
|
d="M200 700h600v-600h-600l-200 300zM350 622l-72 -72l150 -150l-150 -150l72 -72l150 150l150 -150l72 72l-150 150l150 150l-72 72l-150 -150z" />
|
||||||
|
<glyph glyph-name="51" unicode=""
|
||||||
|
d="M400 700c220 0 400 -180 400 -400h-100c0 166 -134 300 -300 300s-300 -134 -300 -300h-100c0 220 180 400 400 400zM341 491l59 -88l59 88c81 -25 141 -101 141 -191c0 -110 -90 -200 -200 -200s-200 90 -200 200c0 90 60 166 141 191z" />
|
||||||
|
<glyph glyph-name="52" unicode=""
|
||||||
|
d="M0 800h300v-400h400v-400h-700v800zM400 800l300 -300h-300v300zM100 600v-100h100v100h-100zM100 400v-100h100v100h-100zM100 200v-100h400v100h-400z" />
|
||||||
|
<glyph glyph-name="53" unicode="" horiz-adv-x="600"
|
||||||
|
d="M200 700h100v-100h75c30 0 58 -6 81 -22s44 -44 44 -78v-100h-100v94c-4 3 -13 6 -25 6h-250c-14 0 -25 -11 -25 -25v-50c0 -15 20 -40 34 -44l257 -65c66 -16 109 -73 109 -141v-50c0 -68 -57 -125 -125 -125h-75v-100h-100v100h-75c-30 0 -58 6 -81 22s-44 44 -44 78
|
||||||
|
v100h100v-94c4 -3 13 -6 25 -6h250c14 0 25 11 25 25v50c0 15 -20 40 -34 44l-257 65c-66 16 -109 73 -109 141v50c0 68 57 125 125 125h75v100z" />
|
||||||
|
<glyph glyph-name="54" unicode=""
|
||||||
|
d="M0 700h300v-300l-300 -300v600zM500 700h300v-300l-300 -300v600z" />
|
||||||
|
<glyph glyph-name="55" unicode=""
|
||||||
|
d="M300 700v-600h-300v300zM800 700v-600h-300v300z" />
|
||||||
|
<glyph glyph-name="56" unicode=""
|
||||||
|
d="M300 700v-100c-111 0 -200 -89 -200 -200h200v-300h-300v300c0 165 135 300 300 300zM800 700v-100c-111 0 -200 -89 -200 -200h200v-300h-300v300c0 165 135 300 300 300z" />
|
||||||
|
<glyph glyph-name="57" unicode=""
|
||||||
|
d="M0 700h300v-300c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-200v300zM500 700h300v-300c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-200v300z" />
|
||||||
|
<glyph glyph-name="58" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800l34 -34c11 -11 266 -270 266 -488c0 -165 -135 -300 -300 -300s-300 135 -300 300c0 218 255 477 266 488zM150 328c-28 0 -50 -22 -50 -50c0 -110 90 -200 200 -200c28 0 50 22 50 50s-22 50 -50 50c-55 0 -100 45 -100 100c0 28 -22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="59" unicode=""
|
||||||
|
d="M400 800l400 -500h-800zM0 200h800v-200h-800v200z" />
|
||||||
|
<glyph glyph-name="5a" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800l300 -300h-600zM0 300h600l-300 -300z" />
|
||||||
|
<glyph glyph-name="5b" unicode=""
|
||||||
|
d="M0 500h200v-200h-200v200zM300 500h200v-200h-200v200zM600 500h200v-200h-200v200z" />
|
||||||
|
<glyph glyph-name="5c" unicode=""
|
||||||
|
d="M0 700h800v-100l-400 -200l-400 200v100zM0 500l400 -200l400 200v-400h-800v400z" />
|
||||||
|
<glyph glyph-name="5d" unicode=""
|
||||||
|
d="M400 800l400 -200v-600h-800v600zM400 688l-300 -150v-188l300 -150l300 150v188zM200 500h400v-100l-200 -100l-200 100v100z" />
|
||||||
|
<glyph glyph-name="5e" unicode=""
|
||||||
|
d="M600 700c69 0 134 -19 191 -50l-16 -106c-49 35 -109 56 -175 56c-131 0 -240 -84 -281 -200h331l-16 -100h-334c0 -36 8 -68 19 -100h297l-16 -100h-222c55 -61 133 -100 222 -100c78 0 147 30 200 78v-122c-59 -35 -127 -56 -200 -56c-147 0 -274 82 -344 200h-256
|
||||||
|
l19 100h197c-8 32 -16 66 -16 100h-200l25 100h191c45 172 198 300 384 300z" />
|
||||||
|
<glyph glyph-name="5f" unicode=""
|
||||||
|
d="M0 700h700v-100h-700v100zM0 500h500v-100h-500v100zM0 300h800v-100h-800v100zM0 100h100v-100h-100v100zM200 100h100v-100h-100v100zM400 100h100v-100h-100v100z" />
|
||||||
|
<glyph glyph-name="60" unicode=""
|
||||||
|
d="M0 800h800v-100h-800v100zM200 600h400l-200 -200zM0 200h800v-200h-800v200z" />
|
||||||
|
<glyph glyph-name="61" unicode=""
|
||||||
|
d="M0 800h100v-800h-100v800zM600 800h200v-800h-200v800zM200 600l200 -200l-200 -200v400z" />
|
||||||
|
<glyph glyph-name="62" unicode=""
|
||||||
|
d="M0 800h200v-800h-200v800zM700 800h100v-800h-100v800zM600 600v-400l-200 200z" />
|
||||||
|
<glyph glyph-name="63" unicode=""
|
||||||
|
d="M0 800h800v-200h-800v200zM400 400l200 -200h-400zM0 100h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="64" unicode=""
|
||||||
|
d="M0 800h200v-100h-100v-600h600v100h100v-200h-800v800zM400 800h400v-400l-150 150l-250 -250l-100 100l250 250z" />
|
||||||
|
<glyph glyph-name="65" unicode=""
|
||||||
|
d="M403 700c247 0 397 -300 397 -300s-150 -300 -397 -300c-253 0 -403 300 -403 300s150 300 403 300zM400 600c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200zM400 500c10 0 19 -3 28 -6c-16 -8 -28 -24 -28 -44c0 -28 22 -50 50 -50
|
||||||
|
c20 0 36 12 44 28c3 -9 6 -18 6 -28c0 -55 -45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||||
|
<glyph glyph-name="66" unicode="" horiz-adv-x="900"
|
||||||
|
d="M331 700h3h3c3 1 7 1 10 1c12 0 29 -8 37 -17l94 -93l66 65c57 57 155 57 212 0c58 -58 58 -154 0 -212l-65 -66l93 -94c10 -8 18 -25 18 -38c0 -28 -22 -50 -50 -50c-13 0 -32 9 -40 20l-62 65l-381 -381h-269v272l375 381l-63 63c-9 8 -16 24 -16 36c0 20 16 42 35 48z
|
||||||
|
M447 481l-313 -315l128 -132l316 316z" />
|
||||||
|
<glyph glyph-name="67" unicode=""
|
||||||
|
d="M0 800h300v-400h400v-400h-700v800zM400 800l300 -300h-300v300z" />
|
||||||
|
<glyph glyph-name="68" unicode=""
|
||||||
|
d="M200 800c0 0 200 -100 200 -300s-298 -302 -200 -500c0 0 -200 100 -200 300s300 300 200 500zM500 500c0 0 200 -100 200 -300c0 -150 -60 -200 -100 -200h-300c0 200 300 300 200 500z" />
|
||||||
|
<glyph glyph-name="69" unicode=""
|
||||||
|
d="M0 800h100v-800h-100v800zM200 800h300v-100h300l-200 -203l200 -197h-400v100h-200v400z" />
|
||||||
|
<glyph glyph-name="6a" unicode="" horiz-adv-x="400"
|
||||||
|
d="M150 800h150l-100 -200h200l-150 -300h150l-300 -300l-100 300h134l66 200h-200z" />
|
||||||
|
<glyph glyph-name="6b" unicode=""
|
||||||
|
d="M0 800h300v-100h500v-100h-800v200zM0 500h800v-450c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v450z" />
|
||||||
|
<glyph glyph-name="6c" unicode=""
|
||||||
|
d="M150 800c83 0 150 -67 150 -150c0 -66 -41 -121 -100 -141v-118c15 5 33 9 50 9h200c28 0 50 22 50 50v59c-59 20 -100 75 -100 141c0 83 67 150 150 150s150 -67 150 -150c0 -66 -41 -121 -100 -141v-59c0 -82 -68 -150 -150 -150h-200c-14 0 -25 -7 -34 -16
|
||||||
|
c50 -24 84 -74 84 -134c0 -83 -67 -150 -150 -150s-150 67 -150 150c0 66 41 121 100 141v218c-59 20 -100 75 -100 141c0 83 67 150 150 150z" />
|
||||||
|
<glyph glyph-name="6d" unicode=""
|
||||||
|
d="M0 800h400l-150 -150l150 -150l-100 -100l-150 150l-150 -150v400zM500 400l150 -150l150 150v-400h-400l150 150l-150 150z" />
|
||||||
|
<glyph glyph-name="6e" unicode=""
|
||||||
|
d="M100 800l150 -150l150 150v-400h-400l150 150l-150 150zM400 400h400l-150 -150l150 -150l-100 -100l-150 150l-150 -150v400z" />
|
||||||
|
<glyph glyph-name="6f" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700c-56 0 -108 -17 -153 -44l22 -19c33 -18 13 -48 -13 -59c-30 -13 -77 10 -65 -41c13 -55 -27 -3 -47 -15c-42 -26 49 -152 31 -156l-59 34c-8 0 -13 -5 -16 -10
|
||||||
|
c1 -30 10 -57 19 -84c28 -11 77 -2 100 -25c47 -28 97 -115 75 -159c34 -13 68 -22 106 -22c101 0 193 48 247 125c3 24 -8 44 -50 44c-69 0 -156 13 -153 97c2 46 101 108 66 143c-30 30 12 39 12 66c0 37 -65 32 -69 50s20 36 41 56c-30 10 -60 19 -94 19zM631 591
|
||||||
|
c-38 -11 -94 -35 -87 -53c6 -15 52 -1 65 -13c11 -10 16 -59 44 -31l22 22v3c-11 26 -26 50 -44 72z" />
|
||||||
|
<glyph glyph-name="70" unicode=""
|
||||||
|
d="M703 800l97 -100l-400 -400l-100 100l-200 -203l-100 100l300 303l100 -100zM0 100h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="71" unicode=""
|
||||||
|
d="M0 700h100v-100h-100v100zM200 700h100v-100h-100v100zM400 700h100v-100h-100v100zM600 700h100v-100h-100v100zM0 500h100v-100h-100v100zM200 500h100v-100h-100v100zM400 500h100v-100h-100v100zM600 500h100v-100h-100v100zM0 300h100v-100h-100v100zM200 300h100
|
||||||
|
v-100h-100v100zM400 300h100v-100h-100v100zM600 300h100v-100h-100v100zM0 100h100v-100h-100v100zM200 100h100v-100h-100v100zM400 100h100v-100h-100v100zM600 100h100v-100h-100v100z" />
|
||||||
|
<glyph glyph-name="72" unicode=""
|
||||||
|
d="M0 800h200v-200h-200v200zM300 800h200v-200h-200v200zM600 800h200v-200h-200v200zM0 500h200v-200h-200v200zM300 500h200v-200h-200v200zM600 500h200v-200h-200v200zM0 200h200v-200h-200v200zM300 200h200v-200h-200v200zM600 200h200v-200h-200v200z" />
|
||||||
|
<glyph glyph-name="73" unicode=""
|
||||||
|
d="M0 800h300v-300h-300v300zM500 800h300v-300h-300v300zM0 300h300v-300h-300v300zM500 300h300v-300h-300v300z" />
|
||||||
|
<glyph glyph-name="74" unicode=""
|
||||||
|
d="M19 800h662c11 0 19 -8 19 -19v-331c0 -28 -22 -50 -50 -50h-600c-28 0 -50 22 -50 50v331c0 11 8 19 19 19zM0 309c16 -6 32 -9 50 -9h600c18 0 34 3 50 9v-290c0 -11 -8 -19 -19 -19h-662c-11 0 -19 8 -19 19v290zM550 200c-28 0 -50 -22 -50 -50s22 -50 50 -50
|
||||||
|
s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="75" unicode=""
|
||||||
|
d="M0 700h300v-100h-50c-28 0 -50 -22 -50 -50v-150h300v150c0 28 -22 50 -50 50h-50v100h300v-100h-50c-28 0 -50 -22 -50 -50v-400c0 -28 22 -50 50 -50h50v-100h-300v100h50c28 0 50 22 50 50v150h-300v-150c0 -28 22 -50 50 -50h50v-100h-300v100h50c28 0 50 22 50 50
|
||||||
|
v400c0 28 -22 50 -50 50h-50v100z" />
|
||||||
|
<glyph glyph-name="76" unicode=""
|
||||||
|
d="M400 700c165 0 300 -135 300 -300v-100h50c28 0 50 -22 50 -50v-200c0 -28 -22 -50 -50 -50h-100c-28 0 -50 22 -50 50v350c0 111 -89 200 -200 200s-200 -89 -200 -200v-350c0 -28 -22 -50 -50 -50h-100c-28 0 -50 22 -50 50v200c0 28 22 50 50 50h50v100
|
||||||
|
c0 165 135 300 300 300z" />
|
||||||
|
<glyph glyph-name="77" unicode=""
|
||||||
|
d="M0 500c0 109 91 200 200 200s200 -91 200 -200c0 109 91 200 200 200s200 -91 200 -200c0 -55 -23 -105 -59 -141l-341 -340l-341 340c-36 36 -59 86 -59 141z" />
|
||||||
|
<glyph glyph-name="78" unicode=""
|
||||||
|
d="M400 700l400 -300l-100 3v-403h-200v200h-200v-200h-200v400h-100z" />
|
||||||
|
<glyph glyph-name="79" unicode=""
|
||||||
|
d="M0 800h800v-800h-800v800zM100 700v-300l100 100l400 -400h100v100l-200 200l100 100l100 -100v300h-600z" />
|
||||||
|
<glyph glyph-name="7a" unicode=""
|
||||||
|
d="M19 800h762c11 0 19 -8 19 -19v-762c0 -11 -8 -19 -19 -19h-762c-11 0 -19 8 -19 19v762c0 11 8 19 19 19zM100 600v-300h100l100 -100h200l100 100h100v300h-600z" />
|
||||||
|
<glyph glyph-name="7b" unicode=""
|
||||||
|
d="M200 600c80 0 142 -56 200 -122c58 66 119 122 200 122c131 0 200 -101 200 -200s-69 -200 -200 -200c-81 0 -142 56 -200 122c-58 -66 -121 -122 -200 -122c-131 0 -200 101 -200 200s69 200 200 200zM200 500c-74 0 -100 -54 -100 -100s26 -100 100 -100
|
||||||
|
c42 0 88 47 134 100c-46 53 -92 100 -134 100zM600 500c-43 0 -88 -47 -134 -100c46 -53 91 -100 134 -100c74 0 100 54 100 100s-26 100 -100 100z" />
|
||||||
|
<glyph glyph-name="7c" unicode="" horiz-adv-x="400"
|
||||||
|
d="M300 800c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100zM150 550c83 0 150 -69 150 -150c0 -66 -100 -214 -100 -250c0 -28 22 -50 50 -50s50 22 50 50h100c0 -83 -67 -150 -150 -150s-150 64 -150 150s100 222 100 250s-22 50 -50 50
|
||||||
|
s-50 -22 -50 -50h-100c0 83 67 150 150 150z" />
|
||||||
|
<glyph glyph-name="7d" unicode=""
|
||||||
|
d="M200 800h500v-100h-122c-77 -197 -156 -392 -234 -588l-6 -12h162v-100h-500v100h122c77 197 156 392 234 588l7 12h-163v100z" />
|
||||||
|
<glyph glyph-name="7e" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM100 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="7f" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="80" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM200 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="81" unicode=""
|
||||||
|
d="M550 800c138 0 250 -112 250 -250s-112 -250 -250 -250c-16 0 -32 0 -47 3l-3 -3v-100h-200v-200h-300v200l303 303c-3 15 -3 31 -3 47c0 138 112 250 250 250zM600 700c-55 0 -100 -45 -100 -100s45 -100 100 -100s100 45 100 100s-45 100 -100 100z" />
|
||||||
|
<glyph glyph-name="82" unicode=""
|
||||||
|
d="M134 600h3h4h4h5h500c28 0 50 -22 50 -50v-350h100v-150c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v150h100v350v2c0 20 15 42 34 48zM200 500v-300h100v-100h200v100h100v300h-400z" />
|
||||||
|
<glyph glyph-name="83" unicode=""
|
||||||
|
d="M0 800h400v-400h-400v400zM500 600h100v-400h-400v100h300v300zM700 400h100v-400h-400v100h300v300z" />
|
||||||
|
<glyph glyph-name="84" unicode="" horiz-adv-x="600"
|
||||||
|
d="M337 694c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-300 -150c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50c0 21 16 44 37 49zM437 544c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-400 -200c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50
|
||||||
|
c0 21 16 44 37 49zM437 344c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-106 -56c24 -4 43 -26 43 -50c0 -28 -23 -51 -51 -51c-2 0 -6 1 -8 1h-200c-26 1 -48 24 -48 50c0 16 12 36 26 44zM151 -50c0 23 20 50 46 50h3h4h5h100c28 0 50 -22 50 -50
|
||||||
|
s-22 -50 -50 -50h-100c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51z" />
|
||||||
|
<glyph glyph-name="85" unicode=""
|
||||||
|
d="M199 800h100v-200h-200v100h100v100zM586 797h1c18 1 38 1 56 -3c36 -8 69 -26 97 -54c78 -78 78 -203 0 -281l-150 -150c-8 -13 -28 -24 -43 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43l150 150c40 40 39 105 0 144c-41 41 -110 34 -144 0l-44 -44
|
||||||
|
c-8 -13 -27 -24 -42 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43l43 44c32 33 72 53 128 56zM208 490c4 5 14 16 22 16h3c2 0 6 1 8 1c28 0 50 -22 50 -50c0 -11 -6 -27 -14 -35l-150 -150c-40 -40 -39 -105 0 -144c41 -41 110 -34 144 0l44 44c8 13 27 24 42 24
|
||||||
|
c28 0 50 -22 50 -50c0 -15 -11 -35 -24 -43l-43 -44c-22 -22 -48 -37 -75 -47c-70 -25 -151 -9 -207 47c-78 78 -78 203 0 281zM499 200h200v-100h-100v-100h-100v200z" />
|
||||||
|
<glyph glyph-name="86" unicode=""
|
||||||
|
d="M586 797c18 1 39 1 57 -3c36 -8 69 -26 97 -54c78 -78 78 -203 0 -281l-150 -150c-62 -62 -132 -81 -182 -78s-69 17 -84 25s-26 27 -26 44c0 28 22 51 50 51c8 0 19 -3 26 -7c0 0 15 -11 41 -13s62 3 106 47l150 150c40 40 39 105 0 144c-41 41 -110 34 -144 0
|
||||||
|
c-8 -13 -28 -24 -43 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43c32 33 72 53 128 56zM386 566c50 -2 64 -17 85 -22s37 -28 37 -49c0 -28 -22 -50 -50 -50c-10 0 -23 5 -31 11c0 0 -19 9 -47 10s-63 -4 -103 -44l-150 -150c-40 -40 -39 -105 0 -144c41 -41 110 -34 144 0
|
||||||
|
c8 13 27 24 42 24c28 0 50 -22 50 -50c0 -15 -10 -35 -23 -43c-22 -22 -48 -37 -75 -47c-70 -25 -151 -9 -207 47c-78 78 -78 203 0 281l150 150c60 60 128 78 178 76z" />
|
||||||
|
<glyph glyph-name="87" unicode=""
|
||||||
|
d="M0 700h300v-300h-300v300zM400 700h400v-100h-400v100zM400 500h300v-100h-300v100zM0 300h300v-300h-300v300zM400 300h400v-100h-400v100zM400 100h300v-100h-300v100z" />
|
||||||
|
<glyph glyph-name="88" unicode=""
|
||||||
|
d="M50 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 700h600v-100h-600v100zM50 500c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 500h600v-100h-600v100zM50 300c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50
|
||||||
|
s22 50 50 50zM200 300h600v-100h-600v100zM50 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 100h600v-100h-600v100z" />
|
||||||
|
<glyph glyph-name="89" unicode=""
|
||||||
|
d="M800 800l-400 -800l-100 300l-300 100z" />
|
||||||
|
<glyph glyph-name="8a" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 700c110 0 200 -90 200 -200v-100h100v-400h-600v400h100v100c0 110 90 200 200 200zM300 600c-56 0 -100 -44 -100 -100v-100h200v100c0 56 -44 100 -100 100z" />
|
||||||
|
<glyph glyph-name="8b" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800c110 0 200 -90 200 -200v-200h100v-400h-600v400h400v200c0 56 -44 100 -100 100s-100 -44 -100 -100h-100c0 110 90 200 200 200z" />
|
||||||
|
<glyph glyph-name="8c" unicode=""
|
||||||
|
d="M400 700v-100c-111 0 -200 -89 -200 -200h100l-150 -200l-150 200h100c0 165 135 300 300 300zM650 600l150 -200h-100c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-100z" />
|
||||||
|
<glyph glyph-name="8d" unicode=""
|
||||||
|
d="M100 800h600v-300h100l-150 -250l-150 250h100v200h-400v-100h-100v200zM150 550l150 -250h-100v-200h400v100h100v-200h-600v300h-100z" />
|
||||||
|
<glyph glyph-name="8e" unicode=""
|
||||||
|
d="M600 700l200 -150l-200 -150v100h-500v-100h-100v100c0 55 45 100 100 100h500v100zM200 300v-100h500v100h100v-100c0 -55 -45 -100 -100 -100h-500v-100l-200 150z" />
|
||||||
|
<glyph glyph-name="8f" unicode="" horiz-adv-x="900"
|
||||||
|
d="M350 800c193 0 350 -157 350 -350c0 -60 -17 -117 -44 -166c5 -3 12 -8 16 -12l100 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 100c-4 3 -9 9 -12 13c-49 -26 -107 -41 -166 -41c-193 0 -350 157 -350 350s157 350 350 350zM350 200
|
||||||
|
c142 0 250 108 250 250c0 139 -111 250 -250 250s-250 -111 -250 -250s111 -250 250 -250z" />
|
||||||
|
<glyph glyph-name="90" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 800c166 0 300 -134 300 -300c0 -200 -300 -500 -300 -500s-300 300 -300 500c0 166 134 300 300 300zM300 700c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200z" />
|
||||||
|
<glyph glyph-name="91" unicode="" horiz-adv-x="900"
|
||||||
|
d="M0 800h800v-541c1 -3 1 -8 1 -11s0 -7 -1 -10v-238h-800v800zM495 250c0 26 22 50 50 50h5h150v400h-600v-600h600v100h-150h-5c-28 0 -50 22 -50 50zM350 600c83 0 150 -67 150 -150c0 -100 -150 -250 -150 -250s-150 150 -150 250c0 83 67 150 150 150zM350 500
|
||||||
|
c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="92" unicode="" horiz-adv-x="600"
|
||||||
|
d="M0 700h200v-600h-200v600zM400 700h200v-600h-200v600z" />
|
||||||
|
<glyph glyph-name="93" unicode="" horiz-adv-x="600"
|
||||||
|
d="M0 700l600 -300l-600 -300v600z" />
|
||||||
|
<glyph glyph-name="94" unicode="" horiz-adv-x="600"
|
||||||
|
d="M300 700c166 0 300 -134 300 -300s-134 -300 -300 -300s-300 134 -300 300s134 300 300 300z" />
|
||||||
|
<glyph glyph-name="95" unicode=""
|
||||||
|
d="M400 700v-600l-400 300zM400 400l400 300v-600z" />
|
||||||
|
<glyph glyph-name="96" unicode=""
|
||||||
|
d="M0 700l400 -300l-400 -300v600zM400 100v600l400 -300z" />
|
||||||
|
<glyph glyph-name="97" unicode=""
|
||||||
|
d="M0 700h200v-600h-200v600zM200 400l500 300v-600z" />
|
||||||
|
<glyph glyph-name="98" unicode=""
|
||||||
|
d="M0 700l500 -300l-500 -300v600zM500 100v600h200v-600h-200z" />
|
||||||
|
<glyph glyph-name="99" unicode="" horiz-adv-x="600"
|
||||||
|
d="M0 700h600v-600h-600v600z" />
|
||||||
|
<glyph glyph-name="9a" unicode=""
|
||||||
|
d="M200 800h400v-200h200v-400h-200v-200h-400v200h-200v400h200v200z" />
|
||||||
|
<glyph glyph-name="9b" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM0 403h800v-100h-800v100zM0 103h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="9c" unicode="" horiz-adv-x="600"
|
||||||
|
d="M278 700c7 2 13 4 22 4c55 0 100 -45 100 -100v-4v-200c0 -55 -45 -100 -100 -100s-100 45 -100 100v200v2c0 44 35 88 78 98zM34 500h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-50c0 -111 89 -200 200 -200s200 89 200 200v50c0 28 22 50 50 50s50 -22 50 -50v-50
|
||||||
|
c0 -148 -109 -270 -250 -294v-106h50c55 0 100 -45 100 -100h-400c0 55 45 100 100 100h50v106c-141 24 -250 146 -250 294v50v2c0 20 15 42 34 48z" />
|
||||||
|
<glyph glyph-name="9d" unicode=""
|
||||||
|
d="M0 500h800v-200h-800v200z" />
|
||||||
|
<glyph glyph-name="9e" unicode=""
|
||||||
|
d="M34 700h4h3h4h5h700c28 0 50 -22 50 -50v-500c0 -28 -22 -50 -50 -50h-250v-100h100c55 0 100 -45 100 -100h-600c0 55 45 100 100 100h100v100h-250c-28 0 -50 22 -50 50v500v2c0 20 15 42 34 48zM100 600v-400h600v400h-600z" />
|
||||||
|
<glyph glyph-name="9f" unicode=""
|
||||||
|
d="M272 700c-14 -40 -22 -83 -22 -128c0 -221 179 -400 400 -400c45 0 88 8 128 22c-53 -158 -202 -272 -378 -272c-221 0 -400 179 -400 400c0 176 114 325 272 378z" />
|
||||||
|
<glyph glyph-name="a0" unicode=""
|
||||||
|
d="M350 700l150 -150h-100v-150h150v100l150 -150l-150 -150v100h-150v-150h100l-150 -150l-150 150h100v150h-150v-100l-150 150l150 150v-100h150v150h-100z" />
|
||||||
|
<glyph glyph-name="a1" unicode=""
|
||||||
|
d="M800 800v-550c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150c17 0 35 -4 50 -9v206c-201 -6 -327 -27 -400 -50v-397c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150c17 0 35 -4 50 -9v409s100 100 600 100z" />
|
||||||
|
<glyph glyph-name="a2" unicode="" horiz-adv-x="700"
|
||||||
|
d="M499 700c51 0 102 -20 141 -59c78 -78 78 -203 0 -281l-250 -244c-48 -48 -127 -48 -175 0s-48 127 0 175l96 97l69 -69l-90 -94l-7 -3c-10 -10 -10 -28 0 -38s28 -10 38 0l250 247c37 40 39 102 0 141s-104 40 -144 0l-278 -275c-66 -69 -68 -179 0 -247
|
||||||
|
c69 -69 181 -69 250 0l9 12l116 113l69 -69l-125 -125c-107 -107 -281 -107 -388 0s-107 281 0 388l278 272c39 39 90 59 141 59z" />
|
||||||
|
<glyph glyph-name="a3" unicode=""
|
||||||
|
d="M600 800l200 -200l-100 -100l-200 200zM400 600l200 -200l-400 -400h-200v200z" />
|
||||||
|
<glyph glyph-name="a4" unicode=""
|
||||||
|
d="M550 800c83 0 150 -90 150 -200s-67 -200 -150 -200c-22 0 -40 8 -59 19c6 26 9 52 9 81c0 84 -27 158 -72 212c27 52 71 88 122 88zM250 700c83 0 150 -90 150 -200s-67 -200 -150 -200s-150 90 -150 200s67 200 150 200zM725 384c44 -22 75 -66 75 -118v-166h-200v66
|
||||||
|
c0 50 -17 96 -44 134c66 2 126 33 169 84zM75 284c45 -53 106 -84 175 -84s130 31 175 84c44 -22 75 -66 75 -118v-166h-500v166c0 52 31 96 75 118z" />
|
||||||
|
<glyph glyph-name="a5" unicode=""
|
||||||
|
d="M400 800c110 0 200 -112 200 -250s-90 -250 -200 -250s-200 112 -200 250s90 250 200 250zM191 300c54 -61 128 -100 209 -100s155 39 209 100c106 -5 191 -92 191 -200v-100h-800v100c0 108 85 195 191 200z" />
|
||||||
|
<glyph glyph-name="a6" unicode="" horiz-adv-x="600"
|
||||||
|
d="M19 800h462c11 0 19 -8 19 -19v-762c0 -11 -8 -19 -19 -19h-462c-11 0 -19 8 -19 19v762c0 11 8 19 19 19zM100 700v-500h300v500h-300zM250 150c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="a7" unicode=""
|
||||||
|
d="M350 800c17 0 34 -1 50 -3v-397l-297 297c63 64 150 103 247 103zM500 694c169 -25 300 -168 300 -344c0 -193 -157 -350 -350 -350c-85 0 -161 31 -222 81l272 272v341zM91 562l237 -234l-212 -212c-70 55 -116 138 -116 234c0 84 35 158 91 212z" />
|
||||||
|
<glyph glyph-name="a8" unicode=""
|
||||||
|
d="M92 650c0 23 20 50 46 50h3h4h5h400c28 0 50 -22 50 -50s-22 -50 -50 -50h-50v-200h100c55 0 100 -45 100 -100h-300v-300l-56 -100l-44 100v300h-300c0 55 45 100 100 100h100v200h-50c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51z" />
|
||||||
|
<glyph glyph-name="a9" unicode=""
|
||||||
|
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM300 600v-400l300 200z" />
|
||||||
|
<glyph glyph-name="aa" unicode=""
|
||||||
|
d="M300 800h200v-300h300v-200h-300v-300h-200v300h-300v200h300v300z" />
|
||||||
|
<glyph glyph-name="ab" unicode=""
|
||||||
|
d="M300 800h100v-400h-100v400zM172 656l62 -78l-40 -31c-58 -46 -94 -117 -94 -197c0 -139 111 -250 250 -250s250 111 250 250c0 80 -39 151 -97 197l-37 31l62 78l38 -31c82 -64 134 -164 134 -275c0 -193 -157 -350 -350 -350s-350 157 -350 350c0 111 53 211 134 275z
|
||||||
|
" />
|
||||||
|
<glyph glyph-name="ac" unicode=""
|
||||||
|
d="M200 800h400v-200h-400v200zM9 500h782c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-91v200h-600v-200h-91c-6 0 -9 3 -9 9v282c0 6 3 9 9 9zM200 300h400v-300h-400v300z" />
|
||||||
|
<glyph glyph-name="ad" unicode=""
|
||||||
|
d="M0 700h100v-700h-100v700zM700 700h100v-700h-100v700zM200 600h200v-100h-200v100zM300 400h200v-100h-200v100zM400 200h200v-100h-200v100z" />
|
||||||
|
<glyph glyph-name="ae" unicode=""
|
||||||
|
d="M325 700c42 -141 87 -280 131 -419c29 74 59 148 88 222c30 -57 58 -114 87 -172h169v-100h-231l-13 28c-37 -92 -74 -184 -112 -275c-38 129 -79 257 -119 385c-42 -133 -83 -267 -125 -400c-28 88 -56 175 -84 262h-116v100h188l9 -34l3 -6c42 137 83 273 125 409z" />
|
||||||
|
<glyph glyph-name="af" unicode=""
|
||||||
|
d="M200 600c0 57 43 100 100 100s100 -43 100 -100c0 -28 -18 -48 -28 -72c-3 -6 -3 -16 -3 -28h231v-231c12 0 22 0 28 3c24 10 44 28 72 28c57 0 100 -43 100 -100s-43 -100 -100 -100c-28 0 -48 18 -72 28c-6 3 -16 3 -28 3v-231h-231c0 12 0 22 3 28c10 24 28 44 28 72
|
||||||
|
c0 57 -43 100 -100 100s-100 -43 -100 -100c0 -28 18 -48 28 -72c3 -6 3 -16 3 -28h-231v600h231c0 12 0 22 -3 28c-10 24 -28 44 -28 72z" />
|
||||||
|
<glyph glyph-name="b0" unicode="" horiz-adv-x="500"
|
||||||
|
d="M247 700c84 0 148 -20 191 -59s59 -93 59 -141c0 -117 -69 -181 -119 -225s-81 -67 -81 -150v-25h-100v25c0 117 65 181 115 225s85 67 85 150c0 25 -8 48 -28 66s-56 34 -122 34s-97 -18 -116 -37s-27 -43 -31 -69l-100 12c5 38 19 88 59 128s103 66 188 66zM197 0h100
|
||||||
|
v-100h-100v100z" />
|
||||||
|
<glyph glyph-name="b1" unicode=""
|
||||||
|
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -69 -48 -127 -112 -144c-22 55 -75 94 -138 94c-20 0 -39 -5 -56 -12c-17 64 -75 112 -144 112s-127 -48 -144 -112c-17 7 -36 12 -56 12c-37 0 -71 -12 -97 -34c-33 36 -53 82 -53 134
|
||||||
|
c0 110 90 200 200 200c23 114 129 200 250 200zM334 300h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-200c0 -28 -22 -50 -50 -50s-50 22 -50 50v200v2c0 20 15 42 34 48zM134 200h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-100c0 -28 -22 -50 -50 -50s-50 22 -50 50v100v2
|
||||||
|
c0 20 15 42 34 48zM534 200h3h4c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-100c0 -28 -22 -50 -50 -50s-50 22 -50 50v100v2c0 20 15 42 34 48z" />
|
||||||
|
<glyph glyph-name="b2" unicode=""
|
||||||
|
d="M600 800l200 -150l-200 -150v100h-50l-153 -191l175 -206l6 -3h22v100l200 -150l-200 -150v100h-25c-35 0 -56 12 -78 38l-166 190l-153 -190c-22 -27 -43 -38 -78 -38h-100v100h100l166 206l-163 191l-3 3h-100v100h100c34 0 56 -12 78 -38l153 -178l141 178
|
||||||
|
c22 27 43 38 78 38h50v100z" />
|
||||||
|
<glyph glyph-name="b3" unicode=""
|
||||||
|
d="M400 800c110 0 209 -47 281 -119l119 119v-300h-300l109 109c-54 55 -126 91 -209 91c-166 0 -300 -134 -300 -300s134 -300 300 -300c83 0 158 34 212 88l72 -72c-72 -72 -174 -116 -284 -116c-220 0 -400 180 -400 400s180 400 400 400z" />
|
||||||
|
<glyph glyph-name="b4" unicode=""
|
||||||
|
d="M400 800h400v-400l-166 166l-400 -400l166 -166h-400v400l166 -166l400 400z" />
|
||||||
|
<glyph glyph-name="b5" unicode="" horiz-adv-x="600"
|
||||||
|
d="M250 800l250 -300h-200v-200h200l-250 -300l-250 300h200v200h-200z" />
|
||||||
|
<glyph glyph-name="b6" unicode=""
|
||||||
|
d="M300 600v-200h200v200l300 -250l-300 -250v200h-200v-200l-300 250z" />
|
||||||
|
<glyph glyph-name="b7" unicode=""
|
||||||
|
d="M0 800c441 0 800 -359 800 -800h-200c0 333 -267 600 -600 600v200zM0 500c275 0 500 -225 500 -500h-200c0 167 -133 300 -300 300v200zM0 200c110 0 200 -90 200 -200h-200v200z" />
|
||||||
|
<glyph glyph-name="b8" unicode=""
|
||||||
|
d="M100 800c386 0 700 -314 700 -700h-100c0 332 -268 600 -600 600v100zM100 600c276 0 500 -224 500 -500h-100c0 222 -178 400 -400 400v100zM100 400c165 0 300 -135 300 -300h-100c0 111 -89 200 -200 200v100zM100 200c55 0 100 -45 100 -100s-45 -100 -100 -100
|
||||||
|
s-100 45 -100 100s45 100 100 100z" />
|
||||||
|
<glyph glyph-name="b9" unicode=""
|
||||||
|
d="M300 800h400c55 0 100 -45 100 -100v-200h-400v150c0 28 -22 50 -50 50s-50 -22 -50 -50v-250h400v-300c0 -55 -45 -100 -100 -100h-500c-55 0 -100 45 -100 100v200h100v-150c0 -28 22 -50 50 -50s50 22 50 50v550c0 55 45 100 100 100z" />
|
||||||
|
<glyph glyph-name="ba" unicode=""
|
||||||
|
d="M75 700h225v-100h-200v-500h400v100h100v-125c0 -41 -34 -75 -75 -75h-450c-41 0 -75 34 -75 75v550c0 41 34 75 75 75zM600 700l200 -200l-200 -200v100h-200c-94 0 -173 -65 -194 -153c23 199 189 353 394 353v100z" />
|
||||||
|
<glyph glyph-name="bb" unicode=""
|
||||||
|
d="M500 700l300 -284l-300 -316v200h-100c-200 0 -348 -102 -400 -300c0 295 100 500 500 500v200z" />
|
||||||
|
<glyph glyph-name="bc" unicode=""
|
||||||
|
d="M381 791l19 9l19 -9c127 -53 253 -108 381 -160v-31c0 -166 -67 -313 -147 -419c-40 -53 -83 -97 -125 -128s-82 -53 -128 -53s-86 22 -128 53s-85 75 -125 128c-80 107 -147 253 -147 419v31c128 52 254 107 381 160zM400 100v591l-294 -122c8 -126 58 -243 122 -328
|
||||||
|
c35 -46 73 -86 106 -110s62 -31 66 -31z" />
|
||||||
|
<glyph glyph-name="bd" unicode=""
|
||||||
|
d="M600 800h100v-800h-100v800zM400 700h100v-700h-100v700zM200 500h100v-500h-100v500zM0 300h100v-300h-100v300z" />
|
||||||
|
<glyph glyph-name="be" unicode=""
|
||||||
|
d="M300 800h100v-200h200l100 -100l-100 -100h-200v-400h-100v500h-200l-100 100l100 100h200v100z" />
|
||||||
|
<glyph glyph-name="bf" unicode=""
|
||||||
|
d="M200 800h100v-600h200l-250 -200l-250 200h200v600zM400 800h200v-100h-200v100zM400 600h300v-100h-300v100zM400 400h400v-100h-400v100z" />
|
||||||
|
<glyph glyph-name="c0" unicode=""
|
||||||
|
d="M200 800h100v-600h200l-250 -200l-250 200h200v600zM400 800h400v-100h-400v100zM400 600h300v-100h-300v100zM400 400h200v-100h-200v100z" />
|
||||||
|
<glyph glyph-name="c1" unicode=""
|
||||||
|
d="M75 700h650c41 0 75 -34 75 -75v-550c0 -41 -34 -75 -75 -75h-650c-41 0 -75 34 -75 75v550c0 41 34 75 75 75zM100 600v-100h100v100h-100zM300 600v-100h400v100h-400zM100 400v-100h100v100h-100zM300 400v-100h400v100h-400zM100 200v-100h100v100h-100zM300 200
|
||||||
|
v-100h400v100h-400z" />
|
||||||
|
<glyph glyph-name="c2" unicode=""
|
||||||
|
d="M400 800l100 -300h300l-250 -200l100 -300l-250 200l-250 -200l100 300l-250 200h300z" />
|
||||||
|
<glyph glyph-name="c3" unicode=""
|
||||||
|
d="M400 800c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM150 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM650 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM400 600c110 0 200 -90 200 -200
|
||||||
|
s-90 -200 -200 -200s-200 90 -200 200s90 200 200 200zM50 450c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM750 450c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM150 200c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50
|
||||||
|
s22 50 50 50zM650 200c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM400 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||||
|
<glyph glyph-name="c4" unicode=""
|
||||||
|
d="M34 800h632c18 0 34 -16 34 -34v-732c0 -18 -16 -34 -34 -34h-632c-18 0 -34 16 -34 34v732c0 18 16 34 34 34zM100 700v-500h500v500h-500zM350 150c-38 0 -63 -42 -44 -75s69 -33 88 0s-6 75 -44 75z" />
|
||||||
|
<glyph glyph-name="c5" unicode=""
|
||||||
|
d="M0 800h300l500 -500l-300 -300l-500 500v300zM200 700c-55 0 -100 -45 -100 -100s45 -100 100 -100s100 45 100 100s-45 100 -100 100z" />
|
||||||
|
<glyph glyph-name="c6" unicode=""
|
||||||
|
d="M0 600h200l300 -300l-200 -200l-300 300v200zM340 600h160l300 -300l-200 -200l-78 78l119 122zM150 500c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="c7" unicode=""
|
||||||
|
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM400 600c110 0 200 -90 200 -200s-90 -200 -200 -200s-200 90 -200 200
|
||||||
|
s90 200 200 200zM400 500c-56 0 -100 -44 -100 -100s44 -100 100 -100s100 44 100 100s-44 100 -100 100z" />
|
||||||
|
<glyph glyph-name="c8" unicode=""
|
||||||
|
d="M0 700h559l-100 -100h-359v-500h500v159l100 100v-359h-700v700zM700 700l100 -100l-400 -400l-200 200l100 100l100 -100z" />
|
||||||
|
<glyph glyph-name="c9" unicode=""
|
||||||
|
d="M9 800h782c6 0 9 -3 9 -9v-782c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v782c0 6 3 9 9 9zM150 722l-72 -72l100 -100l-100 -100l72 -72l172 172zM400 500v-100h300v100h-300z" />
|
||||||
|
<glyph glyph-name="ca" unicode=""
|
||||||
|
d="M0 800h800v-200h-50c0 55 -45 100 -100 100h-150v-550c0 -28 22 -50 50 -50h50v-100h-400v100h50c28 0 50 22 50 50v550h-150c-55 0 -100 -45 -100 -100h-50v200z" />
|
||||||
|
<glyph glyph-name="cb" unicode=""
|
||||||
|
d="M0 700h100v-400h-100v400zM200 700h350c21 0 39 -13 47 -31c0 0 103 -291 103 -319s-22 -50 -50 -50h-150c-28 0 -50 -25 -50 -50s39 -158 47 -184s-5 -55 -31 -63s-52 5 -66 31s-109 219 -128 238s-44 28 -72 28v400z" />
|
||||||
|
<glyph glyph-name="cc" unicode=""
|
||||||
|
d="M400 666c10 19 28 32 47 34l19 -3c26 -8 39 -37 31 -63s-47 -159 -47 -184s22 -50 50 -50h150c28 0 50 -22 50 -50s-103 -319 -103 -319c-8 -18 -26 -31 -47 -31h-350v400c28 0 53 9 72 28s114 212 128 238zM0 400h100v-400h-100v400z" />
|
||||||
|
<glyph glyph-name="cd" unicode=""
|
||||||
|
d="M200 700h300v-100h-100v-6c25 -4 50 -8 72 -16l-34 -94c-28 11 -58 16 -88 16c-139 0 -250 -111 -250 -250s111 -250 250 -250s250 111 250 250c0 31 -5 60 -16 88l91 37c14 -38 25 -81 25 -125c0 -193 -157 -350 -350 -350s-350 157 -350 350c0 176 130 323 300 347v3
|
||||||
|
h-100v100zM700 584c0 0 -296 -348 -316 -368s-48 -20 -68 0s-20 48 0 68s384 300 384 300z" />
|
||||||
|
<glyph glyph-name="ce" unicode=""
|
||||||
|
d="M600 700l200 -150l-200 -150v100h-600v100h600v100zM200 300v-100h600v-100h-600v-100l-200 150z" />
|
||||||
|
<glyph glyph-name="cf" unicode=""
|
||||||
|
d="M300 800h100c55 0 100 -45 100 -100h100c55 0 100 -45 100 -100h-700c0 55 45 100 100 100h100c0 55 45 100 100 100zM100 500h100v-350c0 -28 22 -50 50 -50s50 22 50 50v350h100v-350c0 -28 22 -50 50 -50s50 22 50 50v350h100v-481c0 -11 -8 -19 -19 -19h-462
|
||||||
|
c-11 0 -19 8 -19 19v481z" />
|
||||||
|
<glyph glyph-name="d0" unicode=""
|
||||||
|
d="M100 800h200v-400c0 -55 45 -100 100 -100s100 45 100 100v400h100v-400c0 -110 -90 -200 -200 -200h-50c-138 0 -250 90 -250 200v400zM0 100h700v-100h-700v100z" />
|
||||||
|
<glyph glyph-name="d1" unicode=""
|
||||||
|
d="M9 700h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM609 700h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM309 500h182c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v282
|
||||||
|
c0 6 3 9 9 9zM0 100h800v-100h-800v100z" />
|
||||||
|
<glyph glyph-name="d2" unicode=""
|
||||||
|
d="M10 700h181c6 0 9 -3 9 -9v-191h-200v191c0 6 4 9 10 9zM610 700h181c6 0 9 -3 9 -9v-191h-200v191c0 6 5 9 10 9zM310 600h181c6 0 9 -3 9 -9v-91h-200v91c0 6 4 9 10 9zM0 400h800v-100h-800v100zM0 200h200v-191c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v191zM300 200
|
||||||
|
h200v-91c0 -6 -3 -9 -9 -9h-181c-6 0 -10 3 -10 9v91zM600 200h200v-191c0 -6 -3 -9 -9 -9h-181c-6 0 -10 3 -10 9v191z" />
|
||||||
|
<glyph glyph-name="d3" unicode=""
|
||||||
|
d="M0 700h800v-100h-800v100zM9 500h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM309 500h182c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v282c0 6 3 9 9 9zM609 500h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182
|
||||||
|
c-6 0 -9 3 -9 9v482c0 6 3 9 9 9z" />
|
||||||
|
<glyph glyph-name="d4" unicode=""
|
||||||
|
d="M50 600h500c28 0 50 -22 50 -50v-150l100 100h100v-300h-100l-100 100v-150c0 -28 -22 -50 -50 -50h-500c-28 0 -50 22 -50 50v400c0 28 22 50 50 50z" />
|
||||||
|
<glyph glyph-name="d5" unicode=""
|
||||||
|
d="M334 800h66v-800h-66l-134 200h-200v400h200zM500 600v100c26 0 52 -4 75 -10c130 -33 225 -150 225 -290s-95 -258 -225 -291h-3c-23 -6 -47 -9 -72 -9v100c17 0 34 2 50 6c86 22 150 100 150 194s-64 172 -150 194c-16 4 -33 6 -50 6zM500 500l25 -3
|
||||||
|
c44 -11 75 -51 75 -97s-32 -86 -75 -97l-25 -3v200z" />
|
||||||
|
<glyph glyph-name="d6" unicode="" horiz-adv-x="600"
|
||||||
|
d="M334 800h66v-800h-66l-134 200h-200v400h200zM500 500l25 -3c44 -11 75 -51 75 -97s-32 -86 -75 -97l-25 -3v200z" />
|
||||||
|
<glyph glyph-name="d7" unicode="" horiz-adv-x="400"
|
||||||
|
d="M334 800h66v-800h-66l-134 200h-200v400h200z" />
|
||||||
|
<glyph glyph-name="d8" unicode=""
|
||||||
|
d="M309 800h82c6 0 10 -4 12 -9l294 -682l3 -19v-81c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v81l3 19l294 682c2 5 6 9 12 9zM300 500v-200h100v200h-100zM300 200v-100h100v100h-100z" />
|
||||||
|
<glyph glyph-name="d9" unicode=""
|
||||||
|
d="M375 800c138 0 269 -39 378 -109l-53 -82c-93 60 -205 91 -325 91c-119 0 -229 -32 -322 -91l-53 82c109 70 237 109 375 109zM375 500c78 0 154 -23 216 -62l-53 -85c-46 30 -104 47 -163 47c-60 0 -112 -17 -159 -47l-54 85c62 40 134 62 213 62zM375 200
|
||||||
|
c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||||
|
<glyph glyph-name="da" unicode="" horiz-adv-x="900"
|
||||||
|
d="M551 800c16 0 32 0 47 -3l-97 -97v-200h200l97 97c3 -15 3 -31 3 -47c0 -138 -112 -250 -250 -250c-32 0 -62 8 -90 19l-288 -291c-20 -20 -46 -28 -72 -28s-52 8 -72 28c-39 39 -39 105 0 144l291 287c-11 28 -19 59 -19 91c0 138 112 250 250 250zM101 150
|
||||||
|
c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||||
|
<glyph glyph-name="db" unicode=""
|
||||||
|
d="M141 700c84 -84 169 -167 253 -250c82 83 167 165 247 250l143 -141l-253 -253c84 -82 167 -166 253 -247l-143 -143c-81 86 -165 169 -247 253l-253 -253l-141 143c85 80 167 164 250 247c-83 84 -166 169 -250 253z" />
|
||||||
|
<glyph glyph-name="dc" unicode=""
|
||||||
|
d="M0 800h100l231 -300h38l231 300h100l-225 -300h225v-100h-300v-100h300v-100h-300v-200h-100v200h-300v100h300v100h-300v100h225z" />
|
||||||
|
<glyph glyph-name="dd" unicode="" horiz-adv-x="900"
|
||||||
|
d="M350 800c193 0 350 -157 350 -350c0 -61 -17 -119 -44 -169c4 -2 10 -6 13 -9l103 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 103c-3 3 -7 9 -9 13c-50 -28 -108 -44 -169 -44c-193 0 -350 157 -350 350s157 350 350 350zM350 700
|
||||||
|
c-139 0 -250 -111 -250 -250s111 -250 250 -250c62 0 119 23 163 60c7 11 19 25 31 31l3 3c34 43 53 97 53 156c0 139 -111 250 -250 250zM300 600h100v-100h100v-100h-100v-100h-100v100h-100v100h100v100z" />
|
||||||
|
<glyph glyph-name="de" unicode="" horiz-adv-x="900"
|
||||||
|
d="M350 800c193 0 350 -157 350 -350c0 -61 -17 -119 -44 -169c4 -2 10 -6 13 -9l103 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 103c-3 3 -7 9 -9 13c-50 -28 -108 -44 -169 -44c-193 0 -350 157 -350 350s157 350 350 350zM350 700
|
||||||
|
c-139 0 -250 -111 -250 -250s111 -250 250 -250c62 0 119 23 163 60c7 11 19 25 31 31l3 3c34 43 53 97 53 156c0 139 -111 250 -250 250zM200 500h300v-100h-300v100z" />
|
||||||
|
</font>
|
||||||
|
</defs></svg>
|
||||||
|
After Width: | Height: | Size: 54 KiB |