/* 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 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.net.URISyntaxException; import java.net.URL; import java.util.Iterator; import java.util.List; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Platform; import org.locationtech.geogig.api.RevObject; import org.locationtech.geogig.api.plumbing.ResolveGeogigDir; import org.locationtech.geogig.repository.RepositoryConnectionException; import org.locationtech.geogig.storage.AbstractObjectDatabase; import org.locationtech.geogig.storage.BulkOpListener; import org.locationtech.geogig.storage.ConfigDatabase; import org.locationtech.geogig.storage.ObjectDatabase; import org.locationtech.geogig.storage.datastream.DataStreamSerializationFactoryV1; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.inject.Inject; /** * Provides an implementation of a GeoGig object database that utilizes the file system for the * storage of objects. * * @see AbstractObjectDatabase */ public class FileObjectDatabase extends AbstractObjectDatabase implements ObjectDatabase { private final Platform platform; private final ConfigDatabase configDB; private final String databaseName; private File dataRoot; private String dataRootPath; /** * Constructs a new {@code FileObjectDatabase} using the given platform. * * @param platform the platform to use. */ @Inject public FileObjectDatabase(final Platform platform, final ConfigDatabase configDB) { this(platform, "objects", configDB); } protected FileObjectDatabase(final Platform platform, final String databaseName, final ConfigDatabase configDB) { super(DataStreamSerializationFactoryV1.INSTANCE); checkNotNull(platform); checkNotNull(databaseName); this.platform = platform; this.databaseName = databaseName; this.configDB = configDB; } protected File getDataRoot() { return dataRoot; } protected String getDataRootPath() { return dataRootPath; } /** * @return true if the database is open, false otherwise */ @Override public boolean isOpen() { return dataRoot != null; } /** * Opens the database for use by GeoGig. */ @Override public void open() { if (isOpen()) { return; } final Optional<URL> repoUrl = new ResolveGeogigDir(platform).call(); checkState(repoUrl.isPresent(), "Can't find geogig repository home"); try { dataRoot = new File(new File(repoUrl.get().toURI()), databaseName); } catch (URISyntaxException e) { throw Throwables.propagate(e); } if (!dataRoot.exists() && !dataRoot.mkdirs()) { throw new IllegalStateException("Can't create environment: " + dataRoot.getAbsolutePath()); } if (!dataRoot.isDirectory()) { throw new IllegalStateException("Environment but is not a directory: " + dataRoot.getAbsolutePath()); } if (!dataRoot.canWrite()) { throw new IllegalStateException("Environment is not writable: " + dataRoot.getAbsolutePath()); } dataRootPath = dataRoot.getAbsolutePath(); } /** * Closes the database. */ @Override public void close() { dataRoot = null; dataRootPath = null; } /** * Determines if the given {@link ObjectId} exists in the object database. * * @param id the id to search for * @return true if the object exists, false otherwise */ @Override public boolean exists(final ObjectId id) { File f = filePath(id); return f.exists(); } @Override protected InputStream getRawInternal(ObjectId id, boolean failIfNotFound) { File f = filePath(id); try { return new FileInputStream(f); } catch (FileNotFoundException e) { if (failIfNotFound) { throw Throwables.propagate(e); } return null; } } /** * @see org.locationtech.geogig.storage.AbstractObjectDatabase#putInternal(org.locationtech.geogig.api.ObjectId, byte[]) */ @Override protected boolean putInternal(final ObjectId id, final byte[] rawData) { final File f = filePath(id); if (f.exists()) { return false; } FileOutputStream fileOutputStream; try { fileOutputStream = new FileOutputStream(f); } catch (FileNotFoundException dirDoesNotExist) { final File parent = f.getParentFile(); if (!parent.exists() && !parent.mkdirs()) { throw new RuntimeException("Can't create " + parent.getAbsolutePath()); } try { fileOutputStream = new FileOutputStream(f); } catch (FileNotFoundException e) { throw Throwables.propagate(e); } } try { fileOutputStream.write(rawData); fileOutputStream.flush(); fileOutputStream.close(); } catch (IOException e) { throw Throwables.propagate(e); } return true; } /** * Deletes the object with the provided {@link ObjectId id} from the database. * * @param objectId the id of the object to delete * @return true if the object was deleted, false if it was not found */ @Override public boolean delete(ObjectId objectId) { File filePath = filePath(objectId); boolean delete = filePath.delete(); return delete; } private File filePath(final ObjectId id) { final String idName = id.toString(); return filePath(idName); } private File filePath(final String objectId) { checkNotNull(objectId); checkArgument(objectId.length() > 4, "partial object id is too short"); final char[] path1 = new char[2]; final char[] path2 = new char[2]; objectId.getChars(0, 2, path1, 0); objectId.getChars(2, 4, path2, 0); StringBuilder sb = new StringBuilder(dataRootPath); sb.append(File.separatorChar).append(path1).append(File.separatorChar).append(path2) .append(File.separatorChar).append(objectId); String filePath = sb.toString(); return new File(filePath); } /** * Searches the database for {@link ObjectId}s that match the given partial id. * * @param partialId the partial id to search for * @return a list of matching results */ @Override public List<ObjectId> lookUp(final String partialId) { File parent = filePath(partialId).getParentFile(); String[] list = parent.list(); if (null == list) { return ImmutableList.of(); } Builder<ObjectId> builder = ImmutableList.builder(); for (String oid : list) { if (oid.startsWith(partialId)) { builder.add(ObjectId.valueOf(oid)); } } return builder.build(); } @Override protected List<ObjectId> lookUpInternal(byte[] raw) { throw new UnsupportedOperationException( "This method should not be called, we override lookUp(String) directly"); } @Override public Iterator<RevObject> getAll(Iterable<ObjectId> ids, BulkOpListener listener) { throw new UnsupportedOperationException("This method is not yet implemented"); } @Override public long deleteAll(Iterator<ObjectId> ids, final BulkOpListener listener) { throw new UnsupportedOperationException("This method is not yet implemented"); } @Override public void configure() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.OBJECT.configure(configDB, "file", "1.0"); } @Override public void checkConfig() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.OBJECT.verify(configDB, "file", "1.0"); } @Override public String toString() { return String.format("%s[dir: %s, name: %s]", getClass().getSimpleName(), dataRoot == null ? "<unset>" : dataRoot.getAbsolutePath(), databaseName); } }