001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.util.file;
018
019import java.io.BufferedInputStream;
020import java.io.BufferedOutputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URL;
027import java.nio.charset.StandardCharsets;
028import java.time.Instant;
029import org.apache.wicket.util.encoding.UrlDecoder;
030import org.apache.wicket.util.io.IOUtils;
031import org.apache.wicket.util.io.Streams;
032import org.apache.wicket.util.lang.Args;
033import org.apache.wicket.util.string.Strings;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * File utility methods.
039 * 
040 * @author Jonathan Locke
041 */
042public class Files
043{
044        private static final Logger logger = LoggerFactory.getLogger(Files.class);
045
046        // protocols for urls
047        private static final String URL_FILE_PREFIX = "file:";
048        private static final String URL_LOCAL_JAR_FILE_PREFIX = "jar:file:";
049        
050        // characters not allowed in filenames
051        private static final String FILENAME_FORBIDDEN_CHARACTERS = "\"*/:<>?\\|,";
052
053        /**
054         * Private constructor to prevent instantiation.
055         */
056        private Files()
057        {
058        }
059
060        /**
061         * Strips off the given extension (probably returned from Files.extension()) from the path,
062         * yielding a base pathname.
063         * 
064         * @param path
065         *            The path, possibly with an extension to strip
066         * @param extension
067         *            The extension to strip, or null if no extension exists
068         * @return The path without any extension
069         */
070        public static String basePath(final String path, final String extension)
071        {
072                if (extension != null)
073                {
074                        return path.substring(0, path.length() - extension.length() - 1);
075                }
076                return path;
077        }
078
079        /**
080         * Gets extension from path
081         * 
082         * @param path
083         *            The path
084         * @return The extension, like "bmp" or "html", or null if none can be found
085         */
086        public static String extension(final String path)
087        {
088                if (path.indexOf('.') != -1)
089                {
090                        return Strings.lastPathComponent(path, '.');
091                }
092                return null;
093        }
094
095        /**
096         * Gets filename from path
097         * 
098         * @param path
099         *            The path
100         * @return The filename
101         */
102        public static String filename(final String path)
103        {
104                return Strings.lastPathComponent(path.replace('/', java.io.File.separatorChar),
105                        java.io.File.separatorChar);
106        }
107
108        /**
109         * Deletes a normal file.
110         * <p>
111         * If the file cannot be deleted for any reason then at most 50 retries are attempted with delay
112         * of 100ms at each 10th attempt.
113         * 
114         * @param file
115         *            the file to delete
116         * @return {@code true} if file was deleted, {@code false} if the file don't exist, is a folder
117         *         or cannot be removed for some reason
118         */
119        public static boolean remove(final java.io.File file)
120        {
121                if (file != null && file.isFile())
122                {
123                        for (int j = 0; j < 5; ++j)
124                        {
125                                for (int i = 0; i < 10; ++i)
126                                {
127                                        if (file.delete())
128                                        {
129                                                return true;
130                                        }
131                                }
132                                try
133                                {
134                                        Thread.sleep(100);
135                                }
136                                catch (InterruptedException ix)
137                                {
138                                        Thread.currentThread().interrupt();
139                                }
140                        }
141                }
142
143                return false;
144        }
145
146        /**
147         * Deletes a folder by recursively removing the files and folders inside it. Delegates the work
148         * to {@link #remove(File)} for plain files.
149         * 
150         * @param folder
151         *            the folder to delete
152         * @return {@code true} if the folder is deleted successfully.
153         */
154        public static boolean removeFolder(final File folder)
155        {
156                if (folder == null)
157                {
158                        return false;
159                }
160
161                if (folder.isDirectory())
162                {
163                        File[] files = folder.listFiles();
164                        if (files != null)
165                        {
166                                for (File file : files)
167                                {
168                                        if (file.isDirectory())
169                                        {
170                                                removeFolder(file);
171                                        }
172                                        else
173                                        {
174                                                remove(file);
175                                        }
176                                }
177                        }
178                }
179
180                // delete the empty folder
181                return folder.delete();
182        }
183
184        /**
185         * Schedules a file for removal asynchronously.
186         * 
187         * @param file
188         *            the file to be removed
189         * @param fileCleaner
190         *            the file cleaner that will be used to remove the file
191         * @return {@code false} if the {@code file} is <em>null</em> or a folder, {@code true} -
192         *         otherwise (i.e. if it is scheduled)
193         */
194        public static boolean removeAsync(final File file, final IFileCleaner fileCleaner)
195        {
196                if (file == null || file.isDirectory())
197                {
198                        return false;
199                }
200
201                Args.notNull(fileCleaner, "fileCleaner");
202
203                fileCleaner.track(file, new Object());
204
205                return true;
206        }
207
208
209        /**
210         * Schedules a folder and all files inside it for asynchronous removal.
211         * 
212         * @param folder
213         *            the folder to be removed
214         * @param fileCleaner
215         *            the file cleaner that will be used to remove the file
216         * @return {@code false} if the {@code folder} is <em>null</em> or a normal file, {@code true} -
217         *         otherwise (i.e. if it is scheduled)
218         */
219        public static boolean removeFolderAsync(final File folder, final IFileCleaner fileCleaner)
220        {
221                if (folder == null || folder.isFile())
222                {
223                        return false;
224                }
225
226                Args.notNull(fileCleaner, "fileCleaner");
227
228                fileCleaner.track(folder, new Object(), new FolderDeleteStrategy());
229
230                return true;
231        }
232
233        /**
234         * Writes the given input stream to the given file
235         * 
236         * @param file
237         *            The file to write to
238         * @param input
239         *            The input
240         * @return Number of bytes written
241         * @throws IOException
242         */
243        public static int writeTo(final java.io.File file, final InputStream input)
244                throws IOException
245        {
246                return writeTo(file, input, 4096);
247        }
248
249        /**
250         * read binary file fully
251         * 
252         * @param file
253         *            file to read
254         * @return byte array representing the content of the file
255         * @throws IOException
256         *             is something went wrong
257         */
258        public static byte[] readBytes(final File file) throws IOException
259        {
260                FileInputStream stream = new FileInputStream(file);
261
262                try
263                {
264                        return IOUtils.toByteArray(stream);
265                }
266                finally
267                {
268                        stream.close();
269                }
270        }
271
272        /**
273         * Writes the given input stream to the given file
274         * 
275         * @param file
276         *            The file to write to
277         * @param input
278         *            The input
279         * @param bufSize
280         *            The memory buffer size. 4096 is a good value.
281         * @return Number of bytes written
282         * @throws IOException
283         */
284        public static int writeTo(final java.io.File file, final InputStream input,
285                final int bufSize) throws IOException
286        {
287                final FileOutputStream out = new FileOutputStream(file);
288                try
289                {
290                        return Streams.copy(input, out, bufSize);
291                }
292                finally
293                {
294                        out.close();
295                }
296        }
297
298        /**
299         * <p>
300         * Replaces commonly unsupported characters with '_'
301         * </p>
302         * 
303         * @param filename
304         *            to be cleaned
305         * @return cleaned filename
306         */
307        public static String cleanupFilename(final String filename)
308        {
309                String name = filename;
310                for (int i = 0; i < FILENAME_FORBIDDEN_CHARACTERS.length(); i++)
311                {
312                        name = name.replace(FILENAME_FORBIDDEN_CHARACTERS.charAt(i), '_');
313                }
314                return name;
315        }
316
317        /**
318         * make a copy of a file
319         * 
320         * @param sourceFile
321         *            source file that needs to be cloned
322         * @param targetFile
323         *            target file that should be a duplicate of source file
324         * @throws IOException
325         *             if something went wrong
326         */
327        public static void copy(final File sourceFile, final File targetFile) throws IOException
328        {
329                BufferedInputStream in = null;
330                BufferedOutputStream out = null;
331
332                try
333                {
334                        in = new BufferedInputStream(new FileInputStream(sourceFile));
335                        out = new BufferedOutputStream(new FileOutputStream(targetFile));
336
337                        IOUtils.copy(in, out);
338                }
339                finally
340                {
341                        try
342                        {
343                                IOUtils.close(in);
344
345                        }
346                        finally
347                        {
348                                IOUtils.close(out);
349                        }
350                }
351        }
352
353        /**
354         * for urls that point to local files (e.g. 'file:' or 'jar:file:') this methods returns a
355         * reference to the local file
356         * 
357         * @param url
358         *            url of the resource
359         * 
360         * @return reference to a local file if url contains one, <code>null</code> otherwise
361         * 
362         * @see #getLocalFileFromUrl(String)
363         */
364        public static File getLocalFileFromUrl(URL url)
365        {
366                final URL location = Args.notNull(url, "url");
367                return getLocalFileFromUrl(UrlDecoder.PATH_INSTANCE.decode(location.toExternalForm(), StandardCharsets.UTF_8));
368        }
369
370        /**
371         * for urls that point to local files (e.g. 'file:' or 'jar:file:') this methods returns a
372         * reference to the local file
373         * 
374         * @param url
375         *            url of the resource
376         * 
377         * @return reference to a local file if url contains one, <code>null</code> otherwise
378         * 
379         * @see #getLocalFileFromUrl(URL)
380         */
381        public static File getLocalFileFromUrl(String url)
382        {
383                final String location = Args.notNull(url, "url");
384
385                // check for 'file:'
386                if (location.startsWith(URL_FILE_PREFIX))
387                {
388                        return new File(location.substring(URL_FILE_PREFIX.length()));
389                }
390                // check for 'jar:file:'
391                else if (location.startsWith(URL_LOCAL_JAR_FILE_PREFIX))
392                {
393                        final String path = location.substring(URL_LOCAL_JAR_FILE_PREFIX.length());
394                        final int resourceAt = path.indexOf('!');
395
396                        // for jar:file: the '!' is mandatory
397                        if (resourceAt == -1)
398                        {
399                                return null;
400                        }
401                        return new File(path.substring(0, resourceAt));
402                }
403                else
404                {
405                        return null;
406                }
407        }
408
409        /**
410         * get last modification timestamp for file
411         * 
412         * @param file
413         * 
414         * @return timestamp
415         */
416        public static Instant getLastModified(File file)
417        {
418                // get file modification timestamp
419                long millis = file.lastModified();
420
421                // zero indicates the timestamp could not be retrieved or the file does not exist
422                if (millis == 0)
423                {
424                        return null;
425                }
426
427                // last file modification timestamp
428                return Instant.ofEpochMilli(millis);
429        }
430
431        /**
432         * Utility method for creating a directory. If the creation didn't succeed for some reason then
433         * at most 50 attempts are made with delay of 100ms at every 10th attempt.
434         * 
435         * @param folder
436         *            the folder to create
437         * @return {@code true} if the creation is successful, {@code false} - otherwise
438         */
439        public static boolean mkdirs(File folder)
440        {
441                // for some reason, simple file.mkdirs sometimes fails under heavy load
442                for (int j = 0; j < 5; ++j)
443                {
444                        for (int i = 0; i < 10; ++i)
445                        {
446                                if (folder.mkdirs())
447                                {
448                                        return true;
449                                }
450                        }
451                        try
452                        {
453                                Thread.sleep(100);
454                                if (folder.exists()) return true;
455                        }
456                        catch (InterruptedException ix)
457                        {
458                                Thread.currentThread().interrupt();
459                        }
460                }
461                logger.error("Failed to create directory: " + folder);
462                return false;
463        }
464
465        /**
466         * List all files inside the given file.
467         * 
468         * @param file directory
469         * @return files, never {@code null}
470         */
471        public static File[] list(File file) {
472                File[] files = file.listFiles();
473                if (files == null) {
474                        files = new File[0];
475                }
476                return files;
477        }
478}