The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package KinoSearch1::Store::FSInvIndex;
use strict;
use warnings;
use KinoSearch1::Util::ToolSet;
use base qw( KinoSearch1::Store::InvIndex );

our $LOCK_DIR;    # used by FSLock

use File::Spec::Functions qw( canonpath catfile catdir tmpdir no_upwards );
use Fcntl;

BEGIN {
    __PACKAGE__->init_instance_vars();

    # confirm or create a directory to put lockfiles in
    $LOCK_DIR = catdir( tmpdir, 'kinosearch_lockdir' );
    if ( !-d $LOCK_DIR ) {
        mkdir $LOCK_DIR or die "couldn't mkdir '$LOCK_DIR': $!";
        chmod 0777, $LOCK_DIR;
    }
}

use Digest::MD5 qw( md5_hex );
use KinoSearch1::Store::InStream;
use KinoSearch1::Store::OutStream;
use KinoSearch1::Store::FSLock;
use KinoSearch1::Index::IndexFileNames;

sub init_instance {
    my $self = shift;

    # clean up path.
    my $path = $self->{path} = canonpath( $self->{path} );

    if ( $self->{create} ) {
        # clear out lockfiles related to this path
        my $lock_prefix = $self->get_lock_prefix;
        opendir( my $lock_dh, $LOCK_DIR )
            or confess("Couldn't opendir '$LOCK_DIR': $!");
        my @lockfiles = grep {/$lock_prefix/} readdir $lock_dh;
        closedir $lock_dh
            or confess("Couldn't closedir '$LOCK_DIR': $!");
        for (@lockfiles) {
            $_ = catfile( $LOCK_DIR, $_ );
            unlink $_ or confess("couldn't unlink '$_': $!");
        }

        # blast any existing index files
        if ( -e $path ) {
            opendir( my $invindex_dh, $path )
                or confess("Couldn't opendir '$path': $!");
            my @to_remove;
            for ( readdir $invindex_dh ) {
                if (/(^\w+\.(?:cfs|del)$)/) {
                    push @to_remove, $1;
                }
                elsif ( $_ eq 'segments' ) {
                    push @to_remove, 'segments';
                }
                elsif ( $_ eq 'delqueue' ) {
                    push @to_remove, 'delqueue';
                }
            }
            for my $removable (@to_remove) {
                $removable = catfile( $path, $removable );
                unlink $removable
                    or confess "Couldn't unlink file '$removable': $!";
            }
            closedir $invindex_dh
                or confess("Couldn't closedir '$path': $!");
        }
        if ( !-d $path ) {
            mkdir $path or confess("Couldn't mkdir '$path': $!");
        }
    }

    # by now, we should have a directory, so throw an error if we don't
    if ( !-d $path ) {
        confess("Can't open invindex location '$path': $! ")
            unless -e $path;
        confess("invindex location '$path' isn't a directory");
    }
}

sub open_outstream {
    my ( $self, $filename ) = @_;
    my $filepath = catfile( $self->{path}, $filename );
    sysopen( my $fh, $filepath, O_CREAT | O_RDWR | O_EXCL )
        or confess("Couldn't open file '$filepath': $!");
    binmode($fh);
    return KinoSearch1::Store::OutStream->new($fh);
}

sub open_instream {
    my ( $self, $filename, $offset, $len ) = @_;
    my $filepath = catfile( $self->{path}, $filename );
    # must be unbuffered, or PerlIO messes up with the shared handles
    open( my $fh, "<:unix", $filepath )
        or confess("Couldn't open file '$filepath': $!");
    binmode($fh);
    return KinoSearch1::Store::InStream->new( $fh, $offset, $len );
}

sub list {
    my $self = shift;
    opendir( my $dh, $self->{path} )
        or confess("Couldn't opendir '$self->{path}'");
    my @files = no_upwards( readdir $dh );
    closedir $dh or confess("Couldn't closedir '$self->{path}'");
    return @files;
}

sub file_exists {
    my ( $self, $filename ) = @_;
    return -e catfile( $self->{path}, $filename );
}

sub rename_file {
    my ( $self, $from, $to ) = @_;
    $_ = catfile( $self->{path}, $_ ) for ( $from, $to );
    rename( $from, $to )
        or confess("couldn't rename file '$from' to '$to': $!");
}

sub delete_file {
    my ( $self, $filename ) = @_;
    $filename = catfile( $self->{path}, $filename );
    unlink $filename or confess("couldn't unlink file '$filename': $!");
}

sub slurp_file {
    my ( $self, $filename ) = @_;
    my $filepath = catfile( $self->{path}, $filename );
    open( my $fh, "<", $filepath )
        or confess("Couldn't open file '$filepath': $!");
    binmode($fh);
    local $/;
    return <$fh>;
}

sub make_lock {
    my $self = shift;
    return KinoSearch1::Store::FSLock->new( @_, invindex => $self );
}

# Create a hashed string derived from this invindex's path.
sub get_lock_prefix {
    my $self = shift;
    return "kinosearch-" . md5_hex( canonpath( $self->{path} ) );
}

sub close { }

1;

__END__

=head1 NAME

KinoSearch1::Store::FSInvIndex - file system InvIndex 

=head1 SYNOPSIS

    my $invindex = KinoSearch1::Store::FSInvIndex->new(
        path   => '/path/to/invindex',
        create => 1,
    );

=head1 DESCRIPTION

Implementation of KinoSearch1::Store::InvIndex using a single file system 
directory and multiple files.

=head1 CONSTRUCTOR

=head2 new

C<new> takes two parameters:

=over 

=item

B<path> - the location of the invindex.

=item

B<create> - if set to 1, create a fresh invindex, clobbering an
existing one if necessary. Default value is 0, indicating that an existing
invindex should be opened.

=back

=head1 COPYRIGHT

Copyright 2005-2010 Marvin Humphrey

=head1 LICENSE, DISCLAIMER, BUGS, etc.

See L<KinoSearch1> version 1.01.

=cut