001package co.codewizards.cloudstore.local.transport; 002 003import static co.codewizards.cloudstore.core.io.StreamUtil.*; 004import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*; 005import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 006import static co.codewizards.cloudstore.core.util.HashUtil.*; 007import static java.util.Objects.*; 008 009import java.io.IOException; 010import java.io.OutputStream; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.Map; 014import java.util.TreeMap; 015 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018 019import co.codewizards.cloudstore.core.dto.FileChunkDto; 020import co.codewizards.cloudstore.core.dto.TempChunkFileDto; 021import co.codewizards.cloudstore.core.dto.jaxb.TempChunkFileDtoIo; 022import co.codewizards.cloudstore.core.oio.File; 023import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 024import co.codewizards.cloudstore.core.util.IOUtil; 025 026public class TempChunkFileManager { 027 028 private static final Logger logger = LoggerFactory.getLogger(TempChunkFileManager.class); 029 030 private static final String TEMP_CHUNK_FILE_PREFIX = "chunk_"; 031 private static final String TEMP_CHUNK_FILE_Dto_FILE_SUFFIX = ".xml"; 032 033 private static final class Holder { 034 static final TempChunkFileManager instance = createObject(TempChunkFileManager.class); 035 } 036 037 protected TempChunkFileManager() { } 038 039 public static TempChunkFileManager getInstance() { 040 return Holder.instance; 041 } 042 043 public void writeFileDataToTempChunkFile(final File destFile, final long offset, final byte[] fileData) { 044 requireNonNull(destFile, "destFile"); 045 requireNonNull(fileData, "fileData"); 046 try { 047 final File tempChunkFile = createTempChunkFile(destFile, offset); 048 final File tempChunkFileDtoFile = getTempChunkFileDtoFile(tempChunkFile); 049 050 // Delete the meta-data-file, in case we overwrite an older temp-chunk-file. This way it 051 // is guaranteed, that if the meta-data-file exists, it is consistent with either 052 // the temp-chunk-file or the chunk was already written into the final destination. 053 deleteOrFail(tempChunkFileDtoFile); 054 055 try (final OutputStream out = castStream(tempChunkFile.createOutputStream())) { 056 out.write(fileData); 057 } 058 final String sha1 = sha1(fileData); 059 logger.trace("writeFileDataToTempChunkFile: Wrote {} bytes with SHA1 '{}' to '{}'.", fileData.length, sha1, tempChunkFile.getAbsolutePath()); 060 final TempChunkFileDto tempChunkFileDto = createTempChunkFileDto(offset, tempChunkFile, sha1); 061 new TempChunkFileDtoIo().serialize(tempChunkFileDto, tempChunkFileDtoFile); 062 } catch (final IOException e) { 063 throw new RuntimeException(e); 064 } 065 } 066 067 protected void deleteOrFail(File file) throws IOException { 068 IOUtil.deleteOrFail(file); 069 } 070 071 public void deleteTempChunkFilesWithoutDtoFile(final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles) { 072 for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { 073 final File tempChunkFileDtoFile = tempChunkFileWithDtoFile.getTempChunkFileDtoFile(); 074 if (tempChunkFileDtoFile == null || !tempChunkFileDtoFile.exists()) { 075 final File tempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); 076 logger.warn("deleteTempChunkFilesWithoutDtoFile: No Dto-file for temporary chunk-file '{}'! DELETING this temporary file!", tempChunkFile.getAbsolutePath()); 077 try { 078 deleteOrFail(tempChunkFile); 079 } catch (IOException x) { 080 throw new RuntimeException(x); 081 } 082 continue; 083 } 084 } 085 } 086 087 public Map<Long, TempChunkFileWithDtoFile> getOffset2TempChunkFileWithDtoFile(final File destFile) { 088 final File[] tempFiles = getTempDir(destFile).listFiles(); 089 if (tempFiles == null) 090 return Collections.emptyMap(); 091 092 final String destFileNameHash = sha1(destFile.getName()); 093 final Map<Long, TempChunkFileWithDtoFile> result = new TreeMap<Long, TempChunkFileWithDtoFile>(); 094 for (final File tempFile : tempFiles) { 095 String tempFileName = tempFile.getName(); 096 if (!tempFileName.startsWith(TEMP_CHUNK_FILE_PREFIX)) 097 continue; 098 099 final boolean dtoFile; 100 if (tempFileName.endsWith(TEMP_CHUNK_FILE_Dto_FILE_SUFFIX)) { 101 dtoFile = true; 102 tempFileName = tempFileName.substring(0, tempFileName.length() - TEMP_CHUNK_FILE_Dto_FILE_SUFFIX.length()); 103 } 104 else 105 dtoFile = false; 106 107 final int lastUnderscoreIndex = tempFileName.lastIndexOf('_'); 108 if (lastUnderscoreIndex < 0) 109 throw new IllegalStateException("lastUnderscoreIndex < 0 :: tempFileName='" + tempFileName + '\''); 110 111 final String tempFileDestFileName = tempFileName.substring(TEMP_CHUNK_FILE_PREFIX.length(), lastUnderscoreIndex); 112 if (!destFileNameHash.equals(tempFileDestFileName)) 113 continue; 114 115 final String offsetStr = tempFileName.substring(lastUnderscoreIndex + 1); 116 final Long offset = Long.valueOf(offsetStr, 36); 117 TempChunkFileWithDtoFile tempChunkFileWithDtoFile = result.get(offset); 118 if (tempChunkFileWithDtoFile == null) { 119 tempChunkFileWithDtoFile = new TempChunkFileWithDtoFile(); 120 result.put(offset, tempChunkFileWithDtoFile); 121 } 122 if (dtoFile) 123 tempChunkFileWithDtoFile.setTempChunkFileDtoFile(tempFile); 124 else 125 tempChunkFileWithDtoFile.setTempChunkFile(tempFile); 126 } 127 return Collections.unmodifiableMap(result); 128 } 129 130 public File getTempChunkFileDtoFile(final File tempChunkFile) { 131 return createFile(tempChunkFile.getParentFile(), tempChunkFile.getName() + TEMP_CHUNK_FILE_Dto_FILE_SUFFIX); 132 } 133 134 /** 135 * Create the temporary file for the given {@code destFile} and {@code offset}. 136 * <p> 137 * The returned file is created, if it does not yet exist; but it is <i>not</i> overwritten, 138 * if it already exists. 139 * <p> 140 * The {@linkplain #getTempDir(File) temporary directory} in which the temporary file is located 141 * is created, if necessary. In order to prevent collisions with code trying to delete the empty 142 * temporary directory, this method and the corresponding {@link #deleteTempDirIfEmpty(File)} are 143 * both synchronized. 144 * @param destFile the destination file for which to resolve and create the temporary file. 145 * Must not be <code>null</code>. 146 * @param offset the offset (inside the final destination file and the source file) of the block to 147 * be temporarily stored in the temporary file created by this method. The temporary file will hold 148 * solely this block (thus the offset in the temporary file is 0). 149 * @return the temporary file. Never <code>null</code>. The file is already created in the file system 150 * (empty), if it did not yet exist. 151 */ 152 public synchronized File createTempChunkFile(final File destFile, final long offset) { 153 return createTempChunkFile(destFile, offset, true); 154 } 155 protected synchronized File createTempChunkFile(final File destFile, final long offset, final boolean createNewFile) { 156 final File tempDir = getTempDir(destFile); 157 tempDir.mkdir(); 158 if (!tempDir.isDirectory()) 159 throw new IllegalStateException("Creating the directory failed (it does not exist after mkdir): " + tempDir.getAbsolutePath()); 160 161 final String destFileNameHash = sha1(destFile.getName()); 162 163 final File tempFile = createFile(tempDir, String.format("%s%s_%s", 164 TEMP_CHUNK_FILE_PREFIX, destFileNameHash, Long.toString(offset, 36))); 165 if (createNewFile) { 166 try { 167 tempFile.createNewFile(); 168 } catch (final IOException e) { 169 throw new RuntimeException(e); 170 } 171 } 172 return tempFile; 173 } 174 175 /** If source file was moved, the chunks need to be moved, too. */ 176 public void moveChunks(final File oldDestFile, final File newDestFile) { 177 final Map<Long, TempChunkFileWithDtoFile> offset2TempChunkFileWithDtoFile = getOffset2TempChunkFileWithDtoFile(oldDestFile); 178 for (final Map.Entry<Long, TempChunkFileWithDtoFile> entry : offset2TempChunkFileWithDtoFile.entrySet()) { 179 final Long offset = entry.getKey(); 180 final TempChunkFileWithDtoFile tempChunkFileWithDtoFile = entry.getValue(); 181 final File oldTempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); 182 final File newTempChunkFile = createTempChunkFile(newDestFile, offset, false); 183 final File oldTempChunkFileDtoFile = getTempChunkFileDtoFile(oldTempChunkFile); 184 final File newTempChunkFileDtoFile = getTempChunkFileDtoFile(newTempChunkFile); 185 try { 186// oldTempChunkFileDtoFile.move(newTempChunkFileDtoFile); 187 moveOrFail(oldTempChunkFileDtoFile, newTempChunkFileDtoFile); 188 logger.info("Moved chunkDto from {} to {}", oldTempChunkFileDtoFile, newTempChunkFileDtoFile); 189// oldTempChunkFile.move(newTempChunkFile); 190 moveOrFail(oldTempChunkFile, newTempChunkFile); 191 logger.info("Moved chunk from {} to {}", oldTempChunkFile, newTempChunkFile); 192 } catch (final IOException e) { 193 throw new RuntimeException(e); 194 } 195 } 196 } 197 198 protected void moveOrFail(File oldFile, File newFile) throws IOException { 199 oldFile.move(newFile); 200 } 201 202 /** 203 * Deletes the {@linkplain #getTempDir(File) temporary directory} for the given {@code destFile}, 204 * if this directory is empty. 205 * <p> 206 * This method is synchronized to prevent it from colliding with {@link #createTempChunkFile(File, long)} 207 * which first creates the temporary directory and then the file in it. Without synchronisation, the 208 * newly created directory might be deleted by this method, before the temporary file in it is created. 209 * @param destFile the destination file for which to resolve and delete the temporary directory. 210 * Must not be <code>null</code>. 211 */ 212 public synchronized void deleteTempDirIfEmpty(final File destFile) { 213 final File tempDir = getTempDir(destFile); 214 tempDir.delete(); // deletes only empty directories ;-) 215 } 216 217 public File getTempDir(final File destFile) { 218 requireNonNull(destFile, "destFile"); 219 final File parentDir = destFile.getParentFile(); 220 return createFile(parentDir, LocalRepoManager.TEMP_DIR_NAME); 221 } 222 223 /** 224 * @param offset the offset in the (real) destination file (<i>not</i> in {@code tempChunkFile}! there the offset is always 0). 225 * @param tempChunkFile the tempChunkFile containing the chunk's data. Must not be <code>null</code>. 226 * @param sha1 the sha1 of the single chunk (in {@code tempChunkFile}). Must not be <code>null</code>. 227 * @return the Dto. Never <code>null</code>. 228 */ 229 public TempChunkFileDto createTempChunkFileDto(final long offset, final File tempChunkFile, final String sha1) { 230 requireNonNull(tempChunkFile, "tempChunkFile"); 231 requireNonNull(sha1, "sha1"); 232 233 if (!tempChunkFile.exists()) 234 throw new IllegalArgumentException("The tempChunkFile does not exist: " + tempChunkFile.getAbsolutePath()); 235 236 final FileChunkDto fileChunkDto = new FileChunkDto(); 237 fileChunkDto.setOffset(offset); 238 239 final long tempChunkFileLength = tempChunkFile.length(); 240 if (tempChunkFileLength > Integer.MAX_VALUE) 241 throw new IllegalStateException("tempChunkFile.length > Integer.MAX_VALUE"); 242 243 fileChunkDto.setLength((int) tempChunkFileLength); 244 fileChunkDto.setSha1(sha1); 245 246 final TempChunkFileDto tempChunkFileDto = new TempChunkFileDto(); 247 tempChunkFileDto.setFileChunkDto(fileChunkDto); 248 return tempChunkFileDto; 249 } 250 251 public void deleteTempChunkFiles(final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles) { 252 for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { 253 final File tempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); // tempChunkFile may be null!!! 254 final File tempChunkFileDtoFile = tempChunkFileWithDtoFile.getTempChunkFileDtoFile(); 255 256 try { 257 if (tempChunkFile != null && tempChunkFile.exists()) 258 deleteOrFail(tempChunkFile); 259 260 if (tempChunkFileDtoFile != null && tempChunkFileDtoFile.exists()) 261 deleteOrFail(tempChunkFileDtoFile); 262 } catch (IOException x) { 263 throw new RuntimeException(x); 264 } 265 } 266 } 267 268}