/* * This file is part of Bitsquare. * * Bitsquare is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bitsquare 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 Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.storage; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.Utilities; import io.bitsquare.io.LookAheadObjectInputStream; import org.bitcoinj.core.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.file.Paths; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class FileManager<T> { private static final Logger log = LoggerFactory.getLogger(FileManager.class); private final File dir; private final File storageFile; private final ScheduledThreadPoolExecutor executor; private final AtomicBoolean savePending; private final long delay; private final Callable<Void> saveFileTask; private T serializable; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public FileManager(File dir, File storageFile, long delay) { this.dir = dir; this.storageFile = storageFile; executor = Utilities.getScheduledThreadPoolExecutor("FileManager", 1, 10, 5); // File must only be accessed from the auto-save executor from now on, to avoid simultaneous access. savePending = new AtomicBoolean(); this.delay = delay; saveFileTask = () -> { Thread.currentThread().setName("Save-file-task-" + new Random().nextInt(10000)); // Runs in an auto save thread. if (!savePending.getAndSet(false)) { // Some other scheduled request already beat us to it. return null; } saveNowInternal(serializable); return null; }; Runtime.getRuntime().addShutdownHook(new Thread(() -> { UserThread.execute(FileManager.this::shutDown); }, "FileManager.ShutDownHook")); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// /** * Actually write the wallet file to disk, using an atomic rename when possible. Runs on the current thread. */ public void saveNow(T serializable) { saveNowInternal(serializable); } /** * Queues up a save in the background. Useful for not very important wallet changes. */ public void saveLater(T serializable) { saveLater(serializable, delay); } public void saveLater(T serializable, long delayInMilli) { this.serializable = serializable; if (savePending.getAndSet(true)) return; // Already pending. executor.schedule(saveFileTask, delayInMilli, TimeUnit.MILLISECONDS); } public synchronized T read(File file) throws IOException, ClassNotFoundException { log.debug("read" + file); try (final FileInputStream fileInputStream = new FileInputStream(file); final ObjectInputStream objectInputStream = new LookAheadObjectInputStream(fileInputStream, false)) { return (T) objectInputStream.readObject(); } catch (Throwable t) { log.error("Exception at read: " + t.getMessage()); throw t; } } public synchronized void removeFile(String fileName) { log.debug("removeFile" + fileName); File file = new File(dir, fileName); boolean result = file.delete(); if (!result) log.warn("Could not delete file: " + file.toString()); File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString()); if (backupDir.exists()) { File backupFile = new File(Paths.get(dir.getAbsolutePath(), "backup", fileName).toString()); if (backupFile.exists()) { result = backupFile.delete(); if (!result) log.warn("Could not delete backupFile: " + file.toString()); } } } /** * Shut down auto-saving. */ void shutDown() { executor.shutdown(); try { executor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public synchronized void removeAndBackupFile(String fileName) throws IOException { File corruptedBackupDir = new File(Paths.get(dir.getAbsolutePath(), "backup_of_corrupted_data").toString()); if (!corruptedBackupDir.exists()) if (!corruptedBackupDir.mkdir()) log.warn("make dir failed"); File corruptedFile = new File(Paths.get(dir.getAbsolutePath(), "backup_of_corrupted_data", fileName).toString()); renameTempFileToFile(storageFile, corruptedFile); } public synchronized void backupFile(String fileName, int numMaxBackupFiles) throws IOException { FileUtil.rollingBackup(dir, fileName, numMaxBackupFiles); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void saveNowInternal(T serializable) { long now = System.currentTimeMillis(); saveToFile(serializable, dir, storageFile); UserThread.execute(() -> log.trace("Save {} completed in {}msec", storageFile, System.currentTimeMillis() - now)); } // TODO Sometimes we get a ConcurrentModificationException here private synchronized void saveToFile(T serializable, File dir, File storageFile) { File tempFile = null; FileOutputStream fileOutputStream = null; ObjectOutputStream objectOutputStream = null; PrintWriter printWriter = null; try { if (!dir.exists()) if (!dir.mkdir()) log.warn("make dir failed"); tempFile = File.createTempFile("temp", null, dir); tempFile.deleteOnExit(); if (serializable instanceof PlainTextWrapper) { // When we dump json files we don't want to safe it as java serialized string objects, so we use PrintWriter instead. printWriter = new PrintWriter(tempFile); printWriter.println(((PlainTextWrapper) serializable).plainText); } else { // Don't use auto closeable resources in try() as we would need too many try/catch clauses (for tempFile) // and we need to close it // manually before replacing file with temp file fileOutputStream = new FileOutputStream(tempFile); objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(serializable); // Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide // to not write through to physical media for at least a few seconds, but this is the best we can do. fileOutputStream.flush(); fileOutputStream.getFD().sync(); // Close resources before replacing file with temp file because otherwise it causes problems on windows // when rename temp file fileOutputStream.close(); objectOutputStream.close(); } renameTempFileToFile(tempFile, storageFile); } catch (Throwable t) { log.error("storageFile " + storageFile.toString()); t.printStackTrace(); log.error("Error at saveToFile: " + t.getMessage()); } finally { if (tempFile != null && tempFile.exists()) { log.warn("Temp file still exists after failed save. We will delete it now. storageFile=" + storageFile); if (!tempFile.delete()) log.error("Cannot delete temp file."); } try { if (objectOutputStream != null) objectOutputStream.close(); if (fileOutputStream != null) fileOutputStream.close(); if (printWriter != null) printWriter.close(); } catch (IOException e) { // We swallow that e.printStackTrace(); log.error("Cannot close resources." + e.getMessage()); } } } private synchronized void renameTempFileToFile(File tempFile, File file) throws IOException { if (Utils.isWindows()) { // Work around an issue on Windows whereby you can't rename over existing files. final File canonical = file.getCanonicalFile(); if (canonical.exists() && !canonical.delete()) { throw new IOException("Failed to delete canonical file for replacement with save"); } if (!tempFile.renameTo(canonical)) { throw new IOException("Failed to rename " + tempFile + " to " + canonical); } } else if (!tempFile.renameTo(file)) { throw new IOException("Failed to rename " + tempFile + " to " + file); } } }