#!/usr/bin/perl
#
# DW::BlobStore::LocalDisk
#
# Implementation of meta-blobstore for storing to local disk. This is a grossly
# inefficient implementation designed to just work.
#
# Authors:
#     Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2016 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.  For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#

package DW::BlobStore::LocalDisk;

use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);

use Digest::MD5 qw/ md5_hex /;

sub type { 'localdisk' }

sub init {
    my ( $class, %args ) = @_;
    $log->logcroak('LocalDisk configuration must include "path" element.')
        unless exists $args{path};

    mkdir $args{path};
    $log->logcroak('LocalDisk{path} is invalid/not a directory.')
        unless -d $args{path};

    $log->debug("Initializing blobstore at path: $args{path}");
    my $self = { path => $args{path} };
    return bless $self, $class;
}

sub get_location_for_key {
    my ( $self, $namespace, $key ) = @_;

    # Hash the key, we create two layers of directory structure so the files
    # spread across 256^2 directories
    my $hash = md5_hex($key);

    # Ensure path exists
    my $path = $self->{path} . '/' . $namespace;
    mkdir($path) unless -d $path;
    $path .= '/' . substr( $hash, 0, 2 );
    mkdir($path) unless -d $path;
    $path .= '/' . substr( $hash, 2, 2 );
    mkdir($path) unless -d $path;
    $log->logcroak("Failed to create path: $path")
        unless -d $path;

    # Return fully qualified filename
    my $fqfn = $path . '/' . $hash;
    $log->debug("($namespace, $key) => $fqfn");
    return $fqfn;
}

sub store {
    my ( $self, $namespace, $key, $blobref ) = @_;
    $log->logcroak('Unable to store empty file.')
        unless defined $$blobref && length $$blobref;
    my $fqfn = $self->get_location_for_key( $namespace, $key );

    # Directory should exist now, simply write the file
    my $fh;
    open $fh, '>', $fqfn
        or $log->logcroak("Failed to open $fqfn: $!");
    print $fh $$blobref;
    close $fh
        or $log->logcroak("Failed to close $fqfn: $!");
    $log->debug( "Wrote ", length $$blobref, " bytes to: $fqfn" );

    # Do sanity check that whole file was written and nothing
    # went wrong in the process
    $log->logcroak("Just written file doesn't exist: $fqfn")
        unless -e $fqfn;
    $log->logcroak("Just written file of wrong size: $fqfn")
        unless -s $fqfn == length $$blobref;
    return 1;
}

sub exists {
    my ( $self, $namespace, $key ) = @_;
    my $fqfn = $self->get_location_for_key( $namespace, $key );

    # Simple disk presence check
    $log->debug("Checking disk presence of: $fqfn");
    return -e $fqfn ? 1 : 0;
}

sub retrieve {
    my ( $self, $namespace, $key ) = @_;
    my $fqfn = $self->get_location_for_key( $namespace, $key );

    # The blobstore assumes files exist, since we should never try to
    # load something we aren't sure exists
    unless ( -e $fqfn ) {
        $log->debug("File does not exist: $fqfn");
        return undef;
    }

    # Load file into memory and return
    my ( $fh, $blob );
    open $fh, '<', $fqfn
        or $log->logcroak("Failed to open for reading: $fqfn");
    { local $/ = undef; $blob = <$fh>; }
    close $fh;
    $log->debug( "Read ", length $blob, " bytes from: $fqfn" );
    return \$blob;
}

sub delete {
    my ( $self, $namespace, $key ) = @_;
    my $fqfn = $self->get_location_for_key( $namespace, $key );

    # Can't delete what doesn't exist
    return 0 unless -e $fqfn;

    # Try to remove the file, good-bye
    unlink $fqfn
        or $log->logcroak("Failed to delete file: $fqfn");
    $log->debug("Deleted: $fqfn");
    return 1;
}

1;
