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.commons.vfs2.impl;
018
019import java.io.File;
020import java.util.ArrayList;
021import java.util.Random;
022
023import org.apache.commons.io.FileUtils;
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.apache.commons.vfs2.FileObject;
027import org.apache.commons.vfs2.FileSelector;
028import org.apache.commons.vfs2.FileSystemException;
029import org.apache.commons.vfs2.VfsLog;
030import org.apache.commons.vfs2.provider.AbstractVfsComponent;
031import org.apache.commons.vfs2.provider.FileReplicator;
032import org.apache.commons.vfs2.provider.TemporaryFileStore;
033import org.apache.commons.vfs2.provider.UriParser;
034import org.apache.commons.vfs2.util.Messages;
035
036/**
037 * A simple file replicator and temporary file store.
038 */
039public class DefaultFileReplicator extends AbstractVfsComponent implements FileReplicator, TemporaryFileStore {
040
041    private static final Log log = LogFactory.getLog(DefaultFileReplicator.class);
042    private static final int MASK = 0xffff;
043
044    private static final Random RANDOM = new Random();
045
046    private static final char[] TMP_RESERVED_CHARS = {'?', '/', '\\', ' ', '&', '"', '\'', '*', '#', ';', ':', '<', '>', '|'};
047
048    private final ArrayList<Object> copies = new ArrayList<>();
049    private long fileCount;
050    private File tempDir;
051    private boolean tempDirMessageLogged;
052
053    /**
054     * Constructs a new instance.
055     */
056    public DefaultFileReplicator() {
057    }
058
059    /**
060     * Constructor to set the location of the temporary directory.
061     *
062     * @param tempDir The temporary directory.
063     */
064    public DefaultFileReplicator(final File tempDir) {
065        this.tempDir = tempDir;
066    }
067
068    /**
069     * Adds a file.
070     *
071     * @param file the file to add.
072     */
073    protected void addFile(final Object file) {
074        synchronized (copies) {
075            copies.add(file);
076        }
077    }
078
079    /**
080     * Allocates a new temporary file.
081     *
082     * @param baseName the base file name.
083     * @return The created File.
084     * @throws FileSystemException if an error occurs.
085     */
086    @Override
087    public File allocateFile(final String baseName) throws FileSystemException {
088        // Create a unique-ish file name
089        final String actualBaseName = createFilename(baseName);
090        synchronized (this) {
091            fileCount++;
092        }
093
094        return createAndAddFile(tempDir, actualBaseName);
095    }
096
097    /**
098     * Closes the replicator, deleting all temporary files.
099     */
100    @Override
101    public void close() {
102        // Delete the temporary files
103        synchronized (copies) {
104            while (!copies.isEmpty()) {
105                deleteFile((File) removeFile());
106            }
107        }
108
109        // Clean up the temp directory, if it is empty
110        if (tempDir != null && tempDir.exists()) {
111            final String[] list = tempDir.list();
112            if (list != null && list.length == 0) {
113                tempDir.delete();
114                tempDir = null;
115            }
116        }
117    }
118
119    /**
120     * Adds a file.
121     *
122     * @param parent ignored.
123     * @param baseName the base file name.
124     * @return a File.
125     * @throws FileSystemException if a file system error occurs.
126     */
127    protected File createAndAddFile(final File parent, final String baseName) throws FileSystemException {
128        final File file = createFile(tempDir, baseName);
129        // Keep track to delete later
130        addFile(file);
131        return file;
132    }
133
134    /**
135     * Create the temporary file.
136     *
137     * @param parent The file to use as the parent of the file being created.
138     * @param name The name of the file to create.
139     * @return The File that was created.
140     * @throws FileSystemException if an error occurs creating the file.
141     */
142    protected File createFile(final File parent, final String name) throws FileSystemException {
143        return new File(parent, UriParser.decode(name));
144    }
145
146    /**
147     * Create the temporary file name.
148     *
149     * @param baseName The base to prepend to the file name being created.
150     * @return the name of the File.
151     */
152    protected String createFilename(final String baseName) {
153        // BUG29007
154        // return baseName + "_" + getFilecount() + ".tmp";
155
156        // imario@apache.org: BUG34976 get rid of maybe reserved and dangerous characters
157        // e.g. to allow replication of http://hostname.org/fileservlet?file=abc.txt
158        final String safeBaseName = UriParser.encode(baseName, TMP_RESERVED_CHARS).replace('%', '_');
159        return "tmp_" + getFilecount() + "_" + safeBaseName;
160    }
161
162    /**
163     * Physically deletes the file from the file system.
164     *
165     * @param file The File to delete.
166     */
167    protected void deleteFile(final File file) {
168        try {
169            final FileObject fileObject = getContext().toFileObject(file);
170            fileObject.deleteAll();
171        } catch (final FileSystemException e) {
172            final String message = Messages.getString("vfs.impl/delete-temp.warn", file.getName());
173            VfsLog.warn(getLogger(), log, message, e);
174        }
175    }
176
177    /**
178     * Gets the file count.
179     *
180     * @return the file count.
181     */
182    protected long getFilecount() {
183        return fileCount;
184    }
185
186    /**
187     * Initializes this component.
188     *
189     * @throws FileSystemException if an error occurs.
190     */
191    @Override
192    public void init() throws FileSystemException {
193        if (tempDir == null) {
194            tempDir = new File(FileUtils.getTempDirectoryPath(), "vfs_cache").getAbsoluteFile();
195        }
196
197        fileCount = RANDOM.nextInt() & MASK;
198
199        if (!tempDirMessageLogged) {
200            final String message = Messages.getString("vfs.impl/temp-dir.debug", tempDir);
201            VfsLog.debug(getLogger(), log, message);
202
203            tempDirMessageLogged = true;
204        }
205    }
206
207    /**
208     * Removes a file from the copies list. Will be used for cleanup.
209     * <p>
210     * Notice: The system awaits that the returning object can be cast to a {@link File}.
211     *
212     * @return the File that was removed.
213     */
214    protected Object removeFile() {
215        synchronized (copies) {
216            return copies.remove(0);
217        }
218    }
219
220    /**
221     * Removes an instance from the list of copies.
222     *
223     * @param file The File to remove.
224     */
225    protected void removeFile(final Object file) {
226        synchronized (copies) {
227            copies.remove(file);
228        }
229    }
230
231    /**
232     * Creates a local copy of the file, and all its descendants.
233     *
234     * @param srcFile The file to copy.
235     * @param selector The FileSelector.
236     * @return the created File.
237     * @throws FileSystemException if an error occurs copying the file.
238     */
239    @Override
240    public File replicateFile(final FileObject srcFile, final FileSelector selector) throws FileSystemException {
241        final String baseName = srcFile.getName().getBaseName();
242        final File file = allocateFile(baseName);
243
244        // Copy from the source file
245        final FileObject destFile = getContext().toFileObject(file);
246        destFile.copyFrom(srcFile, selector);
247
248        return file;
249    }
250}