/******************************************************************************* * Australian National University Data Commons * Copyright (C) 2013 The Australian National University * * This file is part of Australian National University Data Commons. * * Australian National University Data Commons 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 au.edu.anu.datacommons.storage.archive; import static java.text.MessageFormat.format; import gov.loc.repository.bagit.Manifest; import gov.loc.repository.bagit.Manifest.Algorithm; import gov.loc.repository.bagit.utilities.MessageDigestHelper; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; import java.util.Date; import java.util.Random; import java.util.concurrent.Callable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import au.edu.anu.datacommons.storage.DcStorage; /** * Task that archives a file by storing it with the same name in a directory with the name of the form: * <pre> * 2013-08-08_112436.DELETE.MD5.82b4c71afa19cea1926f9e7aa13ede32 * <-------1------->.<--2->.<3>.<-------------4----------------> * </pre> * <ol> * <li>Date and time in the format yyyy-MM-dd_HHmmss of the operation that caused the file to be archived.</li> * <li>Operation that caused the file to be archived.</li> * <li>Message Digest algorithm</li> * <li>Calculated Message digest of the file being archived.</li> * </ol> * * @author Rahul Khanna * */ public class ArchiveTask implements Callable<File> { private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveTask.class); private static final MessageFormat archivedFileFormat = new MessageFormat( "{0,date,yyyy-MM-dd_HHmmss}.{1}.{2}.{3}/{4}"); private static final Random rand = new Random(); public enum Operation { DELETE, REPLACE } private File archiveRootDir; private String pid; private File fileToArchive; private Date timeofArchival; private Manifest.Algorithm alg = null; private Operation op; /** * Constructor for Archive Task that moves the file to be archived into a temporary folder with a name of the form: * * <pre> * 2013-08-08_112436.DELETE.temp.[RANDOM_NUMBER] * </pre> * <p> * Then, when the {@code call} method gets called by the executor service, the message digest is calculated and the * directory is renamed to the full format. * * @param archiveRootDir * Root directory for all archive directories. * @param pid * Pid of the record to which the file belonged. * @param fileToArchive * File to archive * @param alg * Algorithm to use to calculate message digest of file's contents. * @param op * Operation that caused the file to be archived. * @throws IOException * when unable to move the file to the temporary directory */ public ArchiveTask(File archiveRootDir, String pid, File fileToArchive, Algorithm alg, Operation op) throws IOException { super(); this.archiveRootDir = archiveRootDir; this.pid = pid; this.op = op; this.timeofArchival = new Date(); this.fileToArchive = fileToArchive; this.fileToArchive = interimArchive(fileToArchive); this.alg = alg; } /** * Calculates the MD5 for the interim-archived file (in the constructor) and renames the directory with the actual * MD5. */ @Override public File call() throws Exception { File archivedFile = new File(getPidArchiveDir(), generateArchivedFilename()); LOGGER.debug("Archiving file {} to {}", fileToArchive.getAbsolutePath(), archivedFile.getAbsolutePath()); if (!fileToArchive.getParentFile().renameTo(archivedFile.getParentFile())) { throw new IOException(format("Unable to archive {0} to {1}", fileToArchive.getAbsolutePath(), archivedFile.getAbsolutePath())); } return archivedFile; } private File interimArchive(File fileToArchive) throws IOException { File interimArchivedFile; do { interimArchivedFile = new File(getPidArchiveDir(), generateArchivedFilename()); } while (interimArchivedFile.getParentFile().exists()); LOGGER.debug("Interim archiving file {} to {}", fileToArchive.getAbsolutePath(), interimArchivedFile.getAbsolutePath()); createIfNotExists(interimArchivedFile.getParentFile()); synchronized (fileToArchive) { if (!fileToArchive.renameTo(interimArchivedFile)) { throw new IOException(format("Unable to move file {0} to {1}", fileToArchive.getAbsolutePath(), interimArchivedFile.getAbsolutePath())); } } return interimArchivedFile; } private String generateArchivedFilename() { String generatedFilename; Object[] filenameElements = new Object[5]; filenameElements[0] = this.timeofArchival; filenameElements[1] = this.op.toString(); if (fileToArchive.isDirectory()) { filenameElements[2] = "DIR"; filenameElements[3] = "NOMD5"; } else { if (this.alg != null) { filenameElements[2] = alg.javaSecurityAlgorithm; filenameElements[3] = MessageDigestHelper.generateFixity(fileToArchive, alg); } else { filenameElements[2] = "temp"; filenameElements[3] = String.valueOf(rand.nextLong()); } } filenameElements[4] = this.fileToArchive.getName(); generatedFilename = archivedFileFormat.format(filenameElements); return generatedFilename; } private File getPidArchiveDir() throws IOException { File archivePidDir = new File(this.archiveRootDir, DcStorage.convertToDiskSafe(pid)); createIfNotExists(archivePidDir); return archivePidDir; } private void createIfNotExists(File dir) throws IOException { if (!dir.isDirectory()) { if (!dir.mkdirs()) { throw new IOException(format("Unable to create directory {0}.", dir.getAbsolutePath())); } } } }