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