Compare commits

...

55 Commits

Author SHA1 Message Date
d392cb3c2d Latest update
Some checks failed
Build and test / requirements (push) Has been cancelled
2024-06-24 13:46:25 +00:00
66ad8aa592 Better matrix builds
Some checks failed
ci/woodpecker/push/woodpecker Pipeline is running
ci/woodpecker/manual/woodpecker Pipeline was successful
Build and test / requirements (push) Failing after 14m39s
2024-03-21 10:12:50 +00:00
aa91f25d3c First matrix attempt
Some checks failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
Build and test / requirements (push) Failing after 3m40s
ci/woodpecker/push/woodpecker/4 Pipeline was successful
2024-03-21 10:09:22 +00:00
1f736ae059 Less warnings
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Failing after 3m34s
2024-03-21 09:56:50 +00:00
4b59f3dfee New buildserver format
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Build and test / requirements (push) Failing after 3m36s
2024-03-21 09:48:08 +00:00
8a699ccf79 Deprecated format
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Build and test / requirements (push) Failing after 3m17s
2024-03-21 09:41:09 +00:00
b67086464c Should now build
Some checks failed
Build and test / requirements (push) Has been cancelled
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-21 09:39:44 +00:00
085006b073 Psalm warning. Investigate getting PHPunit plugin for Psalm.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Build and test / requirements (push) Failing after 1m41s
2024-02-07 11:05:54 +00:00
6ce774236e Quality of life things.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Build and test / requirements (push) Failing after 3m48s
2024-02-07 11:00:08 +00:00
aed385f5e0 Finding minimum required PHP version to be 8.1. (But testing with PHP8.3)
All checks were successful
Build and test / requirements (push) Successful in 3m0s
2024-01-18 14:40:39 +00:00
e826d9cfa9 Upgrade
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Build and test / requirements (push) Failing after 1m52s
2023-12-04 09:31:33 +00:00
de9270476f More docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m51s
2023-11-27 09:58:23 +00:00
55cdcd86b6 File names for output
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m26s
2023-11-16 13:13:57 +00:00
7e8da1a973 Upload folders
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m34s
2023-11-16 11:02:19 +00:00
55e683f645 Old build script
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Failing after 4m10s
2023-11-16 10:39:37 +00:00
3acdcd297c Testing gitea actions
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
2023-11-16 10:36:58 +00:00
344dd52019 Syntax jitter
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-11-16 10:30:24 +00:00
cadb207758 More unittests 2023-11-14 09:09:15 +00:00
bedc666f7c Support for PHP infection testing
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-11-03 13:29:50 +00:00
66ccb3d8d6 Killing a few "mutants"
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-11-03 12:02:44 +00:00
61001edaa0 Requirements update. Experiementation with Infection.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-31 09:50:55 +00:00
14db929af1 Bump requirements.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-04 09:39:36 +00:00
456dc1b1e3 Update dependecies.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-25 07:15:41 +00:00
876702473c More documentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-19 06:34:20 +00:00
f5a34213b5 Rclone update.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-15 06:58:39 +00:00
68ee38d1ff Bump versions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-04 06:53:56 +00:00
7ab7998ac7 Split progressbar output into two windows.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-31 13:15:06 +00:00
37049ba6f4 Template for error notification.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-29 09:44:05 +00:00
236f74c1d8 Remove S3 server
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-21 07:47:46 +00:00
6f1e3dcdd0 Bump dependencies
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-21 07:46:21 +00:00
e59e105394 Cleanup
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-17 07:16:41 +00:00
188a6ad9fa Cleaning up.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-15 11:43:12 +00:00
31891f3e53 More tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-14 14:24:19 +00:00
1d94738b04 Install script and updated requirements.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-07 07:33:11 +00:00
0c316480d5 Dependecies update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-01 07:38:47 +00:00
ca78223606 Update requirements. Include metrics
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-31 08:02:19 +00:00
1d898e350b 100% type coverage for psalm
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-28 12:12:47 +00:00
14b2155595 Dependecy update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-26 11:01:41 +00:00
f10f27f28b Remove dead code.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-25 06:35:51 +00:00
9459045e6c More testing. Removed Gitea actions.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-24 13:32:21 +00:00
bc00b36799 File path error.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m18s
2023-07-24 12:30:11 +00:00
936fa214e6 Directory upload not yet supported.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m15s
2023-07-24 12:26:22 +00:00
ff969aee2c Artifacts uploading
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Successful in 2m26s
2023-07-24 12:02:11 +00:00
83e667016d Typos.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Failing after 2m15s
2023-07-24 11:52:14 +00:00
58f0bc61c7 Testing
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Failing after 2m3s
2023-07-24 11:49:51 +00:00
51a06fa66c More steps
Some checks reported warnings
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Has been cancelled
2023-07-24 11:38:27 +00:00
8cbceb485c On every push
Some checks reported warnings
ci/woodpecker/push/woodpecker Pipeline was successful
Build and test / requirements (push) Has been cancelled
2023-07-24 11:27:45 +00:00
991a844836 Update build scripts
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-24 11:23:55 +00:00
5732d6af63 Testing
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-24 11:20:16 +00:00
5e69975df2 Test of Gitea Actions
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m26s
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-24 11:11:04 +00:00
2d38d82540 Update dependencies
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-24 08:23:11 +00:00
9d7d8cf89c Requirement to make has been removed
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-13 12:10:12 +00:00
6ac9fd5575 Quality of life improvements all around.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-07-13 11:34:56 +00:00
4645488b5d Removed dependecy on Make.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-07-11 12:36:27 +00:00
31ca54cdac Finetuning paths.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-11 11:18:23 +00:00
35 changed files with 1405 additions and 629 deletions

