/*******************************************************************************
* Copyright (c) 2015 IBH SYSTEMS GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBH SYSTEMS GmbH - initial API and implementation
*******************************************************************************/
package org.eclipse.packagedrone.repo.channel.apm.store;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.packagedrone.utils.io.IOConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.ByteStreams;
import com.google.common.io.CountingOutputStream;
public class BlobStore implements Closeable
{
private final static Logger logger = LoggerFactory.getLogger ( BlobStore.TransactionImpl.class );
public class TransactionImpl implements Transaction
{
private boolean done;
private final Set<String> deleted = new HashSet<> ();
private final Set<String> added = new HashSet<> ();
@Override
public boolean delete ( final String id ) throws IOException
{
testDone ();
final boolean existed = BlobStore.this.currentIndex.contains ( id ) || this.added.contains ( id );
this.added.remove ( id );
this.deleted.add ( id );
return existed;
}
@Override
public long create ( final String id, final IOConsumer<OutputStream> consumer ) throws IOException
{
testDone ();
final long size = handleCreate ( id, consumer );
this.added.add ( id );
this.deleted.remove ( id );
return size;
}
@Override
public boolean stream ( final String id, final IOConsumer<InputStream> consumer ) throws IOException
{
testDone ();
if ( this.deleted.contains ( id ) )
{
return false;
}
return handleStream ( id, consumer );
}
@Override
public void commit ()
{
testDone ();
this.done = true;
handleCommit ( this, this.deleted, this.added );
}
@Override
public void rollback ()
{
testDone ();
this.done = true;
handleRollback ( this );
}
protected void testDone ()
{
if ( this.done )
{
throw new IllegalStateException ( "Transaction already closed" );
}
}
}
public static interface Transaction
{
public boolean delete ( String id ) throws IOException;
public default long create ( final String id, final InputStream source ) throws IOException
{
return create ( id, target -> ByteStreams.copy ( source, target ) );
}
public long create ( String id, IOConsumer<OutputStream> consumer ) throws IOException;
public boolean stream ( String id, IOConsumer<InputStream> consumer ) throws IOException;
public void commit ();
public void rollback ();
}
private final Path base;
private final Path dataPath;
private Transaction transaction;
private Set<String> currentIndex;
private final Path indexPath;
public BlobStore ( final Path path ) throws IOException
{
this.base = path;
this.dataPath = this.base.resolve ( "data" ).toAbsolutePath ();
this.indexPath = this.base.resolve ( "index.txt" ).toAbsolutePath ();
Files.createDirectories ( this.dataPath );
Set<String> index = loadIndex ();
if ( index == null )
{
// build index
index = scanIndex ( path );
writeIndex ( index );
}
this.currentIndex = index;
}
public static Set<String> scanIndex ( final Path basePath ) throws IOException
{
try
{
return Files.walk ( basePath.resolve ( "data" ) ).filter ( Files::isRegularFile ).map ( path -> path.getName ( path.getNameCount () - 1 ).toString () ).collect ( Collectors.toSet () );
}
catch ( final NoSuchFileException e )
{
return Collections.emptySet ();
}
}
public boolean handleStream ( final String id, final IOConsumer<InputStream> consumer ) throws IOException
{
return processStream ( id, consumer );
}
public long handleCreate ( final String id, final IOConsumer<OutputStream> consumer ) throws IOException
{
final Path path = makeDataPath ( id );
Files.createDirectories ( path.getParent () );
try ( CountingOutputStream stream = new CountingOutputStream ( new BufferedOutputStream ( Files.newOutputStream ( path, StandardOpenOption.CREATE_NEW ) ) ) )
{
consumer.accept ( stream );
return stream.getCount ();
}
}
@Override
public synchronized void close ()
{
if ( this.transaction != null )
{
handleRollback ( this.transaction );
}
}
public boolean stream ( final String id, final IOConsumer<InputStream> consumer ) throws IOException
{
if ( !this.currentIndex.contains ( id ) )
{
// FIXME: check locking
return false;
}
return processStream ( id, consumer );
}
private boolean processStream ( final String id, final IOConsumer<InputStream> consumer ) throws IOException
{
final Path path = makeDataPath ( id );
try ( InputStream stream = new BufferedInputStream ( Files.newInputStream ( path, StandardOpenOption.READ ) ) )
{
consumer.accept ( stream );
return true;
}
catch ( final NoSuchFileException e )
{
return false;
}
}
public synchronized Transaction start ()
{
if ( this.transaction == null )
{
this.transaction = new TransactionImpl ();
return this.transaction;
}
throw new IllegalStateException ( "Transaction already in progress" );
}
protected synchronized void handleCommit ( final Transaction transaction, final Set<String> deleted, final Set<String> added )
{
if ( this.transaction != transaction )
{
throw new IllegalStateException ( "Invalid transaction" );
}
this.transaction = null;
try
{
final Set<String> index = new HashSet<> ( this.currentIndex );
index.removeAll ( deleted );
index.addAll ( added );
writeIndex ( index );
for ( final String id : deleted )
{
Files.deleteIfExists ( makeDataPath ( id ) );
}
}
catch ( final IOException e )
{
throw new RuntimeException ( "Failed to commit", e );
}
}
private void writeIndex ( final Set<String> index ) throws IOException
{
Files.write ( this.indexPath, index, StandardCharsets.UTF_8 );
this.currentIndex = index;
}
private Set<String> loadIndex () throws IOException
{
try
{
return new HashSet<> ( Files.readAllLines ( this.indexPath, StandardCharsets.UTF_8 ) );
}
catch ( final NoSuchFileException e )
{
return null;
}
}
protected synchronized void handleRollback ( final Transaction transaction )
{
if ( this.transaction != transaction )
{
throw new IllegalStateException ( "Invalid transaction" );
}
this.transaction = null;
try
{
vacuum ();
}
catch ( final IOException e )
{
logger.warn ( "Failed to vacuum", e );
}
}
public synchronized void vacuum () throws IOException
{
final Set<String> ids = scanIndex ( this.base );
ids.removeAll ( this.currentIndex );
for ( final String id : ids )
{
final Path path = makeDataPath ( id );
logger.debug ( "Vacuuming file: {}", path );
Files.deleteIfExists ( path );
}
}
private Path makeDataPath ( final String id )
{
final String l1 = id.substring ( 0, 1 );
final String l2 = id.substring ( 1, 2 );
return this.dataPath.resolve ( Paths.get ( l1, l2, id ) );
}
}