001package co.codewizards.cloudstore.core.oio.nio;
002
003import static co.codewizards.cloudstore.core.util.AssertUtil.*;
004
005import co.codewizards.cloudstore.core.io.ByteArrayOutputStream;
006import java.io.FileFilter;
007import java.io.FilenameFilter;
008import java.io.IOException;
009import java.net.URI;
010import java.nio.file.Files;
011import java.nio.file.LinkOption;
012import java.nio.file.NoSuchFileException;
013import java.nio.file.Path;
014import java.nio.file.Paths;
015import java.nio.file.StandardCopyOption;
016import java.nio.file.attribute.BasicFileAttributeView;
017import java.nio.file.attribute.BasicFileAttributes;
018import java.nio.file.attribute.FileTime;
019import java.text.SimpleDateFormat;
020import java.util.ArrayList;
021import java.util.Date;
022import java.util.List;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import co.codewizards.cloudstore.core.oio.File;
028import co.codewizards.cloudstore.core.oio.IoFile;
029import co.codewizards.cloudstore.core.util.childprocess.DumpStreamThread;
030
031/**
032 * File object with allowed imports to the java Java 1.7 NIO2 classes and packages.
033 *
034 * @author Sebastian Schefczyk
035 */
036public class NioFile extends IoFile implements File {
037        private static final long serialVersionUID = 1L;
038
039        private static final Logger logger = LoggerFactory.getLogger(NioFile.class);
040
041
042        protected NioFile(final String pathname) {
043                super(pathname);
044        }
045
046        protected NioFile(final File parent, final String child) {
047                super(parent, child);
048        }
049
050        protected NioFile(final String parent, final String child) {
051                super(parent, child);
052        }
053
054        protected NioFile(final URI uri) {
055                super(uri);
056        }
057
058        protected NioFile(final java.io.File ioFile) {
059                super(ioFile);
060        }
061
062
063        @Override
064        public File getParentFile() {
065                final java.io.File parentFile = this.ioFile.getParentFile();
066                return parentFile != null ? new NioFile(parentFile) : null;
067        }
068
069        @Override
070        public File[] listFiles() {
071                final java.io.File[] ioFilesListFiles = this.ioFile.listFiles();
072                return NioFileUtil.convert(ioFilesListFiles);
073        }
074
075        @Override
076        public File[] listFiles(final FileFilter fileFilter) {
077                final java.io.File[] ioFilesListFiles = this.ioFile.listFiles(fileFilter);
078                return NioFileUtil.convert(ioFilesListFiles);
079        }
080
081        @Override
082        public File[] listFiles(final FilenameFilter fileFilter) {
083                final java.io.File[] ioFilesListFiles = this.ioFile.listFiles(fileFilter);
084                return NioFileUtil.convert(ioFilesListFiles);
085        }
086
087        @Override
088        public File getAbsoluteFile() {
089                return new NioFile(ioFile.getAbsoluteFile());
090        }
091
092        @Override
093        public boolean existsNoFollow() {
094                return Files.exists(ioFile.toPath(), LinkOption.NOFOLLOW_LINKS);
095        }
096
097        @Override
098        public int compareTo(final File otherFile) {
099                return ioFile.compareTo(otherFile.getIoFile());
100        }
101
102        @Override
103        public File getCanonicalFile() throws IOException {
104                return new NioFile(ioFile.getCanonicalFile());
105        }
106
107        @Override
108        public boolean isRegularFileNoFollowLinks() {
109                return Files.isRegularFile(ioFile.toPath(), LinkOption.NOFOLLOW_LINKS);
110        }
111
112        @Override
113        public boolean isRegularFileFollowLinks() {
114                return Files.isRegularFile(ioFile.toPath());
115        }
116
117        @Override
118        public boolean isDirectoryNoFollowSymLinks() {
119                return Files.isDirectory(ioFile.toPath(), LinkOption.NOFOLLOW_LINKS);
120        }
121
122        @Override
123        public boolean isDirectoryFollowSymLinks() {
124                return Files.isDirectory(ioFile.toPath());
125        }
126
127        @Override
128        public boolean isSymbolicLink() {
129                return Files.isSymbolicLink(ioFile.toPath());
130        }
131
132        @Override
133        public String readSymbolicLinkToPathString() throws IOException {
134                final Path symlinkPath = ioFile.toPath();
135                final Path currentTargetPath = Files.readSymbolicLink(symlinkPath);
136                final String currentTarget = toPathString(currentTargetPath);
137                return currentTarget;
138        }
139
140        @Override
141        public long lastModified() {
142                if (! exists()) // https://github.com/cloudstore/cloudstore/issues/68
143                        return 0;
144
145                try {
146                        final BasicFileAttributes attributes = Files.readAttributes(
147                                        ioFile.toPath(), BasicFileAttributes.class);
148                        return attributes.lastModifiedTime().toMillis();
149                } catch (final IOException e) {
150                        // The file might have been deleted between the check above and the attempt to read the
151                        // attributes => check again and just log + exit, if it does not exist (anymore).
152                        if (! exists()) {
153                                logger.warn("lastModified: Seems, the file '{}' was deleted while we accessed it: {}", getAbsolutePath(), e);
154                                return 0;
155                        }
156                        throw new RuntimeException(e);
157                }
158        }
159
160        @Override
161        public long getLastModifiedNoFollow() {
162                if (! existsNoFollow()) // for symmetry reasons with lastModified() -- see also https://github.com/cloudstore/cloudstore/issues/68
163                        return 0;
164
165                try {
166                        final BasicFileAttributes attributes = Files.readAttributes(
167                                        ioFile.toPath(), BasicFileAttributes.class,
168                                        LinkOption.NOFOLLOW_LINKS);
169                        return attributes.lastModifiedTime().toMillis();
170                } catch (final IOException e) {
171                        // The file might have been deleted between the check above and the attempt to read the
172                        // attributes => check again and just log + exit, if it does not exist (anymore).
173                        if (! existsNoFollow()) {
174                                logger.warn("getLastModifiedNoFollow: Seems, the file '{}' was deleted while we accessed it: {}", getAbsolutePath(), e);
175                                return 0;
176                        }
177                        throw new RuntimeException(e);
178                }
179        }
180
181        private static String toPathString(final Path path) {
182                assertNotNull(path, "path");
183                return path.toString().replace(java.io.File.separatorChar, '/');
184        }
185
186        @Override
187        public boolean renameTo(final File dest) {
188                return ioFile.renameTo(dest.getIoFile());
189        }
190
191        @Override
192        public void createSymbolicLink(final String targetPath) throws IOException {
193                Files.createSymbolicLink(ioFile.toPath(), Paths.get(targetPath)).toString();
194        }
195
196        @Override
197        public void move(final File toFile) throws IOException {
198                Files.move(ioFile.toPath(), toFile.getIoFile().toPath());
199        }
200
201        @Override
202        public void copyToCopyAttributes(final File toFile) throws IOException {
203                Files.copy(ioFile.toPath(), toFile.getIoFile().toPath(), StandardCopyOption.COPY_ATTRIBUTES);
204        }
205
206        @Override
207        public boolean setLastModified(long lastModified) {
208                final FileTime lastModifiedTime = FileTime.fromMillis(lastModified);
209                try {
210                        Files.getFileAttributeView(ioFile.toPath(), BasicFileAttributeView.class).setTimes(lastModifiedTime, null, null);
211                } catch (final Exception e) {
212                        logger.error("Setting the lastModified timestamp of '"+ ioFile +"' failed with the following error: " + e, e);
213                        return false;
214                }
215                return true;
216        }
217
218        @Override
219        public boolean setLastModifiedNoFollow(final long lastModified) {
220                final Path path = ioFile.toPath().toAbsolutePath();
221                final List<Throwable> errors = new ArrayList<>();
222
223                final FileTime lastModifiedTime = FileTime.fromMillis(lastModified);
224                try {
225                        Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS)
226                        .setTimes(lastModifiedTime, null, null);
227
228                        return true;
229                } catch (final IOException e) {
230                        errors.add(e);
231                }
232
233                // It's currently impossible to modify the 'lastModified' timestamp of a symlink :-(
234                // http://stackoverflow.com/questions/17308363/symlink-lastmodifiedtime-in-java-1-7
235                // Therefore, we fall back to the touch command, if the above code failed.
236
237                final String timestamp = new SimpleDateFormat("YYYYMMddHHmm.ss").format(new Date(lastModified));
238                final ProcessBuilder processBuilder = new ProcessBuilder("touch", "-c", "-h", "-m", "-t", timestamp, path.toString());
239                processBuilder.redirectErrorStream(true);
240                try {
241                        final Process process = processBuilder.start();
242                        final ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
243                        final int processExitCode;
244                        final DumpStreamThread dumpInputStreamThread = new DumpStreamThread(process.getInputStream(), stdOut, logger);
245                        try {
246                                dumpInputStreamThread.start();
247                                processExitCode = process.waitFor();
248                        } finally {
249                                dumpInputStreamThread.flushBuffer();
250                                dumpInputStreamThread.interrupt();
251                        }
252
253                        if (processExitCode != 0) {
254                                final String stdOutString = new String(stdOut.toByteArray());
255                                throw new IOException(String.format(
256                                                "Command 'touch' failed with exitCode=%s and the following message: %s",
257                                                processExitCode, stdOutString));
258                        }
259
260                        return true;
261                } catch (IOException | InterruptedException e) {
262                        errors.add(e);
263                }
264
265                if (!errors.isEmpty()) {
266                        logger.error("Setting the lastModified timestamp of '{}' failed with the following errors:", path);
267                        for (final Throwable error : errors) {
268                                logger.error("" + error, error);
269                        }
270                }
271                return false;
272        }
273
274        @Override
275        public String relativize(final File target) {
276                return ioFile.getAbsoluteFile().toPath().relativize(target.getIoFile().getAbsoluteFile().toPath()).toString();
277        }
278
279}