View 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
View File

@ -6,3 +6,5 @@
config.yml
*.phar
*.deb
.phpunit.result.cache
*cache/

51
.phpcs.xml Normal file
View 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>

View File

@ -1,39 +1,27 @@
pipeline:
requirements:
when:
- event: [push, pull_request, pull_request_closed, tag, release, manual]
steps:
- name: requirements
image: composer
commands:
- composer install --no-dev
run:
image: php:cli-bookworm
- name: run
image: php:cli-alpine
commands:
- ./backup show config.example.yml
dependencies:
- name: dependencies
image: composer
commands:
- composer install
analyze:
image: php:cli-bookworm
commands:
- make analyze
test:
- composer analyze
- name: test
image: php:cli-bookworm
commands:
- apt update
- apt install rclone
- vendor/bin/phpunit tests
document:
- vendor/bin/phpunit --no-coverage
- name: document
image: phpdoc/phpdoc
commands:
- phpdoc run
upload:
image: woodpeckerci/plugin-s3
settings:
bucket: buildserver
access_key:
from_secret: access_key_id
secret_key:
from_secret: secret_access_key
source: output/**/*
target: /backupscript/latest/
path_style: true
endpoint: https://s3.jcktrue.dk
- phpdoc

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM php:8.3-cli
COPY . /app
WORKDIR /app

View File

@ -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

View File

@ -4,11 +4,42 @@
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
rm rclone-current-linux-amd64.deb
wget https://downloads.rclone.org/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
rm phpDocumentor.phar
wget https://phpdoc.org/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
View File

@ -8,8 +8,12 @@ use Composer\InstalledVersions;
$package = \Composer\InstalledVersions::getRootPackage();
$application = new Application('backup', $package['version']);
try {
$application->add(new App\CommandBackup());
$application->add(new App\CommandShow());
$application->add(new App\CommandBackup());
$application->add(new App\CommandShow());
$application->run();
$application->run();
} catch(Exception $e)
{
echo("Critical error: ". $e->getMessage());
}

View File

