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/feed.php';
|
||||||
require __DIR__.'/models/item.php';
|
require __DIR__.'/models/item.php';
|
||||||
require __DIR__.'/models/schema.php';
|
require __DIR__.'/models/schema.php';
|
||||||
|
require __DIR__.'/models/auto_update.php';
|
||||||
|
|
||||||
if (file_exists('config.php')) require 'config.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_PORT') or define('PROXY_PORT', 3128);
|
||||||
defined('PROXY_USERNAME') or define('PROXY_USERNAME', '');
|
defined('PROXY_USERNAME') or define('PROXY_USERNAME', '');
|
||||||
defined('PROXY_PASSWORD') or define('PROXY_PASSWORD', '');
|
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);
|
PicoFeed\Client::proxy(PROXY_HOSTNAME, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD);
|
||||||
|
|
||||||
|
@ -7,6 +7,22 @@ use PicoFarad\Session;
|
|||||||
use PicoFarad\Template;
|
use PicoFarad\Template;
|
||||||
use PicoDb\Database;
|
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
|
// Re-generate tokens
|
||||||
Router\get_action('generate-tokens', function() {
|
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',
|
'%d months ago' => 'Il y a %d mois',
|
||||||
'Timezone' => 'Fuseau horaire',
|
'Timezone' => 'Fuseau horaire',
|
||||||
'Update all subscriptions' => 'Mettre à jour tous les abonnements',
|
'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 SimpleValidator\Validators;
|
||||||
use PicoDb\Database;
|
use PicoDb\Database;
|
||||||
|
|
||||||
const DB_VERSION = 23;
|
const DB_VERSION = 24;
|
||||||
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
|
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';
|
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
|
// Write PicoFeed debug output to a file
|
||||||
function write_debug()
|
function write_debug()
|
||||||
{
|
{
|
||||||
@ -211,7 +219,8 @@ function get_all()
|
|||||||
'auth_google_token',
|
'auth_google_token',
|
||||||
'auth_mozilla_token',
|
'auth_mozilla_token',
|
||||||
'items_sorting_direction',
|
'items_sorting_direction',
|
||||||
'redirect_nothing_to_read'
|
'redirect_nothing_to_read',
|
||||||
|
'auto_update_url'
|
||||||
)
|
)
|
||||||
->findOne();
|
->findOne();
|
||||||
}
|
}
|
||||||
@ -219,32 +228,27 @@ function get_all()
|
|||||||
// Validation for edit action
|
// Validation for edit action
|
||||||
function validate_modification(array $values)
|
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'])) {
|
if (! empty($values['password'])) {
|
||||||
|
$rules[] = new Validators\Required('password', t('The password is required'));
|
||||||
$v = new Validator($values, array(
|
$rules[] = new Validators\MinLength('password', t('The minimum length is 6 characters'), 6);
|
||||||
new Validators\Required('username', t('The user name is required')),
|
$rules[] = new Validators\Required('confirmation', t('The confirmation is required'));
|
||||||
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
|
$rules[] = new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match'));
|
||||||
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')),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
|
|
||||||
$v = new Validator($values, array(
|
$v = new Validator($values, $rules);
|
||||||
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')),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
$v->execute(),
|
$v->execute(),
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
namespace Schema;
|
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)
|
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 />
|
<?= 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>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<?php if ($values['auth_google_token']): ?>
|
<?php if ($values['auth_google_token']): ?>
|
||||||
@ -93,6 +98,9 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><?= t('Miniflux version:') ?> <strong><?= APP_VERSION ?></strong></li>
|
<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>
|
<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>
|
<li><a href="?action=console"><?= t('Console') ?></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user