# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::VirtualFS;

use strict;
use warnings;

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::DB',
    'Kernel::System::Log',
    'Kernel::System::Main',
);

=head1 NAME

Kernel::System::VirtualFS - virtual filesystem lib

=head1 DESCRIPTION

All virtual filesystem functions.

=head1 PUBLIC INTERFACE

=head2 new()

Don't use the constructor directly, use the ObjectManager instead:

    my $VirtualFSObject = $Kernel::OM->Get('Kernel::System::VirtualFS');

=cut

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {};
    bless( $Self, $Type );

    # load backend
    $Self->{BackendDefault} = $Kernel::OM->Get('Kernel::Config')->Get('VirtualFS::Backend')
        || 'Kernel::System::VirtualFS::DB';

    if ( !$Kernel::OM->Get('Kernel::System::Main')->Require( $Self->{BackendDefault} ) ) {
        return;
    }

    $Self->{Backend}->{ $Self->{BackendDefault} } = $Self->{BackendDefault}->new();

    return $Self;
}

=head2 Read()

read a file from virtual file system

    my %File = $VirtualFSObject->Read(
        Filename => '/Object/some/name.txt',
        Mode     => 'utf8',

        # optional
        DisableWarnings => 1,
    );

returns

    my %File = (
        Content  => $ContentSCALAR,

        # preferences data
        Preferences => {

            # generated automatically
            FilesizeRaw        => 12345,

            # optional
            ContentType        => 'text/plain',
            ContentID          => '<some_id@example.com>',
            ContentAlternative => 1,
            SomeCustomParams   => 'with our own value',
        },
    );

=cut

sub Read {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(Filename Mode)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # lookup
    my ( $FileID, $BackendKey, $Backend ) = $Self->_FileLookup( $Param{Filename} );
    if ( !$BackendKey ) {
        if ( !$Param{DisableWarnings} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "No such file '$Param{Filename}'!",
            );
        }
        return;
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # get preferences
    my %Preferences;
    return if !$DBObject->Prepare(
        SQL => 'SELECT preferences_key, preferences_value FROM '
            . 'virtual_fs_preferences WHERE virtual_fs_id = ?',
        Bind => [ \$FileID ],
    );

    while ( my @Row = $DBObject->FetchrowArray() ) {
        $Preferences{ $Row[0] } = $Row[1];
    }

    # load backend (if not default)
    if ( !$Self->{Backend}->{$Backend} ) {

        return if !$Kernel::OM->Get('Kernel::System::Main')->Require($Backend);

        $Self->{Backend}->{$Backend} = $Backend->new();

        return if !$Self->{Backend}->{$Backend};
    }

    # get file
    my $Content = $Self->{Backend}->{$Backend}->Read(
        %Param,
        BackendKey => $BackendKey,
    );
    return if !$Content;

    return (
        Preferences => \%Preferences,
        Content     => $Content,
    );
}

=head2 Write()

write a file to virtual file system

    my $Success = $VirtualFSObject->Write(
        Content  => \$Content,
        Filename => '/Object/SomeFileName.txt',
        Mode     => 'binary',            # (binary|utf8)

        # optional, preferences data
        Preferences => {
            ContentType        => 'text/plain',
            ContentID          => '<some_id@example.com>',
            ContentAlternative => 1,
            SomeCustomParams   => 'with our own value',
        },
    );

=cut

sub Write {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(Filename Content Mode)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # lookup
    my ($FileID) = $Self->_FileLookup( $Param{Filename} );
    if ($FileID) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "File already exists '$Param{Filename}'!",
        );
        return;
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # insert
    return if !$DBObject->Do(
        SQL => 'INSERT INTO virtual_fs (filename, backend_key, backend, create_time)'
            . ' VALUES ( ?, \'TMP\', ?, current_timestamp)',
        Bind => [ \$Param{Filename}, \$Self->{BackendDefault} ],
    );

    ($FileID) = $Self->_FileLookup( $Param{Filename} );

    if ( !$FileID ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Unable to store '$Param{Filename}'!",
        );
        return;
    }

    # size calculation
    $Param{Preferences}->{FilesizeRaw} = bytes::length( ${ $Param{Content} } );

    # insert preferences
    for my $Key ( sort keys %{ $Param{Preferences} } ) {
        return if !$DBObject->Do(
            SQL => 'INSERT INTO virtual_fs_preferences '
                . '(virtual_fs_id, preferences_key, preferences_value) VALUES ( ?, ?, ?)',
            Bind => [ \$FileID, \$Key, \$Param{Preferences}->{$Key} ],
        );
    }

    # store file
    my $BackendKey = $Self->{Backend}->{ $Self->{BackendDefault} }->Write(%Param);
    return if !$BackendKey;

    # update backend key
    return if !$DBObject->Do(
        SQL  => 'UPDATE virtual_fs SET backend_key = ? WHERE id = ?',
        Bind => [ \$BackendKey, \$FileID ],
    );

    return 1;
}

=head2 Delete()

delete a file from virtual file system

    my $Success = $VirtualFSObject->Delete(
        Filename => '/Object/SomeFileName.txt',

        # optional
        DisableWarnings => 1,
    );

=cut

