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