2
Riepilogo post febbraio 2016
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Stamattina mi è capitato un errore insolito, durante l'importazione di un file di dump di mysql da un server verso un altro server.

Inspiegabilmente, il conteggio delle righe di una tabella sul "server sorgente" era molto più alto rispetto al conteggio delle stesse righe sulla stessa tabella, ma del server di backup.

Quindi, per tentare di capire la ragione del problema, ho creato dapprima un dump della sola tabella "incriminata" dal server sorgente, con

mysqldump -u UTENTE -pPASSWORD --skip-add-locks --compact --quick --lock-tables=false --delayed-insert=true --no-create-info --skip-triggers DATABASE TABELLA | gzip > dump.sql.gz

Una volta effettuato il dump, ho provato ad importarlo sul server di backup, con

gunzip < dump.sql.gz | mysql -u UTENTE -pPASSWORD --force DATABASE

Risultato? Per quella tabella, durante l'importazione, venivano sollevati tantissimi errori da mysql, che recitavano:

ERROR 1265 (01000) at line 683: Data truncated for column 'column_name' at row 10

La ragione del problema? La configurazione del server mysql sul server di backup, in particolare la configurazione del parametro sql-mode su my.cnf, relativa alle impostazioni di safety dell'ambiente mysql.

Ho scoperto che quelle righe nel file di dump erano relative ad un vecchio sottoinsieme di valori che nel server principale erano stati scritti senza la modalità "NO_AUTO_VALUE_ON_ZERO", ovvero la modalità secondo la quale il server mysql non inserirà dei valori NULL di default per le colonne nullabili, se non direttamente specificato.

Per ovviare al problema, quindi, abbiamo 2 soluzioni: la prima è ricostruire la tabella "sorgente" con dei dati corretti, usando il metodo che più vi è comodo. La seconda soluzione è quella di disabilitare alcune impostazioni di safety sul server di backup.

Vi basterà quindi semplicemente rimuovere il token "NO_AUTO_VALUE_ON_ZERO" dalla configurazione di my.cnf per non ricevere più errori nell'importazione.

Ovviamente la soluzione 2 va bene sul server di backup perchè è appunto un server di backup, quindi nessuno vi verrà mai a dire nulla sulla coerenza dei dati o sulla bontà delle configurazioni di safety su quel server mysql. Ad ogni modo, la soluzione più corretta sarebbe quella di correggere le righe con errori di coerenza dei dati all'interno del server di origine e mantenere le impostazioni di safety così come sono per lavorare "allo stato dell'arte".

Nella fattispecie, per correggere la coerenza dei dati, dovrete necessariamente ciclare su tutte le righe della tabella, costruirvi una lista di insert, troncare la tabella, ed eseguire le insert calcolate prima.

Traduzione Contact Form 7 in Italiano

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Stanchi di vedere il vostro modulo di Wordpress Contact Form 7 tradotto male? Nessun problema, vi viene in aiuto la traduzione in italiano per il plugin Contact Form 7, sviluppata per essere comprensibile, con correzione degli errori di validazione, e aggiornata al Febbraio 2016, ovvero all'ultima release di Contact Form 7, la 4.3.1

Pe installare questa traduzione in italiano bastano pochi minuti: è sufficiente scaricare il pacchetto zip contenente la traduzione e sovrascrivere i file della traduzione attuale nella cartella dei languages di Contact Form 7, che dovrebbe essere:

.../wp-content/plugins/contact-form-7/languages/

Con queste traduzioni aggiornate, vengono corretti gli errori sulla traduzione dei messaggi di validazione, che in questo momento erano in parte italiani ed in parte inglesi, dato dal fatto che la traduzione era stata sviluppata nel lontano 2009 ...

Sapevate che creare traduzioni di plugin di Wordpressè facile? Basta usare il tool PoEdit, aprire il file .pot contenente le "stringhe madri" del plugin, ed iniziare la traduzione da zero. Se, invece, volete correggere o aggiornare un file già localizzato, basterà aprire il file #NOME_PLUGIN#-it_IT.po e aggiornare direttamente le stringhe già tradotte in precedenza.