sub Delete {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(Filename)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # lookup
    my ( $FileID, $BackendKey, $Backend ) = $Self->_FileLookup( $Param{Filename} );
    if ( !$FileID ) {
        if ( !$Param{DisableWarnings} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "No such file '$Param{Filename}'!",
            );
        }
        return;
    }

    # load backend (if not default)
    if ( !$Self->{Backend}->{$Backend} ) {

        return if !$Kernel::OM->Get('Kernel::System::Main')->Require($Backend);

        $Self->{Backend}->{$Backend} = $Backend->new();

        return if !$Self->{Backend}->{$Backend};
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # delete preferences
    return if !$DBObject->Do(
        SQL  => 'DELETE FROM virtual_fs_preferences WHERE virtual_fs_id = ?',
        Bind => [ \$FileID ],
    );

    # delete
    return if !$DBObject->Do(
        SQL  => 'DELETE FROM virtual_fs WHERE id = ?',
        Bind => [ \$FileID ],
    );

    # delete file
    return $Self->{Backend}->{$Backend}->Delete(
        %Param,
        BackendKey => $BackendKey,
    );
}

=head2 Find()

find files in virtual file system

only for file name

    my @List = $VirtualFSObject->Find(
        Filename => '/Object/some_what/*.txt',
    );

only for preferences

    my @List = $VirtualFSObject->Find(
        Preferences => {
            ContentType => 'text/plain',
        },
    );

for file name and for preferences

    my @List = $VirtualFSObject->Find(
        Filename    => '/Object/some_what/*.txt',
        Preferences => {
            ContentType => 'text/plain',
        },
    );

Returns:

    my @List = (
        '/Object/some/file.txt',
        '/Object/my.pdf',
    );

=cut

sub Find {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Filename} && !$Param{Preferences} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need Filename or/and Preferences!',
        );
        return;
    }

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # get like escape string needed for some databases (e.g. oracle)
    my $LikeEscapeString = $DBObject->GetDatabaseFunction('LikeEscapeString');

    # prepare file name search
    my $SQLResult = 'vfs.filename';
    my $SQLTable  = 'virtual_fs vfs ';
    my $SQLWhere  = '';
    my @SQLBind;
    if ( $Param{Filename} ) {
        my $Like = $Param{Filename};
        $Like =~ s/\*/%/g;
        $Like = $DBObject->Quote( $Like, 'Like' );
        $SQLWhere .= "vfs.filename LIKE '$Like' $LikeEscapeString";
    }

    # prepare preferences search
    if ( $Param{Preferences} ) {
        $SQLResult = 'vfs.filename, vfsp.preferences_key, vfsp.preferences_value';
        $SQLTable .= ', virtual_fs_preferences vfsp';
        if ($SQLWhere) {
            $SQLWhere .= ' AND ';
        }
        $SQLWhere .= 'vfs.id = vfsp.virtual_fs_id ';
        my $SQL = '';
        for my $Key ( sort keys %{ $Param{Preferences} } ) {
            if ($SQL) {
                $SQL .= ' OR ';
            }
            $SQL .= '(vfsp.preferences_key = ? AND ';
            push @SQLBind, \$Key;

            my $Value = $Param{Preferences}->{$Key};
            if ( $Value =~ /(\*|\%)/ ) {
                $Value =~ s/\*/%/g;
                $Value = $DBObject->Quote( $Value, 'Like' );
                $SQL .= "vfsp.preferences_value LIKE '$Value' $LikeEscapeString";
            }
            else {
                $SQL .= 'vfsp.preferences_value = ?';
                push @SQLBind, \$Value;
            }
            $SQL .= ')';
        }

        $SQLWhere .= " AND ($SQL)";
    }

    # search
    return if !$DBObject->Prepare(
        SQL  => "SELECT $SQLResult FROM $SQLTable WHERE $SQLWhere",
        Bind => \@SQLBind,
    );
    my @List;
    my %Result;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        if ( $Param{Preferences} ) {
            for my $Key ( sort keys %{ $Param{Preferences} } ) {
                $Result{ $Row[0] }->{ $Row[1] } = $Row[2];
            }
        }
        else {
            push @List, $Row[0];
        }
    }

    # check preferences search
    if ( $Param{Preferences} ) {
        FILE:
        for my $File ( sort keys %Result ) {
            for my $Key ( sort keys %{ $Param{Preferences} } ) {
                my $DB    = $Result{$File}->{$Key};
                my $Given = $Param{Preferences}->{$Key};
                next FILE if defined $DB  && !defined $Given;
                next FILE if !defined $DB && defined $Given;
                if ( $Given =~ /\*/ ) {
                    $Given           =~ s/\*/.\*/g;
                    $Given           =~ s/\//\\\//g;
                    next FILE if $DB !~ /$Given/;
                }
                else {
                    next FILE if $DB ne $Given;
                }
            }
            push @List, $File;
        }
    }

    # return result
    return @List;
}

=begin Internal:

Private functions used by this package (not part of the documented public API).

=end Internal:

=head2 _FileLookup()

returns internal meta information, unique file id, where and with what arguments the
file is stored

    my ( $FileID, $BackendKey, $Backend ) = $Self->_FileLookup( '/Object/SomeFile.txt' );

=cut

sub _FileLookup {
    my ( $Self, $Filename ) = @_;

    # get database object
    my $DBObject = $Kernel::OM->Get('Kernel::System::DB');

    # lookup
    return if !$DBObject->Prepare(
        SQL  => 'SELECT id, backend_key, backend FROM virtual_fs WHERE filename = ?',
        Bind => [ \$Filename ],
    );

    my $FileID;
    my $BackendKey;
    my $Backend;
    while ( my @Row = $DBObject->FetchrowArray() ) {
        $FileID     = $Row[0];
        $BackendKey = $Row[1];
        $Backend    = $Row[2];
    }

    return ( $FileID, $BackendKey, $Backend );
}

1;

=head1 TERMS AND CONDITIONS

This software is part of the OTRS project (L<https://otrs.org/>).

This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.

=cut
