package co.codewizards.cloudstore.local.transport; import static co.codewizards.cloudstore.core.io.StreamUtil.*; import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import co.codewizards.cloudstore.core.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.dto.FileChunkDto; import co.codewizards.cloudstore.core.dto.TempChunkFileDto; import co.codewizards.cloudstore.core.dto.jaxb.TempChunkFileDtoIo; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; import co.codewizards.cloudstore.core.util.AssertUtil; import co.codewizards.cloudstore.core.util.HashUtil; import co.codewizards.cloudstore.core.util.IOUtil; public class TempChunkFileManager { private static final Logger logger = LoggerFactory.getLogger(TempChunkFileManager.class); private static final String TEMP_CHUNK_FILE_PREFIX = "chunk_"; private static final String TEMP_CHUNK_FILE_Dto_FILE_SUFFIX = ".xml"; private static final class Holder { static final TempChunkFileManager instance = createObject(TempChunkFileManager.class); } protected TempChunkFileManager() { } public static TempChunkFileManager getInstance() { return Holder.instance; } public void writeFileDataToTempChunkFile(final File destFile, final long offset, final byte[] fileData) { AssertUtil.assertNotNull(destFile, "destFile"); AssertUtil.assertNotNull(fileData, "fileData"); try { final File tempChunkFile = createTempChunkFile(destFile, offset); final File tempChunkFileDtoFile = getTempChunkFileDtoFile(tempChunkFile); // Delete the meta-data-file, in case we overwrite an older temp-chunk-file. This way it // is guaranteed, that if the meta-data-file exists, it is consistent with either // the temp-chunk-file or the chunk was already written into the final destination. deleteOrFail(tempChunkFileDtoFile); try (final OutputStream out = castStream(tempChunkFile.createOutputStream())) { out.write(fileData); } final String sha1 = sha1(fileData); logger.trace("writeFileDataToTempChunkFile: Wrote {} bytes with SHA1 '{}' to '{}'.", fileData.length, sha1, tempChunkFile.getAbsolutePath()); final TempChunkFileDto tempChunkFileDto = createTempChunkFileDto(offset, tempChunkFile, sha1); new TempChunkFileDtoIo().serialize(tempChunkFileDto, tempChunkFileDtoFile); } catch (final IOException e) { throw new RuntimeException(e); } } protected void deleteOrFail(File file) throws IOException { IOUtil.deleteOrFail(file); } public void deleteTempChunkFilesWithoutDtoFile(final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles) { for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { final File tempChunkFileDtoFile = tempChunkFileWithDtoFile.getTempChunkFileDtoFile(); if (tempChunkFileDtoFile == null || !tempChunkFileDtoFile.exists()) { final File tempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); logger.warn("deleteTempChunkFilesWithoutDtoFile: No Dto-file for temporary chunk-file '{}'! DELETING this temporary file!", tempChunkFile.getAbsolutePath()); try { deleteOrFail(tempChunkFile); } catch (IOException x) { throw new RuntimeException(x); } continue; } } } public Map<Long, TempChunkFileWithDtoFile> getOffset2TempChunkFileWithDtoFile(final File destFile) { final File[] tempFiles = getTempDir(destFile).listFiles(); if (tempFiles == null) return Collections.emptyMap(); final String destFileName = destFile.getName(); final Map<Long, TempChunkFileWithDtoFile> result = new TreeMap<Long, TempChunkFileWithDtoFile>(); for (final File tempFile : tempFiles) { String tempFileName = tempFile.getName(); if (!tempFileName.startsWith(TEMP_CHUNK_FILE_PREFIX)) continue; final boolean dtoFile; if (tempFileName.endsWith(TEMP_CHUNK_FILE_Dto_FILE_SUFFIX)) { dtoFile = true; tempFileName = tempFileName.substring(0, tempFileName.length() - TEMP_CHUNK_FILE_Dto_FILE_SUFFIX.length()); } else dtoFile = false; final int lastUnderscoreIndex = tempFileName.lastIndexOf('_'); if (lastUnderscoreIndex < 0) throw new IllegalStateException("lastUnderscoreIndex < 0 :: tempFileName='" + tempFileName + '\''); final String tempFileDestFileName = tempFileName.substring(TEMP_CHUNK_FILE_PREFIX.length(), lastUnderscoreIndex); if (!destFileName.equals(tempFileDestFileName)) continue; final String offsetStr = tempFileName.substring(lastUnderscoreIndex + 1); final Long offset = Long.valueOf(offsetStr, 36); TempChunkFileWithDtoFile tempChunkFileWithDtoFile = result.get(offset); if (tempChunkFileWithDtoFile == null) { tempChunkFileWithDtoFile = new TempChunkFileWithDtoFile(); result.put(offset, tempChunkFileWithDtoFile); } if (dtoFile) tempChunkFileWithDtoFile.setTempChunkFileDtoFile(tempFile); else tempChunkFileWithDtoFile.setTempChunkFile(tempFile); } return Collections.unmodifiableMap(result); } public File getTempChunkFileDtoFile(final File file) { return createFile(file.getParentFile(), file.getName() + TEMP_CHUNK_FILE_Dto_FILE_SUFFIX); } private String sha1(final byte[] data) { AssertUtil.assertNotNull(data, "data"); try { final byte[] hash = HashUtil.hash(HashUtil.HASH_ALGORITHM_SHA, new ByteArrayInputStream(data)); return HashUtil.encodeHexStr(hash); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (final IOException e) { throw new RuntimeException(e); } } /** * Create the temporary file for the given {@code destFile} and {@code offset}. * <p> * The returned file is created, if it does not yet exist; but it is <i>not</i> overwritten, * if it already exists. * <p> * The {@linkplain #getTempDir(File) temporary directory} in which the temporary file is located * is created, if necessary. In order to prevent collisions with code trying to delete the empty * temporary directory, this method and the corresponding {@link #deleteTempDirIfEmpty(File)} are * both synchronized. * @param destFile the destination file for which to resolve and create the temporary file. * Must not be <code>null</code>. * @param offset the offset (inside the final destination file and the source file) of the block to * be temporarily stored in the temporary file created by this method. The temporary file will hold * solely this block (thus the offset in the temporary file is 0). * @return the temporary file. Never <code>null</code>. The file is already created in the file system * (empty), if it did not yet exist. */ public synchronized File createTempChunkFile(final File destFile, final long offset) { return createTempChunkFile(destFile, offset, true); } protected synchronized File createTempChunkFile(final File destFile, final long offset, final boolean createNewFile) { final File tempDir = getTempDir(destFile); tempDir.mkdir(); if (!tempDir.isDirectory()) throw new IllegalStateException("Creating the directory failed (it does not exist after mkdir): " + tempDir.getAbsolutePath()); final File tempFile = createFile(tempDir, String.format("%s%s_%s", TEMP_CHUNK_FILE_PREFIX, destFile.getName(), Long.toString(offset, 36))); if (createNewFile) { try { tempFile.createNewFile(); } catch (final IOException e) { throw new RuntimeException(e); } } return tempFile; } /** If source file was moved, the chunks need to be moved, too. */ public void moveChunks(final File oldDestFile, final File newDestFile) { final Map<Long, TempChunkFileWithDtoFile> offset2TempChunkFileWithDtoFile = getOffset2TempChunkFileWithDtoFile(oldDestFile); for (final Map.Entry<Long, TempChunkFileWithDtoFile> entry : offset2TempChunkFileWithDtoFile.entrySet()) { final Long offset = entry.getKey(); final TempChunkFileWithDtoFile tempChunkFileWithDtoFile = entry.getValue(); final File oldTempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); final File newTempChunkFile = createTempChunkFile(newDestFile, offset, false); final File oldTempChunkFileDtoFile = getTempChunkFileDtoFile(oldTempChunkFile); final File newTempChunkFileDtoFile = getTempChunkFileDtoFile(newTempChunkFile); try { // oldTempChunkFileDtoFile.move(newTempChunkFileDtoFile); moveOrFail(oldTempChunkFileDtoFile, newTempChunkFileDtoFile); logger.info("Moved chunkDto from {} to {}", oldTempChunkFileDtoFile, newTempChunkFileDtoFile); // oldTempChunkFile.move(newTempChunkFile); moveOrFail(oldTempChunkFile, newTempChunkFile); logger.info("Moved chunk from {} to {}", oldTempChunkFile, newTempChunkFile); } catch (final IOException e) { throw new RuntimeException(e); } } } protected void moveOrFail(File oldFile, File newFile) throws IOException { oldFile.move(newFile); } /** * Deletes the {@linkplain #getTempDir(File) temporary directory} for the given {@code destFile}, * if this directory is empty. * <p> * This method is synchronized to prevent it from colliding with {@link #createTempChunkFile(File, long)} * which first creates the temporary directory and then the file in it. Without synchronisation, the * newly created directory might be deleted by this method, before the temporary file in it is created. * @param destFile the destination file for which to resolve and delete the temporary directory. * Must not be <code>null</code>. */ public synchronized void deleteTempDirIfEmpty(final File destFile) { final File tempDir = getTempDir(destFile); tempDir.delete(); // deletes only empty directories ;-) } public File getTempDir(final File destFile) { AssertUtil.assertNotNull(destFile, "destFile"); final File parentDir = destFile.getParentFile(); return createFile(parentDir, LocalRepoManager.TEMP_DIR_NAME); } /** * @param offset the offset in the (real) destination file (<i>not</i> in {@code tempChunkFile}! there the offset is always 0). * @param tempChunkFile the tempChunkFile containing the chunk's data. Must not be <code>null</code>. * @param sha1 the sha1 of the single chunk (in {@code tempChunkFile}). Must not be <code>null</code>. * @return the Dto. Never <code>null</code>. */ public TempChunkFileDto createTempChunkFileDto(final long offset, final File tempChunkFile, final String sha1) { AssertUtil.assertNotNull(tempChunkFile, "tempChunkFile"); AssertUtil.assertNotNull(sha1, "sha1"); if (!tempChunkFile.exists()) throw new IllegalArgumentException("The tempChunkFile does not exist: " + tempChunkFile.getAbsolutePath()); final FileChunkDto fileChunkDto = new FileChunkDto(); fileChunkDto.setOffset(offset); final long tempChunkFileLength = tempChunkFile.length(); if (tempChunkFileLength > Integer.MAX_VALUE) throw new IllegalStateException("tempChunkFile.length > Integer.MAX_VALUE"); fileChunkDto.setLength((int) tempChunkFileLength); fileChunkDto.setSha1(sha1); final TempChunkFileDto tempChunkFileDto = new TempChunkFileDto(); tempChunkFileDto.setFileChunkDto(fileChunkDto); return tempChunkFileDto; } public void deleteTempChunkFiles(final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles) { for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { final File tempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); // tempChunkFile may be null!!! final File tempChunkFileDtoFile = tempChunkFileWithDtoFile.getTempChunkFileDtoFile(); try { if (tempChunkFile != null && tempChunkFile.exists()) deleteOrFail(tempChunkFile); if (tempChunkFileDtoFile != null && tempChunkFileDtoFile.exists()) deleteOrFail(tempChunkFileDtoFile); } catch (IOException x) { throw new RuntimeException(x); } } } }