Compare commits
99 Commits
45ea239d7c
...
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 | |||
3b9cea1c70 | |||
534c8cdbe6 | |||
46121bfcca | |||
5705822129 | |||
50ae396000 | |||
c1fb42c4ff | |||
f7b4a8f9cd | |||
ea18b8b9b3 | |||
9add013038 | |||
51f8436e9d | |||
006a5410b5 | |||
a070849293 | |||
be2985c797 | |||
49141719b5 | |||
27001e63c2 | |||
9324def64a | |||
0bddd2a1ef | |||
c140d5883c | |||
fb2f37a189 | |||
f41571cfd2 | |||
72058335ca | |||
ba4a3f273f | |||
e041db6ba3 | |||
baf34236f0 | |||
3efa791d5e | |||
78c1ea0326 | |||
286b5faff5 | |||
4e9248c8fd | |||
4ffac17b62 | |||
0f3bbf1f47 | |||
d0270b00ca | |||
afc4a1e3ce | |||
79dfcd6495 | |||
9d0d188d01 | |||
668c375f4b | |||
c59de96874 | |||
5369de6377 | |||
2fb2dcaba8 | |||
69e7962b6b | |||
36b043e307 | |||
f1c1c5456c | |||
1f902ed6c3 |
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
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,6 +1,10 @@
|
||||
/vendor/
|
||||
/output/
|
||||
/temp/
|
||||
/.phpdoc/
|
||||
*.log
|
||||
config.yml
|
||||
composer.phar
|
||||
*.phar
|
||||
*.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>
|
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@ -4,6 +4,22 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug test script",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"program": "",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"port": 0,
|
||||
"args": ["vendor/bin/phpunit", "${file}"],
|
||||
"runtimeArgs": [
|
||||
"-dxdebug.start_with_request=yes"
|
||||
],
|
||||
"env": {
|
||||
"XDEBUG_MODE": "debug,develop",
|
||||
"XDEBUG_CONFIG": "client_port=${port}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch currently open script",
|
||||
"type": "php",
|
||||
|
@ -1,22 +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
|
||||
- name: run
|
||||
image: php:cli-alpine
|
||||
commands:
|
||||
- ./backup show config.example.yml
|
||||
test:
|
||||
- name: dependencies
|
||||
image: composer
|
||||
commands:
|
||||
- composer install
|
||||
- ./vendor/bin/phpunit tests
|
||||
analyze:
|
||||
image: php
|
||||
- composer analyze
|
||||
- name: test
|
||||
image: php:cli-bookworm
|
||||
commands:
|
||||
- ./vendor/bin/phpmd src text cleancode,codesize,controversial,design,naming,unusedcode
|
||||
- ./vendor/bin/phpstan analyze --level=7 src/ backup
|
||||
- ./vendor/bin/psalm
|
||||
- ./vendor/bin/phpcs src
|
||||
failure: ignore
|
||||
- apt update
|
||||
- apt install rclone
|
||||
- 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
|
16
Makefile
16
Makefile
@ -1,16 +0,0 @@
|
||||
analyze-all:
|
||||
@echo PHPMessDetector
|
||||
-./vendor/bin/phpmd src text cleancode,codesize,controversial,design,naming,unusedcode
|
||||
@echo PHPStan
|
||||
-./vendor/bin/phpstan analyze --level=7 src/ backup
|
||||
@echo Psalm
|
||||
-./vendor/bin/psalm
|
||||
@echo PHP_CodeSniffer
|
||||
-./vendor/bin/phpcs src
|
||||
install:
|
||||
php composer.phar install --no-dev
|
||||
install-dev:
|
||||
php composer.phar install
|
||||
test:
|
||||
./vendor/bin/phpunit tests --testdox --coverage-filter src --coverage-html output/coverage
|
||||
|
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Backup script
|
||||
|
||||
[![Build status](https://build.jcktrue.dk/api/badges/jct/backupscript/status.svg)](https://build.jcktrue.dk/jct/backupscript)
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
34
backup
34
backup
@ -1,26 +1,6 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
$autoload = null;
|
||||
|
||||
$autoloadFiles = [
|
||||
__DIR__ . '/../vendor/autoload.php',
|
||||
__DIR__ . '/../../../autoload.php',
|
||||
__DIR__ . '/vendor/autoload.php'
|
||||
];
|
||||
|
||||
foreach ($autoloadFiles as $autoloadFile) {
|
||||
if (file_exists($autoloadFile)) {
|
||||
$autoload = $autoloadFile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $autoload) {
|
||||
echo "Autoload file not found; try 'composer dump-autoload' first." . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require $autoload;
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
use Symfony\Component\Console\Application;
|
||||
use Composer\InstalledVersions;
|
||||
|
||||
@ -28,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());
|
||||
}
|
@ -1,14 +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": "^5.4",
|
||||
"symfony/yaml": "^5.4",
|
||||
"twig/twig": "^3.6",
|
||||
"symfony/process": "^5.4",
|
||||
"monolog/monolog": "^2.9"
|
||||
"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": {
|
||||
@ -16,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": "^9.6"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
2549
composer.lock
generated
2549
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,11 @@
|
||||
notification:
|
||||
domain: ntfy.jcktrue.dk
|
||||
topic: backup
|
||||
log: output.log
|
||||
- type: Ntfy
|
||||
domain: https://ntfy.jcktrue.dk
|
||||
topic: testing
|
||||
log: output.log
|
||||
rclone:
|
||||
bwlimit: 6M
|
||||
options:
|
||||
bwlimit: 6M
|
||||
backup:
|
||||
- title: Example
|
||||
source: temp/source
|
||||
@ -18,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
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
|
||||
}
|
||||
}
|
11
output.log
11
output.log
@ -1,11 +0,0 @@
|
||||
[2023-05-31T14:30:00.890085+00:00] app.INFO: Initialization complete [] []
|
||||
[2023-05-31T14:30:00.946882+00:00] rclone.INFO: Execute command ["'rclone' 'size' '--json' 'temp/source'"] []
|
||||
[2023-05-31T14:30:00.989664+00:00] rclone.INFO: Return code [0] []
|
||||
[2023-05-31T14:30:00.989960+00:00] rclone.INFO: Execute command ["'rclone' 'size' '--json' 'temp/destination'"] []
|
||||
[2023-05-31T14:30:01.047504+00:00] rclone.INFO: Return code [0] []
|
||||
[2023-05-31T14:30:01.047810+00:00] rclone.INFO: Execute command ["'rclone' 'copy' 'temp/source' 'temp/destination' '--bwlimit' '6M'"] []
|
||||
[2023-05-31T14:30:01.097485+00:00] rclone.INFO: Return code [0] []
|
||||
[2023-05-31T14:30:01.097797+00:00] rclone.INFO: Execute command ["'rclone' 'size' '--json' 'temp/destination'"] []
|
||||
[2023-05-31T14:30:01.142119+00:00] rclone.INFO: Return code [0] []
|
||||
[2023-05-31T14:30:01.174134+00:00] notification.DEBUG: Sending ntfy notification {"topic":"backup","title":"Example","message":"Example\nFrom temp/source to temp/destination\nBackup started: May 31, 2023 14:30\nSource size: 8.00B\nDestination before: 8.00B\nDestination after: 8.00B\nDestination change : 0.00B\nBackup completed: May 31, 2023 14:30\n"} []
|
||||
[2023-05-31T14:30:01.363282+00:00] notification.DEBUG: Result of ntfy notification ["{\"id\":\"iHfqUGi4dUsC\",\"time\":1685543401,\"expires\":1685586601,\"event\":\"message\",\"topic\":\"backup\",\"title\":\"Example\",\"message\":\"Example\\nFrom temp/source to temp/destination\\nBackup started: May 31, 2023 14:30\\nSource size: 8.00B\\nDestination before: 8.00B\\nDestination after: 8.00B\\nDestination change : 0.00B\\nBackup completed: May 31, 2023 14:30\"}\n"] []
|
18
phpdoc.dist.xml
Normal file
18
phpdoc.dist.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<phpdocumentor
|
||||
configVersion="3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://www.phpdoc.org"
|
||||
>
|
||||
<title>backupscript</title>
|
||||
<paths>
|
||||
<output>output/docs</output>
|
||||
</paths>
|
||||
<version number="latest">
|
||||
<api>
|
||||
<source dsn=".">
|
||||
<path>src</path>
|
||||
</source>
|
||||
</api>
|
||||
</version>
|
||||
</phpdocumentor>
|
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>
|
11
psalm.xml
11
psalm.xml
@ -1,15 +1,24 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="2"
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
findUnusedBaselineEntry="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>
|
||||
|
93
src/App.php
93
src/App.php
@ -1,39 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Psr\Log\NullLogger;
|
||||
use League\Config\Configuration;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
/**
|
||||
* Mostly working as a register pattern for the logging and configuration.
|
||||
*/
|
||||
class App
|
||||
{
|
||||
/// Logging instance
|
||||
protected Logger $logger;
|
||||
protected array $config;
|
||||
/// Configuration singleton
|
||||
protected Configuration $config;
|
||||
|
||||
function __construct(string $configFile)
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$this->config = Yaml::parseFile($configFile);
|
||||
// Define your configuration schema
|
||||
$this->config = new Configuration([
|
||||
'rclone' => Expect::structure([
|
||||
'path' => Expect::string()->default('rclone'),
|
||||
'options' => Expect::arrayOf('string', 'string')
|
||||
]),
|
||||
'backup' => Expect::arrayOf(Expect::structure([
|
||||
'title' => Expect::string()->required(),
|
||||
'source' => Expect::string()->required(),
|
||||
'destination' => Expect::string()->required(),
|
||||
]))->required(),
|
||||
'notification' => Expect::arrayOf(Expect::structure([
|
||||
'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()->required(),
|
||||
'error' => Expect::string()->required()
|
||||
]
|
||||
)->required()
|
||||
]);
|
||||
|
||||
$parser = new Yaml();
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
$parsedConfig = $parser->parseFile($configFile);
|
||||
|
||||
// Merge those values into the configuration schema:
|
||||
$this->config->merge($parsedConfig);
|
||||
|
||||
$logger = new Logger('app');
|
||||
if (isset($this->config['log'])) {
|
||||
$logger->pushHandler(new StreamHandler($this->getConfig()['log']));
|
||||
if ($this->config->get('log')) {
|
||||
$logger->pushHandler(new StreamHandler((string)$this->config->get('log')));
|
||||
$logger->info("Logging enabled");
|
||||
}
|
||||
$logger->info("Initialization complete");
|
||||
|
||||
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
|
||||
function getConfig() : array
|
||||
/**
|
||||
* Get configuration from key
|
||||
*
|
||||
* @param non-empty-string $key Key to fetch
|
||||
*
|
||||
* @return mixed Configuration value
|
||||
*/
|
||||
public function getConfig(string $key): mixed
|
||||
{
|
||||
return $this->config;
|
||||
/**
|
||||
* @var string|array<string, string>
|
||||
*/
|
||||
$ret = $this->config->get($key);
|
||||
$this->logger->debug("Fetching configuration key", [$key, $ret]);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function getLogger() : Logger
|
||||
/**
|
||||
* Get the logger instance.
|
||||
*
|
||||
* @return Logger Instance of logger
|
||||
*/
|
||||
public function getLogger(): Logger
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,74 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
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 Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
|
||||
use App\Template\Twig;
|
||||
use App\Notification\Notification;
|
||||
use App\Rclone\Rclone;
|
||||
use DateTime;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'backup',
|
||||
description: 'Start backup to assigned buckets',
|
||||
)]
|
||||
class CommandBackup extends Command
|
||||
{
|
||||
static $defaultName = "backup";
|
||||
static $defaultDescription = "Start backup to assigned buckets";
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('config', InputArgument::OPTIONAL, 'Configuration file', "config.yml");
|
||||
$this->addArgument(
|
||||
'config',
|
||||
InputArgument::REQUIRED,
|
||||
'Configuration file'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Start backup process');
|
||||
|
||||
try {
|
||||
$app = new App($input->getArgument('config'));
|
||||
$sio = new SymfonyStyle($input, $output);
|
||||
$sioProgressbar = &$sio;
|
||||
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$sio = new SymfonyStyle($input, $output->section());
|
||||
$sioProgressbar = new SymfonyStyle($input, $output->section());
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
$io->error('Unable to parse the YAML string: '. $e->getMessage());
|
||||
|
||||
$sio->title('Start backup process');
|
||||
|
||||
try {
|
||||
$app = new App((string)$input->getArgument('config'));
|
||||
} catch (\Throwable $e) {
|
||||
$sio->error('Configuration error:' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
|
||||
$rclone = new Rclone\Rclone();
|
||||
$rclone->setLogger($app->getLogger()->withName('rclone'));
|
||||
|
||||
$ntfy = new Ntfy\Ntfy($app->getConfig()['notification']['domain']);
|
||||
$ntfy->setLogger($app->getLogger()->withName('notification'));
|
||||
$rclone = new Rclone($app->getLogger()->withName('rclone'), (string)$app->getConfig('rclone.path'));
|
||||
|
||||
$loader = new ArrayLoader($app->getConfig()['templates']);
|
||||
$twig = new Environment($loader);
|
||||
$twig->addExtension(new Twig\AppExtension());
|
||||
$notification = new Notification();
|
||||
/**
|
||||
* @var array<array-key,array<string,string>>
|
||||
*/
|
||||
$notificationConfig = $app->getConfig('notification');
|
||||
$notification->loadMany($notificationConfig);
|
||||
|
||||
foreach ($io->progressIterate($app->getConfig()['backup']) as $conf) {
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
$templateConfig = $app->getConfig('templates');
|
||||
$render = new Twig($templateConfig);
|
||||
|
||||
/**
|
||||
* @var array{title: string, source: string, destination: string}[]
|
||||
*/
|
||||
$backupElements = $app->getConfig('backup');
|
||||
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']);
|
||||
|
||||
$rclone->copy($conf['source'], $conf['destination'], "6M");
|
||||
/**
|
||||
* @var array<array-key, string>
|
||||
*/
|
||||
$rcloneOptions = $app->getConfig('rclone.options');
|
||||
$rclone->copy($conf['source'], $conf['destination'], $rcloneOptions);
|
||||
|
||||
$template['destination_size_after'] = $rclone->getSize($conf['destination']);
|
||||
$template['end'] = new DateTime();
|
||||
|
||||
$message = $twig->render('notify', $template);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$message = $e->getMessage();
|
||||
$message = $render->render('notify', $template);
|
||||
} catch (\Exception $e) {
|
||||
$template['exception'] = $e->getMessage();
|
||||
$message = $render->render('error', $template);
|
||||
$sio->error($message);
|
||||
}
|
||||
$ntfy->send($app->getConfig()['notification']['topic'], $conf['title'], $message);
|
||||
$notification->send($title, $message);
|
||||
}
|
||||
|
||||
$io->success("Complete");
|
||||
$sio->success("Complete");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
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\Style\SymfonyStyle;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'show',
|
||||
description: 'Show all backup entries.',
|
||||
)]
|
||||
class CommandShow extends Command
|
||||
{
|
||||
static $defaultName = "show";
|
||||
static $defaultDescription = "Show all backup entries";
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('config', InputArgument::OPTIONAL, 'Configuration file', "config.yml");
|
||||
$this->addArgument(
|
||||
'config',
|
||||
InputArgument::REQUIRED,
|
||||
'Configuration file'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Read the configuration file.
|
||||
* 2. For each configured backup print the details
|
||||
* 3. Exit
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('List backup entities');
|
||||
$sio = new SymfonyStyle($input, $output);
|
||||
$sio->title('List backup entities');
|
||||
|
||||
try {
|
||||
$app = new App($input->getArgument('config'));
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
$io->error('Unable to parse the YAML string: '. $e->getMessage());
|
||||
$app = new App((string)$input->getArgument('config'));
|
||||
} catch (\Throwable $e) {
|
||||
$sio->error('Configuration error: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
/**
|
||||
* @var array{title: string, source: string, destination: string}[]
|
||||
*/
|
||||
$backupElements = $app->getConfig('backup');
|
||||
$sio->table(
|
||||
['Description', 'Source', 'Destination'],
|
||||
$backupElements
|
||||
);
|
||||
|
||||
$io->table(['Description', 'Source', 'Destination'], $app->getConfig()['backup']);
|
||||
|
||||
$io->success("Done");
|
||||
$sio->success("Done");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
src/Notification/Notification.php
Normal file
87
src/Notification/Notification.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use App\Notification\Ntfy;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class Notification
|
||||
{
|
||||
/**
|
||||
* @var NotificationInterface[] $notifiers
|
||||
*/
|
||||
private array $notifiers = [];
|
||||
|
||||
public function __construct(private NullLogger $logger = new NullLogger())
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Load multiple configurations
|
||||
*
|
||||
* @param array<string[]> $config Array of notifier configurations.
|
||||
*/
|
||||
public function loadMany(array $config): void
|
||||
{
|
||||
foreach ($config as $conf) {
|
||||
$this->loadSingle($conf['type'], $conf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single configuration
|
||||
*
|
||||
* @param string $key Notification class
|
||||
* @param string[] $config Implementation specific configuration
|
||||
*
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*/
|
||||
public function loadSingle(string $key, array $config): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'ntfy':
|
||||
case 'Ntfy':
|
||||
case 'NTFY':
|
||||
$this->addNotifier(Ntfy::factory($config));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single notifier instance.
|
||||
*/
|
||||
public function addNotifier(NotificationInterface $instance): void
|
||||
{
|
||||
$this->notifiers[] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active notifiers.
|
||||
*
|
||||
* @return NotificationInterface[] All notifiers.
|
||||
*/
|
||||
public function getNotifiers(): array
|
||||
{
|
||||
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) {
|
||||
try {
|
||||
$notifier->send($title, $message);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/Notification/NotificationInterface.php
Normal file
18
src/Notification/NotificationInterface.php
Normal file
@ -0,0 +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;
|
||||
}
|
83
src/Notification/Ntfy.php
Normal file
83
src/Notification/Ntfy.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use Ntfy\Server;
|
||||
use Ntfy\Message;
|
||||
use Ntfy\Client;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Send a notification through a ntfy server
|
||||
*/
|
||||
class Ntfy implements NotificationInterface
|
||||
{
|
||||
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
|
||||
{
|
||||
$instance = new self(new Client(new Server($config['domain'])));
|
||||
if (isset($config['topic'])) {
|
||||
$instance->setTopic($config['topic']);
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function __construct(private 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) || 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) || strlen($title) > self::TITLE_MAX_LENGTH) {
|
||||
throw new InvalidArgumentException("Invalid title length");
|
||||
}
|
||||
|
||||
if (! strlen($message) || strlen($message) > self::MESSAGE_MAX_LENGTH) {
|
||||
throw new InvalidArgumentException("Invalid message length");
|
||||
}
|
||||
|
||||
$msg = new Message();
|
||||
$msg->topic($this->getTopic());
|
||||
$msg->title($title);
|
||||
$msg->body($message);
|
||||
$this->client->send($msg);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
namespace App\Ntfy;
|
||||
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class Ntfy
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
protected string $domain;
|
||||
function __construct(string $domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
function send(string $topic, string $title, string $message): void
|
||||
{
|
||||
$this->logger->debug("Sending ntfy notification", ["topic"=>$topic, "title"=>$title, "message"=>$message]);
|
||||
$result = file_get_contents(
|
||||
'https://'.$this->domain.'/'.$topic, false, stream_context_create(
|
||||
['http' => [
|
||||
'method' => 'POST',
|
||||
'header' =>
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"Title: $title\r\n",
|
||||
'content' => $message]
|
||||
]
|
||||
)
|
||||
);
|
||||
$this->logger->debug("Result of ntfy notification", [$result]);
|
||||
}
|
||||
|
||||
}
|
@ -1,102 +1,135 @@
|
||||
<?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;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Wrapper for the rclone command line utility
|
||||
*
|
||||
* Installation of rclone is required.
|
||||
* Configuration of the mounts must be done before use.
|
||||
* 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 = "";
|
||||
|
||||
function __construct(string $rclonePath = "rclone")
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* Default it looks for "rclone" on the path.
|
||||
* But the path can be configured to be absolute.
|
||||
*
|
||||
* @param string $rclonePath Relative or absolute path
|
||||
*/
|
||||
public function __construct(protected LoggerInterface $logger, protected string $rclonePath = "rclone")
|
||||
{
|
||||
$this->rclonePath = $rclonePath;
|
||||
$this->setLogger(new NullLogger);
|
||||
try
|
||||
{
|
||||
$version = $this->exec('--version');
|
||||
}
|
||||
catch(ProcessFailedException $e)
|
||||
{
|
||||
|
||||
$process = $this->exec('--version');
|
||||
if (! $process->isSuccessful()) {
|
||||
throw new Exception("Check installation of rclone");
|
||||
}
|
||||
|
||||
$this->version = explode("\n", $version)[0];
|
||||
|
||||
if (!\str_contains($this->version, 'rclone')) {
|
||||
throw new Exception("Rclone not recognized");
|
||||
$this->version = explode("\n", $process->getOutput())[0];
|
||||
|
||||
if (! \str_contains($this->version, 'rclone')) {
|
||||
throw new Exception("rclone not recognized");
|
||||
}
|
||||
}
|
||||
|
||||
function getVersion(): string
|
||||
/**
|
||||
* Get the rclone version.
|
||||
*
|
||||
* @return string Version string
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
function getSize(string $path): int
|
||||
/**
|
||||
* Calculate the size of a mount/path.
|
||||
*
|
||||
* @param string $path mount/path.
|
||||
*
|
||||
* @return int Size in bytes
|
||||
*/
|
||||
public function getSize(string $path): int
|
||||
{
|
||||
$output = $this->exec('size', ['--json', $path]);
|
||||
return (int)json_decode($output)->bytes;
|
||||
}
|
||||
$process = $this->exec('size', ['--json', $path]);
|
||||
|
||||
function copy(string $src, string $dest, string $bandwidth = null): string
|
||||
{
|
||||
$options = array();
|
||||
|
||||
$options[] = $src;
|
||||
$options[] = $dest;
|
||||
if ($bandwidth) {
|
||||
$options[] = "--bwlimit";
|
||||
$options[] = $bandwidth;
|
||||
if (! $process->isSuccessful()) {
|
||||
throw new Exception($process->getErrorOutput());
|
||||
}
|
||||
|
||||
return $this->exec('copy', $options);
|
||||
/**
|
||||
* @var array{bytes: int}
|
||||
*/
|
||||
$output = json_decode($process->getOutput(), TRUE);
|
||||
return $output['bytes'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command on the rclone binary
|
||||
*
|
||||
* Copy from source to destination.
|
||||
*
|
||||
* @param $src Source mount and path
|
||||
* @param $dest Destination mount and path
|
||||
* @param string[] $additionalOptions Additional options
|
||||
*/
|
||||
public function copy(string $src, string $dest, array $additionalOptions = []): void
|
||||
{
|
||||
$options = [];
|
||||
|
||||
$options[] = $src;
|
||||
$options[] = $dest;
|
||||
|
||||
foreach ($additionalOptions as $key => $value) {
|
||||
$options[] = '--' . (string)$key;
|
||||
$options[] = $value;
|
||||
}
|
||||
|
||||
$process = $this->exec('copy', $options);
|
||||
if (! $process->isSuccessful()) {
|
||||
throw new Exception($process->getErrorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command on the rclone binary.
|
||||
*
|
||||
* @param string $command Top level Rclone command
|
||||
* @param array<String> $options Array of additional options
|
||||
*
|
||||
* @return string stdout data.
|
||||
*
|
||||
* @return Process Instance.
|
||||
*/
|
||||
protected function exec(string $command, array $options = array()) : string
|
||||
private function exec(string $command, array $options = []): Process
|
||||
{
|
||||
$process = new Process(
|
||||
array_merge(
|
||||
[$this->rclonePath],
|
||||
$this->globalOptions,
|
||||
[$command],
|
||||
$options
|
||||
)
|
||||
);
|
||||
|
||||
$this->logger->info("Execute command", [$process->getCommandLine()]);
|
||||
$process->setTimeout(4*3600);
|
||||
|
||||
$process->setTimeout(self::MAX_RUNTIME);
|
||||
$process->run();
|
||||
|
||||
// executes after the command finishes
|
||||
if (!$process->isSuccessful()) {
|
||||
if (! $process->isSuccessful()) {
|
||||
$this->logger->error("Failed execution");
|
||||
throw new ProcessFailedException($process);
|
||||
}
|
||||
$this->logger->info("Return code", [$process->getExitCode()]);
|
||||
return $process->getOutput();
|
||||
return $process;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
src/Template/Twig.php
Normal file
22
src/Template/Twig.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Template;
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use App\Template\TwigExtension;
|
||||
|
||||
class Twig extends Environment
|
||||
{
|
||||
/**
|
||||
* @param string[] $templates Array of templates
|
||||
*/
|
||||
public function __construct(array $templates)
|
||||
{
|
||||
$loader = new ArrayLoader($templates);
|
||||
parent::__construct($loader);
|
||||
$this->addExtension(new TwigExtension());
|
||||
}
|
||||
}
|
@ -1,33 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Template;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class AppExtension extends AbstractExtension
|
||||
/**
|
||||
* Twig extension
|
||||
*
|
||||
* Additional formatters for templates
|
||||
*/
|
||||
class TwigExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters()
|
||||
/**
|
||||
* Extend the filters
|
||||
*
|
||||
* @return TwigFilter[]
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
return array(
|
||||
new TwigFilter('formatBytes', array($this, 'formatBytes')),
|
||||
);
|
||||
return [
|
||||
new TwigFilter('formatBytes', [$this, 'formatBytes']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a file size to be human readable
|
||||
*
|
||||
*
|
||||
* @param int $bytes Number of bytes
|
||||
* @param int $precision Precision
|
||||
*
|
||||
*
|
||||
* @return string Formatted string
|
||||
*/
|
||||
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
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');
|
||||
}
|
||||
}
|
110
tests/CommandBackupTest.php
Normal file
110
tests/CommandBackupTest.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use App\CommandBackup;
|
||||
|
||||
#[CoversClass(CommandBackup::class)]
|
||||
final class CommandBackupTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
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
|
||||
{
|
||||
$applicationd = new Application('backup', "1.1.1");
|
||||
|
||||
$applicationd->add(new CommandBackup());
|
||||
|
||||
$command = $applicationd->find('backup');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(['config' => "bad_file"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[ERROR] Configuration error:File "bad_file" does not exist.', $output);
|
||||
}
|
||||
|
||||
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);
|
||||
$commandTester->execute(['config' => "config.example.yml"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] Complete ', $output);
|
||||
}
|
||||
|
||||
public function testNoDestinationFolder(): void
|
||||
{
|
||||
exec('rclone purge temp/destination 2>&1', $output);
|
||||
$applicationd = new Application('backup', "1.1.1");
|
||||
|
||||
$applicationd->add(new CommandBackup());
|
||||
|
||||
$command = $applicationd->find('backup');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(['config' => "config.example.yml"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] Complete ', $output);
|
||||
}
|
||||
|
||||
public function testNoSourceFolder(): void
|
||||
{
|
||||
exec('rclone purge temp/source 2>&1', $output);
|
||||
$applicationd = new Application('backup', "1.1.1");
|
||||
|
||||
$applicationd->add(new CommandBackup());
|
||||
|
||||
$command = $applicationd->find('backup');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(['config' => "config.example.yml"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[OK] Complete ', $output);
|
||||
}
|
||||
}
|
47
tests/CommandShowTest.php
Normal file
47
tests/CommandShowTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use App\CommandShow;
|
||||
|
||||
#[CoversClass(CommandShow::class)]
|
||||
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"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[ERROR] Configuration error: File "bad_file" does not exist.', $output);
|
||||
}
|
||||
|
||||
public function testGoodConfig(): void
|
||||
{
|
||||
exec('rclone test makefiles temp/source 2>&1');
|
||||
|
||||
$applicationd = new Application('backup', "1.1.1");
|
||||
|
||||
$applicationd->add(new CommandShow());
|
||||
|
||||
$command = $applicationd->find('show');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute(['config' => "config.example.yml"]);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Example', $output);
|
||||
$this->assertStringContainsString('temp/source', $output);
|
||||
$this->assertStringContainsString('temp/destination', $output);
|
||||
}
|
||||
}
|
89
tests/Notification/NotificationTest.php
Normal file
89
tests/Notification/NotificationTest.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
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>
|
||||
*/
|
||||
private static array $config = ['type' => 'ntfy', 'domain' => 'https://test.com', 'topic' => 'testing'];
|
||||
|
||||
public function testloadSingle(): void
|
||||
{
|
||||
$dut = new Notification();
|
||||
$dut->loadSingle('ntfy', self::$config);
|
||||
$this->assertEquals(1, count($dut->getNotifiers()));
|
||||
|
||||
$dut->loadSingle('Ntfy', self::$config);
|
||||
$this->assertEquals(2, count($dut->getNotifiers()));
|
||||
|
||||
$dut->loadSingle('NTFY', self::$config);
|
||||
$this->assertEquals(3, count($dut->getNotifiers()));
|
||||
|
||||
$dut->loadSingle('invalid', self::$config);
|
||||
$this->assertEquals(3, count($dut->getNotifiers()));
|
||||
}
|
||||
|
||||
public function testloadMany(): void
|
||||
{
|
||||
$dut = new Notification();
|
||||
|
||||
$arr = [];
|
||||
$arr[] = self::$config;
|
||||
$arr[] = self::$config;
|
||||
$arr[] = self::$config;
|
||||
$arr[] = self::$config;
|
||||
$dut->loadMany($arr);
|
||||
|
||||
$this->assertEquals(4, count($dut->getNotifiers()));
|
||||
}
|
||||
|
||||
public function testSendNoNotifier(): void
|
||||
{
|
||||
$dut = new Notification();
|
||||
$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');
|
||||
}
|
||||
}
|
103
tests/Notification/NtfyTest.php
Normal file
103
tests/Notification/NtfyTest.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Notification\Ntfy;
|
||||
use Ntfy\Client;
|
||||
use Ntfy\Message;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
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
|
||||
{
|
||||
$this->client = $this->createMock(Client::class);
|
||||
$this->instance = new Ntfy($this->client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.StaticAccess)
|
||||
*/
|
||||
public function testFactory(): void
|
||||
{
|
||||
$config = ['domain' => 'https://test.com', 'topic' => 'something'];
|
||||
$instance = Ntfy::factory($config);
|
||||
$this->assertInstanceOf(Ntfy::class, $instance);
|
||||
$this->assertEquals($instance->getTopic(), "something");
|
||||
}
|
||||
|
||||
public function testSendShort(): void
|
||||
{
|
||||
$this->client->expects($this->once())->method('send')->with($this->isInstanceOf(Message::class));
|
||||
$this->instance->send('t', 's');
|
||||
}
|
||||
|
||||
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", Ntfy::TITLE_MAX_LENGTH + 1), 'text'],
|
||||
['title',str_repeat("t", Ntfy::MESSAGE_MAX_LENGTH + 1)],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('sendBadParameterProvider')]
|
||||
public function testSendInvalidParameters(string $title, string $message): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->instance->send($title, $message);
|
||||
}
|
||||
|
||||
public function testSetTopic(): void
|
||||
{
|
||||
$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>>
|
||||
*/
|
||||
public static function topicBadParameterProvider(): array
|
||||
{
|
||||
return [
|
||||
[''],
|
||||
[str_repeat("t", Ntfy::TOPIC_MAX_LENGTH + 1)],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('topicBadParameterProvider')]
|
||||
public function testSetTopicInvalidParameters(string $topic): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->instance->setTopic($topic);
|
||||
}
|
||||
}
|
@ -1,23 +1,86 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use App\Rclone\Rclone;
|
||||
use Psr\Log\NullLogger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class RcloneTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
exec('rclone test makefiles temp/source 2>&1');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
exec('rclone purge temp 2>&1');
|
||||
}
|
||||
|
||||
public function testRclonePath(): void
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$rclone = new \App\Rclone\Rclone('invalid');
|
||||
new Rclone(new NullLogger(), 'invalid');
|
||||
$this->fail('Exception was not thrown');
|
||||
}
|
||||
|
||||
public function testRcloneInvalidVersion(): void
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$rclone = new \App\Rclone\Rclone('uname');
|
||||
new Rclone(new NullLogger(), 'uname');
|
||||
$this->fail('Exception was not thrown');
|
||||
}
|
||||
|
||||
public function testRcloneValidVersion(): void
|
||||
{
|
||||
$rclone = new \App\Rclone\Rclone('./tests/mock-rclone');
|
||||
$this->assertStringContainsString('rclone', $rclone->getVersion());
|
||||
$rclone = new Rclone(new NullLogger());
|
||||
$this->assertStringStartsWith('rclone', $rclone->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public function testRcloneSize(): void
|
||||
{
|
||||
$rclone = new Rclone(new NullLogger());
|
||||
$size = $rclone->getSize('temp/source');
|
||||
$this->assertGreaterThan(10000, $size);
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage("ERROR");
|
||||
$rclone->getSize('temp/bogus-source');
|
||||
$this->fail('Exception was not thrown');
|
||||
}
|
||||
|
||||
public function testRcloneCopy(): void
|
||||
{
|
||||
$rclone = new Rclone(new NullLogger());
|
||||
$rclone->copy('temp/source', 'temp/destination');
|
||||
$this->assertDirectoryExists('temp/destination');
|
||||
}
|
||||
|
||||
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->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 }}
|
@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo rclone v1.53.3-DEV
|
Reference in New Issue
Block a user