Add Miniflux auto-update feature
This commit is contained in:
parent
d8424ddae9
commit
c1221de62c
@ -10,6 +10,7 @@ require __DIR__.'/models/user.php';
|
||||
require __DIR__.'/models/feed.php';
|
||||
require __DIR__.'/models/item.php';
|
||||
require __DIR__.'/models/schema.php';
|
||||
require __DIR__.'/models/auto_update.php';
|
||||
|
||||
if (file_exists('config.php')) require 'config.php';
|
||||
|
||||
@ -24,6 +25,11 @@ defined('PROXY_HOSTNAME') or define('PROXY_HOSTNAME', '');
|
||||
defined('PROXY_PORT') or define('PROXY_PORT', 3128);
|
||||
defined('PROXY_USERNAME') or define('PROXY_USERNAME', '');
|
||||
defined('PROXY_PASSWORD') or define('PROXY_PASSWORD', '');
|
||||
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
|
||||
defined('ENABLE_AUTO_UPDATE') or define('ENABLE_AUTO_UPDATE', true);
|
||||
defined('AUTO_UPDATE_DOWNLOAD_DIRECTORY') or define('AUTO_UPDATE_DOWNLOAD_DIRECTORY', 'data/download');
|
||||
defined('AUTO_UPDATE_ARCHIVE_DIRECTORY') or define('AUTO_UPDATE_ARCHIVE_DIRECTORY', 'data/archive');
|
||||
defined('AUTO_UPDATE_BACKUP_DIRECTORY') or define('AUTO_UPDATE_BACKUP_DIRECTORY', 'data/backup');
|
||||
|
||||
PicoFeed\Client::proxy(PROXY_HOSTNAME, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD);
|
||||
|
||||
|
@ -7,6 +7,22 @@ use PicoFarad\Session;
|
||||
use PicoFarad\Template;
|
||||
use PicoDb\Database;
|
||||
|
||||
// Auto-update
|
||||
Router\get_action('auto-update', function() {
|
||||
|
||||
if (ENABLE_AUTO_UPDATE) {
|
||||
|
||||
if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) {
|
||||
Session\flash(t('Miniflux is updated!'));
|
||||
}
|
||||
else {
|
||||
Session\flash_error(t('Unable to update Miniflux, check the console for errors.'));
|
||||
}
|
||||
}
|
||||
|
||||
Response\redirect('?action=config');
|
||||
});
|
||||
|
||||
// Re-generate tokens
|
||||
Router\get_action('generate-tokens', function() {
|
||||
|
||||
|
4
data/archive/.gitignore
vendored
Normal file
4
data/archive/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
4
data/backup/.gitignore
vendored
Normal file
4
data/backup/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
4
data/download/.gitignore
vendored
Normal file
4
data/download/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
@ -208,4 +208,9 @@ return array(
|
||||
'%d months ago' => 'Il y a %d mois',
|
||||
'Timezone' => 'Fuseau horaire',
|
||||
'Update all subscriptions' => 'Mettre à jour tous les abonnements',
|
||||
'Auto-Update URL' => 'URL de mise à jour automatique',
|
||||
'Update Miniflux' => 'Mettre à jour Miniflux',
|
||||
'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !',
|
||||
'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.',
|
||||
'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données',
|
||||
);
|
||||
|
229
models/auto_update.php
Normal file
229
models/auto_update.php
Normal file
@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Model\AutoUpdate;
|
||||
|
||||
use ZipArchive;
|
||||
use DirectoryIterator;
|
||||
use RecursiveIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
|
||||
// Get all files of a given directory
|
||||
function get_files_list($directory)
|
||||
{
|
||||
$exclude_list = array(
|
||||
'.git',
|
||||
'data',
|
||||
'scripts',
|
||||
'config.php',
|
||||
);
|
||||
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), RecursiveIteratorIterator::SELF_FIRST);
|
||||
$files = array();
|
||||
|
||||
while ($it->valid()) {
|
||||
|
||||
if ($it->isFile() && ! is_excluded_path($it->getSubPathname(), $exclude_list)) {
|
||||
$files[] = $it->getSubPathname();
|
||||
}
|
||||
|
||||
$it->next();
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Check if the given path is excluded
|
||||
function is_excluded_path($path, array $exclude_list)
|
||||
{
|
||||
foreach ($exclude_list as $excluded_path) {
|
||||
|
||||
if (strpos($path, $excluded_path) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Synchronize 2 directories (copy/remove files)
|
||||
function synchronize($source_directory, $destination_directory)
|
||||
{
|
||||
\Model\Config\debug('[SYNCHRONIZE] '.$source_directory.' to '.$destination_directory);
|
||||
|
||||
$src_files = get_files_list($source_directory);
|
||||
$dst_files = get_files_list($destination_directory);
|
||||
|
||||
// Remove files
|
||||
$remove_files = array_diff($dst_files, $src_files);
|
||||
|
||||
foreach ($remove_files as $file) {
|
||||
|
||||
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
|
||||
\Model\Config\debug('[REMOVE] '.$destination_file);
|
||||
|
||||
if (! @unlink($destination_file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite all files
|
||||
foreach ($src_files as $file) {
|
||||
|
||||
$directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
|
||||
\Model\Config\debug('[MKDIR] '.$directory);
|
||||
|
||||
if (! @mkdir($directory, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$source_file = $source_directory.DIRECTORY_SEPARATOR.$file;
|
||||
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
|
||||
|
||||
\Model\Config\debug('[COPY] '.$source_file.' to '.$destination_file);
|
||||
|
||||
if (! @copy($source_file, $destination_file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Download and unzip the archive
|
||||
function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
|
||||
{
|
||||
$archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip';
|
||||
|
||||
\Model\Config\debug('[DOWNLOAD] '.$url);
|
||||
|
||||
if (($data = @file_get_contents($url)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@file_put_contents($archive_file, $data) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
\Model\Config\debug('[UNZIP] '.$archive_file);
|
||||
|
||||
$zip = new ZipArchive;
|
||||
|
||||
if (! $zip->open($archive_file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$zip->extractTo($archive_directory);
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove all files for a given directory
|
||||
function cleanup_directory($directory)
|
||||
{
|
||||
\Model\Config\debug('[CLEANUP] '.$directory);
|
||||
|
||||
$dir = new DirectoryIterator($directory);
|
||||
|
||||
foreach ($dir as $fileinfo) {
|
||||
|
||||
if (! $fileinfo->isDot()) {
|
||||
|
||||
$filename = $fileinfo->getRealPath();
|
||||
|
||||
if ($fileinfo->isFile()) {
|
||||
\Model\Config\debug('[REMOVE] '.$filename);
|
||||
@unlink($filename);
|
||||
}
|
||||
else {
|
||||
cleanup_directory($filename);
|
||||
@rmdir($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup all temporary directories
|
||||
function cleanup_directories()
|
||||
{
|
||||
cleanup_directory(AUTO_UPDATE_DOWNLOAD_DIRECTORY);
|
||||
cleanup_directory(AUTO_UPDATE_ARCHIVE_DIRECTORY);
|
||||
cleanup_directory(AUTO_UPDATE_BACKUP_DIRECTORY);
|
||||
}
|
||||
|
||||
// Find the archive directory name
|
||||
function find_archive_root($base_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
|
||||
{
|
||||
$directory = '';
|
||||
$dir = new DirectoryIterator($base_directory);
|
||||
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (! $fileinfo->isDot() && $fileinfo->isDir()) {
|
||||
$directory = $fileinfo->getFilename();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($directory)) {
|
||||
\Model\Config\debug('[FIND ARCHIVE] No directory found');
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = $base_directory.DIRECTORY_SEPARATOR.$directory;
|
||||
\Model\Config\debug('[FIND ARCHIVE] '.$path);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Check if everything is setup correctly
|
||||
function check_setup()
|
||||
{
|
||||
if (! class_exists('ZipArchive')) die('To use this feature, your PHP installation must be able to uncompress zip files!');
|
||||
|
||||
if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!');
|
||||
if (AUTO_UPDATE_ARCHIVE_DIRECTORY === '') die('The constant AUTO_UPDATE_ARCHIVE_DIRECTORY is not set!');
|
||||
if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!');
|
||||
|
||||
if (! is_dir(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) @mkdir(AUTO_UPDATE_DOWNLOAD_DIRECTORY, 0755);
|
||||
if (! is_dir(AUTO_UPDATE_ARCHIVE_DIRECTORY)) @mkdir(AUTO_UPDATE_ARCHIVE_DIRECTORY, 0755);
|
||||
if (! is_dir(AUTO_UPDATE_BACKUP_DIRECTORY)) @mkdir(AUTO_UPDATE_BACKUP_DIRECTORY, 0755);
|
||||
|
||||
if (! is_writable(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) die('Update directories must be writable by your web server user!');
|
||||
if (! is_writable(__DIR__)) die('Source files must be writable by your web server user!');
|
||||
}
|
||||
|
||||
// Update the source code
|
||||
function execute($url)
|
||||
{
|
||||
check_setup();
|
||||
cleanup_directories();
|
||||
|
||||
if (uncompress_archive($url)) {
|
||||
|
||||
$update_directory = find_archive_root();
|
||||
|
||||
if ($update_directory) {
|
||||
|
||||
// Backup first
|
||||
if (synchronize(ROOT_DIRECTORY, AUTO_UPDATE_BACKUP_DIRECTORY)) {
|
||||
|
||||
// Update
|
||||
if (synchronize($update_directory, ROOT_DIRECTORY)) {
|
||||
cleanup_directories();
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// If update failed, rollback
|
||||
synchronize(AUTO_UPDATE_BACKUP_DIRECTORY, ROOT_DIRECTORY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -16,10 +16,18 @@ use SimpleValidator\Validator;
|
||||
use SimpleValidator\Validators;
|
||||
use PicoDb\Database;
|
||||
|
||||
const DB_VERSION = 23;
|
||||
const DB_VERSION = 24;
|
||||
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
|
||||
const HTTP_FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36';
|
||||
|
||||
|
||||
// Send a debug message to the console
|
||||
function debug($line)
|
||||
{
|
||||
\PicoFeed\Logging::log($line);
|
||||
write_debug();
|
||||
}
|
||||
|
||||
// Write PicoFeed debug output to a file
|
||||
function write_debug()
|
||||
{
|
||||
@ -211,7 +219,8 @@ function get_all()
|
||||
'auth_google_token',
|
||||
'auth_mozilla_token',
|
||||
'items_sorting_direction',
|
||||
'redirect_nothing_to_read'
|
||||
'redirect_nothing_to_read',
|
||||
'auto_update_url'
|
||||
)
|
||||
->findOne();
|
||||
}
|
||||
@ -219,32 +228,27 @@ function get_all()
|
||||
// Validation for edit action
|
||||
function validate_modification(array $values)
|
||||
{
|
||||
$rules = array(
|
||||
new Validators\Required('username', t('The user name is required')),
|
||||
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
|
||||
new Validators\Required('autoflush', t('Value required')),
|
||||
new Validators\Required('items_per_page', t('Value required')),
|
||||
new Validators\Integer('items_per_page', t('Must be an integer')),
|
||||
new Validators\Required('theme', t('Value required')),
|
||||
);
|
||||
|
||||
if (ENABLE_AUTO_UPDATE) {
|
||||
$rules[] = new Validators\Required('auto_update_url', t('Value required'));
|
||||
}
|
||||
|
||||
if (! empty($values['password'])) {
|
||||
|
||||
$v = new Validator($values, array(
|
||||
new Validators\Required('username', t('The user name is required')),
|
||||
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
|
||||
new Validators\Required('password', t('The password is required')),
|
||||
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
|
||||
new Validators\Required('confirmation', t('The confirmation is required')),
|
||||
new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')),
|
||||
new Validators\Required('autoflush', t('Value required')),
|
||||
new Validators\Required('items_per_page', t('Value required')),
|
||||
new Validators\Integer('items_per_page', t('Must be an integer')),
|
||||
new Validators\Required('theme', t('Value required')),
|
||||
));
|
||||
$rules[] = new Validators\Required('password', t('The password is required'));
|
||||
$rules[] = new Validators\MinLength('password', t('The minimum length is 6 characters'), 6);
|
||||
$rules[] = new Validators\Required('confirmation', t('The confirmation is required'));
|
||||
$rules[] = new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match'));
|
||||
}
|
||||
else {
|
||||
|
||||
$v = new Validator($values, array(
|
||||
new Validators\Required('username', t('The user name is required')),
|
||||
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
|
||||
new Validators\Required('autoflush', t('Value required')),
|
||||
new Validators\Required('items_per_page', t('Value required')),
|
||||
new Validators\Integer('items_per_page', t('Must be an integer')),
|
||||
new Validators\Required('theme', t('Value required')),
|
||||
));
|
||||
}
|
||||
$v = new Validator($values, $rules);
|
||||
|
||||
return array(
|
||||
$v->execute(),
|
||||
|
@ -2,6 +2,11 @@
|
||||
|
||||
namespace Schema;
|
||||
|
||||
function version_24($pdo)
|
||||
{
|
||||
$pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
|
||||
}
|
||||
|
||||
|
||||
function version_23($pdo)
|
||||
{
|
||||
|
@ -36,6 +36,11 @@
|
||||
|
||||
<?= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) ? $values['nocontent'] : false) ?><br />
|
||||
|
||||
<?php if (ENABLE_AUTO_UPDATE): ?>
|
||||
<?= Helper\form_label(t('Auto-Update URL'), 'auto_update_url') ?>
|
||||
<?= Helper\form_text('auto_update_url', $values, $errors, array('required')) ?><br/>
|
||||
<?php endif ?>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<?php if ($values['auth_google_token']): ?>
|
||||
@ -93,6 +98,9 @@
|
||||
<ul>
|
||||
<li><?= t('Miniflux version:') ?> <strong><?= APP_VERSION ?></strong></li>
|
||||
<li><?= t('Official website:') ?> <a href="http://miniflux.net" rel="noreferer" target="_blank">http://miniflux.net</a></li>
|
||||
<?php if (ENABLE_AUTO_UPDATE): ?>
|
||||
<li><a href="?action=auto-update"><?= t('Update Miniflux') ?></a> (<?= t('Don\'t forget to backup your database') ?>)</li>
|
||||
<?php endif ?>
|
||||
<li><a href="?action=console"><?= t('Console') ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user