package org.exist.storage; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.EXistException; import org.exist.backup.Backup; import org.exist.util.Configuration; import org.exist.util.FileUtils; import org.exist.xmldb.XmldbURI; import org.xml.sax.SAXException; import org.xmldb.api.base.XMLDBException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; /** * BackupSystemTask creates an XML backup of the current database into a directory * or zip file. Running the backup as a system task guarantees a consistent backup. No * other transactions will be allowed while the backup is in progress. * * The following properties can be used to configure the backup task if passed to the * {@link #configure(org.exist.util.Configuration, java.util.Properties)} method: * * <table> * <tr> * <td>collection</td> * <td>the collection to backup, specified as an absolute path into the db, e.g. /db/back-me-up</td> * </tr> * <tr> * <td>user</td> * <td>a valid user for writing the backup. Usually, this needs to be a user in the dba * database admin group.</td> * </tr> * <tr> * <td>password</td> * <td>the password for the user</td> * </tr> * <tr> * <td>dir</td> * <td>the output directory where the backup will be written</td> * </tr> * <tr> * <td>prefix</td> * <td>a prefix for the generated file name. the final file name will consist of * prefix + current-dateTime + suffix</td> * </tr> * <tr> * <td>suffix</td> * <td>a suffix for the generated file name. If it ends with .zip, BackupSystemTask will * directly write the backup into a zip file. Otherwise, it will write into a plain directory.</td> * </tr> * </table> */ public class BackupSystemTask implements SystemTask { private static final Logger LOG = LogManager.getLogger(BackupSystemTask.class); private final SimpleDateFormat creationDateFormat = new SimpleDateFormat(DataBackup.DATE_FORMAT_PICTURE); private String user; private String password; private Path directory; private String suffix; private XmldbURI collection; private String prefix; // purge old zip backup files private int zipFilesMax = -1; @Override public String getName() { return "Backup Task"; } @Override public void configure(final Configuration config, final Properties properties) throws EXistException { user = properties.getProperty("user", "guest"); password = properties.getProperty("password", "guest"); String collName = properties.getProperty("collection", "xmldb:exist:///db"); if (!collName.startsWith("xmldb:exist:")) { collName = "xmldb:exist://" + collName; } collection = XmldbURI.create(collName); LOG.debug("Collection to backup: " + collection.toString() + ". User: " + user); suffix = properties.getProperty("suffix", ""); prefix = properties.getProperty("prefix", ""); final String dir = properties.getProperty("dir", "backup"); directory = Paths.get(dir); if (!directory.isAbsolute()) { directory = ((Path)config.getProperty(BrokerPool.PROPERTY_DATA_DIR)).resolve(dir); } try { Files.createDirectories(directory); } catch(final IOException ioe) { throw new EXistException("Unable to create backup directory: " + directory.toAbsolutePath().toString(), ioe); } // check for max zip files final String filesMaxStr = properties.getProperty("zip-files-max"); if (LOG.isDebugEnabled()) {LOG.debug("zip-files-max: " + filesMaxStr);} if (null != filesMaxStr) { try { zipFilesMax = Integer.parseInt(filesMaxStr); } catch (final NumberFormatException e) { LOG.debug("zip-files-max property error", e); } } } @Override public void execute(final DBBroker broker) throws EXistException { final String dateTime = creationDateFormat.format(Calendar.getInstance().getTime()); final Path dest = directory.resolve(prefix + dateTime + suffix); final Backup backup = new Backup(user, password, dest, collection); try { backup.backup(false, null); } catch (final XMLDBException | SAXException | IOException e) { LOG.debug(e.getMessage(), e); throw new EXistException(e.getMessage(), e); } // see if old zip files need to be purged if (".zip".equals(suffix) && zipFilesMax > 0) { try { purgeZipFiles(); } catch(final IOException ioe) { throw new EXistException("Unable to purge zip files", ioe); } } } public void purgeZipFiles() throws IOException { if (LOG.isDebugEnabled()) {LOG.debug("starting purgeZipFiles()");} // get all files in target directory final List<Path> files = FileUtils.list(directory); if (!files.isEmpty()) { final Map<String, Path> sorted = new TreeMap<>(); for (final Path file : files) { //check for prefix and suffix match if (file.getFileName().startsWith(prefix) && FileUtils.fileName(file).endsWith(suffix)) { sorted.put(Long.toString(Files.getLastModifiedTime(file).toMillis()), file); } } if (sorted.size() > zipFilesMax) { final Set<String> keys = sorted.keySet(); final Iterator<String> ki = keys.iterator(); int i = sorted.size() - zipFilesMax; while (ki.hasNext()) { final Path f = sorted.get(ki.next()); if (i > 0) { if (LOG.isDebugEnabled()) { LOG.debug("Purging backup : " + FileUtils.fileName(f)); } FileUtils.deleteQuietly(f); } i--; } } } } @Override public boolean afterCheckpoint() { return false; } }