/*************************************************************************************** * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com> * * * * 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; either version 3 of the License, or (at your option) any later * * version. * * * * 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, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.anki; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.UnknownFormatConversionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ichi2.anki.db.AnkiDatabaseManager; public class BackupManager { private static Logger log = LoggerFactory.getLogger(BackupManager.class); static String mBackupDirectoryPath; static String mBrokenDirectoryPath; static int mMaxBackups; static File mLastCreatedBackup; static File[] mLastDeckBackups; public final static int RETURN_BACKUP_CREATED = 0; public final static int RETURN_ERROR = 1; public final static int RETURN_TODAY_ALREADY_BACKUP_DONE = 2; public final static int RETURN_NOT_ENOUGH_SPACE = 3; public final static int RETURN_DECK_NOT_CHANGED = 4; public final static int RETURN_DECK_RESTORED = 5; public final static int RETURN_NULL = 6; public final static int RETURN_LOW_SYSTEM_SPACE = 7; public final static int RETURN_BACKUP_NEEDED = 8; public final static int MIN_FREE_SPACE = 10; public final static String BACKUP_SUFFIX = "/backup"; public final static String BROKEN_DECKS_SUFFIX = "/broken"; private static ArrayList<String> mDeckPickerDecks; private static boolean mUseBackups = true; /** Number of day, after which a backup is done on first non-studyoptions-opening (for safety reasons) */ public static final int SAFETY_BACKUP_THRESHOLD = 3; /* Prevent class from being instantiated */ private BackupManager() { } public static void initBackup() { // FIXME #URGENT !! mUseBackups = true; // FIXME #URGENT !! mDeckPickerDecks = new ArrayList<String>(); } public static boolean isActivated() { return mUseBackups; } private static File getBackupDirectory() { // FIXME #URGENT !! int maxBackups = 3; String deckPath = ""; // FIXME #URGENT !! if (mBackupDirectoryPath == null) { mBackupDirectoryPath = deckPath + BACKUP_SUFFIX; mMaxBackups = maxBackups; } File directory = new File(mBackupDirectoryPath); if (!directory.isDirectory()) { directory.mkdirs(); } return directory; } private static File getBrokenDirectory() { // FIXME #URGENT !! String deckPath = ""; // FIXME #URGENT !! if (mBrokenDirectoryPath == null) { mBrokenDirectoryPath = deckPath + BROKEN_DECKS_SUFFIX; } File directory = new File(mBrokenDirectoryPath); if (!directory.isDirectory()) { directory.mkdirs(); } return directory; } /** If deck has not been opened for a long time, we perform a backup here because Android deleted sometimes corrupted decks */ public static boolean safetyBackupNeeded(String deckpath, int days) { if (mDeckPickerDecks == null) { initBackup(); } if (!mUseBackups || mDeckPickerDecks.contains(deckpath)) { return false; } File[] deckBackups = getDeckBackups(new File(deckpath)); int len = deckBackups.length; if (len == 0) { // no backup available return true; } String backupDateString = deckBackups[len - 1].getName().replaceAll("^.*-(\\d{4}-\\d{2}-\\d{2}).anki$", "$1"); Date backupDate; try { backupDate = new SimpleDateFormat("yyyy-MM-dd").parse(backupDateString); } catch (ParseException e) { log.error("BackupManager - safetyBackupNeeded - Error on parsing backups: " + e); return true; } Date target = Utils.genToday(Utils.utcOffset() + (days * 86400)); if (backupDate.before(target)) { return true; } else { mDeckPickerDecks.add(deckpath); return false; } } /** Restores the current deck from backup if Android deleted it */ public static void restoreDeckIfMissing(String deckpath) { if (mUseBackups && !(new File(deckpath)).exists()) { log.error("BackupManager: Deck " + deckpath + " has been deleted by Android. Restoring it:"); File[] fl = BackupManager.getDeckBackups(new File(deckpath)); if (fl.length > 0) { log.error("BackupManager: Deck " + deckpath + " successfully restored"); BackupManager.restoreDeckBackup(deckpath, fl[fl.length - 1].getAbsolutePath()); } else { log.error("BackupManager: Deck " + deckpath + " could not be restored"); } } } public static int backupDeck(String deckpath) { if (mDeckPickerDecks == null) { initBackup(); } mDeckPickerDecks.add(deckpath); mLastCreatedBackup = null; mLastDeckBackups = null; File deckFile = new File(deckpath); File[] deckBackups = getDeckBackups(deckFile); int len = deckBackups.length; if (len > 0 && deckBackups[len - 1].lastModified() == deckFile.lastModified()) { deleteDeckBackups(deckBackups, mMaxBackups); return RETURN_DECK_NOT_CHANGED; } Date value = Utils.genToday(Utils.utcOffset()); String backupFilename; try { backupFilename = String.format(Utils.ENGLISH_LOCALE, deckFile.getName().replace(".anki", "") + "-%tF.anki", value); } catch (UnknownFormatConversionException e) { log.error("backupDeck: error on creating backup filename: ", e); return RETURN_ERROR; } File backupFile = new File(getBackupDirectory().getPath(), backupFilename); if (backupFile.exists()) { log.info("No new backup of " + deckFile.getName() + " created. Already made one today"); deleteDeckBackups(deckBackups, mMaxBackups); return RETURN_TODAY_ALREADY_BACKUP_DONE; } if (getFreeDiscSpace(deckFile) < deckFile.length() + (MIN_FREE_SPACE * 1024 * 1024)) { log.error("Not enough space on sd card to backup " + deckFile.getName() + "."); return RETURN_NOT_ENOUGH_SPACE; } try { InputStream stream = new FileInputStream(deckFile); Utils.writeToFile(stream, backupFile.getAbsolutePath()); stream.close(); // set timestamp of file in order to avoid creating a new backup unless its changed backupFile.setLastModified(deckFile.lastModified()); } catch (IOException e) { log.error("Backup file " + deckFile.getName() + " - Copying of file failed.", e); return RETURN_ERROR; } mLastCreatedBackup = backupFile; mLastDeckBackups = deckBackups; return RETURN_BACKUP_CREATED; } public static long getFreeDiscSpace(String path) { return getFreeDiscSpace(new File(path)); } public static long getFreeDiscSpace(File file) { try { /* StatFs stat = new StatFs(file.getParentFile().getPath()); long blocks = stat.getAvailableBlocks(); long blocksize = stat.getBlockSize(); return blocks * blocksize; */ //FIXME: PAS SUR return file.getFreeSpace(); } catch (IllegalArgumentException e) { log.error("Free space could not be retrieved: " + e); return MIN_FREE_SPACE * 1024 * 1024; } } public static boolean cleanUpAfterBackupCreation(boolean deckLoaded) { if (deckLoaded) { return deleteDeckBackups(mLastDeckBackups, mMaxBackups - 1); } else if (mLastCreatedBackup != null) { return mLastCreatedBackup.delete(); } return false; } public static int restoreDeckBackup(String deckpath, String backupPath) { // rename old file and move it to subdirectory if ((new File(deckpath)).exists() && !moveDeckToBrokenFolder(deckpath)) { return RETURN_ERROR; } // copy backup to new position and rename it File backupFile = new File(backupPath); File deckFile = new File(deckpath); if (getFreeDiscSpace(deckFile) < deckFile.length() + (MIN_FREE_SPACE * 1024 * 1024)) { log.error("Not enough space on sd card to restore " + deckFile.getName() + "."); return RETURN_NOT_ENOUGH_SPACE; } try { InputStream stream = new FileInputStream(backupFile); Utils.writeToFile(stream, deckFile.getAbsolutePath()); stream.close(); // set timestamp of file in order to avoid creating a new backup unless its changed deckFile.setLastModified(backupFile.lastModified()); } catch (IOException e) { log.error("Restore of file " + deckFile.getName() + " failed.", e); return RETURN_ERROR; } return RETURN_DECK_RESTORED; } public static boolean repairDeck(String deckPath) { File deckFile = new File(deckPath); AnkiDatabaseManager.closeDatabase(deckPath); // repair file String execString = "sqlite3 " + deckPath + " .dump | sqlite3 " + deckPath + ".tmp"; log.info("repairDeck - Execute: " + execString); try { String[] cmd = {"/system/bin/sh", "-c", execString }; Process process = Runtime.getRuntime().exec(cmd); process.waitFor(); // move deck to broken folder String brokenDirectory = getBrokenDirectory().getPath(); Date value = Utils.genToday(Utils.utcOffset()); String movedFilename = String.format(Utils.ENGLISH_LOCALE, deckFile.getName().replace(".anki", "") + "-corrupt-%tF.anki", value); File movedFile = new File(brokenDirectory, movedFilename); int i = 1; while (movedFile.exists()) { movedFile = new File(brokenDirectory, movedFilename.replace(".anki", "-" + Integer.toString(i) + ".anki")); i++; } movedFilename = movedFile.getName(); if (!deckFile.renameTo(movedFile)) { return false; } log.info("repairDeck - moved corrupt file to " + movedFile.getAbsolutePath()); File repairedFile = new File(deckPath + ".tmp"); if (!repairedFile.renameTo(deckFile)) { return false; } return true; } catch (IOException e) { log.error("repairDeck - error: ", e); } catch (InterruptedException e) { log.error("repairDeck - error: ", e); } return false; } public static boolean moveDeckToBrokenFolder(String deckPath) { File deckFile = new File(deckPath); AnkiDatabaseManager.closeDatabase(deckPath); Date value = Utils.genToday(Utils.utcOffset()); String movedFilename = String.format(Utils.ENGLISH_LOCALE, deckFile.getName().replace(".anki", "") + "-corrupt-%tF.anki", value); File movedFile = new File(getBrokenDirectory().getPath(), movedFilename); int i = 1; while (movedFile.exists()) { movedFile = new File(getBrokenDirectory().getPath(), movedFilename.replace(".anki", "-" + Integer.toString(i) + ".anki")); i++; } movedFilename = movedFile.getName(); if (!deckFile.renameTo(movedFile)) { return false; } // move all connected files (like journals, directories...) too String deckName = deckFile.getName(); File directory = new File(deckFile.getParent()); for (File f : directory.listFiles()) { if (f.getName().startsWith(deckName)) { if (!f.renameTo(new File(getBrokenDirectory().getPath(), f.getName().replace(deckName, movedFilename)))) { return false; } } } return true; } public static File[] getDeckBackups(File deckFile) { File[] files = getBackupDirectory().listFiles(); ArrayList<File> deckBackups = new ArrayList<File>(); for (File aktFile : files){ if (aktFile.getName().replaceAll("^(.*)-\\d{4}-\\d{2}-\\d{2}.anki$", "$1.anki").equals(deckFile.getName())) { deckBackups.add(aktFile); } } Collections.sort(deckBackups); File[] fileList = new File[deckBackups.size()]; deckBackups.toArray(fileList); return fileList; } public static boolean removeDeck(File deckFile) { String deckName = deckFile.getName(); File directory = new File(deckFile.getParent()); for (File f : directory.listFiles()) { if (f.getName().startsWith(deckName)) { if (!removeDir(f)) { return false; } } } return true; } public static boolean deleteDeckBackups(String deckpath, int keepNumber) { return deleteDeckBackups(getDeckBackups(new File(deckpath)), keepNumber); } public static boolean deleteDeckBackups(File deckFile, int keepNumber) { return deleteDeckBackups(getDeckBackups(deckFile), keepNumber); } public static boolean deleteDeckBackups(File[] deckBackups, int keepNumber) { if (deckBackups == null) { return false; } for (int i = 0; i < deckBackups.length - keepNumber; i++) { deckBackups[i].delete(); } return true; } public static boolean deleteAllBackups() { return removeDir(getBackupDirectory()); } public static boolean removeDir(File dir){ if (dir.isDirectory()){ File[] files = dir.listFiles(); for (File aktFile: files){ removeDir(aktFile); } } return dir.delete(); } }