001package co.codewizards.cloudstore.core.oio;
002
003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
004import static java.util.Objects.*;
005
006import java.io.IOException;
007import java.util.LinkedList;
008
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * @author Sebastian Schefczyk
014 */
015public class IoFileUtil {
016
017        private IoFileUtil() { }
018
019        private static final Logger logger = LoggerFactory.getLogger(IoFileUtil.class);
020
021        /**
022         * Discussion for best solution:
023         * http://stackoverflow.com/questions/617414/create-a-temporary-directory-in-java
024         */
025        public static File createTempDirectory(final String prefix) throws IOException {
026                final String checkedPrefix = (prefix == null) ? "" : prefix;
027                final java.io.File javaIoTmpDir = new java.io.File(System.getProperty("java.io.tmpdir"));
028                final String baseName = checkedPrefix + System.currentTimeMillis();
029                final int attempts = 1000;
030                for (int i = 0; i < attempts; i++) {
031                        final java.io.File tmpDir = new java.io.File(javaIoTmpDir, baseName + i);
032                        if (tmpDir.mkdir()) {
033                                return createFile(tmpDir);
034                        }
035                }
036                throw new IllegalStateException("Could not create a tmpDir in the directory '" + javaIoTmpDir + "'!");
037        }
038
039        public static File createTempFile(final String prefix, final String suffix) throws IOException {
040                return createFile(java.io.File.createTempFile (prefix, suffix));
041        }
042
043        public static File createTempFile(final String prefix, final String suffix, final File dir) throws IOException {
044                return createFile(java.io.File.createTempFile (prefix, suffix, castOrFail(dir).ioFile));
045        }
046
047        public static java.io.File getIoFile(final File file) {
048                return castOrFail(file).ioFile;
049        }
050
051        public static File[] listRoots() {
052                final java.io.File[] roots = java.io.File.listRoots();
053                requireNonNull(roots, "java.io.File.listRoots()");
054                final File[] result = new File[roots.length];
055                for (int i = 0; i < roots.length; i++)
056                        result[i] = createFile(roots[i]);
057
058                return result;
059        }
060
061        public static IoFile castOrFail(final File file) {
062                if (file instanceof IoFile)
063                        return (IoFile) file;
064                else
065                        throw new IllegalArgumentException("Could not cast file: "
066                                        + file.getClass().getCanonicalName());
067        }
068
069        static File[] convert(final java.io.File[] ioFilesListFiles) {
070                if (ioFilesListFiles == null)
071                        return null;
072                final File[] listFiles = new File[ioFilesListFiles.length];
073                for (int i = 0; i < ioFilesListFiles.length; i++) {
074                        listFiles[i] = createFile(ioFilesListFiles[i]);
075                }
076                return listFiles;
077        }
078
079        /**
080         * Deletes recursively, but uses a stack instead of recursion, so it should be
081         * safe for very large file storages.
082         * <p/>
083         * If a symlink is found, only this link will be deleted,
084         * neither the symlinked directory nor its content (due to {@link File#delete()}).
085         * <p/>
086         * Discussion of best solution: http://stackoverflow.com/a/10337535/2287604
087         *
088         * @param dir The directory to delete recursively.
089         * @return True, if the param dir does not exist at the end.
090         */
091        static boolean deleteRecursively(final File dir) {
092                final LinkedList<File> stack = new LinkedList<File>();
093                stack.addFirst(dir);
094                while (!stack.isEmpty()) {
095                        final File stackElement = stack.getFirst();
096                        final File[] currList = stackElement.listFiles();
097                        if (null != currList && currList.length > 0) {
098                                for (final File curr : currList) {
099                                        try {
100                                                final boolean delete = curr.delete();
101                                                /* Remark: delete() should succeed on empty directories, files and symlinks;
102                                                 * only if was not successful, this *directory* should be
103                                                 * pushed to the stack. So symlinked directories
104                                                 * and its contents will not be deleted.
105                                                 */
106                                                if (!delete) {
107                                                        stack.addFirst(curr);
108                                                }
109                                        } catch(final SecurityException e) {
110                                                logger.warn("Problem on delete of '{}'! ", curr, e.getMessage());
111                                        }
112                                }
113                        } else {
114                                if (stackElement != stack.removeFirst())
115                                        throw new IllegalStateException("WTF?!");
116
117                                deleteOrLog(stackElement);
118                        }
119                }
120                return !dir.exists();
121        }
122
123        private static void deleteOrLog(final File file) {
124                try {
125                        file.delete();
126                } catch(final SecurityException e) {
127                        logger.warn("Problem on delete of '{}'! ", file, e.getMessage());
128                }
129        }
130
131        /**
132         * Directories will be created/deleted, files renamed.
133         * There is no rollback, if something happens during operation.
134         * @throws IOException
135         *                      If a directory itself could not be renamed, created or deleted.
136         * @throws SecurityException Not catched. If this happens, some files will
137         *                      be moved, some not, but no data loss should have happened.
138         */
139        public static void moveRecursively(final File fromDir, final File toDir) throws IOException {
140                checkRenameDir(fromDir, toDir);
141                final boolean mkdir = toDir.mkdir();
142                if (!mkdir)
143                        throw new IOException("Could not create directory toDir, aborting move!");
144
145                final File[] listFiles = fromDir.listFiles();
146                for (final File file : listFiles) {
147                        final File newFileName = newFileNameForRenameTo(fromDir, toDir, file);
148                        if (file.isDirectory()) {
149                                newFileName.mkdir();
150                                 // remove this ; this could crash on big file collections!
151                                moveRecursively(file, newFileName);
152                        } else if (file.isFile()) {
153                                file.renameTo(newFileName);
154                        }
155                }
156                final boolean delete = fromDir.delete();
157                if (!delete)
158                        throw new IOException("Could not delete directory, which should be empty, aborting move! file=" + fromDir.getAbsolutePath());
159        }
160
161        /**
162         * @param fromDir The from-dir of a move operation.
163         * @param toDir The destination dir of a move operation.
164         * @param current The current file, to compute the new name for. Must be inside of fromDir
165         * @return On moving from /a/fromDir/ to /a/toDir/, and current file is
166         *                      /a/fromDir/b/c, it will return /a/toDir/b/c
167         * @throws IOException
168         */
169        public static File newFileNameForRenameTo(final File fromDir, final File toDir, final File current) throws IOException {
170                final String newParentDirName = toDir.getAbsolutePath() + OioFileFactory.FILE_SEPARATOR_CHAR +
171                                IoFileRelativePathUtil.getRelativePath(fromDir.getAbsolutePath(), current.getAbsolutePath(), true, OioFileFactory.FILE_SEPARATOR_CHAR);
172                return createFile(newParentDirName);
173        }
174
175        /** Before starting a moveRecursively operation, which will make use of the
176         * File.renameTo method, it is useful to determine, whether this is possible
177         * or not. The reason: On a recursive rename, you will first move the inner
178         * content, and as last the root-directory.
179         */
180        protected static void checkRenameDir(final File fromDir, final File toDir) throws IOException {
181                //first, lets check with a simple rename:
182                if (!fromDir.isDirectory())
183                        throw new IOException("fromDir must be directory, aborting move!");
184                final File f = createFile(fromDir.getParentFile(), Long.toString(System.currentTimeMillis()));
185                final boolean mkdir = f.mkdir();
186                if (!mkdir)
187                        throw new IOException("Can not create mkdir, aborting move!");
188                final boolean renameTo = f.renameTo(toDir);
189                if (!renameTo)
190                        throw new IOException("Rename would not work, aborting move!");
191                final boolean delete = toDir.delete();
192                if (!delete)
193                        throw new IOException("Could not delete, right after renaming!!!");
194        }
195
196}