/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.riotfamily.media.store;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import javax.servlet.ServletContext;
import org.riotfamily.common.io.RecursiveFileIterator;
import org.riotfamily.common.util.FormatUtils;
import org.riotfamily.common.util.RandomStringGenerator;
import org.riotfamily.common.util.RandomStringGenerator.Chars;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ServletContextAware;
/**
* Default FileStore implementation.
* @author Felix Gnass [fgnass at neteye dot de]
*/
public class DefaultFileStore implements FileStore, ServletContextAware,
InitializingBean {
private Logger log = LoggerFactory.getLogger(DefaultFileStore.class);
private String uriPrefix;
private String storagePath;
private File baseDir;
private File storageDir;
private int storageDirIndex = 0;
private int maxFilesPerDir = 500;
private RandomStringGenerator dirNameGenerator =
new RandomStringGenerator(14, true, Chars.DIGITS);
private ServletContext servletContext;
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
* Sets the path to the directory where the files are stored. Relative
* paths are resolved against the webapp's root directory by calling
* {@link ServletContext#getRealPath(String)}.
*
* @param storagePath Either an absolute or relative path denoting a directory
*/
public void setStoragePath(String storagePath) {
this.storagePath = storagePath;
}
/**
* Sets a prefix that is added to all URIs returned by the
* {@link #store(File, String) store()} method.
*/
public void setUriPrefix(String uriPrefix) {
Assert.notNull(uriPrefix, "The uriPrefix must not be null");
if (uriPrefix.endsWith("/")) {
uriPrefix = uriPrefix.substring(0, uriPrefix.length() - 1);
}
this.uriPrefix = uriPrefix;
}
/**
* Sets the maximum number of files that may be stored in a directory
* before a new storageDir is created. Defaults to 500.
*/
public void setMaxFilesPerDir(int maxFilesPerDir) {
this.maxFilesPerDir = maxFilesPerDir;
}
/**
* Creates the baseDir after all properties have been set.
*/
public void afterPropertiesSet() throws IOException {
Assert.notNull(uriPrefix, "The uriPrefix must not be null");
if (storagePath == null) {
storagePath = servletContext.getRealPath(uriPrefix);
}
else if (!storagePath.startsWith(File.separator)
&& storagePath.indexOf(":") != 1) {
storagePath = servletContext.getRealPath(storagePath);
}
baseDir = createDir(new File(storagePath));
log.info("Files will be stored in "
+ baseDir.getCanonicalPath());
getStorageDir();
}
/**
* Creates the given directory and all parent directories (unless they
* already exist). If the directory can't be created an IOException is
* thrown.
*/
protected File createDir(File dir) throws IOException {
if (dir.exists()) {
return dir;
}
if (!dir.mkdirs()) {
throw new IOException("Can't create directory " + dir.getPath()
+ " as user " + System.getProperty("user.name"));
}
return dir;
}
/**
* Returns whether the next storage directory should be used.
*/
private boolean shouldUseNewStorageDir() {
return storageDir == null || !storageDir.exists()
|| storageDir.list().length >= maxFilesPerDir;
}
/**
* Returns the directory where the files should be stored. The default
* implementation limits the number of files per directory and creates a
* new directory when the number of files exceeds the
* {@link #setMaxFilesPerDir(int) maxFilesPerDir} value.
*/
protected File getStorageDir() throws IOException {
if (shouldUseNewStorageDir()) {
synchronized (this) {
while (shouldUseNewStorageDir()) {
String name = String.valueOf(storageDirIndex++);
storageDir = createDir(new File(baseDir, name));
}
}
}
return storageDir;
}
/**
* Returns an empty new directory with an unique name within the current
* storageDir.
*/
protected File getUniqueDir() throws IOException {
File parent = getStorageDir();
for (int i = 0; i < maxFilesPerDir; i++) {
File dir = new File(parent, dirNameGenerator.generate());
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IOException("Can't create directory " + dir.getPath()
+ " as user " + System.getProperty("user.name"));
}
return dir;
}
}
//This should never happen ...
throw new RuntimeException("Failed to create a unique directory name.");
}
public String store(InputStream in, String fileName, String bucket)
throws IOException {
File dest = new File(getUniqueDir(), FormatUtils.toFilename(fileName));
if (in != null) {
FileCopyUtils.copy(in, new FileOutputStream(dest));
log.debug("stored at: " + dest.getAbsolutePath());
}
else {
dest.createNewFile();
}
return getUri(dest);
}
public String getUri(File file) {
String path = file.getPath();
if (path.startsWith(storagePath)) {
path = path.substring(storagePath.length());
path = StringUtils.replace(path, File.separator, "/");
return uriPrefix + path;
}
return null;
}
/**
* Retrieves a file from the store that was previously added via the
* {@link #store(File, String) store()} method.
*/
public File retrieve(String uri) {
log.debug("Retrieving file for URI: " + uri);
if (!uri.startsWith(uriPrefix)) {
return null;
}
uri = stripQueryString(uri.substring(uriPrefix.length() + 1));
uri = StringUtils.replace(uri, "/", File.separator);
File file = new File(baseDir, uri);
log.debug("File: " + file);
return file;
}
/**
* Strips the query string from the given URI. Older FileStore
* implementations had the option to append a timestamp parameter to the
* URI therefore this method is used to ensure backwards compatibility.
*/
private String stripQueryString(String uri) {
int i = uri.indexOf('?');
if (i != -1) {
uri = uri.substring(0, i);
}
return uri;
}
/**
* Deletes the file denoted by the given URI from the store.
*/
public void delete(String uri) {
File file = retrieve(uri);
file.delete();
File dir = file.getParentFile();
if (dir.isDirectory() && dir.list().length == 0 &&
!dir.equals(baseDir)) {
dir.delete();
}
}
public Iterator<String> iterator() {
return new FileUriIterator();
}
private class FileUriIterator implements Iterator<String> {
private RecursiveFileIterator it = new RecursiveFileIterator(baseDir);
public boolean hasNext() {
return it.hasNext();
}
public String next() {
return getUri(it.next());
}
public void remove() {
it.remove();
}
}
}