/* * Created on Jun 22, 2012 * Created by Paul Gardner * * Copyright 2012 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.backup.impl; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TimeZone; import org.gudy.azureus2.core3.config.COConfigurationListener; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.config.ParameterListener; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.core3.logging.LogAlert; import org.gudy.azureus2.core3.logging.Logger; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AETemporaryFileHandler; import org.gudy.azureus2.core3.util.AsyncDispatcher; import org.gudy.azureus2.core3.util.BDecoder; import org.gudy.azureus2.core3.util.BEncoder; import org.gudy.azureus2.core3.util.Constants; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DisplayFormatters; import org.gudy.azureus2.core3.util.FileUtil; import org.gudy.azureus2.core3.util.SimpleTimer; import org.gudy.azureus2.core3.util.SystemProperties; import org.gudy.azureus2.core3.util.SystemTime; import org.gudy.azureus2.core3.util.TimeFormatter; import org.gudy.azureus2.core3.util.TimerEvent; import org.gudy.azureus2.core3.util.TimerEventPerformer; import org.gudy.azureus2.plugins.PluginInterface; import org.gudy.azureus2.plugins.update.UpdateInstaller; import com.aelitis.azureus.core.AzureusCore; import com.aelitis.azureus.core.AzureusCoreLifecycleAdapter; import com.aelitis.azureus.core.backup.BackupManager; public class BackupManagerImpl implements BackupManager { private static BackupManagerImpl singleton; public static synchronized BackupManager getSingleton( AzureusCore core ) { if ( singleton == null ){ singleton = new BackupManagerImpl( core ); } return( singleton ); } private AzureusCore core; private AsyncDispatcher dispatcher = new AsyncDispatcher(); private boolean first_schedule_check = true; private TimerEvent backup_event; private long last_auto_backup; private volatile boolean closing; private BackupManagerImpl( AzureusCore _core ) { core = _core; COConfigurationManager.addParameterListener( new String[]{ "br.backup.auto.enable", "br.backup.auto.everydays", "br.backup.auto.retain", }, new ParameterListener() { private COConfigurationListener save_listener; private Object lock = this; public void parameterChanged( String parameter ) { synchronized( lock ){ if ( save_listener == null ){ save_listener = new COConfigurationListener() { public void configurationSaved() { checkSchedule(); COConfigurationManager.removeListener( this ); synchronized( lock ){ if ( save_listener == this ){ save_listener = null; } } } }; COConfigurationManager.addListener( save_listener ); } } } }); checkSchedule(); core.addLifecycleListener( new AzureusCoreLifecycleAdapter() { @Override public void stopping( AzureusCore core ) { closing = true; } }); } public long getLastBackupTime() { return( COConfigurationManager.getLongParameter( "br.backup.last.time", 0 )); } public String getLastBackupError() { return( COConfigurationManager.getStringParameter( "br.backup.last.error", "" )); } private void checkSchedule() { checkSchedule( null, false ); } private void checkSchedule( final BackupListener listener, boolean force ) { boolean enabled = COConfigurationManager.getBooleanParameter( "br.backup.auto.enable" ); boolean do_backup = false; synchronized( this ){ if ( backup_event != null ){ backup_event.cancel(); backup_event = null; } if ( first_schedule_check ){ if ( !enabled ){ String last_ver = COConfigurationManager.getStringParameter( "br.backup.config.info.ver", "" ); String current_ver = Constants.AZUREUS_VERSION; if ( !last_ver.equals( current_ver )){ COConfigurationManager.setParameter( "br.backup.config.info.ver", current_ver ); Logger.log( new LogAlert( false, LogAlert.AT_INFORMATION, MessageText.getString("br.backup.setup.info"))); } } first_schedule_check = false; if ( !force ){ if ( enabled ){ backup_event = SimpleTimer.addEvent( "BM:startup", SystemTime.getCurrentTime() + 5*60*1000, new TimerEventPerformer() { public void perform( TimerEvent event ) { checkSchedule(); } }); } return; } } if ( !enabled ){ System.out.println( "Auto backup is disabled" ); if ( listener != null ){ listener.reportError( new Exception( "Auto-backup not enabled" )); } return; } long now_utc = SystemTime.getCurrentTime(); int offset = TimeZone.getDefault().getOffset( now_utc ); long now_local = now_utc + offset; long DAY = 24*60*60*1000L; long local_day_index = now_local/DAY; long last_auto_backup_day = COConfigurationManager.getLongParameter( "br.backup.auto.last_backup_day", 0 ); if ( last_auto_backup_day > local_day_index ){ last_auto_backup_day = local_day_index; } long backup_every_days = COConfigurationManager.getLongParameter( "br.backup.auto.everydays" ); backup_every_days = Math.max( 1, backup_every_days ); long utc_next_backup = ( last_auto_backup_day + backup_every_days ) * DAY; long time_to_next_backup = utc_next_backup - now_local; if ( time_to_next_backup <= 0 || force ){ if ( now_utc - last_auto_backup >= 4*60*60*1000 || force ){ do_backup = true; last_auto_backup = now_utc; COConfigurationManager.setParameter( "br.backup.auto.last_backup_day", local_day_index ); }else{ time_to_next_backup = 4*60*60*1000; } } if ( !do_backup ){ time_to_next_backup = Math.max( time_to_next_backup, 60*1000 ); System.out.println( "Scheduling next backup in " + TimeFormatter.format( time_to_next_backup/1000 )); backup_event = SimpleTimer.addEvent( "BM:auto", now_utc + time_to_next_backup, new TimerEventPerformer() { public void perform( TimerEvent event ) { checkSchedule(); } }); } } if ( do_backup ){ String backup_dir = COConfigurationManager.getStringParameter( "br.backup.auto.dir", "" ); System.out.println( "Auto backup starting: folder=" + backup_dir ); final File target_dir = new File( backup_dir ); backup( target_dir, new BackupListener() { public boolean reportProgress( String str ) { if ( listener != null ){ try{ return( listener.reportProgress( str )); }catch( Throwable e ){ Debug.out( e ); } } return( true ); } public void reportComplete() { try{ System.out.println( "Auto backup completed" ); COConfigurationManager.save(); if (COConfigurationManager.getBooleanParameter("br.backup.notify")) { Logger.log( new LogAlert( true, LogAlert.AT_INFORMATION, "Backup completed at " + new Date())); } int backup_retain = COConfigurationManager.getIntParameter( "br.backup.auto.retain" ); backup_retain = Math.max( 1, backup_retain ); File[] backups = target_dir.listFiles(); List<File> backup_dirs = new ArrayList<File>(); for ( File f: backups ){ if ( f.isDirectory() && getBackupDirTime( f ) > 0 ){ File test_file = new File( f, "azureus.config" ); if ( test_file.exists()){ backup_dirs.add( f ); } } } Collections.sort( backup_dirs, new Comparator<File>() { public int compare( File o1, File o2 ) { long t1 = getBackupDirTime( o1 ); long t2 = getBackupDirTime( o2 ); long res = t2 - t1; if ( res < 0 ){ return( -1 ); }else if ( res > 0 ){ return( 1 ); }else{ Debug.out( "hmm: " + o1 + "/" + o2 ); return( 0 ); } } }); for ( int i=backup_retain;i< backup_dirs.size();i++){ File f = backup_dirs.get( i ); System.out.println( "Deleting old backup: " + f ); FileUtil.recursiveDeleteNoCheck( f ); } }finally{ if ( listener != null ){ try{ listener.reportComplete(); }catch( Throwable e ){ Debug.out( e ); } } checkSchedule(); } } public void reportError( Throwable error ) { try{ System.out.println( "Auto backup failed" ); Logger.log( new LogAlert( true, LogAlert.AT_ERROR, "Backup failed at " + new Date(), error )); }finally{ if ( listener != null ){ try{ listener.reportError( error ); }catch( Throwable e ){ Debug.out( e ); } } checkSchedule(); } } }); }else{ if ( listener != null ){ listener.reportError( new Exception( "Backup not scheduled to run now" )); } } } public void runAutoBackup( BackupListener listener ) { checkSchedule( listener, true ); } public void backup( final File parent_folder, final BackupListener _listener ) { dispatcher.dispatch( new AERunnable() { public void runSupport() { BackupListener listener = new BackupListener() { public boolean reportProgress( String str ) { return( _listener.reportProgress(str)); } public void reportComplete() { try{ setStatus( "" ); }finally{ _listener.reportComplete(); } } public void reportError( Throwable error ) { try{ setStatus( Debug.getNestedExceptionMessage( error )); }finally{ _listener.reportError( error ); } } }; backupSupport( parent_folder, listener ); } private void setStatus( String error ) { COConfigurationManager.setParameter( "br.backup.last.time", SystemTime.getCurrentTime()); COConfigurationManager.setParameter( "br.backup.last.error", error ); } }); } private void checkClosing() throws Exception { if ( closing ){ throw( new Exception( "operation cancelled, app is closing" )); } } private long[] copyFiles( File from_file, File to_file ) throws Exception { return( copyFilesSupport( from_file, to_file, 1 )); } private long[] copyFilesSupport( File from_file, File to_file, int depth ) throws Exception { long total_files = 0; long total_copied = 0; if ( depth > 16 ){ // lazy but whatever, our config never gets this deep throw( new Exception( "Loop detected in backup path, abandoning" )); } if ( from_file.isDirectory()){ if ( !to_file.mkdirs()){ throw( new Exception( "Failed to create '" + to_file.getAbsolutePath() + "'" )); } File[] files = from_file.listFiles(); for ( File f: files ){ checkClosing(); long[] temp = copyFilesSupport( f, new File( to_file, f.getName()), depth+1 ); total_files += temp[0]; total_copied += temp[1]; } }else{ if ( !FileUtil.copyFile( from_file, to_file )){ // a few exceptions here (e.g. dasu plugin has a 'lock' file that breaks things) String name = from_file.getName().toLowerCase(); if ( name.equals( ".lock" ) || name.equals( "lock" ) || // dasu name.equals( "stats.lck" )){ // advanced stats plugin return( new long[]{ total_files, total_copied }); } throw( new Exception( "Failed to copy file '" + from_file + "'" )); } total_files++; total_copied = from_file.length(); } return( new long[]{ total_files, total_copied }); } private long getBackupDirTime( File file ) { String name = file.getName(); int pos = name.indexOf( "." ); long suffix = 0; if ( pos != -1 ){ try{ suffix = Integer.parseInt( name.substring( pos+1 )); name = name.substring( 0, pos ); }catch( Throwable e ){ return( -1 ); } } try{ return( new SimpleDateFormat( "yyyy-MM-dd" ).parse( name ).getTime() + suffix ); }catch( Throwable e ){ return( -1 ); } } private void backupSupport( File parent_folder, final BackupListener _listener ) { try{ String date_dir = new SimpleDateFormat( "yyyy-MM-dd" ).format( new Date()); File backup_folder = null; boolean ok = false; try{ checkClosing(); if ( parent_folder.getName().length() == 0 || !parent_folder.isDirectory()){ throw( new Exception( "Backup folder '" + parent_folder + "' is invalid" )); } BackupListener listener = new BackupListener() { public boolean reportProgress( String str ) { if ( !_listener.reportProgress( str )){ throw( new RuntimeException( "Operation abandoned by listener" )); } return( true ); } public void reportComplete() { _listener.reportComplete(); } public void reportError( Throwable error ) { _listener.reportError( error ); } }; for ( int i=0;i<100;i++){ String test_dir = date_dir; if ( i > 0 ){ test_dir = test_dir + "." + i; } File test_file = new File( parent_folder, test_dir ); if ( !test_file.exists()){ backup_folder = test_file; backup_folder.mkdirs(); break; } } if ( backup_folder == null ){ backup_folder = new File( parent_folder, date_dir ); } File user_dir = new File( SystemProperties.getUserPath()); File temp_dir = backup_folder; while( temp_dir != null ){ if ( temp_dir.equals( user_dir )){ throw( new Exception( "Backup folder '" + backup_folder + "' is not permitted to be within the configuration folder '" + user_dir + "'.\r\nSelect an alternative location." )); } temp_dir = temp_dir.getParentFile(); } listener.reportProgress( "Writing to " + backup_folder.getAbsolutePath()); if ( !backup_folder.exists() && !backup_folder.mkdirs()){ throw( new Exception( "Failed to create '" + backup_folder.getAbsolutePath() + "'" )); } listener.reportProgress( "Syncing current state" ); core.saveState(); try{ listener.reportProgress( "Reading configuration data from " + user_dir.getAbsolutePath()); File[] user_files = user_dir.listFiles(); for ( File f: user_files ){ checkClosing(); String name = f.getName(); if ( f.isDirectory()){ if ( name.equals( "cache" ) || name.equals( "tmp" ) || name.equals( "logs" ) || name.equals( "updates" ) || name.equals( "debug")){ continue; } }else if ( name.equals( ".lock" ) || name.equals( "update.properties" ) || name.endsWith( ".log" )){ continue; } File dest_file = new File( backup_folder, name ); listener.reportProgress( "Copying '" + name + "' ..." ); long[] result = copyFiles( f, dest_file ); String result_str = DisplayFormatters.formatByteCountToKiBEtc( result[1] ); if ( result[0] > 1 ){ result_str = result[0] + " files, " + result_str; } listener.reportProgress( result_str ); } listener.reportComplete(); ok = true; }catch( Throwable e ){ throw( e ); } }finally{ if ( !ok ){ if ( backup_folder != null ){ FileUtil.recursiveDeleteNoCheck( backup_folder ); } } } }catch( Throwable e ){ _listener.reportError( e ); } } public void restore( final File backup_folder, final BackupListener listener ) { dispatcher.dispatch( new AERunnable() { public void runSupport() { restoreSupport( backup_folder, listener ); } }); } private void addActions( UpdateInstaller installer, File source, File target ) throws Exception { if ( source.isDirectory()){ File[] files = source.listFiles(); for ( File f: files ){ addActions( installer, f, new File( target, f.getName())); } }else{ installer.addMoveAction( source.getAbsolutePath(), target.getAbsolutePath()); } } private int patch( Map<String,Object> map, String from, String to ) { int mods = 0; Iterator<Map.Entry<String,Object>> it = map.entrySet().iterator(); Map<String,Object> replacements = new HashMap<String, Object>(); while( it.hasNext()){ Map.Entry<String,Object> entry = it.next(); String key = entry.getKey(); Object value = entry.getValue(); Object new_value = value; if ( value instanceof Map ){ mods += patch((Map)value, from, to ); }else if ( value instanceof List ){ mods += patch((List)value, from, to ); }else if ( value instanceof byte[] ){ try{ String str = new String((byte[])value, "UTF-8" ); if ( str.startsWith( from )){ new_value = to + str.substring( from.length()); mods++; } }catch( Throwable e ){ } } if ( key.startsWith( from )){ // shouldn't really have file names as keys due to charset issues... String new_key = to + key.substring( from.length()); mods++; it.remove(); replacements.put( new_key, new_value ); }else{ if ( value != new_value ){ entry.setValue( new_value ); } } } map.putAll( replacements ); return( mods ); } private int patch( List list, String from, String to ) { int mods = 0; for ( int i=0;i<list.size();i++){ Object entry = list.get( i ); if ( entry instanceof Map ){ mods += patch((Map)entry, from , to ); }else if ( entry instanceof List ){ mods += patch((List)entry, from , to ); }else if ( entry instanceof byte[] ){ try{ String str = new String((byte[])entry, "UTF-8" ); if ( str.startsWith( from )){ list.set( i, to + str.substring( from.length())); mods++; } }catch( Throwable e ){ } } } return( mods ); } private void restoreSupport( File backup_folder, BackupListener listener ) { try{ UpdateInstaller installer = null; File temp_dir = null; boolean ok = false; try{ listener.reportProgress( "Reading from " + backup_folder.getAbsolutePath()); if ( !backup_folder.isDirectory()){ throw( new Exception( "Location '" + backup_folder.getAbsolutePath() + "' must be a directory" )); } listener.reportProgress( "Analysing backup" ); File config = new File( backup_folder, "azureus.config" ); if ( !config.exists()){ throw( new Exception( "Invalid backup: azureus.config not found" )); } Map config_map = BDecoder.decode( FileUtil.readFileAsByteArray( config )); byte[] temp = (byte[])config_map.get( "azureus.user.directory" ); if ( temp == null ){ throw( new Exception( "Invalid backup: azureus.config doesn't contain user directory details" )); } File current_user_dir = new File( SystemProperties.getUserPath()); File backup_user_dir = new File( new String( temp, "UTF-8" )); listener.reportProgress( "Current user directory:\t" + current_user_dir.getAbsolutePath()); listener.reportProgress( "Backup's user directory:\t" + backup_user_dir.getAbsolutePath()); temp_dir = AETemporaryFileHandler.createTempDir(); PluginInterface pi = core.getPluginManager().getDefaultPluginInterface(); installer = pi.getUpdateManager().createInstaller(); File[] files = backup_folder.listFiles(); if ( current_user_dir.equals( backup_user_dir )){ listener.reportProgress( "Directories are the same, no patching required" ); for ( File f: files ){ File source = new File( temp_dir, f.getName()); listener.reportProgress( "Creating restore action for '" + f.getName() + "'" ); copyFiles( f, source ); File target = new File( current_user_dir, f.getName()); addActions( installer, source, target ); } }else{ listener.reportProgress( "Directories are different, backup requires patching" ); for ( File f: files ){ File source = new File( temp_dir, f.getName()); listener.reportProgress( "Creating restore action for '" + f.getName() + "'" ); if ( f.isDirectory() || !f.getName().contains( ".config" )){ copyFiles( f, source ); }else{ boolean patched = false; BufferedInputStream bis = new BufferedInputStream( new FileInputStream( f ), 1024*1024 ); try{ Map m = BDecoder.decode( bis ); bis.close(); bis = null; if ( m.size() > 0 ){ int applied = patch( m, backup_user_dir.getAbsolutePath(), current_user_dir.getAbsolutePath()); if ( applied > 0 ){ listener.reportProgress( " Applied " + applied + " patches" ); patched = FileUtil.writeBytesAsFile2( source.getAbsolutePath(), BEncoder.encode( m )); if ( !patched ){ throw( new Exception( "Failed to write " + source )); } } } }finally{ if ( bis != null ){ try{ bis.close(); }catch( Throwable e ){ } } } if ( !patched ){ copyFiles( f, source ); } } File target = new File( current_user_dir, f.getName()); addActions( installer, source, target ); } } listener.reportProgress( "Restore action creation complete, restart required to complete the operation" ); listener.reportComplete(); ok = true; }finally{ if ( !ok ){ if ( installer != null ){ installer.destroy(); } if ( temp_dir != null ){ FileUtil.recursiveDeleteNoCheck( temp_dir ); } } } }catch( Throwable e ){ listener.reportError( e ); } } }