Eccovi il link per scaricare la traduzione in italiano di Contact Form 7 aggiornata al 2016

Se avete bisogno di una traduzione di un plugin difficile da reperire, contattatemi tramite la pagina "contattami", potrei darvi una mano!

Backup di Mysql con mysqldump senza lock sulle tabelle

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Effettuare il backup di uno o più database Mysql è un compito facilmente eseguibile con il tool mysqldump. Basta dargli in pasto qualche parametro, e il backup è pronto. Ad esempio, per effettuare il backup di DB1, DB2 e DB3:

mysqldump -u UTENTE -pPASSWORD --databases DB1 DB2 DB3 | gzip > mysql.sql.gz

Il comando creerà un archivio .gz contenente il dump in formato .sql dei vostri database. Però, c'è un problema: se volete eseguire mysqldump mentre tutti i servizi sono operativi, quindi ad esempio, su un server di produzione attualmente in uso di un sito web, lanciare il comando mysqldump implicherà un serio rallentamento, se non uno stato di blocco, di tutti i servizi che utilizzano mysql.

Per ovviare a questo problema, c'è la soluzione: bisogna lanciare mysqldump senza lock sulle tabelle.

Il comando qui di seguito vi permetterà di lanciare un dump completo di mysql senza particolari problemi su un server di produzione attualmente attivo e in uso, anche se dovesse avere un load molto elevato.

nice -15 mysqldump -u UTENTE -pPASSWORD --skip-add-locks --compact --quick --lock-tables=false --databases DB1 DB2 DB3 | gzip > mysql.sql.gz

Ho personalmente testato il tutto su un VPS Debian da 16GB di RAM, Quad Core, con doppio SSD, lanciando mysqldump su un database di circa 30GB di dati con circa 500 milioni di righe. Il risultato è stato un dump completo in 65 minuti, che non ha assolutamente comportato il blocco dei servizi attivi.

Questo perchè, oltre alle direttive --skip-add-locks e --lock-tables=false ho anche aggiunto --compact --quick ( meno output verboso sul dump ) e soprattutto ho usato nice, che è un semplice tool che modifica la priorità del processo che segue. La priorità di nice va da -20 ( urgentissimo ) a +19 ( fallo quando riesci ). Con nice -15, quindi, andiamo a dare molta meno priorità al processo di mysqldump, che verrà eseguito proprio quando non c'è null'altro da fare a livello di risorse del sistema.

Per finire, un'ultimo consiglio. I backup, in generale, *non* dovrebbero essere mantenuti e conservati sulla macchina principale, quella dalla quale abbiamo eseguito il backup. Il senso del backup è sempre lo stesso: se si rompe tutto sulla macchina A, devo possedere una macchina B che mi faccia da disaster recovery. Quindi, prendete l'abitudine di spostare i vostri backup verso altre macchine remote, meglio se siano preposte solo a contenere backup.

Il comando per inviare un file ad un server remoto, via SSH, è molto facile e potete integrarlo all'interno dei vostri script .sh preposti ai dump e backup ( leggere la nota in fondo alla guida )

sshpass -p 'PASSWORD' scp -l 3000 /file/del/backup UTENTE@SERVER_REMOTO:/directory/destinazione/

Con questo parametro, inviamo al SERVER_REMOTO il file /file/del/backup all'interno della cartella /directory/destinazione usando UTENTE, PASSWORD per l'accesso. In più, con la direttiva -l 3000 passata a scp, impostiamo un limite in kbit/s per l'invio del dump. Molto utile sui server di produzione, dove non bisogna assolutamente saturare la banda di upload.

