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}