blob: 6df9670ee6392884607e20e4d2841e7d8fd3453f [file] [log] [blame]
/*
* 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.
*/
package org.apache.ace.obr.storage.file;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.List;
import java.util.Stack;
import org.apache.ace.obr.metadata.MetadataGenerator;
import org.apache.ace.obr.metadata.util.ResourceMetaData;
import org.apache.ace.obr.storage.BundleStore;
import org.apache.ace.obr.storage.OBRFileStoreConstants;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.log.LogService;
/**
* This BundleStore retrieves the files from the file system. Via the Configurator the relative path is set, and all
* bundles and the index.xml should be retrievable from that path (which will internally be converted to an
* absolute path).
*/
public class BundleFileStore implements BundleStore, ManagedService {
private static final String REPOSITORY_XML = "index.xml";
private static int BUFFER_SIZE = 8 * 1024;
private final Object m_lock = new Object();
// injected by dependencymanager
private volatile MetadataGenerator m_metadata;
private volatile LogService m_log;
private volatile String m_dirChecksum;
private volatile File m_dir;
/**
* Checks if the the directory was modified since we last checked. If so, the meta-data generator is called.
*
* @throws IOException
* If there is a problem synchronizing the meta-data.
*/
public void synchronizeMetadata() throws IOException {
synchronized (m_lock) {
File dir = m_dir;
if (m_dirChecksum == null || !m_dirChecksum.equals(getDirChecksum(dir))) {
m_metadata.generateMetadata(dir);
m_dirChecksum = getDirChecksum(dir);
}
}
}
public InputStream get(String fileName) throws IOException {
if (REPOSITORY_XML.equals(fileName)) {
synchronizeMetadata();
}
FileInputStream result = null;
try {
result = new FileInputStream(createFile(fileName));
}
catch (FileNotFoundException e) {
// Resource does not exist; notify caller by returning null...
}
return result;
}
public String put(InputStream data, String fileName, boolean replace) throws IOException {
if (fileName == null) {
fileName = "";
}
File tempFile = downloadToTempFile(data);
ResourceMetaData metaData = ResourceMetaData.getBundleMetaData(tempFile);
if (metaData == null) {
metaData = ResourceMetaData.getArtifactMetaData(fileName);
}
if (metaData == null) {
tempFile.delete();
throw new IOException("Not a valid bundle and no filename found (filename = " + fileName + ")");
}
File storeLocation = getResourceFile(metaData);
if (storeLocation == null) {
tempFile.delete();
throw new IOException("Failed to store resource (filename = " + fileName + ")");
}
if (storeLocation.exists()) {
if (replace || compare(storeLocation, tempFile)) {
m_log.log(LogService.LOG_DEBUG, "Exact same resource already existed in OBR (filename = " + fileName + ")");
}
else {
m_log.log(LogService.LOG_ERROR, "Different resource with same name already existed in OBR (filename = " + fileName + ")");
return null;
}
}
moveFile(tempFile, storeLocation);
String filePath = storeLocation.toURI().toString().substring(getWorkingDir().toURI().toString().length());
if (filePath.startsWith("/")) {
filePath = filePath.substring(1);
}
return filePath;
}
/** Compares the contents of two files, returns <code>true</code> if they're exactly the same. */
private boolean compare(File first, File second) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(first));
BufferedInputStream bis2 = new BufferedInputStream(new FileInputStream(second));
int b1, b2;
try {
do {
b1 = bis.read();
b2 = bis2.read();
if (b1 != b2) {
return false;
}
}
while (b1 != -1 && b2 != -1);
return (b1 == b2);
}
finally {
if (bis != null) {
try {
bis.close();
}
catch (IOException e) {
}
}
if (bis2 != null) {
try {
bis2.close();
}
catch (IOException e) {
}
}
}
}
public boolean remove(String fileName) throws IOException {
File dir;
synchronized (m_lock) {
dir = m_dir;
}
File file = createFile(fileName);
if (file.exists()) {
if (file.delete()) {
// deleting empty parent dirs
while ((file = file.getParentFile()) != null && !file.equals(dir) && file.list().length == 0) {
file.delete();
}
return true;
}
else {
throw new IOException("Unable to delete file (" + file.getAbsolutePath() + ")");
}
}
return false;
}
public void updated(Dictionary<String, ?> dict) throws ConfigurationException {
if (dict != null) {
String path = (String) dict.get(OBRFileStoreConstants.FILE_LOCATION_KEY);
if (path == null) {
throw new ConfigurationException(OBRFileStoreConstants.FILE_LOCATION_KEY, "Missing property");
}
File newDir = new File(path);
File curDir = getWorkingDir();
if (!newDir.equals(curDir)) {
if (!newDir.exists()) {
newDir.mkdirs();
}
else if (!newDir.isDirectory()) {
throw new ConfigurationException(OBRFileStoreConstants.FILE_LOCATION_KEY, "Is not a directory: " + newDir);
}
synchronized (m_lock) {
m_dir = newDir;
m_dirChecksum = "";
}
}
}
}
/**
* Called by dependencymanager upon start of this component.
*/
protected void start() {
try {
synchronizeMetadata();
}
catch (IOException e) {
m_log.log(LogService.LOG_ERROR, "Could not generate initial meta data for bundle repository");
}
}
/**
* Computes a magic checksum used to determine whether there where changes in the directory without actually looking
* into the files or using observation.
*
* @param dir
* The directory
* @return The checksum
*/
private String getDirChecksum(File dir) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("MD5");
}
catch (NoSuchAlgorithmException e) {
// really should not happen
m_log.log(LogService.LOG_WARNING, "Unable to get an MD5 digest. Metadata will refresh every ten minutes.", e);
return "" + (System.currentTimeMillis() / 600000);
}
Stack<File> dirs = new Stack<>();
dirs.push(dir);
while (!dirs.isEmpty()) {
File pwd = dirs.pop();
for (File file : pwd.listFiles()) {
if (file.isDirectory()) {
dirs.push(file);
continue;
}
// basically we hash the filenames, but...
// include last-modified to detect touched files
// include length to work around last-modified rounding issues
String magic = file.getName() + file.length() + file.lastModified();
digest.update(magic.getBytes());
}
}
String checksum = new BigInteger(digest.digest()).toString();
return checksum;
}
/**
* Downloads a given input stream to a temporary file.
*
* @param source
* the input stream to download;
* @throws IOException
* in case of I/O problems.
*/
private File downloadToTempFile(InputStream source) throws IOException {
File tempFile = File.createTempFile("obr", ".tmp");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tempFile);
int read;
byte[] buffer = new byte[BUFFER_SIZE];
while ((read = source.read(buffer)) >= 0) {
fos.write(buffer, 0, read);
}
fos.flush();
fos.close();
return tempFile;
}
finally {
closeQuietly(fos);
}
}
/**
* Encapsulated the store layout strategy by creating the resource file based on the provided meta-data.
*
* @param metaData
* the meta-data for the resource
* @return the resource file
* @throws IOException
* in case of I/O problems.
*/
private File getResourceFile(ResourceMetaData metaData) throws IOException {
File resourceDirectory = getWorkingDir();
String[] dirs = split(metaData.getSymbolicName());
for (int i = 0; i < (dirs.length - 1); i++) {
String subDir = dirs[i];
resourceDirectory = new File(resourceDirectory, subDir);
}
if (!resourceDirectory.exists() && !resourceDirectory.mkdirs()) {
throw new IOException("Failed to create store directory");
}
String name = metaData.getSymbolicName();
String version = metaData.getVersion();
if (version != null && !version.equals("") && !version.equals("0.0.0")) {
name += "-" + version;
}
String extension = metaData.getExtension();
if (extension != null && !extension.equals("")) {
name += "." + extension;
}
return new File(resourceDirectory, name);
}
/**
* Splits a name into parts, breaking at all dots as long as what's behind the dot resembles a Java package name
* (ie. it starts with a lowercase character).
*
* @param name
* the name to split
* @return an array of parts
*/
public static String[] split(String name) {
List<String> result = new ArrayList<>();
int startPos = 0;
for (int i = 0; i < (name.length() - 1); i++) {
if (name.charAt(i) == '.') {
if (Character.isLowerCase(name.charAt(i + 1))) {
result.add(name.substring(startPos, i));
i++;
startPos = i;
}
else {
break;
}
}
}
result.add(name.substring(startPos));
return result.toArray(new String[result.size()]);
}
/**
* @return the working directory of this file store.
*/
private File getWorkingDir() {
synchronized (m_lock) {
return m_dir;
}
}
/**
* Moves a given source file to a destination location, effectively resulting in a rename.
*
* @param source
* the source file to move;
* @param dest
* the destination file to move the file to.
* @return <code>true</code> if the move succeeded.
* @throws IOException
* in case of I/O problems.
*/
private boolean moveFile(File source, File dest) throws IOException {
final int bufferSize = 1024 * 1024; // 1MB
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel input = null;
FileChannel output = null;
try {
fis = new FileInputStream(source);
input = fis.getChannel();
fos = new FileOutputStream(dest);
output = fos.getChannel();
long size = input.size();
long pos = 0;
while (pos < size) {
pos += output.transferFrom(input, pos, Math.min(size - pos, bufferSize));
}
}
finally {
closeQuietly(fos);
closeQuietly(fis);
closeQuietly(output);
closeQuietly(input);
}
if (source.length() != dest.length()) {
throw new IOException("Failed to move file! Not all contents from '" + source + "' copied to '" + dest + "'!");
}
dest.setLastModified(source.lastModified());
if (!source.delete()) {
dest.delete();
throw new IOException("Failed to move file! Source file (" + source + ") locked?");
}
return true;
}
/**
* Safely closes a given resource, ignoring any I/O exceptions that might occur by this.
*
* @param resource
* the resource to close, can be <code>null</code>.
*/
private void closeQuietly(Closeable resource) {
try {
if (resource != null) {
resource.close();
}
}
catch (IOException e) {
// Ignored...
}
}
/**
* Creates a {@link File} object with the given file name in the current working directory.
*
* @param fileName
* the name of the file.
* @return a {@link File} object, never <code>null</code>.
* @see #getWorkingDir()
*/
private File createFile(String fileName) {
return new File(getWorkingDir(), fileName);
}
}