org.apache.jackrabbit.core.data.FSBackend Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/**
* File system {@link Backend} used with {@link CachingDataStore}.
* The file system can be network storage.
*/
package org.apache.jackrabbit.core.data;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FSBackend extends AbstractBackend {
private Properties properties;
private String fsPath;
File fsPathDir;
public static final String FS_BACKEND_PATH = "fsBackendPath";
/**
* Logger instance.
*/
private static final Logger LOG = LoggerFactory.getLogger(FSBackend.class);
/**
* The maximum last modified time resolution of the file system.
*/
private static final int ACCESS_TIME_RESOLUTION = 2000;
@Override
public void init(CachingDataStore store, String homeDir, String config)
throws DataStoreException {
super.init(store, homeDir, config);
Properties initProps = null;
// Check is configuration is already provided. That takes precedence
// over config provided via file based config
if (this.properties != null) {
initProps = this.properties;
} else {
initProps = new Properties();
InputStream in = null;
try {
in = new FileInputStream(config);
initProps.load(in);
} catch (IOException e) {
throw new DataStoreException(
"Could not initialize FSBackend from " + config, e);
} finally {
IOUtils.closeQuietly(in);
}
this.properties = initProps;
}
init(store, homeDir, initProps);
}
public void init(CachingDataStore store, String homeDir, Properties prop)
throws DataStoreException {
setDataStore(store);
setHomeDir(homeDir);
this.fsPath = prop.getProperty(FS_BACKEND_PATH);
if (this.fsPath == null || "".equals(this.fsPath)) {
throw new DataStoreException("Could not initialize FSBackend from "
+ getConfig() + ". [" + FS_BACKEND_PATH + "] property not found.");
}
fsPathDir = new File(this.fsPath);
if (fsPathDir.exists() && fsPathDir.isFile()) {
throw new DataStoreException("Can not create a directory "
+ "because a file exists with the same name: " + this.fsPath);
}
if (!fsPathDir.exists()) {
boolean created = fsPathDir.mkdirs();
if (!created) {
throw new DataStoreException("Could not create directory: "
+ fsPathDir.getAbsolutePath());
}
}
}
@Override
public InputStream read(DataIdentifier identifier)
throws DataStoreException {
File file = getFile(identifier);
try {
return new LazyFileInputStream(file);
} catch (IOException e) {
throw new DataStoreException("Error opening input stream of "
+ file.getAbsolutePath(), e);
}
}
@Override
public long getLength(DataIdentifier identifier) throws DataStoreException {
File file = getFile(identifier);
if (file.isFile()) {
return file.length();
}
throw new DataStoreException("Could not length of dataIdentifier ["
+ identifier + "]");
}
@Override
public long getLastModified(DataIdentifier identifier)
throws DataStoreException {
long start = System.currentTimeMillis();
File f = getFile(identifier);
if (f.isFile()) {
return getLastModified(f);
}
LOG.info("getLastModified:Identifier [{}] not found. Took [{}] ms.",
identifier, (System.currentTimeMillis() - start));
throw new DataStoreException("Identifier [" + identifier
+ "] not found.");
}
@Override
public void write(DataIdentifier identifier, File src)
throws DataStoreException {
File dest = getFile(identifier);
synchronized (this) {
if (dest.exists()) {
long now = System.currentTimeMillis();
if (getLastModified(dest) < now + ACCESS_TIME_RESOLUTION) {
setLastModified(dest, now + ACCESS_TIME_RESOLUTION);
}
} else {
try {
FileUtils.copyFile(src, dest);
} catch (IOException ioe) {
LOG.error("failed to copy [{}] to [{}]",
src.getAbsolutePath(), dest.getAbsolutePath());
throw new DataStoreException("Not able to write file ["
+ identifier + "]", ioe);
}
}
}
}
@Override
public void writeAsync(final DataIdentifier identifier, final File src,
final AsyncUploadCallback callback)
throws DataStoreException {
if (callback == null) {
throw new IllegalArgumentException(
"callback parameter cannot be null in asyncUpload");
}
getAsyncWriteExecutor().execute(new Runnable() {
@Override
public void run() {
try {
write(identifier, src);
callback.onSuccess(new AsyncUploadResult(identifier, src));
} catch (DataStoreException dse) {
AsyncUploadResult res = new AsyncUploadResult(identifier,
src);
res.setException(dse);
callback.onFailure(res);
}
}
});
}
@Override
public Iterator getAllIdentifiers()
throws DataStoreException {
ArrayList files = new ArrayList();
for (File file : fsPathDir.listFiles()) {
if (file.isDirectory()) { // skip top-level files
listRecursive(files, file);
}
}
ArrayList identifiers = new ArrayList();
for (File f : files) {
String name = f.getName();
identifiers.add(new DataIdentifier(name));
}
LOG.debug("Found " + identifiers.size() + " identifiers.");
return identifiers.iterator();
}
@Override
public boolean exists(DataIdentifier identifier, boolean touch)
throws DataStoreException {
File file = getFile(identifier);
if (file.isFile()) {
if (touch) {
long now = System.currentTimeMillis();
setLastModified(file, now + ACCESS_TIME_RESOLUTION);
}
return true;
}
return false;
}
@Override
public boolean exists(DataIdentifier identifier) throws DataStoreException {
return exists(identifier, false);
}
@Override
public void touch(DataIdentifier identifier, long minModifiedDate)
throws DataStoreException {
File file = getFile(identifier);
long now = System.currentTimeMillis();
if (minModifiedDate > 0 && minModifiedDate > getLastModified(file)) {
setLastModified(file, now + ACCESS_TIME_RESOLUTION);
}
}
@Override
public void touchAsync(final DataIdentifier identifier,
final long minModifiedDate,
final AsyncTouchCallback callback)
throws DataStoreException {
try {
if (callback == null) {
throw new IllegalArgumentException(
"callback parameter cannot be null in touchAsync");
}
Thread.currentThread().setContextClassLoader(
getClass().getClassLoader());
getAsyncWriteExecutor().execute(new Runnable() {
@Override
public void run() {
try {
touch(identifier, minModifiedDate);
callback.onSuccess(new AsyncTouchResult(identifier));
} catch (DataStoreException e) {
AsyncTouchResult result = new AsyncTouchResult(
identifier);
result.setException(e);
callback.onFailure(result);
}
}
});
} catch (Exception e) {
if (callback != null) {
callback.onAbort(new AsyncTouchResult(identifier));
}
throw new DataStoreException("Cannot touch the record "
+ identifier.toString(), e);
}
}
@Override
public Set deleteAllOlderThan(long min)
throws DataStoreException {
Set deleteIdSet = new HashSet(30);
for (File file : fsPathDir.listFiles()) {
if (file.isDirectory()) { // skip top-level files
deleteOlderRecursive(file, min, deleteIdSet);
}
}
return deleteIdSet;
}
@Override
public void deleteRecord(DataIdentifier identifier)
throws DataStoreException {
File file = getFile(identifier);
synchronized (this) {
if (file.exists()) {
if (file.delete()) {
deleteEmptyParentDirs(file);
} else {
LOG.warn("Failed to delete file " + file.getAbsolutePath());
}
}
}
}
/**
* Properties used to configure the backend. If provided explicitly before
* init is invoked then these take precedence
* @param properties to configure S3Backend
*/
public void setProperties(Properties properties) {
this.properties = properties;
}
/**
* Returns the identified file. This method implements the pattern used to
* avoid problems with too many files in a single directory.
*
* No sanity checks are performed on the given identifier.
* @param identifier data identifier
* @return identified file
*/
private File getFile(DataIdentifier identifier) {
String string = identifier.toString();
File file = this.fsPathDir;
file = new File(file, string.substring(0, 2));
file = new File(file, string.substring(2, 4));
file = new File(file, string.substring(4, 6));
return new File(file, string);
}
/**
* Set the last modified date of a file, if the file is writable.
* @param file the file
* @param time the new last modified date
* @throws DataStoreException if the file is writable but modifying the date
* fails
*/
private static void setLastModified(File file, long time)
throws DataStoreException {
if (!file.setLastModified(time)) {
if (!file.canWrite()) {
// if we can't write to the file, so garbage collection will
// also not delete it
// (read only files or file systems)
return;
}
try {
// workaround for Windows: if the file is already open for
// reading
// (in this or another process), then setting the last modified
// date
// doesn't work - see also JCR-2872
RandomAccessFile r = new RandomAccessFile(file, "rw");
try {
r.setLength(r.length());
} finally {
r.close();
}
} catch (IOException e) {
throw new DataStoreException(
"An IO Exception occurred while trying to set the last modified date: "
+ file.getAbsolutePath(), e);
}
}
}
/**
* Get the last modified date of a file.
* @param file the file
* @return the last modified date
* @throws DataStoreException if reading fails
*/
private static long getLastModified(File file) throws DataStoreException {
long lastModified = file.lastModified();
if (lastModified == 0) {
throw new DataStoreException(
"Failed to read record modified date: "
+ file.getAbsolutePath());
}
return lastModified;
}
private void listRecursive(List list, File file) {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
if (f.isDirectory()) {
listRecursive(list, f);
} else {
list.add(f);
}
}
}
}
private void deleteEmptyParentDirs(File file) {
File parent = file.getParentFile();
try {
// Only iterate & delete if parent directory of the blob file is
// child
// of the base directory and if it is empty
while (FileUtils.directoryContains(fsPathDir, parent)) {
String[] entries = parent.list();
if (entries == null) {
LOG.warn("Failed to list directory {}",
parent.getAbsolutePath());
break;
}
if (entries.length > 0) {
break;
}
boolean deleted = parent.delete();
LOG.debug("Deleted parent [{}] of file [{}]: {}", new Object[] {
parent, file.getAbsolutePath(), deleted });
parent = parent.getParentFile();
}
} catch (IOException e) {
LOG.warn("Error in parents deletion for " + file.getAbsoluteFile(),
e);
}
}
private void deleteOlderRecursive(File file, long min,
Set deleteIdSet) throws DataStoreException {
if (file.isFile() && file.exists() && file.canWrite()) {
synchronized (this) {
long lastModified;
try {
lastModified = getLastModified(file);
} catch (DataStoreException e) {
LOG.warn(
"Failed to read modification date; file not deleted", e);
// don't delete the file, since the lastModified date is
// uncertain
lastModified = min;
}
if (lastModified < min) {
DataIdentifier id = new DataIdentifier(file.getName());
if (getDataStore().confirmDelete(id)) {
getDataStore().deleteFromCache(id);
if (LOG.isInfoEnabled()) {
LOG.info("Deleting old file "
+ file.getAbsolutePath() + " modified: "
+ new Timestamp(lastModified).toString()
+ " length: " + file.length());
}
if (file.delete()) {
deleteIdSet.add(id);
} else {
LOG.warn("Failed to delete old file "
+ file.getAbsolutePath());
}
}
}
}
} else if (file.isDirectory()) {
File[] list = file.listFiles();
if (list != null) {
for (File f : list) {
deleteOlderRecursive(f, min, deleteIdSet);
}
}
// JCR-1396: FileDataStore Garbage Collector and empty directories
// Automatic removal of empty directories (but not the root!)
synchronized (this) {
list = file.listFiles();
if (list != null && list.length == 0) {
file.delete();
}
}
}
}
}