Attenzione: il comando qui sopra deve essere utilizzato *solo* in fase di test dei sistemi di backup. A regime, bisogna *sempre* usare un fattore di autenticazione a chiave pubblica / privata nell'utilizzo dei tool via SSH come ad esempio scp. Ripeto, è assolutamente sconsigliato usare sshpass se non per testare i sistemi usando un comando veloce e mnemonico.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Le storiche funzioni mysql_ di PHP sono diventate deprecate dalla versione 5.5.0, e sono state completamente rimosse dalla nuovissima versione di PHP 7.0 che ha fatto la sua nascita alla fine del 2015.

Quindi, dato che la maggior parte del vecchio codice sorgente di quasi tutti i siti o le applicazioni web "hand-made" contengono in misura piàù o meno pesante queste chiamate a funzione mysql, bisogna trovare un modo veloce per eseguire dei "refactoring completi" e arginare il problema prima che php 7.0 diventi lo standard.

Tra l'altro, queste funzioni deprecate riempiono i log degli errori di PHP di stringhe di questo tipo:

mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead

Ricordatevi che proprio tutte le funzioni del vecchio mysql_ sono diventate deprecate, quindi mi riferisco a mysql_connect, mysql_query, mysql_select_db, mysql_fetch_assoc e via dicendo. Tutto il vecchio armamentario mysql.

Fortunatamente i bravi coder in PHP ( in questo caso, più scripter che coder ) hanno già trovato e scritto una soluzione, che è disponibile su GitHub: MysqlConverterTool su GitHub ( fork )

Una volta scaricata ( Link al Master su GitHub ) è da scompattare dove più preferite sul vostro sistema. Dispone sia di una GUI sia di una interfaccia a riga di comando per impostarne i parametri. In questa guida utilizzeremo la modalità a riga di comando, più comoda per gli smanettoni su *nix.

cd MySQLConverterTool-master
php -q cli.php

Questo comando senza parametri ci spiega come utilizzare lo strumento:

Usage of cli.php :

-f <file>         Convert file
-d <directory>    Convert directory
-p <pattern>      File name pattern for -d, e.g. -p "*.php,*.php3". Default: *
-s <code>         Convert code snippet

-u                Update (modify) input file during the conversion
-b                Backup files to [original_name].org before they get updated

-v                verbose - print conversion details
-w                warnings - print errors/warnings, if any
-q                quiet - don't print the generated code

Quindi, dopo che abbiamo compreso le istruzioni, possiamo mandare il comando

php cli.php -d /home/maurizio/public_html -p "*.php" -u -q

A questo punto, il tool manderà in output sulla riga di comando tutti gli script modificati. Adesso quindi, è meglio controllare se l'applicazione sta funzionando. Molto probabilmente il tool avrà dimenticato qualcosa, come nel mio caso, nel quale in una applicazione molto vetusta erano presenti delle chiamate alla funzione mysql_result. Per aggiustare il problema, mi è bastato testare l'applicazione, analizzare il file di log degli errori di PHP segnando le righe degli script che causavano l'eccezione sulla mysql_result, e poi andare a integrare nell'applicazione una piccola funzione che fa da compatibilità tra la vecchia mysql_result e il nuovo mysqli.

function mysqli_result($res,$row=0,$col=0){ 
	$numrows = mysqli_num_rows($res); 
	if ($numrows && $row <= ($numrows-1) && $row >=0){
		mysqli_data_seek($res,$row);
		$resrow = (is_numeric($col)) ? mysqli_fetch_row($res) : mysqli_fetch_assoc($res);
		if (isset($resrow[$col])){
			return $resrow[$col];
		}
	}
	return false;
}

A questo punto, basta modificare manualmente tutte le chiamate alla funzione mysql_result, usando appunto la mysqli_result, con gli stessi identici parametri.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Quando capita di dover fare uno spostamento completo di un sito da un server ad un altro, e non si ha accesso SSH, ma soltanto un accesso FTP, scaricare tutti i file della webroot usando un client FTP come FileZilla o WinSCP è lento e noioso, e in più la connessione può cadere durante il trasferimento, obbligando quindi di fatto a ricominciare il mirroring da zero per non avere il pericolo di tralasciare qualche file.