@ -1,16 +1,21 @@
{
"name": "furyfire/backup",
"description": "Wrapper for Rclone based backup",
"name": "furyfire/backupscript",
"description": "Wrapper for Rclone based backup with push notifications",
"homepage": "https://jcktrue.dk",
"version": "0.1.0",
"version": "0.1.1",
"require": {
"symfony/console": "^6.3",
"symfony/yaml": "^6.3",
"twig/twig": "^3.6",
"symfony/process": "^6.3",
"monolog/monolog": "^3.3",
"verifiedjoseph/ntfy-php-library": "^4.2",
"league/config": "^1.2"
"php": "^8.1",
"ext-date": "*",
"ext-SPL": "*",
"ext-json": "*",
"symfony/console": "^6.3.4",
"symfony/yaml": "^6.3.3",
"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": {
"psr-4": {
@ -18,10 +23,30 @@
}
},
"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",
"vimeo/psalm": "^5.12",
"phpmd/phpmd": "^2.13",
"phpunit/phpunit": "^10.2"
"squizlabs/php_codesniffer": "^3.9"
},
"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

File diff suppressed because it is too large Load Diff

View File

@ -20,4 +20,7 @@ templates:
Destination after: {{ destination_size_after | formatBytes}}
Destination change : {{ (destination_size_after - destination_size_before) | formatBytes}}
Backup completed: {{ end | date }}
error: |
{{ config.title }}
Error {{ config.source }} to {{ config.destination }}
{{ exception }}

16
infection.json5 Normal file
View 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
View File

@ -0,0 +1,5 @@
parameters:
level: 8
paths:
- src
- tests

24
phpunit.xml Normal file
View 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>

View File

@ -6,11 +6,19 @@
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
findUnusedCode="false"
strictBinaryOperands="true"
checkForThrowsInGlobalScope="true"
ignoreInternalFunctionFalseReturn="false"
ignoreInternalFunctionNullReturn ="false"
findUnusedVariablesAndParams="true"
findUnusedPsalmSuppress="true"
restrictReturnTypes="true"
>
<projectFiles>
<directory name="src/" />
<file name="backup" />
<directory name="tests" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App;
use Symfony\Component\Yaml\Yaml;
@ -11,28 +13,24 @@ use League\Config\Configuration;
use Nette\Schema\Expect;
/**
* Application class
*
* 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
{
/// Logging instance
protected Logger $logger;
/// Configuration singleton
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.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public function __construct(string $configFile)
{
// Define your configuration schema
$this->config = new Configuration([
'rclone' => Expect::structure([
@ -40,28 +38,34 @@ class App
'options' => Expect::arrayOf('string', 'string')
]),
'backup' => Expect::arrayOf(Expect::structure([
'title' => Expect::string(),
'source' => Expect::string(),
'destination' => Expect::string(),
])),
'title' => Expect::string()->required(),
'source' => Expect::string()->required(),
'destination' => Expect::string()->required(),
]))->required(),
'notification' => Expect::arrayOf(Expect::structure([
'type' => Expect::string(),
'domain' => Expect::string(),
'topic' => Expect::string(),
'type' => Expect::string()->required(),
'domain' => Expect::string()->required(),
'topic' => Expect::string()->required(),
])),
'log' => Expect::string()->assert(
function (string $path): bool {
return touch($path);
}
),
'templates' => Expect::structure(['notify' => Expect::string()])
'templates' => Expect::structure(
[
'notify' => Expect::string()->required(),
'error' => Expect::string()->required()
]
)->required()
]);
$parser = new Yaml();
/** @var array<string, mixed> */
/**
* @var array<string, mixed>
*/
$parsedConfig = $parser->parseFile($configFile);
// Merge those values into the configuration schema:
$this->config->merge($parsedConfig);
@ -79,11 +83,14 @@ class App
* Get configuration from key
*
* @param non-empty-string $key Key to fetch
*
* @return mixed Configuration value
*/
public function getConfig(string $key): mixed
{
/** @var mixed */
/**
* @var string|array<string, string>
*/
$ret = $this->config->get($key);
$this->logger->debug("Fetching configuration key", [$key, $ret]);
return $ret;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App;
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\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Template\Twig;
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
{
$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');
try {
@ -40,32 +62,38 @@ class CommandBackup extends Command
return Command::FAILURE;
}
$rclone = new Rclone((string)$app->getConfig('rclone.path'));
$rclone->setLogger($app->getLogger()->withName('rclone'));
$rclone = new Rclone($app->getLogger()->withName('rclone'), (string)$app->getConfig('rclone.path'));
$notification = new Notification();
/** @var array<array-key,array<string,string>> */
/**
* @var array<array-key,array<string,string>>
*/
$notificationConfig = $app->getConfig('notification');
$notification->loadMany($notificationConfig);
/** @var array<string,string> */
/**
* @var array<string,string>
*/
$templateConfig = $app->getConfig('templates');
$render = new Twig($templateConfig);
/** @var array{title: string, source: string, destination: string}[] */
/**
* @var array{title: string, source: string, destination: string}[]
*/
$backupElements = $app->getConfig('backup');
/** @var array{title: string, source: string, destination: string} $conf */
foreach ($sio->progressIterate($backupElements) as $conf) {
foreach ($sioProgressbar->progressIterate($backupElements) as $conf) {
$title = $conf['title'];
$template = [];
$template['config'] = $conf;
try {
$template = array();
$template['config'] = $conf;
$template['start'] = new DateTime();
$template['source_size'] = $rclone->getSize($conf['source']);
$template['rclone_version'] = $rclone->getVersion();
$template['destination_size_before'] = $rclone->getSize($conf['destination']);
/** @var array<array-key, string> */
/**
* @var array<array-key, string>
*/
$rcloneOptions = $app->getConfig('rclone.options');
$rclone->copy($conf['source'], $conf['destination'], $rcloneOptions);
@ -74,10 +102,10 @@ class CommandBackup extends Command
$message = $render->render('notify', $template);
} catch (\Exception $e) {
$message = $e->getMessage();
$template['exception'] = $e->getMessage();
$message = $render->render('error', $template);
$sio->error($message);
}
$notification->send($title, $message);
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App;
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
{
$sio = new SymfonyStyle($input, $output);
@ -35,7 +42,9 @@ class CommandShow extends Command
$sio->error('Configuration error: ' . $e->getMessage());
return Command::FAILURE;
}
/** @var array{title: string, source: string, destination: string}[] */
/**
* @var array{title: string, source: string, destination: string}[]
*/
$backupElements = $app->getConfig('backup');
$sio->table(
['Description', 'Source', 'Destination'],

View File

@ -1,20 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Notification;
use App\Notification\Ntfy;
use Psr\Log\NullLogger;
class Notification
{
/**
* @var NotificationInterface[] $notifiers
*/
private array $notifiers = array();
private array $notifiers = [];
public function __construct(private NullLogger $logger = new NullLogger())
{
}
/**
* 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
{
@ -28,6 +35,7 @@ class Notification
*
* @param string $key Notification class
* @param string[] $config Implementation specific configuration
*
* @SuppressWarnings(PHPMD)
*/
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
{
$this->notifiers[] = $instance;
@ -58,10 +69,19 @@ class Notification
return $this->notifiers;
}
/**
* Push a notification to all notifiers.
*
* Logs an error if sending fails.
*/
public function send(string $title, string $message): void
{
foreach ($this->getNotifiers() as $notifier) {
$notifier->send($title, $message);
try {
$notifier->send($title, $message);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
}
}
}
}

View File

@ -1,8 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Notification;
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;
}

View File

@ -1,20 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Notification;
use Ntfy\Server;
use Ntfy\Message;
use Ntfy\Client;
use Exception;
use InvalidArgumentException;
/**
* Send a notification through a ntfy server
*/
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';
/**
* Initialize with configuration.
*
* Factory method.
*
* @param string[] $config Configuration
*/
public static function factory(array $config): self
@ -26,32 +35,42 @@ class Ntfy implements NotificationInterface
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
{
if (strlen($topic) < 1 || strlen($topic) >= 256) {
if (! strlen($topic) || strlen($topic) > self::TOPIC_MAX_LENGTH) {
throw new InvalidArgumentException("Invalid topic length");
}
$this->topic = $topic;
}
/**
* Return the currently set topic.
*/
public function getTopic(): string
{
return $this->topic;
}
/**
* Push a message with Ntfy
*/
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");
}
if (strlen($message) < 1 || strlen($message) >= 4096) {
if (! strlen($message) || strlen($message) > self::MESSAGE_MAX_LENGTH) {
throw new InvalidArgumentException("Invalid message length");
}

View File

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Rclone;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
@ -14,20 +14,11 @@ use Exception;
*
* Installation of rclone is required.
* Configuration of the mounts must be done before use.
* Tested using rclone v1.53.3-DEV
* Tested using rclone v1.64.0
*/
class Rclone
{
use LoggerAwareTrait;
protected string $rclonePath;
/**
* Global options
*
* @var array<string>
*/
protected array $globalOptions = [];
private const MAX_RUNTIME = 4 * 3600; //4 hours maximum
protected string $version = "";
/**
@ -38,20 +29,19 @@ class Rclone
*
* @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->setLogger(new NullLogger());
$process = $this->exec('--version');
if (!$process->isSuccessful()) {
if (! $process->isSuccessful()) {
throw new Exception("Check installation of rclone");
}
$this->version = explode("\n", $process->getOutput())[0];
if (!\str_contains($this->version, 'rclone')) {
throw new Exception("Rclone not recognized");
if (! \str_contains($this->version, 'rclone')) {
throw new Exception("rclone not recognized");
}
}
@ -76,12 +66,14 @@ class Rclone
{
$process = $this->exec('size', ['--json', $path]);
if (!$process->isSuccessful()) {
if (! $process->isSuccessful()) {
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'];
}
@ -92,20 +84,20 @@ class Rclone
* @param $dest Destination mount and path
* @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[] = $dest;
foreach ($additionalOptions as $key => $value) {
$options[] = '--' . $key;
$options[] = '--' . (string)$key;
$options[] = $value;
}
$process = $this->exec('copy', $options);
if (!$process->isSuccessful()) {
if (! $process->isSuccessful()) {
throw new Exception($process->getErrorOutput());
}
}
@ -118,32 +110,26 @@ class Rclone
*
* @return Process Instance.
*/
protected function exec(string $command, array $options = array()): Process
private function exec(string $command, array $options = []): Process
{
$process = new Process(
array_merge(
[$this->rclonePath],
$this->globalOptions,
[$command],
$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();
// executes after the command finishes
if (!$process->isSuccessful()) {
if ($this->logger instanceof LoggerInterface) {
$this->logger->error("Failed execution");
}
}
if ($this->logger instanceof LoggerInterface) {
$this->logger->info("Return code", [$process->getExitCode()]);
if (! $process->isSuccessful()) {
$this->logger->error("Failed execution");
}
$this->logger->info("Return code", [$process->getExitCode()]);
return $process;
}
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Template;
use Twig\Environment;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Template;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -20,9 +22,9 @@ class TwigExtension extends AbstractExtension
*/
public function getFilters(): array
{
return array(
new TwigFilter('formatBytes', array($this, 'formatBytes')),
);
return [
new TwigFilter('formatBytes', [$this, 'formatBytes']),
];
}
/**
@ -36,7 +38,7 @@ class TwigExtension extends AbstractExtension
public function formatBytes($bytes, $precision = 2)
{
$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];
}
}

80
tests/AppTest.php Normal file
View 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');
}
}

View File

@ -15,14 +15,26 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
{
protected function setUp(): void
{
mkdir('temp');
mkdir('temp/destination');
exec('rclone test makefiles temp/source 2>&1');
if (! is_dir('temp')) {
mkdir('temp');
}
if (! is_dir('temp/destination')) {
mkdir('temp/destination');
}
exec('rclone test makefiles --files 10 temp/source 2>&1');
}
protected function tearDown(): void
{
exec('rclone purge temp 2>&1');
if (is_dir('temp/destination')) {
rmdir('temp/destination');
}
if (is_dir("temp")) {
rmdir('temp');
}
}
public function testBadConfig(): void
@ -31,7 +43,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
$applicationd->add(new CommandBackup());
$command = $applicationd->find('backup');
$commandTester = new CommandTester($command);
$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);
}
public function testGoodConfig(): void
public function testNoCommand(): void
{
$applicationd = new Application('backup', "1.1.1");
$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');
$commandTester = new CommandTester($command);
@ -62,7 +85,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
$applicationd->add(new CommandBackup());
$command = $applicationd->find('backup');
$commandTester = new CommandTester($command);
$commandTester->execute(['config' => "config.example.yml"]);
@ -78,7 +100,6 @@ final class CommandBackupTest extends \PHPUnit\Framework\TestCase
$applicationd->add(new CommandBackup());
$command = $applicationd->find('backup');
$commandTester = new CommandTester($command);
$commandTester->execute(['config' => "config.example.yml"]);

View File

@ -15,12 +15,10 @@ final class CommandShowTest extends \PHPUnit\Framework\TestCase
{
public function testInvalidConfig(): void
{
$applicationd = new Application('backup', "1.1.1");
$applicationd->add(new CommandShow());
$command = $applicationd->find('show');
$commandTester = new CommandTester($command);
$commandTester->execute(['config' => "bad_file"]);
@ -37,7 +35,6 @@ final class CommandShowTest extends \PHPUnit\Framework\TestCase
$applicationd->add(new CommandShow());
$command = $applicationd->find('show');
$commandTester = new CommandTester($command);
$commandTester->execute(['config' => "config.example.yml"]);

View File

@ -8,10 +8,13 @@ use PHPUnit\Framework\TestCase;
use App\Notification\Notification;
use App\Notification\NotificationInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use Exception;
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'];
public function testloadSingle(): void
@ -44,12 +47,43 @@ final class NotificationTest extends TestCase
$this->assertEquals(4, count($dut->getNotifiers()));
}
public function testSend(): void
public function testSendNoNotifier(): void
{
$dut = new Notification();
$mock = $this->createMock(NotificationInterface::class);
$dut->addNotifier($mock);
$mock->expects($this->once())->method('send');
$dut->send('title', 'topic');
$this->assertEquals(0, count($dut->getNotifiers()));
}
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');
}
}

View File

@ -13,7 +13,13 @@ use PHPUnit\Framework\MockObject\MockObject;
final class NtfyTest extends TestCase
{
/**
* @psalm-suppress PropertyNotSetInConstructor
*/
private Ntfy $instance;
/**
* @psalm-suppress PropertyNotSetInConstructor
*/
private MockObject $client;
protected function setUp(): void
@ -30,23 +36,32 @@ final class NtfyTest extends TestCase
$config = ['domain' => 'https://test.com', 'topic' => 'something'];
$instance = Ntfy::factory($config);
$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->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
{
return [
['', ''],
['', 'text'],
['title', ''],
[str_repeat("t", 256),'text'],
['title',str_repeat("t", 4096)],
[str_repeat("t", Ntfy::TITLE_MAX_LENGTH + 1), 'text'],
['title',str_repeat("t", Ntfy::MESSAGE_MAX_LENGTH + 1)],
];
}
@ -59,18 +74,23 @@ final class NtfyTest extends TestCase
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->assertEquals($topic, $this->instance->getTopic());
}
/** @return array<int, array<int, string>> */
/**
* @return array<int, array<int, string>>
*/
public static function topicBadParameterProvider(): array
{
return [
[''],
[str_repeat("t", 256)],
[str_repeat("t", 4096)],
[str_repeat("t", Ntfy::TOPIC_MAX_LENGTH + 1)],
];
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Tests;
use App\Rclone\Rclone;
use Psr\Log\NullLogger;
use PHPUnit\Framework\TestCase;
final class RcloneTest extends TestCase
@ -19,52 +20,67 @@ final class RcloneTest extends TestCase
exec('rclone purge temp 2>&1');
}
public function testRclonePath(): void
{
$this->expectException(\Exception::class);
$rclone = new Rclone('invalid');
$this->assertEquals('', $rclone->getVersion());
new Rclone(new NullLogger(), 'invalid');
$this->fail('Exception was not thrown');
}
public function testRcloneInvalidVersion(): void
{
$this->expectException(\Exception::class);
$rclone = new Rclone('uname');
$this->assertEquals('', $rclone->getVersion());
new Rclone(new NullLogger(), 'uname');
$this->fail('Exception was not thrown');
}
public function testRcloneValidVersion(): void
{
$rclone = new Rclone();
$rclone = new Rclone(new NullLogger());
$this->assertStringStartsWith('rclone', $rclone->getVersion());
}
public function testRcloneSize(): void
{
$rclone = new Rclone();
$rclone = new Rclone(new NullLogger());
$size = $rclone->getSize('temp/source');
$this->assertGreaterThan(10000, $size);
$this->expectException(\Exception::class);
$this->expectExceptionMessage("ERROR");
$size = $rclone->getSize('temp/bogus-source');
$this->assertEquals(0, $size);
$rclone->getSize('temp/bogus-source');
$this->fail('Exception was not thrown');
}
public function testRcloneCopy(): void
{
$rclone = new Rclone();
$rclone = new Rclone(new NullLogger());
$rclone->copy('temp/source', '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']);
$this->assertDirectoryExists('temp/destination');
}
public function testRcloneCopyBad(): void
{
$rclone = new Rclone(new NullLogger());
$this->expectException(\Exception::class);
$this->expectExceptionMessage("ERROR");
$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');
}
}

View 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));
}
}

View 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);
}
}

View File

View File

@ -0,0 +1,6 @@
<note>
<to>Someone</to>
<from>Me</from>
<heading>Title</heading>
<body>Text</body>
</note>

View 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 }}