Compare commits
57 Commits
3b9cea1c70
...
master
Author | SHA1 | Date | |
---|---|---|---|
d392cb3c2d | |||
66ad8aa592 | |||
aa91f25d3c | |||
1f736ae059 | |||
4b59f3dfee | |||
8a699ccf79 | |||
b67086464c | |||
085006b073 | |||
6ce774236e | |||
aed385f5e0 | |||
e826d9cfa9 | |||
de9270476f | |||
55cdcd86b6 | |||
7e8da1a973 | |||
55e683f645 | |||
3acdcd297c | |||
344dd52019 | |||
cadb207758 | |||
bedc666f7c | |||
66ccb3d8d6 | |||
61001edaa0 | |||
14db929af1 | |||
456dc1b1e3 | |||
876702473c | |||
f5a34213b5 | |||
68ee38d1ff | |||
7ab7998ac7 | |||
37049ba6f4 | |||
236f74c1d8 | |||
6f1e3dcdd0 | |||
e59e105394 | |||
188a6ad9fa | |||
31891f3e53 | |||
1d94738b04 | |||
0c316480d5 | |||
ca78223606 | |||
1d898e350b | |||
14b2155595 | |||
f10f27f28b | |||
9459045e6c | |||
bc00b36799 | |||
936fa214e6 | |||
ff969aee2c | |||
83e667016d | |||
58f0bc61c7 | |||
51a06fa66c | |||
8cbceb485c | |||
991a844836 | |||
5732d6af63 | |||
5e69975df2 | |||
2d38d82540 | |||
9d7d8cf89c | |||
6ac9fd5575 | |||
4645488b5d | |||
31ca54cdac | |||
c38b088fff | |||
f247734385 |
44
.gitea/workflows/build.yaml
Normal file
44
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Build and test
|
||||||
|
run-name: Perform a regular build and test
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
requirements:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
extensions: mbstring, xml, curl, zip
|
||||||
|
tools: composer, phpdoc
|
||||||
|
coverage: xdebug
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
composer config github-oauth.github.com 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
|
||||||
|
composer install
|
||||||
|
wget https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
||||||
|
dpkg -i rclone-current-linux-amd64.deb
|
||||||
|
- name: Dry run
|
||||||
|
run: ./backup show config.example.yml
|
||||||
|
- name: Static analysis
|
||||||
|
run: composer analyze
|
||||||
|
- name: Test
|
||||||
|
run: composer test-full
|
||||||
|
- name: Upload Test report
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: output/test.html
|
||||||
|
name: test.html
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: output/coverage
|
||||||
|
name: test-coverage.zip
|
||||||
|
- name: Document
|
||||||
|
run: phpdoc run
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: output/docs
|
||||||
|
name: documentation.zip
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@
|
|||||||
config.yml
|
config.yml
|
||||||
*.phar
|
*.phar
|
||||||
*.deb
|
*.deb
|
||||||
|
.phpunit.result.cache
|
||||||
|
*cache/
|
51
.phpcs.xml
Normal file
51
.phpcs.xml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PHP_CodeSniffer" xsi:noNamespaceSchemaLocation="phpcs.xsd">
|
||||||
|
<description>Coding standard</description>
|
||||||
|
|
||||||
|
<file>src/</file>
|
||||||
|
<file>tests/</file>
|
||||||
|
|
||||||
|
<rule ref="PSR1">
|
||||||
|
<exclude name="Generic.Files.LineLength"/>
|
||||||
|
</rule>
|
||||||
|
<rule ref="PSR2"></rule>
|
||||||
|
<rule ref="PSR12"></rule>
|
||||||
|
|
||||||
|
<rule ref="Generic">
|
||||||
|
<exclude name="Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed"/>
|
||||||
|
<exclude name="Generic.Files.LowercasedFilename.NotFound"/>
|
||||||
|
<exclude name="Generic.PHP.ClosingPHPTag.NotFound"/>
|
||||||
|
<exclude name="Generic.Files.EndFileNoNewline.Found"/>
|
||||||
|
<exclude name="Generic.Files.EndFileNoNewline.Found"/>
|
||||||
|
<exclude name="Generic.Arrays.DisallowShortArraySyntax.Found"/>
|
||||||
|
<exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine"/>
|
||||||
|
<exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine"/>
|
||||||
|
<exclude name="Generic.PHP.LowerCaseConstant.Found"/>
|
||||||
|
<exclude name="Generic.Formatting.SpaceAfterCast"/>
|
||||||
|
<exclude name="Generic.Formatting.MultipleStatementAlignment.NotSameWarning"/>
|
||||||
|
<exclude name="Generic.Commenting.DocComment.MissingShort"/>
|
||||||
|
<exclude name="Generic.NamingConventions.AbstractClassNamePrefix.Missing"/>
|
||||||
|
<exclude name="Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed"/>
|
||||||
|
<exclude name="Generic.NamingConventions.InterfaceNameSuffix.Missing"/>
|
||||||
|
<exclude name="Generic.Commenting.Todo.TaskFound"/>
|
||||||
|
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUse"/>
|
||||||
|
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed"/>
|
||||||
|
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed"/>
|
||||||
|
<exclude name="Generic.Formatting.SpaceBeforeCast.NoSpace"/>
|
||||||
|
<exclude name="Generic.CodeAnalysis.UselessOverridingMethod.Found"/>
|
||||||
|
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace"/>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Ban some functions -->
|
||||||
|
<rule ref="Generic.PHP.ForbiddenFunctions">
|
||||||
|
<properties>
|
||||||
|
<property name="forbiddenFunctions" type="array">
|
||||||
|
<element key="sizeof" value="count"/>
|
||||||
|
<element key="delete" value="unset"/>
|
||||||
|
<element key="print" value="echo"/>
|
||||||
|
<element key="is_null" value="null"/>
|
||||||
|
<element key="create_function" value="null"/>
|
||||||
|
</property>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
</ruleset>
|
@ -1,23 +1,27 @@
|
|||||||
pipeline:
|
when:
|
||||||
requirements:
|
- event: [push, pull_request, pull_request_closed, tag, release, manual]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: requirements
|
||||||
image: composer
|
image: composer
|
||||||
commands:
|
commands:
|
||||||
- composer install --no-dev
|
- composer install --no-dev
|
||||||
run:
|
- name: run
|
||||||
image: php:cli-bookworm
|
image: php:cli-alpine
|
||||||
commands:
|
commands:
|
||||||
- ./backup show config.example.yml
|
- ./backup show config.example.yml
|
||||||
dependencies:
|
- name: dependencies
|
||||||
image: composer
|
image: composer
|
||||||
commands:
|
commands:
|
||||||
- composer install
|
- composer install
|
||||||
analyze:
|
- composer analyze
|
||||||
image: php:cli-bookworm
|
- name: test
|
||||||
commands:
|
|
||||||
- make analyze
|
|
||||||
test:
|
|
||||||
image: php:cli-bookworm
|
image: php:cli-bookworm
|
||||||
commands:
|
commands:
|
||||||
- apt update
|
- apt update
|
||||||
- apt install rclone
|
- apt install rclone
|
||||||
- vendor/bin/phpunit tests
|
- vendor/bin/phpunit --no-coverage
|
||||||
|
- name: document
|
||||||
|
image: phpdoc/phpdoc
|
||||||
|
commands:
|
||||||
|
- phpdoc
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FROM php:8.3-cli
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
25
Makefile
25
Makefile
@ -1,25 +0,0 @@
|
|||||||
analyze: analyze-yaml analyze-phpmd analyze-phpstan analyze-psalm analyze-phpcs
|
|
||||||
|
|
||||||
analyze-yaml:
|
|
||||||
vendor/bin/yaml-lint *.yml
|
|
||||||
analyze-phpmd:
|
|
||||||
vendor/bin/phpmd src,tests text cleancode,codesize,controversial,design,naming,unusedcode
|
|
||||||
analyze-phpstan:
|
|
||||||
vendor/bin/phpstan analyze --level=8 --error-format=raw src/ backup tests
|
|
||||||
analyze-psalm:
|
|
||||||
vendor/bin/psalm --no-cache
|
|
||||||
analyze-phpcs:
|
|
||||||
vendor/bin/phpcs src backup tests --report=emacs --standard=PSR12
|
|
||||||
|
|
||||||
docs:
|
|
||||||
./phpDocumentor.phar --setting=graphs.enabled=true
|
|
||||||
|
|
||||||
install:
|
|
||||||
php composer.phar install --no-dev
|
|
||||||
install-dev:
|
|
||||||
php composer.phar install
|
|
||||||
test:
|
|
||||||
vendor/bin/phpunit tests --display-warnings
|
|
||||||
test-coverage:
|
|
||||||
vendor/bin/phpunit tests --testdox --coverage-filter src --coverage-html output/coverage --coverage-text --path-coverage --testdox-html output/test.html
|
|
||||||
|
|
33
README.md
33
README.md
@ -4,11 +4,42 @@
|
|||||||
|
|
||||||
Backup script utilizing Rclone to backup local file systems and send notifications.
|
Backup script utilizing Rclone to backup local file systems and send notifications.
|
||||||
|
|
||||||
|
# Minimum requirements
|
||||||
|
- PHP8.1
|
||||||
|
- Composer to install required packages.
|
||||||
|
- Rclone installed
|
||||||
|
|
||||||
# Rclone install
|
# Rclone install
|
||||||
|
rm rclone-current-linux-amd64.deb
|
||||||
wget https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
wget https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
||||||
sudo dpkg -i rclone-current-linux-amd64.deb
|
sudo dpkg -i rclone-current-linux-amd64.deb
|
||||||
|
|
||||||
|
# For development
|
||||||
|
|
||||||
|
# PHP latest for debian
|
||||||
|
curl -sSL https://packages.sury.org/php/README.txt | sudo bash -x
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install php8.3-cli php8.3-xml php8.3-curl php8.3-zip php8.3-xdebug php8.3-mbstring unzip wget graphviz plantuml
|
||||||
|
|
||||||
# PHP Docs
|
# PHP Docs
|
||||||
|
rm phpDocumentor.phar
|
||||||
wget https://phpdoc.org/phpDocumentor.phar
|
wget https://phpdoc.org/phpDocumentor.phar
|
||||||
chmod +x phpDocumentor.phar
|
chmod +x phpDocumentor.phar
|
||||||
./phpDocumentor.phar
|
|
||||||
|
|
||||||
|
|
||||||
|
# Infection install
|
||||||
|
rm infection.phar
|
||||||
|
wget https://github.com/infection/infection/releases/download/0.27.8/infection.phar
|
||||||
|
chmod +x infection.phar
|
||||||
|
|
||||||
|
# PHP CopyPasteDetector install
|
||||||
|
rm phpcpd.phar
|
||||||
|
wget https://phar.phpunit.de/phpcpd.phar
|
||||||
|
chmod +x phpcpd.phar
|
||||||
|
|
||||||
|
# Test, analyze, metrics, document
|
||||||
|
./composer.phar test-full && ./composer.phar analyze && ./composer.phar metrics && ./composer.phar doc
|
||||||
|
|
||||||
|
|
||||||
|
12
backup
12
backup
@ -8,8 +8,12 @@ use Composer\InstalledVersions;
|
|||||||
$package = \Composer\InstalledVersions::getRootPackage();
|
$package = \Composer\InstalledVersions::getRootPackage();
|
||||||
|
|
||||||
$application = new Application('backup', $package['version']);
|
$application = new Application('backup', $package['version']);
|
||||||
|
try {
|
||||||
|
$application->add(new App\CommandBackup());
|
||||||
|
$application->add(new App\CommandShow());
|
||||||
|
|
||||||
$application->add(new App\CommandBackup());
|
$application->run();
|
||||||
$application->add(new App\CommandShow());
|
} catch(Exception $e)
|
||||||
|
{
|
||||||
$application->run();
|
echo("Critical error: ". $e->getMessage());
|
||||||
|
}
|
@ -1,16 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "furyfire/backup",
|
"name": "furyfire/backupscript",
|
||||||
"description": "Wrapper for Rclone based backup",
|
"description": "Wrapper for Rclone based backup with push notifications",
|
||||||
"homepage": "https://jcktrue.dk",
|
"homepage": "https://jcktrue.dk",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"require": {
|
"require": {
|
||||||
"symfony/console": "^6.3",
|
"php": "^8.1",
|
||||||
"symfony/yaml": "^6.3",
|
"ext-date": "*",
|
||||||
"twig/twig": "^3.6",
|
"ext-SPL": "*",
|
||||||
"symfony/process": "^6.3",
|
"ext-json": "*",
|
||||||
"monolog/monolog": "^3.3",
|
"symfony/console": "^6.3.4",
|
||||||
"verifiedjoseph/ntfy-php-library": "^4.2",
|
"symfony/yaml": "^6.3.3",
|
||||||
"league/config": "^1.2"
|
"twig/twig": "^3.7.1",
|
||||||
|
"symfony/process": "^6.3.4",
|
||||||
|
"monolog/monolog": "^3.4",
|
||||||
|
"verifiedjoseph/ntfy-php-library": "^4.3",
|
||||||
|
"league/config": "^1.2",
|
||||||
|
"psr/log": "^3.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@ -18,10 +23,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "*",
|
"phpunit/phpunit": "^10.3.5",
|
||||||
|
"phpmetrics/phpmetrics": "^2.8.2",
|
||||||
|
"rector/rector": "^1.0",
|
||||||
|
"vimeo/psalm": "^5.23",
|
||||||
|
"phpmd/phpmd": "^2.15",
|
||||||
"phpstan/phpstan": "^1.10",
|
"phpstan/phpstan": "^1.10",
|
||||||
"vimeo/psalm": "^5.12",
|
"squizlabs/php_codesniffer": "^3.9"
|
||||||
"phpmd/phpmd": "^2.13",
|
},
|
||||||
"phpunit/phpunit": "^10.2"
|
"scripts": {
|
||||||
|
"test": "vendor/bin/phpunit tests --display-warnings",
|
||||||
|
"test-full": "vendor/bin/phpunit -c phpunit.full.xml",
|
||||||
|
"metrics": "vendor/bin/phpmetrics --report-html=output/metrics --junit=output/test.xml src/",
|
||||||
|
"docs": "phpDocumentor --setting=graphs.enabled=true",
|
||||||
|
"analyze": [
|
||||||
|
"@analyze-yaml",
|
||||||
|
"@analyze-phpmd",
|
||||||
|
"@analyze-phpstan",
|
||||||
|
"@analyze-psalm",
|
||||||
|
"@analyze-phpcs"
|
||||||
|
],
|
||||||
|
"analyze-yaml": "vendor/bin/yaml-lint *.yml .*.yml *.json",
|
||||||
|
"analyze-phpmd": "phpmd src,tests text cleancode,codesize,controversial,design,naming,unusedcode",
|
||||||
|
"analyze-phpstan":"phpstan",
|
||||||
|
"analyze-psalm": "psalm --no-cache",
|
||||||
|
"analyze-phpcs": "phpcs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1120
composer.lock
generated
1120
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,4 +20,7 @@ templates:
|
|||||||
Destination after: {{ destination_size_after | formatBytes}}
|
Destination after: {{ destination_size_after | formatBytes}}
|
||||||
Destination change : {{ (destination_size_after - destination_size_before) | formatBytes}}
|
Destination change : {{ (destination_size_after - destination_size_before) | formatBytes}}
|
||||||
Backup completed: {{ end | date }}
|
Backup completed: {{ end | date }}
|
||||||
|
error: |
|
||||||
|
{{ config.title }}
|
||||||
|
Error {{ config.source }} to {{ config.destination }}
|
||||||
|
{{ exception }}
|
||||||
|
16
infection.json5
Normal file
16
infection.json5
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/infection/infection/0.27.0/resources/schema.json",
|
||||||
|
"source": {
|
||||||
|
"directories": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"text": "output/mutation/infection.log",
|
||||||
|
"html": "output/mutation/infection.html",
|
||||||
|
"summary": "output/mutation/summary.log",
|
||||||
|
},
|
||||||
|
"mutators": {
|
||||||
|
"@default": true
|
||||||
|
}
|
||||||
|
}
|
5
phpstan.neon
Normal file
5
phpstan.neon
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 8
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
24
phpunit.xml
Normal file
24
phpunit.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false" displayDetailsOnTestsThatTriggerWarnings="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Backupscript Test Suite">
|
||||||
|
<directory>./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<logging>
|
||||||
|
<junit outputFile="output/test/junit.xml"/>
|
||||||
|
<testdoxHtml outputFile="output/test/index.html"/>
|
||||||
|
</logging>
|
||||||
|
|
||||||
|
<coverage>
|
||||||
|
<report>
|
||||||
|
<html outputDirectory="output/coverage" />
|
||||||
|
<clover outputFile ="output/coverage/clover.xml" />
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
|
</phpunit>
|
10
psalm.xml
10
psalm.xml
@ -6,11 +6,19 @@
|
|||||||
xmlns="https://getpsalm.org/schema/config"
|
xmlns="https://getpsalm.org/schema/config"
|
||||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||||
findUnusedBaselineEntry="true"
|
findUnusedBaselineEntry="true"
|
||||||
findUnusedCode="true"
|
findUnusedCode="false"
|
||||||
|
strictBinaryOperands="true"
|
||||||
|
checkForThrowsInGlobalScope="true"
|
||||||
|
ignoreInternalFunctionFalseReturn="false"
|
||||||
|
ignoreInternalFunctionNullReturn ="false"
|
||||||
|
findUnusedVariablesAndParams="true"
|
||||||
|
findUnusedPsalmSuppress="true"
|
||||||
|
restrictReturnTypes="true"
|
||||||
>
|
>
|
||||||
<projectFiles>
|
<projectFiles>
|
||||||
<directory name="src/" />
|
<directory name="src/" />
|
||||||
<file name="backup" />
|
<file name="backup" />
|
||||||
|
<directory name="tests" />
|
||||||
<ignoreFiles>
|
<ignoreFiles>
|
||||||
<directory name="vendor" />
|
<directory name="vendor" />
|
||||||
</ignoreFiles>
|
</ignoreFiles>
|
||||||
|
45
src/App.php
45
src/App.php
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@ -11,28 +13,24 @@ use League\Config\Configuration;
|
|||||||
use Nette\Schema\Expect;
|
use Nette\Schema\Expect;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application class
|
|
||||||
*
|
|
||||||
* Mostly working as a register pattern for the logging and configuration.
|
* Mostly working as a register pattern for the logging and configuration.
|
||||||
*
|
|
||||||
* @author Jens True <jens.chr.true@gmail.com>
|
|
||||||
* @license https://opensource.org/licenses/gpl-license.php GNU Public License
|
|
||||||
* @link https://jcktrue.dk
|
|
||||||
*/
|
*/
|
||||||
class App
|
class App
|
||||||
{
|
{
|
||||||
|
/// Logging instance
|
||||||
protected Logger $logger;
|
protected Logger $logger;
|
||||||
|
/// Configuration singleton
|
||||||
protected Configuration $config;
|
protected Configuration $config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance providing a config file
|
* Create a new instance providing a config file.
|
||||||
*
|
*
|
||||||
* @param string $configFile Relative or full path to YML config.
|
* @param string $configFile Relative or full path to YML config.
|
||||||
|
*
|
||||||
* @SuppressWarnings(PHPMD.StaticAccess)
|
* @SuppressWarnings(PHPMD.StaticAccess)
|
||||||
*/
|
*/
|
||||||
public function __construct(string $configFile)
|
public function __construct(string $configFile)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Define your configuration schema
|
// Define your configuration schema
|
||||||
$this->config = new Configuration([
|
$this->config = new Configuration([
|
||||||
'rclone' => Expect::structure([
|
'rclone' => Expect::structure([
|
||||||
@ -40,28 +38,34 @@ class App
|
|||||||
'options' => Expect::arrayOf('string', 'string')
|
'options' => Expect::arrayOf('string', 'string')
|
||||||
]),
|
]),
|
||||||
'backup' => Expect::arrayOf(Expect::structure([
|
'backup' => Expect::arrayOf(Expect::structure([
|
||||||
'title' => Expect::string(),
|
'title' => Expect::string()->required(),
|
||||||
'source' => Expect::string(),
|
'source' => Expect::string()->required(),
|
||||||
'destination' => Expect::string(),
|
'destination' => Expect::string()->required(),
|
||||||
])),
|
]))->required(),
|
||||||
'notification' => Expect::arrayOf(Expect::structure([
|
'notification' => Expect::arrayOf(Expect::structure([
|
||||||
'type' => Expect::string(),
|
'type' => Expect::string()->required(),
|
||||||
'domain' => Expect::string(),
|
'domain' => Expect::string()->required(),
|
||||||
'topic' => Expect::string(),
|
'topic' => Expect::string()->required(),
|
||||||
])),
|
])),
|
||||||
'log' => Expect::string()->assert(
|
'log' => Expect::string()->assert(
|
||||||
function (string $path): bool {
|
function (string $path): bool {
|
||||||
return touch($path);
|
return touch($path);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'templates' => Expect::structure(['notify' => Expect::string()])
|
'templates' => Expect::structure(
|
||||||
|
[
|
||||||
|
'notify' => Expect::string()->required(),
|
||||||
|
'error' => Expect::string()->required()
|
||||||
|
]
|
||||||
|
)->required()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$parser = new Yaml();
|
$parser = new Yaml();
|
||||||
/** @var array<string, mixed> */
|
/**
|
||||||
|
* @var array<string, mixed>
|
||||||
|
*/
|
||||||
$parsedConfig = $parser->parseFile($configFile);
|
$parsedConfig = $parser->parseFile($configFile);
|
||||||
|
|
||||||
|
|
||||||
// Merge those values into the configuration schema:
|
// Merge those values into the configuration schema:
|
||||||
$this->config->merge($parsedConfig);
|
$this->config->merge($parsedConfig);
|
||||||
|
|
||||||
@ -79,11 +83,14 @@ class App
|
|||||||
* Get configuration from key
|
* Get configuration from key
|
||||||
*
|
*
|
||||||
* @param non-empty-string $key Key to fetch
|
* @param non-empty-string $key Key to fetch
|
||||||
|
*
|
||||||
* @return mixed Configuration value
|
* @return mixed Configuration value
|
||||||
*/
|
*/
|
||||||
public function getConfig(string $key): mixed
|
public function getConfig(string $key): mixed
|
||||||
{
|
{
|
||||||
/** @var mixed */
|
/**
|
||||||
|
* @var string|array<string, string>
|
||||||
|
*/
|
||||||
$ret = $this->config->get($key);
|
$ret = $this->config->get($key);
|
||||||
$this->logger->debug("Fetching configuration key", [$key, $ret]);
|
$this->logger->debug("Fetching configuration key", [$key, $ret]);
|
||||||
return $ret;
|
return $ret;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
@ -7,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
|
|||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use App\Template\Twig;
|
use App\Template\Twig;
|
||||||
use App\Notification\Notification;
|
use App\Notification\Notification;
|
||||||
@ -28,9 +31,28 @@ class CommandBackup extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the backup process.
|
||||||
|
*
|
||||||
|
* 1. Read the configuration file.
|
||||||
|
* 2. For each configured backup entry
|
||||||
|
* 1. Get the size of the source
|
||||||
|
* 2. Get the size of the destination
|
||||||
|
* 3. Perform the backup
|
||||||
|
* 4. Get the new size of the destination
|
||||||
|
* 5. Send push notifications.
|
||||||
|
* 3. Report final success
|
||||||
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$sio = new SymfonyStyle($input, $output);
|
$sio = new SymfonyStyle($input, $output);
|
||||||
|
$sioProgressbar = &$sio;
|
||||||
|
|
||||||
|
if ($output instanceof ConsoleOutputInterface) {
|
||||||
|
$sio = new SymfonyStyle($input, $output->section());
|
||||||
|
$sioProgressbar = new SymfonyStyle($input, $output->section());
|
||||||
|
}
|
||||||
|
|
||||||
$sio->title('Start backup process');
|
$sio->title('Start backup process');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -40,32 +62,38 @@ class CommandBackup extends Command
|
|||||||
return Command::FAILURE;
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$rclone = new Rclone((string)$app->getConfig('rclone.path'));
|
$rclone = new Rclone($app->getLogger()->withName('rclone'), (string)$app->getConfig('rclone.path'));
|
||||||
$rclone->setLogger($app->getLogger()->withName('rclone'));
|
|
||||||
|
|
||||||
$notification = new Notification();
|
$notification = new Notification();
|
||||||
/** @var array<array-key,array<string,string>> */
|
/**
|
||||||
|
* @var array<array-key,array<string,string>>
|
||||||
|
*/
|
||||||
$notificationConfig = $app->getConfig('notification');
|
$notificationConfig = $app->getConfig('notification');
|
||||||
$notification->loadMany($notificationConfig);
|
$notification->loadMany($notificationConfig);
|
||||||
|
|
||||||
/** @var array<string,string> */
|
/**
|
||||||
|
* @var array<string,string>
|
||||||
|
*/
|
||||||
$templateConfig = $app->getConfig('templates');
|
$templateConfig = $app->getConfig('templates');
|
||||||
$render = new Twig($templateConfig);
|
$render = new Twig($templateConfig);
|
||||||
|
|
||||||
/** @var array{title: string, source: string, destination: string}[] */
|
/**
|
||||||
|
* @var array{title: string, source: string, destination: string}[]
|
||||||
|
*/
|
||||||
$backupElements = $app->getConfig('backup');
|
$backupElements = $app->getConfig('backup');
|
||||||
/** @var array{title: string, source: string, destination: string} $conf */
|
foreach ($sioProgressbar->progressIterate($backupElements) as $conf) {
|
||||||
foreach ($sio->progressIterate($backupElements) as $conf) {
|
|
||||||
$title = $conf['title'];
|
$title = $conf['title'];
|
||||||
try {
|
$template = [];
|
||||||
$template = array();
|
|
||||||
$template['config'] = $conf;
|
$template['config'] = $conf;
|
||||||
|
try {
|
||||||
$template['start'] = new DateTime();
|
$template['start'] = new DateTime();
|
||||||
$template['source_size'] = $rclone->getSize($conf['source']);
|
$template['source_size'] = $rclone->getSize($conf['source']);
|
||||||
$template['rclone_version'] = $rclone->getVersion();
|
$template['rclone_version'] = $rclone->getVersion();
|
||||||
$template['destination_size_before'] = $rclone->getSize($conf['destination']);
|
$template['destination_size_before'] = $rclone->getSize($conf['destination']);
|
||||||
|
|
||||||
/** @var array<array-key, string> */
|
/**
|
||||||
|
* @var array<array-key, string>
|
||||||
|
*/
|
||||||
$rcloneOptions = $app->getConfig('rclone.options');
|
$rcloneOptions = $app->getConfig('rclone.options');
|
||||||
$rclone->copy($conf['source'], $conf['destination'], $rcloneOptions);
|
$rclone->copy($conf['source'], $conf['destination'], $rcloneOptions);
|
||||||
|
|
||||||
@ -74,10 +102,10 @@ class CommandBackup extends Command
|
|||||||
|
|
||||||
$message = $render->render('notify', $template);
|
$message = $render->render('notify', $template);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$message = $e->getMessage();
|
$template['exception'] = $e->getMessage();
|
||||||
|
$message = $render->render('error', $template);
|
||||||
$sio->error($message);
|
$sio->error($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
$notification->send($title, $message);
|
$notification->send($title, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
@ -24,6 +26,11 @@ class CommandShow extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Read the configuration file.
|
||||||
|
* 2. For each configured backup print the details
|
||||||
|
* 3. Exit
|
||||||
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$sio = new SymfonyStyle($input, $output);
|
$sio = new SymfonyStyle($input, $output);
|
||||||
@ -35,7 +42,9 @@ class CommandShow extends Command
|
|||||||
$sio->error('Configuration error: ' . $e->getMessage());
|
$sio->error('Configuration error: ' . $e->getMessage());
|
||||||
return Command::FAILURE;
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
/** @var array{title: string, source: string, destination: string}[] */
|
/**
|
||||||
|
* @var array{title: string, source: string, destination: string}[]
|
||||||
|
*/
|
||||||
$backupElements = $app->getConfig('backup');
|
$backupElements = $app->getConfig('backup');
|
||||||
$sio->table(
|
$sio->table(
|
||||||
['Description', 'Source', 'Destination'],
|
['Description', 'Source', 'Destination'],
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Notification;
|
namespace App\Notification;
|
||||||
|
|
||||||
use App\Notification\Ntfy;
|
use App\Notification\Ntfy;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
class Notification
|
class Notification
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var NotificationInterface[] $notifiers
|
* @var NotificationInterface[] $notifiers
|
||||||
*/
|
*/
|
||||||
private array $notifiers = array();
|
private array $notifiers = [];
|
||||||
|
|
||||||
|
public function __construct(private NullLogger $logger = new NullLogger())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load multiple configurations
|
* Load multiple configurations
|
||||||
*
|
*
|
||||||
* @param array<array<string>> $config Array of notifier configurations.
|
* @param array<string[]> $config Array of notifier configurations.
|
||||||
*/
|
*/
|
||||||
public function loadMany(array $config): void
|
public function loadMany(array $config): void
|
||||||
{
|
{
|
||||||
@ -28,6 +35,7 @@ class Notification
|
|||||||
*
|
*
|
||||||
* @param string $key Notification class
|
* @param string $key Notification class
|
||||||
* @param string[] $config Implementation specific configuration
|
* @param string[] $config Implementation specific configuration
|
||||||
|
*
|
||||||
* @SuppressWarnings(PHPMD)
|
* @SuppressWarnings(PHPMD)
|
||||||
*/
|
*/
|
||||||
public function loadSingle(string $key, array $config): void
|
public function loadSingle(string $key, array $config): void
|
||||||
@ -43,6 +51,9 @@ class Notification
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single notifier instance.
|
||||||
|
*/
|
||||||
public function addNotifier(NotificationInterface $instance): void
|
public function addNotifier(NotificationInterface $instance): void
|
||||||
{
|
{
|
||||||
$this->notifiers[] = $instance;
|
$this->notifiers[] = $instance;
|
||||||
@ -58,10 +69,19 @@ class Notification
|
|||||||
return $this->notifiers;
|
return $this->notifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a notification to all notifiers.
|
||||||
|
*
|
||||||
|
* Logs an error if sending fails.
|
||||||
|
*/
|
||||||
public function send(string $title, string $message): void
|
public function send(string $title, string $message): void
|
||||||
{
|
{
|
||||||
foreach ($this->getNotifiers() as $notifier) {
|
foreach ($this->getNotifiers() as $notifier) {
|
||||||
|
try {
|
||||||
$notifier->send($title, $message);
|
$notifier->send($title, $message);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Notification;
|
namespace App\Notification;
|
||||||
|
|
||||||
interface NotificationInterface
|
interface NotificationInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param string[] $config Configuration
|
||||||
|
*/
|
||||||
|
public static function factory(array $config): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throw Exception on error.
|
||||||
|
*/
|
||||||
public function send(string $title, string $message): void;
|
public function send(string $title, string $message): void;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Notification;
|
namespace App\Notification;
|
||||||
|
|
||||||
use Ntfy\Server;
|
use Ntfy\Server;
|
||||||
use Ntfy\Message;
|
use Ntfy\Message;
|
||||||
use Ntfy\Client;
|
use Ntfy\Client;
|
||||||
use Exception;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a notification through a ntfy server
|
||||||
|
*/
|
||||||
class Ntfy implements NotificationInterface
|
class Ntfy implements NotificationInterface
|
||||||
{
|
{
|
||||||
private Client $client;
|
public const TOPIC_MAX_LENGTH = 256;
|
||||||
|
public const TITLE_MAX_LENGTH = 256;
|
||||||
|
public const MESSAGE_MAX_LENGTH = 4096;
|
||||||
private string $topic = 'default';
|
private string $topic = 'default';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize with configuration.
|
* Initialize with configuration.
|
||||||
*
|
*
|
||||||
|
* Factory method.
|
||||||
|
*
|
||||||
* @param string[] $config Configuration
|
* @param string[] $config Configuration
|
||||||
*/
|
*/
|
||||||
public static function factory(array $config): self
|
public static function factory(array $config): self
|
||||||
@ -26,32 +35,42 @@ class Ntfy implements NotificationInterface
|
|||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __construct(Client $client)
|
public function __construct(private Client $client)
|
||||||
{
|
{
|
||||||
$this->client = $client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the topic of the notification message.
|
||||||
|
*
|
||||||
|
* @param string $topic Topic length between 1 and TOPIC_MAX_LENGTH characters.
|
||||||
|
*/
|
||||||
public function setTopic(string $topic): void
|
public function setTopic(string $topic): void
|
||||||
{
|
{
|
||||||
if (strlen($topic) < 1 || strlen($topic) >= 256) {
|
if (! strlen($topic) || strlen($topic) > self::TOPIC_MAX_LENGTH) {
|
||||||
throw new InvalidArgumentException("Invalid topic length");
|
throw new InvalidArgumentException("Invalid topic length");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->topic = $topic;
|
$this->topic = $topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the currently set topic.
|
||||||
|
*/
|
||||||
public function getTopic(): string
|
public function getTopic(): string
|
||||||
{
|
{
|
||||||
return $this->topic;
|
return $this->topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a message with Ntfy
|
||||||
|
*/
|
||||||
public function send(string $title, string $message): void
|
public function send(string $title, string $message): void
|
||||||
{
|
{
|
||||||
if (strlen($title) < 1 || strlen($title) >= 256) {
|
if (! strlen($title) || strlen($title) > self::TITLE_MAX_LENGTH) {
|
||||||
throw new InvalidArgumentException("Invalid title length");
|
throw new InvalidArgumentException("Invalid title length");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strlen($message) < 1 || strlen($message) >= 4096) {
|
if (! strlen($message) || strlen($message) > self::MESSAGE_MAX_LENGTH) {
|
||||||
throw new InvalidArgumentException("Invalid message length");
|
throw new InvalidArgumentException("Invalid message length");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Rclone;
|
namespace App\Rclone;
|
||||||
|
|
||||||
use Psr\Log\LoggerAwareTrait;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||||
@ -14,20 +14,11 @@ use Exception;
|
|||||||
*
|
*
|
||||||
* Installation of rclone is required.
|
* Installation of rclone is required.
|
||||||
* Configuration of the mounts must be done before use.
|
* Configuration of the mounts must be done before use.
|
||||||
* Tested using rclone v1.53.3-DEV
|
* Tested using rclone v1.64.0
|
||||||
*/
|
*/
|
||||||
class Rclone
|
class Rclone
|
||||||
{
|
{
|
||||||
use LoggerAwareTrait;
|
private const MAX_RUNTIME = 4 * 3600; //4 hours maximum
|
||||||
|
|
||||||
protected string $rclonePath;
|
|
||||||
/**
|
|
||||||
* Global options
|
|
||||||
*
|
|
||||||
* @var array<string>
|
|
||||||
*/
|
|
||||||
protected array $globalOptions = [];
|
|
||||||
|
|
||||||
protected string $version = "";
|
protected string $version = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,20 +29,19 @@ class Rclone
|
|||||||
*
|
*
|
||||||
* @param string $rclonePath Relative or absolute path
|
* @param string $rclonePath Relative or absolute path
|
||||||
*/
|
*/
|
||||||
public function __construct(string $rclonePath = "rclone")
|
public function __construct(protected LoggerInterface $logger, protected string $rclonePath = "rclone")
|
||||||
{
|
{
|
||||||
$this->rclonePath = $rclonePath;
|
$this->rclonePath = $rclonePath;
|
||||||
$this->setLogger(new NullLogger());
|
|
||||||
|
|
||||||
$process = $this->exec('--version');
|
$process = $this->exec('--version');
|
||||||
if (!$process->isSuccessful()) {
|
if (! $process->isSuccessful()) {
|
||||||
throw new Exception("Check installation of rclone");
|
throw new Exception("Check installation of rclone");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->version = explode("\n", $process->getOutput())[0];
|
$this->version = explode("\n", $process->getOutput())[0];
|
||||||
|
|
||||||
if (!\str_contains($this->version, 'rclone')) {
|
if (! \str_contains($this->version, 'rclone')) {
|
||||||
throw new Exception("Rclone not recognized");
|
throw new Exception("rclone not recognized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +66,14 @@ class Rclone
|
|||||||
{
|
{
|
||||||
$process = $this->exec('size', ['--json', $path]);
|
$process = $this->exec('size', ['--json', $path]);
|
||||||
|
|
||||||
if (!$process->isSuccessful()) {
|
if (! $process->isSuccessful()) {
|
||||||
throw new Exception($process->getErrorOutput());
|
throw new Exception($process->getErrorOutput());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var array{bytes: int} */
|
/**
|
||||||
$output = json_decode($process->getOutput(), true);
|
* @var array{bytes: int}
|
||||||
|
*/
|
||||||
|
$output = json_decode($process->getOutput(), TRUE);
|
||||||
return $output['bytes'];
|
return $output['bytes'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,20 +84,20 @@ class Rclone
|
|||||||
* @param $dest Destination mount and path
|
* @param $dest Destination mount and path
|
||||||
* @param string[] $additionalOptions Additional options
|
* @param string[] $additionalOptions Additional options
|
||||||
*/
|
*/
|
||||||
public function copy(string $src, string $dest, array $additionalOptions = array()): void
|
public function copy(string $src, string $dest, array $additionalOptions = []): void
|
||||||
{
|
{
|
||||||
$options = array();
|
$options = [];
|
||||||
|
|
||||||
$options[] = $src;
|
$options[] = $src;
|
||||||
$options[] = $dest;
|
$options[] = $dest;
|
||||||
|
|
||||||
foreach ($additionalOptions as $key => $value) {
|
foreach ($additionalOptions as $key => $value) {
|
||||||
$options[] = '--' . $key;
|
$options[] = '--' . (string)$key;
|
||||||
$options[] = $value;
|
$options[] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$process = $this->exec('copy', $options);
|
$process = $this->exec('copy', $options);
|
||||||
if (!$process->isSuccessful()) {
|
if (! $process->isSuccessful()) {
|
||||||
throw new Exception($process->getErrorOutput());
|
throw new Exception($process->getErrorOutput());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,32 +110,26 @@ class Rclone
|
|||||||
*
|
*
|
||||||
* @return Process Instance.
|
* @return Process Instance.
|
||||||
*/
|
*/
|
||||||
protected function exec(string $command, array $options = array()): Process
|
private function exec(string $command, array $options = []): Process
|
||||||
{
|
{
|
||||||
$process = new Process(
|
$process = new Process(
|
||||||
array_merge(
|
array_merge(
|
||||||
[$this->rclonePath],
|
[$this->rclonePath],
|
||||||
$this->globalOptions,
|
|
||||||
[$command],
|
[$command],
|
||||||
$options
|
$options
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if ($this->logger instanceof LoggerInterface) {
|
|
||||||
$this->logger->info("Execute command", [$process->getCommandLine()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$process->setTimeout(4 * 3600);
|
$this->logger->info("Execute command", [$process->getCommandLine()]);
|
||||||
|
|
||||||
|
$process->setTimeout(self::MAX_RUNTIME);
|
||||||
$process->run();
|
$process->run();
|
||||||
|
|
||||||
// executes after the command finishes
|
// executes after the command finishes
|
||||||
if (!$process->isSuccessful()) {
|
if (! $process->isSuccessful()) {
|
||||||
if ($this->logger instanceof LoggerInterface) {
|
|
||||||
$this->logger->error("Failed execution");
|
$this->logger->error("Failed execution");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if ($this->logger instanceof LoggerInterface) {
|
|
||||||
$this->logger->info("Return code", [$process->getExitCode()]);
|
$this->logger->info("Return code", [$process->getExitCode()]);
|
||||||
}
|
|
||||||
return $process;
|
return $process;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Template;
|
namespace App\Template;
|
||||||
|
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Template;
|
namespace App\Template;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
@ -20,9 +22,9 @@ class TwigExtension extends AbstractExtension
|
|||||||
*/
|
*/
|
||||||
public function getFilters(): array
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return array(
|
return [
|
||||||
new TwigFilter('formatBytes', array($this, 'formatBytes')),
|
new TwigFilter('formatBytes', [$this, 'formatBytes']),
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +38,7 @@ class TwigExtension extends AbstractExtension
|
|||||||
public function formatBytes($bytes, $precision = 2)
|
public function formatBytes($bytes, $precision = 2)
|
||||||
{
|
{
|
||||||
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
||||||
$fact = (int)(floor((strlen((string)$bytes) - 1) / 3));
|
$fact = (int)floor((strlen((string)$bytes) - 1) / 3);
|
||||||
return sprintf("%.{$precision}f", $bytes / pow(1024, $fact)) . $size[$fact];
|
return sprintf("%.{$precision}f", $bytes / pow(1024, $fact)) . $size[$fact];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
tests/AppTest.php
Normal file
80
tests/AppTest.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use App\App;
|
||||||
|
|
||||||
|
#[CoversClass(App::class)]
|
||||||
|
final class AppTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function testNonexistentConfigFile(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
new App('no_file');
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGoodConfig(): void
|
||||||
|
{
|
||||||
|
$app = new App('config.example.yml');
|
||||||
|
$this->assertEquals('output.log', $app->getConfig('log'));
|
||||||
|
$this->assertInstanceOf(LoggerInterface::class, $app->getLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBadConfigFileSyntaxError(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
|
||||||
|
$app = new App('tests/config/bad/syntaxerror.yml');
|
||||||
|
$app->getConfig('rclone');
|
||||||
|
$app->getConfig('rclone.path');
|
||||||
|
$app->getConfig('backup');
|
||||||
|
$app->getConfig('notification');
|
||||||
|
$app->getConfig('log');
|
||||||
|
$app->getConfig('templates');
|
||||||
|
$app->getConfig('templates.notify');
|
||||||
|
$app->getConfig('templates.error');
|
||||||
|
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBadConfigFileEmpty(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
|
||||||
|
$app = new App('tests/config/bad/empty.yml');
|
||||||
|
$app->getConfig('rclone');
|
||||||
|
$app->getConfig('rclone.path');
|
||||||
|
$app->getConfig('backup');
|
||||||
|
$app->getConfig('notification');
|
||||||
|
$app->getConfig('log');
|
||||||
|
$app->getConfig('templates');
|
||||||
|
$app->getConfig('templates.notify');
|
||||||
|
$app->getConfig('templates.error');
|
||||||
|
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBadConfigFileTypos(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\League\Config\Exception\ValidationException::class);
|
||||||
|
|
||||||
|
$app = new App('tests/config/bad/typos.yml');
|
||||||
|
$app->getConfig('rclone');
|
||||||
|
$app->getConfig('rclone.path');
|
||||||
|
$app->getConfig('backup');
|
||||||
|
$app->getConfig('notification');
|
||||||
|
$app->getConfig('log');
|
||||||
|
$app->getConfig('templates');
|
||||||
|
$app->getConfig('templates.notify');
|
||||||
|
$app->getConfig('templates.error');
|
||||||
|
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
}
|
@ -15,14 +15,26 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
|||||||
{
|
{
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
|
if (! is_dir('temp')) {
|
||||||
mkdir('temp');
|
mkdir('temp');
|
||||||
|
}
|
||||||
|
if (! is_dir('temp/destination')) {
|
||||||
mkdir('temp/destination');
|
mkdir('temp/destination');
|
||||||
exec('rclone test makefiles temp/source 2>&1');
|
}
|
||||||
|
exec('rclone test makefiles --files 10 temp/source 2>&1');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
exec('rclone purge temp 2>&1');
|
exec('rclone purge temp 2>&1');
|
||||||
|
|
||||||
|
if (is_dir('temp/destination')) {
|
||||||
|
rmdir('temp/destination');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir("temp")) {
|
||||||
|
rmdir('temp');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testBadConfig(): void
|
public function testBadConfig(): void
|
||||||
@ -31,7 +43,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
|||||||
|
|
||||||
$applicationd->add(new CommandBackup());
|
$applicationd->add(new CommandBackup());
|
||||||
|
|
||||||
|
|
||||||
$command = $applicationd->find('backup');
|
$command = $applicationd->find('backup');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
$commandTester->execute(['config' => "bad_file"]);
|
$commandTester->execute(['config' => "bad_file"]);
|
||||||
@ -40,12 +51,24 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertStringContainsString('[ERROR] Configuration error:File "bad_file" does not exist.', $output);
|
$this->assertStringContainsString('[ERROR] Configuration error:File "bad_file" does not exist.', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGoodConfig(): void
|
public function testNoCommand(): void
|
||||||
{
|
{
|
||||||
$applicationd = new Application('backup', "1.1.1");
|
$applicationd = new Application('backup', "1.1.1");
|
||||||
|
|
||||||
$applicationd->add(new CommandBackup());
|
$applicationd->add(new CommandBackup());
|
||||||
|
|
||||||
|
$command = $applicationd->find('backup');
|
||||||
|
$commandTester = new CommandTester($command);
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$commandTester->execute([]);
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGoodConfig(): void
|
||||||
|
{
|
||||||
|
$applicationd = new Application('backup', "1.1.1");
|
||||||
|
|
||||||
|
$applicationd->add(new CommandBackup());
|
||||||
|
|
||||||
$command = $applicationd->find('backup');
|
$command = $applicationd->find('backup');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
@ -62,7 +85,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
|||||||
|
|
||||||
$applicationd->add(new CommandBackup());
|
$applicationd->add(new CommandBackup());
|
||||||
|
|
||||||
|
|
||||||
$command = $applicationd->find('backup');
|
$command = $applicationd->find('backup');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
$commandTester->execute(['config' => "config.example.yml"]);
|
$commandTester->execute(['config' => "config.example.yml"]);
|
||||||
@ -78,7 +100,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
|||||||
|
|
||||||
$applicationd->add(new CommandBackup());
|
$applicationd->add(new CommandBackup());
|
||||||
|
|
||||||
|
|
||||||
$command = $applicationd->find('backup');
|
$command = $applicationd->find('backup');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
$commandTester->execute(['config' => "config.example.yml"]);
|
$commandTester->execute(['config' => "config.example.yml"]);
|
||||||
|
@ -15,12 +15,10 @@ final class CommandShowTest extends \PHPUnit\Framework\TestCase
|
|||||||
{
|
{
|
||||||
public function testInvalidConfig(): void
|
public function testInvalidConfig(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
$applicationd = new Application('backup', "1.1.1");
|
$applicationd = new Application('backup', "1.1.1");
|
||||||
|
|
||||||
$applicationd->add(new CommandShow());
|
$applicationd->add(new CommandShow());
|
||||||
|
|
||||||
|
|
||||||
$command = $applicationd->find('show');
|
$command = $applicationd->find('show');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
$commandTester->execute(['config' => "bad_file"]);
|
$commandTester->execute(['config' => "bad_file"]);
|
||||||
@ -37,7 +35,6 @@ final class CommandShowTest extends \PHPUnit\Framework\TestCase
|
|||||||
|
|
||||||
$applicationd->add(new CommandShow());
|
$applicationd->add(new CommandShow());
|
||||||
|
|
||||||
|
|
||||||
$command = $applicationd->find('show');
|
$command = $applicationd->find('show');
|
||||||
$commandTester = new CommandTester($command);
|
$commandTester = new CommandTester($command);
|
||||||
$commandTester->execute(['config' => "config.example.yml"]);
|
$commandTester->execute(['config' => "config.example.yml"]);
|
||||||
|
@ -8,10 +8,13 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use App\Notification\Notification;
|
use App\Notification\Notification;
|
||||||
use App\Notification\NotificationInterface;
|
use App\Notification\NotificationInterface;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
final class NotificationTest extends TestCase
|
final class NotificationTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
private static array $config = ['type' => 'ntfy', 'domain' => 'https://test.com', 'topic' => 'testing'];
|
private static array $config = ['type' => 'ntfy', 'domain' => 'https://test.com', 'topic' => 'testing'];
|
||||||
|
|
||||||
public function testloadSingle(): void
|
public function testloadSingle(): void
|
||||||
@ -44,12 +47,43 @@ final class NotificationTest extends TestCase
|
|||||||
$this->assertEquals(4, count($dut->getNotifiers()));
|
$this->assertEquals(4, count($dut->getNotifiers()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSend(): void
|
public function testSendNoNotifier(): void
|
||||||
{
|
{
|
||||||
$dut = new Notification();
|
$dut = new Notification();
|
||||||
$mock = $this->createMock(NotificationInterface::class);
|
$dut->send('title', 'topic');
|
||||||
$dut->addNotifier($mock);
|
$this->assertEquals(0, count($dut->getNotifiers()));
|
||||||
$mock->expects($this->once())->method('send');
|
}
|
||||||
|
|
||||||
|
public function testSendOneNotifier(): void
|
||||||
|
{
|
||||||
|
$dut = new Notification();
|
||||||
|
$mock1 = $this->createMock(NotificationInterface::class);
|
||||||
|
$dut->addNotifier($mock1);
|
||||||
|
$mock1->expects($this->once())->method('send');
|
||||||
|
$dut->send('title', 'topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendMoreNotifiers(): void
|
||||||
|
{
|
||||||
|
$dut = new Notification();
|
||||||
|
$mock1 = $this->createMock(NotificationInterface::class);
|
||||||
|
$mock2 = $this->createMock(NotificationInterface::class);
|
||||||
|
$dut->addNotifier($mock1);
|
||||||
|
$dut->addNotifier($mock2);
|
||||||
|
$mock1->expects($this->once())->method('send');
|
||||||
|
$mock2->expects($this->once())->method('send');
|
||||||
|
$dut->send('title', 'topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendErrorInNotifiers(): void
|
||||||
|
{
|
||||||
|
$dut = new Notification();
|
||||||
|
$mock1 = $this->createMock(NotificationInterface::class);
|
||||||
|
$mock2 = $this->createMock(NotificationInterface::class);
|
||||||
|
$dut->addNotifier($mock1);
|
||||||
|
$dut->addNotifier($mock2);
|
||||||
|
$mock1->expects($this->once())->method('send')->willThrowException(new Exception());
|
||||||
|
$mock2->expects($this->once())->method('send');
|
||||||
$dut->send('title', 'topic');
|
$dut->send('title', 'topic');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,13 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||||||
|
|
||||||
final class NtfyTest extends TestCase
|
final class NtfyTest extends TestCase
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @psalm-suppress PropertyNotSetInConstructor
|
||||||
|
*/
|
||||||
private Ntfy $instance;
|
private Ntfy $instance;
|
||||||
|
/**
|
||||||
|
* @psalm-suppress PropertyNotSetInConstructor
|
||||||
|
*/
|
||||||
private MockObject $client;
|
private MockObject $client;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
@ -30,23 +36,32 @@ final class NtfyTest extends TestCase
|
|||||||
$config = ['domain' => 'https://test.com', 'topic' => 'something'];
|
$config = ['domain' => 'https://test.com', 'topic' => 'something'];
|
||||||
$instance = Ntfy::factory($config);
|
$instance = Ntfy::factory($config);
|
||||||
$this->assertInstanceOf(Ntfy::class, $instance);
|
$this->assertInstanceOf(Ntfy::class, $instance);
|
||||||
|
$this->assertEquals($instance->getTopic(), "something");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSend(): void
|
public function testSendShort(): void
|
||||||
{
|
{
|
||||||
$this->client->expects($this->once())->method('send')->with($this->isInstanceOf(Message::class));
|
$this->client->expects($this->once())->method('send')->with($this->isInstanceOf(Message::class));
|
||||||
$this->instance->send('title', 'text');
|
$this->instance->send('t', 's');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, array<int, string>> */
|
public function testSendLong(): void
|
||||||
|
{
|
||||||
|
$this->client->expects($this->once())->method('send')->with($this->isInstanceOf(Message::class));
|
||||||
|
$this->instance->send(str_repeat("t", Ntfy::TITLE_MAX_LENGTH), str_repeat("t", Ntfy::MESSAGE_MAX_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<int, string>>
|
||||||
|
*/
|
||||||
public static function sendBadParameterProvider(): array
|
public static function sendBadParameterProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['', ''],
|
['', ''],
|
||||||
['', 'text'],
|
['', 'text'],
|
||||||
['title', ''],
|
['title', ''],
|
||||||
[str_repeat("t", 256),'text'],
|
[str_repeat("t", Ntfy::TITLE_MAX_LENGTH + 1), 'text'],
|
||||||
['title',str_repeat("t", 4096)],
|
['title',str_repeat("t", Ntfy::MESSAGE_MAX_LENGTH + 1)],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,18 +74,23 @@ final class NtfyTest extends TestCase
|
|||||||
|
|
||||||
public function testSetTopic(): void
|
public function testSetTopic(): void
|
||||||
{
|
{
|
||||||
$topic = "abcdefg";
|
$topic = "a";
|
||||||
|
$this->instance->setTopic($topic);
|
||||||
|
$this->assertEquals($topic, $this->instance->getTopic());
|
||||||
|
|
||||||
|
$topic = str_repeat("a", Ntfy::TOPIC_MAX_LENGTH);
|
||||||
$this->instance->setTopic($topic);
|
$this->instance->setTopic($topic);
|
||||||
$this->assertEquals($topic, $this->instance->getTopic());
|
$this->assertEquals($topic, $this->instance->getTopic());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, array<int, string>> */
|
/**
|
||||||
|
* @return array<int, array<int, string>>
|
||||||
|
*/
|
||||||
public static function topicBadParameterProvider(): array
|
public static function topicBadParameterProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[''],
|
[''],
|
||||||
[str_repeat("t", 256)],
|
[str_repeat("t", Ntfy::TOPIC_MAX_LENGTH + 1)],
|
||||||
[str_repeat("t", 4096)],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Tests;
|
namespace App\Tests;
|
||||||
|
|
||||||
use App\Rclone\Rclone;
|
use App\Rclone\Rclone;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
final class RcloneTest extends TestCase
|
final class RcloneTest extends TestCase
|
||||||
@ -19,52 +20,67 @@ final class RcloneTest extends TestCase
|
|||||||
exec('rclone purge temp 2>&1');
|
exec('rclone purge temp 2>&1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testRclonePath(): void
|
public function testRclonePath(): void
|
||||||
{
|
{
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
$rclone = new Rclone('invalid');
|
new Rclone(new NullLogger(), 'invalid');
|
||||||
$this->assertEquals('', $rclone->getVersion());
|
$this->fail('Exception was not thrown');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRcloneInvalidVersion(): void
|
public function testRcloneInvalidVersion(): void
|
||||||
{
|
{
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
$rclone = new Rclone('uname');
|
new Rclone(new NullLogger(), 'uname');
|
||||||
$this->assertEquals('', $rclone->getVersion());
|
$this->fail('Exception was not thrown');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRcloneValidVersion(): void
|
public function testRcloneValidVersion(): void
|
||||||
{
|
{
|
||||||
$rclone = new Rclone();
|
$rclone = new Rclone(new NullLogger());
|
||||||
$this->assertStringStartsWith('rclone', $rclone->getVersion());
|
$this->assertStringStartsWith('rclone', $rclone->getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRcloneSize(): void
|
public function testRcloneSize(): void
|
||||||
{
|
{
|
||||||
$rclone = new Rclone();
|
$rclone = new Rclone(new NullLogger());
|
||||||
$size = $rclone->getSize('temp/source');
|
$size = $rclone->getSize('temp/source');
|
||||||
$this->assertGreaterThan(10000, $size);
|
$this->assertGreaterThan(10000, $size);
|
||||||
|
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
$this->expectExceptionMessage("ERROR");
|
$this->expectExceptionMessage("ERROR");
|
||||||
$size = $rclone->getSize('temp/bogus-source');
|
$rclone->getSize('temp/bogus-source');
|
||||||
$this->assertEquals(0, $size);
|
$this->fail('Exception was not thrown');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRcloneCopy(): void
|
public function testRcloneCopy(): void
|
||||||
{
|
{
|
||||||
$rclone = new Rclone();
|
$rclone = new Rclone(new NullLogger());
|
||||||
$rclone->copy('temp/source', 'temp/destination');
|
$rclone->copy('temp/source', 'temp/destination');
|
||||||
$this->assertDirectoryExists('temp/destination');
|
$this->assertDirectoryExists('temp/destination');
|
||||||
|
}
|
||||||
|
|
||||||
$rclone = new Rclone();
|
public function testRcloneCopyParam(): void
|
||||||
|
{
|
||||||
|
$rclone = new Rclone(new NullLogger());
|
||||||
$rclone->copy('temp/source', 'temp/destination', ['bwlimit' => '6M']);
|
$rclone->copy('temp/source', 'temp/destination', ['bwlimit' => '6M']);
|
||||||
$this->assertDirectoryExists('temp/destination');
|
$this->assertDirectoryExists('temp/destination');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRcloneCopyBad(): void
|
||||||
|
{
|
||||||
|
$rclone = new Rclone(new NullLogger());
|
||||||
$this->expectException(\Exception::class);
|
$this->expectException(\Exception::class);
|
||||||
$this->expectExceptionMessage("ERROR");
|
$this->expectExceptionMessage("ERROR");
|
||||||
$rclone->copy('temp/bogus-source', 'temp/bogus-destination');
|
$rclone->copy('temp/bogus-source', 'temp/bogus-destination');
|
||||||
$this->assertDirectoryDoesNotExist('temp/bogus-destination');
|
$this->fail('Exception was not thrown');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRcloneCopyBadParam(): void
|
||||||
|
{
|
||||||
|
$rclone = new Rclone(new NullLogger());
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$this->expectExceptionMessage("ERROR");
|
||||||
|
$rclone->copy('temp/bogus-source', 'temp/bogus-destination', ['bwlimit' => '6M']);
|
||||||
|
$this->fail('Exception was not thrown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
tests/Template/TwigExtensionTest.php
Normal file
39
tests/Template/TwigExtensionTest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Template\TwigExtension;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
final class TwigExtensionTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function testGetFilters(): void
|
||||||
|
{
|
||||||
|
$obj = new TwigExtension();
|
||||||
|
$filters = $obj->getFilters();
|
||||||
|
$this->assertNotEmpty($filters, "Filters must not be empty");
|
||||||
|
$this->assertContainsOnlyInstancesOf(TwigFilter::class, $filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testformatBytes(): void
|
||||||
|
{
|
||||||
|
$obj = new TwigExtension();
|
||||||
|
$this->assertEquals('1.00B', $obj->formatBytes(1, 2));
|
||||||
|
$this->assertEquals('2.00B', $obj->formatBytes(2, 2));
|
||||||
|
$this->assertEquals('10.00B', $obj->formatBytes(10, 2));
|
||||||
|
$this->assertEquals('0.98kB', $obj->formatBytes(1000, 2));
|
||||||
|
$this->assertEquals('1.00kB', $obj->formatBytes(1024, 2));
|
||||||
|
$this->assertEquals('512.00MB', $obj->formatBytes(1024 ** 3 / 2, 2));
|
||||||
|
$this->assertEquals('1.00GB', $obj->formatBytes(1024 ** 3 - 1, 2));
|
||||||
|
$this->assertEquals('1.00GB', $obj->formatBytes(1024 ** 3, 2));
|
||||||
|
$this->assertEquals('512.00GB', $obj->formatBytes(1024 ** 4 / 2, 2));
|
||||||
|
$this->assertEquals('1.00TB', $obj->formatBytes(1024 ** 4, 2));
|
||||||
|
|
||||||
|
$this->assertEquals('1.00B', $obj->formatBytes(1));
|
||||||
|
$this->assertEquals('1.00TB', $obj->formatBytes(1024 ** 4));
|
||||||
|
}
|
||||||
|
}
|
28
tests/Template/TwigTest.php
Normal file
28
tests/Template/TwigTest.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Template\Twig;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
final class TwigTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function testInstance(): void
|
||||||
|
{
|
||||||
|
$obj = new Twig(['template' => 'start {{ var }} end']);
|
||||||
|
$template = $obj->load('template');
|
||||||
|
$output = $template->render(['var' => 'middle']);
|
||||||
|
$this->assertEquals('start middle end', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExtensionsLoaded(): void
|
||||||
|
{
|
||||||
|
$obj = new Twig(['template' => 'start {{ var | formatBytes}} end']);
|
||||||
|
$template = $obj->load('template');
|
||||||
|
$output = $template->render(['var' => 1]);
|
||||||
|
$this->assertEquals('start 1.00B end', $output);
|
||||||
|
}
|
||||||
|
}
|
0
tests/config/bad/empty.yml
Normal file
0
tests/config/bad/empty.yml
Normal file
6
tests/config/bad/syntaxerror.yml
Normal file
6
tests/config/bad/syntaxerror.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<note>
|
||||||
|
<to>Someone</to>
|
||||||
|
<from>Me</from>
|
||||||
|
<heading>Title</heading>
|
||||||
|
<body>Text</body>
|
||||||
|
</note>
|
26
tests/config/bad/typos.yml
Normal file
26
tests/config/bad/typos.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
notifikation:
|
||||||
|
- type: Ntfy
|
||||||
|
domain: https://ntfy.jcktrue.dk
|
||||||
|
topic: testing
|
||||||
|
logger: output.log
|
||||||
|
rclone:
|
||||||
|
option:
|
||||||
|
bwlimit: 6M
|
||||||
|
backup:
|
||||||
|
- titel: Example
|
||||||
|
src: temp/source
|
||||||
|
dest: temp/destination
|
||||||
|
template:
|
||||||
|
notification: |
|
||||||
|
{{ config.title }}
|
||||||
|
From {{ config.source }} to {{ config.destination }}
|
||||||
|
Backup started: {{ start | date }}
|
||||||
|
Source size: {{ source_size | formatBytes}}
|
||||||
|
Destination before: {{ destination_size_before | formatBytes}}
|
||||||
|
Destination after: {{ destination_size_after | formatBytes}}
|
||||||
|
Destination change : {{ (destination_size_after - destination_size_before) | formatBytes}}
|
||||||
|
Backup completed: {{ end | date }}
|
||||||
|
warn: |
|
||||||
|
{{ config.title }}
|
||||||
|
Error {{ config.source }} to {{ config.destination }}
|
||||||
|
{{ exception }}
|
Reference in New Issue
Block a user