Quindi, se vi capita di dover effettuare un mirroring di una webroot completa, di cui avete gli accessi FTP ma non gli accessi SSH, potete comunque risolvere il problema velocemente. Basta avere sottomano una qualsiasi distribuzione Linux e operare da linea di comando. Vediamo come fare un mirror completo da bash di una webroot accessibile da FTP. I comandi riportati sono validi per Debian / Ubuntu, ma sono facilmente replicabili su qualsiasi macchina Linux.

sudo apt-get install wget
mkdir /home/nome_del_backup; cd /home/nome_del_backup
wget -r -nc -l inf ftp://username_ftp:password_ftp@nome_host

Attenzione: se lo username dovesse contenere un @ ( come ad esempio, il tanto amato e tanto odiato Aruba ) la sintassi del comando qui sopra non andrebbe bene, perchè wget non riuscirebbe ad interpretare correttamente il nome dell'host, quindi, per ovviare al problema, basta eseguire

wget -r -nc -l inf --ftp-user=user_ftp@example --ftp-password=password_ftp ftp://nome_host

Prendendo proprio ad esempio Aruba, la sintassi sarebbe:

wget -r -nc -l inf --ftp-user=utente@aruba.it --ftp-password=password_ftp ftp://ftp.nome_del_dominio.it

Ovviamente, modificare "/home/nome_del_backup" con la propria cartella desiderata di destinazione del mirror, e cambiare "username_ftp", "password_ftp" e "dominio_da_dumpare.it" con i vostri dati di accesso e il dominio da dumpare.

Se il dump dovesse interrompersi prima del tempo, o se dovesse capitare qualsiasi altro problema che implichi lo stallo del dump via WGET, l'opzione -nc permette di riavviare lo stesso identico comando dicendo a wget di non riscaricare di nuovo le risorse già acquisite nel run precedente.

Una volta completato il tutto, si può creare un archivio .tar.gz del mirror con il comando

tar -zcvf nome-archivio-backup.tar.gz /home/nome_del_backup

Anche in questo caso, cambiare il nome dell'archivio e della directory sorgente con i vostri parametri. Per il resto, avete finito il lavoro!

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

You surely have experienced a lot of pain when handling your backups coming from the various servers you have online. A very important thing to remember is, in fact, to have backups of your live web servers, so if things go crazy, you have the "last resource".

Plus, backups should be kept for at least some days, so that if you find out a web server breach or something you haven't noticed before, you can roll back the situation to the backup of N-days before.

If you're running cPanel, getting automated backups is actually pretty easy and straightforward. With cPanel you can even have a default backup retention period of N days, and retain that backups directly on the main system. But, that retention period is relative to the HDD of the main server you're running on. And, another negative fact is, if your main cPanel server that keeps your 10-days old backups fails or gets destroyed by the sys admin?

This is why, in my opinion, the best thing to do is have an external backup server where you send, via FTP, every type of backup from the webservers you are currently running.

Having an external backup server also enables you to have more than one server to be backed-up. Personally, I run a backup server that accepts backups from 3 different cPanel servers sources.

To help your backup server, and to help you setting everything up quick, I've coded a little script, that can be cron-jobbed and run via PHP CLI, that reads the backup destination directory ( the one into which you make your remote cPanel FTP send into ) and organizes the files contained into it by day, purging the backups that are too old to be retained.

If you're looking at guides on how to turn on automated cPanel backup and FTP remote send, there are plenty of guides and walkthroughs. Google is you friend.

Let's have a look at the code:

