/* * Copyright (c) 2014 Dennis Fischer. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0+ * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: Dennis Fischer */ package de.chaosfisch.uploader.persistence.dao; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer; import com.google.common.base.Charsets; import com.google.common.io.Files; import com.google.inject.Inject; import com.google.inject.name.Named; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; import de.chaosfisch.google.account.Account; import de.chaosfisch.google.youtube.playlist.Playlist; import de.chaosfisch.google.youtube.upload.Upload; import de.chaosfisch.uploader.template.Template; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.*; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParameterSpec; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.security.GeneralSecurityException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; class PersistenceService implements IPersistenceService { private static final Pattern STORAGE_PATTERN = Pattern.compile("data-([0-9]+)\\.data"); private static final Pattern BACKUP_PATTERN = Pattern.compile("(.*)\\.xml"); private static final int ITERATIONS = 42; private static final Logger LOGGER = LoggerFactory.getLogger(PersistenceService.class); private final IAccountDao accountDao; private final IPlaylistDao playlistDao; private final ITemplateDao templateDao; private final IUploadDao uploadDao; private final String storage; private final AtomicInteger version = new AtomicInteger(); private Data data = new Data(); private String masterPassword; private final Serializer<File> fileSerializer = new Serializer<File>() { @Override public void write(final Kryo kryo, final Output output, final File object) { output.writeString(null == object ? null : object.getAbsolutePath()); } @Override public File read(final Kryo kryo, final Input input, final Class type) { final String path = input.readString(); return null == path ? null : new File(path); } }; private final Serializer<DateTime> dateTimeSerializer = new Serializer<DateTime>() { @Override public void write(final Kryo kryo, final Output output, final DateTime object) { output.writeLong(null == object ? 0 : object.getMillis()); } @Override public DateTime read(final Kryo kryo, final Input input, final Class type) { final long milis = input.readLong(); return 0 == milis ? null : new DateTime(milis); } }; private final ThreadLocal<Kryo> kryo = new ThreadLocal<Kryo>() { @Override protected Kryo initialValue() { final Kryo kryo = new Kryo(); kryo.setDefaultSerializer(CompatibleFieldSerializer.class); kryo.register(File.class, fileSerializer); kryo.register(DateTime.class, dateTimeSerializer); return kryo; } }; //Arbitrarily selected 8-byte salt sequence: private static final byte[] salt = {(byte) 0x43, (byte) 0x76, (byte) 0x95, (byte) 0xc7, (byte) 0x5b, (byte) 0xd7, (byte) 0x45, (byte) 0x17}; @Inject public PersistenceService(final IAccountDao accountDao, final IPlaylistDao playlistDao, final ITemplateDao templateDao, final IUploadDao uploadDao, @Named(PERSISTENCE_FOLDER) final String storage) { this.accountDao = accountDao; this.playlistDao = playlistDao; this.templateDao = templateDao; this.uploadDao = uploadDao; this.storage = storage; final Timer timer = new Timer(true); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { generateBackup(); } }, TimeUnit.MINUTES.toMillis(10), TimeUnit.MINUTES.toMillis(10)); } @Override public void saveToStorage() { final Data storageData = new Data(); storageData.playlists = playlistDao.getPlaylists(); storageData.accounts = accountDao.getAccounts(); storageData.uploads = uploadDao.getUploads(); storageData.templates = templateDao.getTemplates(); final Thread thread = new Thread(new Runnable() { @Override public void run() { final File storageFile = new File(storage + String.format("/data-%07d.data", version.incrementAndGet())); try (final Output output = new Output(new FileOutputStream(storageFile))) { if (null == masterPassword) { kryo.get().writeObject(output, storageData); } else { final Cipher cipher = makeCipher(masterPassword, false); kryo.get().writeObject(output, new SealedObject(storageData, cipher)); } } catch (final Exception e) { LOGGER.error("Couldn't save data", e); storageFile.delete(); } } }, "Persistence"); thread.setDaemon(true); thread.start(); } @Override public boolean loadFromStorage() { try { data = getData(); generateBackup(); loadPlaylists(data); loadAccounts(data); loadTemplates(data); loadUploads(data); } catch (final Exception e) { LOGGER.error("Data load error", e); LOGGER.info("Trying to use latest backup"); final File backupFile = getNewestBackupFile(); if (null != backupFile) { loadBackup(backupFile); return true; } else { LOGGER.warn("Backup not existing!"); } return false; } return true; } private Data getData() throws Exception { final File storageFile = getStorageFile(); if (null == storageFile) { return new Data(); } final Data loadedData; try (final Input input = new Input(new FileInputStream(storageFile))) { if (null == masterPassword) { loadedData = kryo.get().readObject(input, Data.class); } else { final Cipher cipher = makeCipher(masterPassword, true); loadedData = (Data) kryo.get().readObject(input, SealedObject.class).getObject(cipher); } return loadedData; } } @Override public void cleanStorage() { getStorageFile(true); } private File getStorageFile() { return getStorageFile(false); } File getNewestBackupFile() { final File backupDir = new File(storage + "/backups/"); if (!backupDir.isDirectory()) { return null; } final List<File> list = Arrays.asList(backupDir.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return BACKUP_PATTERN.matcher(name).matches(); // or something else } })); if (list.isEmpty()) { return null; } Collections.sort(list, new Comparator<File>() { @Override public int compare(final File o1, final File o2) { return o1.getAbsolutePath().compareToIgnoreCase(o2.getAbsolutePath()); } }); return list.get(list.size() - 1); } File getStorageFile(final boolean cleanup) { final File storageDir = new File(storage); if (!storageDir.isDirectory()) { return null; } final List<File> list = Arrays.asList(storageDir.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return STORAGE_PATTERN.matcher(name).matches(); // or something else } })); if (list.isEmpty()) { return null; } Collections.sort(list, new Comparator<File>() { @Override public int compare(final File o1, final File o2) { return o1.getAbsolutePath().compareToIgnoreCase(o2.getAbsolutePath()); } }); final File storageFile = list.get(list.size() - 1); final Matcher matcher = STORAGE_PATTERN.matcher(storageFile.getAbsolutePath()); if (matcher.find()) { version.set(Integer.parseInt(matcher.group(1))); } if (cleanup) { for (final File file : list) { if (!file.equals(storageFile)) { file.delete(); } } } return storageFile; } @Override public boolean loadBackup(final File file) { if (!file.exists()) { LOGGER.warn("Backup file not existing: {}", file.getAbsolutePath()); return false; } try { final XStream xStream = new XStream(new DomDriver(Charsets.UTF_8.name())); final Data loadedData; if (null == masterPassword) { try (FileInputStream fileInputStream = new FileInputStream(file)) { loadedData = (Data) xStream.fromXML(fileInputStream); } } else { final Cipher cipher = makeCipher(masterPassword, true); try (FileInputStream fileInputStream = new FileInputStream(file); CipherInputStream cis = new CipherInputStream(fileInputStream, cipher)) { loadedData = (Data) xStream.fromXML(cis); } } loadPlaylists(loadedData); loadAccounts(loadedData); loadTemplates(loadedData); loadUploads(loadedData); saveToStorage(); } catch (final Exception e) { LOGGER.error("Couldn't load backup!", e); return false; } return true; } @Override public void setMasterPassword(final String masterPassword) { this.masterPassword = masterPassword; } @Override public void generateBackup() { final File backupFile = new File(storage + String.format("/backups/%s.xml", new SimpleDateFormat("dd-MM-yyyy HH-mm-ss") .format(new Date()))); try { LOGGER.info("Creating backup {}", backupFile); Files.createParentDirs(backupFile); final XStream xStream = new XStream(new DomDriver(Charsets.UTF_8.name())); if (null == masterPassword) { try (FileOutputStream fileOutputStream = new FileOutputStream(backupFile)) { xStream.toXML(data, fileOutputStream); } } else { final Cipher cipher = makeCipher(masterPassword, false); try (FileOutputStream fileOutputStream = new FileOutputStream(backupFile); CipherOutputStream cos = new CipherOutputStream(fileOutputStream, cipher)) { xStream.toXML(data, cos); } } } catch (final Exception e) { LOGGER.error("Couldn't create backup!", e); } } private Cipher makeCipher(final String pass, final boolean decryptMode) throws GeneralSecurityException { //Use a KeyFactory to derive the corresponding key from the passphrase: final PBEKeySpec keySpec = new PBEKeySpec(pass.toCharArray()); final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); final SecretKey key = keyFactory.generateSecret(keySpec); //Create parameters from the salt and an arbitrary number of iterations: final PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, ITERATIONS); //Set up the cipher: final Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES"); //Set the cipher mode to decryption or encryption: if (decryptMode) { cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec); } else { cipher.init(Cipher.ENCRYPT_MODE, key, pbeParamSpec); } return cipher; } private void loadUploads(final Data data) { final List<Upload> uploads = data.uploads; for (final Upload upload : uploads) { if (null != upload.getAccount()) { if (accountDao.getAccounts().contains(upload.getAccount())) { upload.setAccount(accountDao.getAccounts() .get(accountDao.getAccounts().indexOf(upload.getAccount()))); } else { upload.setAccount(null); } } for (final Playlist playlist : data.playlists) { if (upload.getPlaylists().contains(playlist)) { upload.getPlaylists().remove(playlist); upload.getPlaylists().add(playlist); } } } uploadDao.setUploads(uploads); } private void loadTemplates(final Data data) { final List<Template> templates = data.templates; for (final Template template : templates) { if (null != template.getAccount()) { if (accountDao.getAccounts().contains(template.getAccount())) { template.setAccount(accountDao.getAccounts() .get(accountDao.getAccounts().indexOf(template.getAccount()))); } else { template.setAccount(null); } } for (final Playlist playlist : data.playlists) { if (template.getPlaylists().contains(playlist)) { template.getPlaylists().remove(playlist); template.getPlaylists().add(playlist); } } } templateDao.setTemplates(templates); } private void loadAccounts(final Data data) { final List<Account> accounts = data.accounts; for (final Account account : accounts) { for (final Playlist playlist : playlistDao.getPlaylists()) { if (account.getPlaylists().contains(playlist)) { account.getPlaylists().remove(playlist); account.getPlaylists().add(playlist); } } } accountDao.setAccounts(accounts); } private void loadPlaylists(final Data data) { playlistDao.setPlaylists(data.playlists); } }