/* Copyright (c) 2012-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.storage.fs;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.locationtech.geogig.api.Ref.append;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Platform;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.plumbing.ResolveGeogigDir;
import org.locationtech.geogig.repository.RepositoryConnectionException;
import org.locationtech.geogig.storage.AbstractRefDatabase;
import org.locationtech.geogig.storage.ConfigDatabase;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.inject.Inject;
/**
* Provides an implementation of a GeoGig ref database that utilizes the file system for the storage
* of refs.
*/
public class FileRefDatabase extends AbstractRefDatabase {
private static final Charset CHARSET = Charset.forName("UTF-8");
private final Platform platform;
private final ConfigDatabase configDB;
/**
* Constructs a new {@code FileRefDatabase} with the given platform.
*
* @param platform the platform to use
*/
@Inject
public FileRefDatabase(Platform platform, ConfigDatabase configDB) {
this.platform = platform;
this.configDB = configDB;
}
/**
* Creates the reference database.
*/
@Override
public void create() {
Optional<URL> envHome = new ResolveGeogigDir(platform).call();
checkState(envHome.isPresent(), "Not inside a geogig directory");
final URL envURL = envHome.get();
if (!"file".equals(envURL.getProtocol())) {
throw new UnsupportedOperationException(
"This References Database works only against file system repositories. "
+ "Repository location: " + envURL.toExternalForm());
}
File repoDir;
try {
repoDir = new File(envURL.toURI());
} catch (URISyntaxException e) {
throw Throwables.propagate(e);
}
File refs = new File(repoDir, "refs");
if (!refs.exists() && !refs.mkdir()) {
throw new IllegalStateException("Cannot create refs directory '"
+ refs.getAbsolutePath() + "'");
}
}
/**
* Closes the reference database.
*/
@Override
public void close() {
// nothing to close
}
/**
* @param name the name of the ref (e.g. {@code "refs/remotes/origin"}, etc).
* @return the ref, or {@code null} if it doesn't exist
*/
@Override
public String getRef(String name) {
checkNotNull(name);
String value = getInternal(name);
if (value == null) {
return null;
}
try {
ObjectId.valueOf(value);
} catch (IllegalArgumentException e) {
throw e;
}
return value;
}
/**
* @param name the name of the symbolic ref (e.g. {@code "HEAD"}, etc).
* @return the ref, or {@code null} if it doesn't exist
*/
@Override
public String getSymRef(String name) {
checkNotNull(name);
String value = getInternal(name);
if (value == null) {
return null;
}
if (!value.startsWith("ref: ")) {
throw new IllegalArgumentException(name + " is not a symbolic ref: '" + value + "'");
}
value = value.substring("ref: ".length());
return value;
}
private String getInternal(String name) {
File refFile = toFile(name);
if (!refFile.exists() || refFile.isDirectory()) {
return null;
}
String value = readRef(refFile);
return value;
}
/**
* @param refName the name of the ref
* @param refValue the value of the ref
* @return {@code null} if the ref didn't exist already, its old value otherwise
*/
@Override
public void putRef(String refName, String refValue) {
checkNotNull(refName);
checkNotNull(refValue);
try {
ObjectId.forString(refValue);
} catch (IllegalArgumentException e) {
throw e;
}
store(refName, refValue);
}
/**
* @param name the name of the symbolic ref
* @param val the value of the symbolic ref
* @return {@code null} if the ref didn't exist already, its old value otherwise
*/
@Override
public void putSymRef(String name, String val) {
checkNotNull(name);
checkNotNull(val);
checkArgument(!name.equals(val), "Trying to store cyclic symbolic ref: %s", name);
checkArgument(!name.startsWith("ref: "),
"Wrong value, should not contain 'ref: ': %s -> '%s'", name, val);
val = "ref: " + val;
store(name, val);
}
/**
* @param refName the name of the ref to remove (e.g. {@code "HEAD"},
* {@code "refs/remotes/origin"}, etc).
* @return the value of the ref before removing it, or {@code null} if it didn't exist
*/
@Override
public String remove(String refName) {
checkNotNull(refName);
File refFile = toFile(refName);
String oldRef;
if (refFile.exists()) {
oldRef = readRef(refFile);
if (!refFile.delete()) {
throw new RuntimeException("Unable to delete ref file '"
+ refFile.getAbsolutePath() + "'");
}
} else {
oldRef = null;
}
return oldRef;
}
/**
* @param refPath
* @return
*/
private File toFile(String refPath) {
Optional<URL> envHome = new ResolveGeogigDir(platform).call();
String[] path = refPath.split("/");
try {
File file = new File(envHome.get().toURI());
for (String subpath : path) {
file = new File(file, subpath);
}
return file;
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
private String readRef(final File refFile) {
try {
// make sure no other thread changes the ref as we read it
synchronized (refFile.getCanonicalPath().intern()) {
return Files.readFirstLine(refFile, CHARSET);
}
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* @param refName the full name of the ref (e.g.
* {@code refs/heads/master, HEAD, transaction/<tx id>/refs/orig/refs/heads/master, etc.}
* @param refValue
*/
private void store(String refName, String refValue) {
final File refFile = toFile(refName);
try {
synchronized (refFile.getCanonicalPath().intern()) {
Files.createParentDirs(refFile);
checkState(refFile.exists() || refFile.createNewFile(),
"Unable to create file for ref %s", refFile);
FileOutputStream fout = new FileOutputStream(refFile);
try {
FileDescriptor fd = fout.getFD();
fout.write((refValue + "\n").getBytes(CHARSET));
fout.flush();
// force change to be persisted to disk
fd.sync();
} finally {
fout.close();
}
}
} catch (IOException e) {
e.printStackTrace();
throw Throwables.propagate(e);
}
}
@Override
public Map<String, String> getAll() {
Builder<String, String> builder = ImmutableMap.<String, String> builder();
builder.putAll(getAll(Ref.HEADS_PREFIX));
builder.putAll(getAll(Ref.TAGS_PREFIX));
builder.putAll(getAll(Ref.REMOTES_PREFIX));
addIfPresent(builder, Ref.CHERRY_PICK_HEAD);
addIfPresent(builder, Ref.ORIG_HEAD);
addIfPresent(builder, Ref.HEAD);
addIfPresent(builder, Ref.WORK_HEAD);
addIfPresent(builder, Ref.STAGE_HEAD);
addIfPresent(builder, Ref.MERGE_HEAD);
ImmutableMap<String, String> all = builder.build();
return all;
}
private void addIfPresent(Builder<String, String> builder, String name) {
String value = getInternal(name);
if (value != null) {
if (value.startsWith("ref: ")) {
value = value.substring("ref: ".length());
}
builder.put(name, value);
}
}
/**
* @return all references under the specified namespace
*/
@Override
public Map<String, String> getAll(String namespace) {
Preconditions.checkNotNull(namespace);
File refsRoot;
try {
Optional<URL> envHome = new ResolveGeogigDir(platform).call();
refsRoot = new File(envHome.get().toURI());
} catch (Exception e) {
throw Throwables.propagate(e);
}
if (namespace.endsWith("/")) {
namespace = namespace.substring(0, namespace.length() - 1);
}
Map<String, String> refs = Maps.newTreeMap();
findRefs(refsRoot, namespace, refs);
return ImmutableMap.copyOf(refs);
}
private void findRefs(final File refsRoot, final String namespace,
final Map<String, String> target) {
String[] subdirs = namespace.split("/");
File nsDir = refsRoot;
for (String subdir : subdirs) {
nsDir = new File(nsDir, subdir);
if (!nsDir.exists() || !nsDir.isDirectory()) {
return;
}
}
addAll(nsDir, namespace, target);
}
private void addAll(File nsDir, String prefix, Map<String/* name */, String/* value */> target) {
File[] children = nsDir.listFiles();
for (File f : children) {
final String fileName = f.getName();
if (f.isDirectory()) {
String namespace = append(prefix, fileName);
addAll(f, namespace, target);
} else if (fileName.length() == 0 || fileName.charAt(0) != '.') {
String refName = append(prefix, fileName);
String refValue = readRef(f);
if (refValue.startsWith("ref: ")) {
refValue = refValue.substring("ref: ".length());
}
target.put(refName, refValue);
}
}
}
@Override
public Map<String, String> removeAll(String namespace) {
final File file = toFile(namespace);
if (file.exists() && file.isDirectory()) {
deleteDir(file);
}
return null;
}
/**
* @param directory
*/
private void deleteDir(final File directory) {
if (!directory.exists()) {
return;
}
File[] files = directory.listFiles();
if (files == null) {
throw new RuntimeException("Unable to list files of " + directory);
}
for (File f : files) {
if (f.isDirectory()) {
deleteDir(f);
} else if (!f.delete()) {
throw new RuntimeException("Unable to delete file " + f.getAbsolutePath());
}
}
if (!directory.delete()) {
throw new RuntimeException("Unable to delete directory " + directory.getAbsolutePath());
}
}
@Override
public void configure() throws RepositoryConnectionException {
RepositoryConnectionException.StorageType.REF.configure(configDB, "file", "1.0");
}
@Override
public void checkConfig() throws RepositoryConnectionException {
RepositoryConnectionException.StorageType.REF.verify(configDB, "file", "1.0");
}
@Override
public String toString() {
Optional<URL> envHome = new ResolveGeogigDir(platform).call();
return String.format("%s[geogig dir: %s]", getClass().getSimpleName(), envHome.orNull());
}
}