<?php
	/*
	|--------------------------------------------------------------------------
	| PHP / CGI Automated "Backup File Rotation" script
	|--------------------------------------------------------------------------
	|
	| Version: 1.1.0
	| Author: Maurizio Fonte
	| Author URL: https://www.mauriziofonte.it
	| Description:
	|     This script ( that needs to be run via CLI e.g /usr/bin/php -q this_script.php ) serves as an automated backup rotation organization.
	|     In fact, it takes all subdirectories under the main BACKUP_DIR folder and copies them to another location in which these backup files will be stored and rotated.
	|     For example, consider these directories and their contents:
	|         
	|     BACKUP_DIR = /root/backup
	|
	|             /root/backup
	|                 |-> folder_a
	|                 |       -> folder_a_backup_file_1.tar.gz
	|                 |       -> folder_a_backup_file_2.tar.gz
	|                 |-> folder_b
	|                 |       -> folder_b_backup_file_1.tar.gz
	|                 |       -> folder_b_backup_file_2.tar.gz
	|                 |       -> folder_b_backup_file_3.tar.gz
	|                 |-> folder_c
	|                 |       -> folder_c_backup_file_1.tar.gz
	|
	|    For every execution day, the script will read the contents of the /root/backup folder, and re-organize the files under the directory /back_rot/
	|    The script will create this rotation structure:
	|
	|    BACKUP_ROTATION_DIR = /back_rot/
	|
	|        /back_rot/
	|             |-> 2016-01-01
	|                 |-> folder_a
	|                 |       -> folder_a_backup_file_1.tar.gz
	|                 |       -> folder_a_backup_file_2.tar.gz
	|                 |-> folder_b
	|                 |       -> folder_b_backup_file_1.tar.gz
	|                 |       -> folder_b_backup_file_2.tar.gz
	|                 |       -> folder_b_backup_file_3.tar.gz
	|                 |-> folder_c
	|                 |       -> folder_c_backup_file_1.tar.gz
	|             |-> 2016-01-02
	|                 ...
	|                 ...
	|
	|    After a backup file / folder in the BACKUP_ROTATION_DIR becomes "too old to survive" ( after BACKUP_RETENTION_PERIOD days ), the folder will be automatically purged.
	|    
	|    Please consider that, in order for this automated backup rotator to work *properly*, these conditions have to be met:
	|        1) Each day, some other script ( from a remote server, for example ) has to fill its own directory on BACKUP_DIR ( in the example provided, "folder_a", "folder_b", "folder_c" )
	|        2) This script needs to be run once per day ( nothing harmful will happen if you run this more than once per day, though. Simply, it will see there are no actions to do ).
	|        3) This script is meant to be executed via CLI
	*/
	@ini_set('max_execution_time', 300);
	@ini_set('memory_limit', '256M');
	date_default_timezone_set ( 'Europe/Rome' );
	define ( 'HOME_DIR', rtrim ( dirname ( __FILE__ ), '/' ) . '/' );
	define ( 'LOGS_DIR', 'logs/' );
	define ( 'BACKUP_DIR', '/BACKUPS/' );
	define ( 'BACKUP_ROTATION_DIR', '/BACKUPS_ROTATION/' );
	define ( 'BACKUP_RETENTION_PERIOD', 20 );
	define ( 'EXCLUDE_BACKUP_FILENAME', 'exclude_this_backup_filename.tar.gz' );
	define ( 'EXCLUDE_BACKUP_FOLDER', 'exclude_this_directory_name_while_parsing' );
	
	ob_start ();
	out ( '#############################################################################################' );
	out ( '#############                                                                   #############' );
	out ( '#############     Automated PHP/CGI BACKUP FILE ROTATION ALGORITHM  v1.1.0      #############' );
	out ( '#############       Copyright(c) 2016 Maurizio Fonte - mauriziofonte.it         #############' );
	out ( '#############                                                                   #############' );
	out ( '#############################################################################################' );
	out ( '###############                   ' . date ( 'Y-m-d H:i:s' ) . '                         ###############' );
	out ( '#############################################################################################' );
	
	// fase 0: se non esiste la cartella BACKUP_DIR, non possiamo fare ovviamente nulla...
	if ( ! is_dir ( BACKUP_DIR ) ) closenow ( true, 'This magic script can't do anything as long BACKUP_DIR does not exists ... actual BACKUP_DIR is "' . BACKUP_DIR . '"' );
	
	// fase 1: controllo che la directory dei backup giornalieri e dei backup rotation sia leggibile e scrivibile e mi pre-carico i suoi contenuti in un semplice array di file
	out ( ' ** Starting "' . BACKUP_DIR . '" recursive directory iterator...' );
	$backup_dir_contents = Array ();
	$objects = new RecursiveIteratorIterator ( new RecursiveDirectoryIterator ( BACKUP_DIR ), RecursiveIteratorIterator::SELF_FIRST );
	if ( $objects ) {
		foreach ( $objects as $name => $object ){
			if ( $name !== '.' && $name !== '..' ) {
				$name = realpath ( $name );
				if ( ! in_array ( $name, $backup_dir_contents ) && $name !== '/' && $name !== rtrim ( BACKUP_DIR, '/' ) ) $backup_dir_contents[] = $name;
			}
		}
	}
	
	// fase 1a: controllo che abbiamo realmente qualcosa da fare dentro $backup_dir_contents e creo le directory di destinazione, se non esistono
	if ( count ( $backup_dir_contents ) == 0 ) closenow ( true, 'Nothing found on "' . BACKUP_DIR . '" that can be eligible to a copy/paste! Is that directory empty?' );
	
	if ( ! is_dir ( BACKUP_ROTATION_DIR . '_placeholder' ) ) @mkdir ( BACKUP_ROTATION_DIR . '_placeholder', 0755 );
	if ( ! is_dir ( BACKUP_ROTATION_DIR . '_placeholder' ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
	@touch ( BACKUP_ROTATION_DIR . 'test.txt' );
	if ( ! is_file ( BACKUP_ROTATION_DIR . 'test.txt' ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
	@unlink ( BACKUP_ROTATION_DIR . 'test.txt' );
	
	// fase 2: ciclo di ricognizione del file tree della directory SORGENTE dei backup
	out ( ' ** Starting "' . BACKUP_DIR . '" folder accounts+files recognition...' );
	$files_found = 0;
	$accounts_found = 0;
	$backup_tree = Array ();
	$remember_backup_account_roots = Array ();
	foreach ( $backup_dir_contents as $i => $fullpath ) {
		$stripped_path = str_replace ( BACKUP_DIR, '', $fullpath );
		$chunks = explode ( '/', $stripped_path );
		if ( count ( $chunks ) == 1 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
				continue;
			}
		}
		else if ( count ( $chunks ) == 2 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
			}
			if ( is_file ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] ) && $chunks[1] != EXCLUDE_BACKUP_FILENAME ) {
				$mtime = filemtime ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] );
				$backup_tree[$chunks[0]][bdate($mtime)][] = BACKUP_DIR . $chunks[0] . '/' . $chunks[1];
				$files_found++;
			}
			else continue;
		}
		else if ( count ( $chunks ) == 3 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
			}
			if ( is_file ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2] ) && $chunks[2] != EXCLUDE_BACKUP_FILENAME ) {
				$mtime = filemtime ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2] );
				$backup_tree[$chunks[0]][bdate($mtime)][] = BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2];
				$files_found++;
			}
			else continue;
		}
	}
	out ( ' // Done working on "' . BACKUP_DIR . '": found ' . $accounts_found . ' eligible ACCOUNTS and ' . $files_found . ' eligible FILES to be copied into backup rotation directory' );
	
	// fase 3: per ogni elemento nell'array $backup_tree controllo nella directory dei backup rotation se ho quella data e quell'account "root" salvato
	out ( ' ** Starting "' . BACKUP_ROTATION_DIR . '" smart copy...' );
	foreach ( $backup_tree as $account_name => $date_to_filename ) {
		foreach ( $date_to_filename as $date_ymd => $filenames ) {
			foreach ( $filenames as $i => $filename ) {
				// prima di tutto, se non esiste ancora, creo la directory con la data come "livello zero"
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd ) ) {
					out ( ' --> Going to create a folder: ' . BACKUP_ROTATION_DIR . $date_ymd );
					@mkdir ( BACKUP_ROTATION_DIR . $date_ymd, 0755 );
				}
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
				
				// poi, se non esiste ancora, creo la directory dell'account
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name ) ) {
					out ( ' --> Going to create a folder: ' . BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name );
					@mkdir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name, 0755 );
				}
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
				
				// in ultimo, se non esiste il file, lo copio
				if ( ! is_file ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $filename ) ) {
					out ( ' --> Going to copy a backup file: ' . $filename );
					$file_basename = pathinfo ( $filename, PATHINFO_BASENAME );
					if ( copy ( $filename, BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $file_basename ) ) {
						@unlink ( $filename );
					}
					else closenow ( true, 'Closing backup rotation now... the file copy of "' . $filename . '" to "' . BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $file_basename . '" failed with no apparent reason...' );
				}
			}
		}
	}
	
	// fase 4: creo l'albero delle cartelle/file sulla directory di rotazione backup
	out ( ' ** Starting "' . BACKUP_ROTATION_DIR . '" automated erase of backup files greater than ' . BACKUP_RETENTION_PERIOD . ' days old' );
	$backuprotation_dir_contents = Array ();
	$objects = new RecursiveIteratorIterator ( new RecursiveDirectoryIterator ( BACKUP_ROTATION_DIR ), RecursiveIteratorIterator::SELF_FIRST );
	if ( $objects ) {
		foreach ( $objects as $name => $object ){
			if ( $name !== '.' && $name !== '..' ) {
				$name = realpath ( $name );
				if ( ! in_array ( $name, $backuprotation_dir_contents ) && $name !== '/' && $name !== rtrim ( BACKUP_ROTATION_DIR, '/' ) ) $backuprotation_dir_contents[] = $name;
			}
		}
	}
	
	// fase 5: cancello tutti i file, sulla directory di backup, che sono più anziani della BACKUP_RETENTION_PERIOD ( ovvero, i backup troppo vecchi )
	$today_tstamp = strtotime ( date ( 'Y' ) . '-' . date ( 'm' ) . '-' . date ( 'd' ) . ' 00:00:00' );
	$backup_retention_deadline_tstamp = strtotime ( '-' . BACKUP_RETENTION_PERIOD . ' days', $today_tstamp );
	foreach ( $backuprotation_dir_contents as $i => $fullpath ) {
		// sicuramente il primo chunk è relativo alla data, per come è costruito l'albero di copia/incolla
		$stripped_path = str_replace ( BACKUP_ROTATION_DIR, '', $fullpath );
		$chunks = explode ( '/', $stripped_path );
		if ( count ( $chunks ) == 1 && $chunks[0] !== '_placeholder' ) {
			if ( preg_match ( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $chunks[0], $match ) ) {
				$date_of_folder_tstamp = strtotime ( $chunks[0] . ' 00:00:00' );
				if ( $date_of_folder_tstamp < $backup_retention_deadline_tstamp ) {
					out ( ' --> folder ' . $chunks[0] . ' needs to be purged, it's more than ' . BACKUP_RETENTION_PERIOD . ' days old...' );
					emptyDirectory ( BACKUP_ROTATION_DIR . $chunks[0], true );
				}
				else out ( ' --> folder ' . $chunks[0] . ' can be left on place... it's not time for its death ... for now ...' );
			}
			else closenow ( true, 'Damnit... folder ' . $fullpath . ' failed the preg_match check on the first chunk ( ' . $chunks[0] . ' )' );
		}
	}
	
	// fase 5: per ogni "$remember_backup_account_roots" devo svuotare la cartella perchè è già stata processata
	out ( ' ** Starting "' . BACKUP_DIR . '" automated erase of backup files that have been already copied in this session...' );
	foreach ( $remember_backup_account_roots as $i => $folder ) {
		emptyDirectory ( $folder );
	}
	
	// fatto!
	out ( '' );
	out ( '    yayyy!! everything done!' );
	out ( '' );
	out ( '    Copyright (c) Maurizio Fonte 2016 - https://www.mauriziofonte.it' );
	closenow ();
	
	function bdate ( $tstamp ) {
		return date ( 'Y-m-d', $tstamp );
	}
	
	function emptyDirectory ( $directory, $remove_parent = false ) {
		$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator ( $directory, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
		if ( $files ) {
			foreach ( $files as $fileinfo ) {
				$todo = ( $fileinfo -> isDir ( ) ) ? 'rmdir' : 'unlink';
				$todo ( $fileinfo -> getRealPath ( ) );
			}
			if ( $remove_parent ) rmdir ( $directory );
		}
	}
	
	function closenow ( $error = false, $error_string = null ) {
		
		if ( ! is_dir ( HOME_DIR . LOGS_DIR ) ) mkdir ( HOME_DIR . LOGS_DIR, 0755 );
		if ( ! is_dir ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/' ) ) mkdir ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/', 0755 );
		
		if ( $error && ! empty ( $error_string ) ) {
			out ( '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' );
			out ( '!! program detected an error and is going to be killed NOW !!' );
			out ( '!!' );
			out ( '!! ERROR_REASON = ' . $error_string );
		}
		
		$out = ob_get_clean ();
		file_put_contents ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/' . date ( 'Y-m-d-H-i-s' ) . '.txt', $out );
		exit ();
	}
	
	function out ( $string ) {
		echo $string . chr(10);
	}
?>

The code is pretty straightforward. If your backups fall every day into /BACKUPS/name_of_server, then you make this script "read" the contents of the /BACKUP/ directory. It will take care of moving the backups into "BACKUP_ROTATION_DIR" and will make sure that backup files that are older than "BACKUP_RETENTION_PERIOD" will get deleted at the right day.

To make it run correctly, place this script into its own directory, because it will automatically create a "logs" folder for you. If things go bad, go check the script verbose output into the right log file. Then, make it run via a new cronjob, and make sure that the user that actually runs the PHP script has the necessary permissions to read/write into the backup rotation folder.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Su Debian capita che possano succedere dei problemi con il file system ext3 o ext4, specialmente se state lavorando su un server che viene usato principalmente per lo storage di file / backup di grosse dimensioni, e specialmente se esiste più di un processo simultaneo che scrive e/o rimuove questi file di grosse dimensioni.

Quindi, per ovviare al problema, consiglio di impostare il controllo automatico del file system durante il boot con il modulo fsck ( che è l'alter-ego del vetusto chkdisk su Windows ) in modo che l'hard disk di sistema non risulti ancora montato, e in aggiunta un cron per il reboot automatico giornaliero per fare in modo che questo controllo venga effettuato giornalmente.

Ovviamente, il riavvio automatico giornaliero non è ottimale ed è da tralasciare nel caso in cui stiate facendo girare un webserver o un server mysql, in quanto non vi permetterebbe di avere abbastanza storicità nell'utilizzo di mysql per effettuare delle migliorie nelle performance ( ad esempio con mysqltuner )

Ecco come attivare il fsck automatico ( guida basata su Debian ):

sudo nano /etc/default/rcS
# be more verbose during the boot process
VERBOSE=yes
# automatically repair filesystems with inconsistencies during boot
FSCKFIX=yes

Poi, attivate il logging dei messaggi di boot per avere più controllo sulle operazioni effettuate durante il boot:

sudo apt-get install bootlogd sudo nano /etc/default/bootlogd

Nel file /etc/default/bootlogd inserite questa riga:

BOOTLOGD_ENABLE=yes

Infine, attivate l'auto-reboot giornaliero con una riga nel crontab. Consiglio di effettuare questa operazione sempre con sudo o con i permessi di root.

sudo crontab -e
# REBOOT GIORNALIERO AUTOMATICO ALLE 4 DEL MATTINO
0 4 * * * /sbin/shutdown -r now

Avete finito. Per fare una prova, date il comando

shutdown -r now

E, una volta completato l'avvio, controllate il file di log su /var/log/boot.

Buon lavoro!