<commit_before>%% ===================================================================
%% @author <NAME>
%% @copyright (C): 2013 ACK CYFRONET AGH
%% This software is released under the MIT license 
%% cited in 'LICENSE.txt'.
%% @end
%% ===================================================================
%% @doc: This module gives high level DB API which contain veil file system specific methods.
%% All DAO API functions should not be called directly. Call dao:handle(_, {vfs, MethodName, ListOfArgs) instead.
%% See dao:handle/2 for more details.
%% @end
%% ===================================================================
-module(dao_vfs).

-include_lib("veil_modules/dao/dao.hrl").
-include_lib("veil_modules/dao/dao_helper.hrl").
-include_lib("veil_modules/dao/dao_types.hrl").
-include_lib("files_common.hrl").

%% API - File system management
-export([list_dir/3, rename_file/2, lock_file/3, unlock_file/3, test_init/0]). %% High level API functions
-export([save_descriptor/1, remove_descriptor/1, get_descriptor/1, list_descriptors/3]). %% Base descriptor management API functions
-export([save_file/1, remove_file/1, get_file/1, get_path_info/1]). %% Base file management API function


-ifdef(TEST).
-compile([export_all]).
-endif.

%% ===================================================================
%% API functions
%% ===================================================================

%% ===================================================================
%% File Descriptors Management
%% ===================================================================

%% save_descriptor/1
%% ====================================================================
%% @doc Saves file descriptor to DB. Argument should be either #file_descriptor{} record
%% (if you want to save it as new document) <br/>
%% or #veil_document{} that wraps #file_descriptor{} if you want to update descriptor in DB. <br/>
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec save_descriptor(Fd :: fd_info() | fd_doc()) -> {ok, uuid()} | {error, any()} | no_return().
%% ====================================================================
save_descriptor(#file_descriptor{} = Fd) ->
    save_descriptor(#veil_document{record = Fd});
save_descriptor(#veil_document{record = #file_descriptor{}} = FdDoc) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    dao:save_record(FdDoc).


%% remove_descriptor/1
%% ====================================================================
%% @doc Removes file descriptor from DB. Argument should be uuid() of #file_descriptor.
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec remove_descriptor(Fd :: fd()) -> ok | {error, any()} | no_return().
%% ====================================================================
remove_descriptor(Fd) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    dao:remove_record(Fd).


%% get_descriptor/1
%% ====================================================================
%% @doc Gets file descriptor from DB. Argument should be uuid() of #file_descriptor record
%% Non-error return value is always {ok, #veil_document{record = #file_descriptor}.
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec get_descriptor(Fd :: fd()) -> {ok, fd_doc()} | {error, any()} | no_return().
%% ====================================================================
get_descriptor(Fd) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    %% TODO: record type checking
    dao:get_record(Fd).


%% list_descriptors/3
%% ====================================================================
%% @doc Lists file descriptor from DB. <br/>
%% First argument is a two-element tuple containing type of resource used to filter descriptors and resource itself<br/>
%% Currently only {by_file, File :: file()} is supported. <br/>
%% Second argument limits number of rows returned. 3rd argument sets offset of query (skips first Offset rows) <br/>
%% Non-error return value is always  {ok, [#veil_document{record = #file_descriptor]}.
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec list_descriptors(MatchCriteria, N :: pos_integer(), Offset :: non_neg_integer()) ->
    {ok, fd_doc()} | {error, any()} | no_return() when
    MatchCriteria :: {by_file, File :: file()}.
%% ====================================================================
list_descriptors({by_file, File}, N, Offset) ->
    {ok, #veil_document{uuid = FileId}} = get_file(File),
    Res = dao_helper:query_view(?FD_BY_FILE_VIEW#view_info.db_name, ?FD_BY_FILE_VIEW#view_info.design, ?FD_BY_FILE_VIEW#view_info.name,
        #view_query_args{keys = [dao_helper:name(FileId)], include_docs = true,
            limit = N, skip = Offset}),
    case dao_helper:parse_view_result(Res) of
        {ok, #view_result{rows = Rows}} ->
            {ok, [FdDoc || #view_row{doc = #veil_document{record = #file_descriptor{file = FileId1}} = FdDoc} <- Rows, FileId1 == FileId]};
        Data ->
            %% TODO: error handling
            throw({inavlid_data, Data})
    end;
list_descriptors({_Type, _Resouce}, _N, _Offset) ->
    not_yet_implemented.


%% ===================================================================
%% Files Management
%% ===================================================================

%% save_file/1
%% ====================================================================
%% @doc Saves file to DB. Argument should be either #file{} record
%% (if you want to save it as new document) <br/>
%% or #veil_document{} that wraps #file{} if you want to update file in DB. <br/>
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec save_file(File :: file_info() | file_doc()) -> {ok, uuid()} | {error, any()} | no_return().
%% ====================================================================
save_file(#file{} = File) ->
    save_file(#veil_document{record = File});
save_file(#veil_document{record = #file{}} = FileDoc) ->
    dao:set_db(?FILES_DB_NAME),
    dao:save_record(FileDoc).

%% remove_file/1
%% ====================================================================
%% @doc Removes file from DB. Argument should be file() - see dao_types.hrl for more details <br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec remove_file(File :: file()) -> ok | {error, any()} | no_return().
%% ====================================================================
remove_file(File) ->
    dao:set_db(?FILES_DB_NAME),
    {ok, FData} = get_file(File),
    dao:remove_record(FData#veil_document.uuid).

%% get_file/1
%% ====================================================================
%% @doc Gets file from DB. Argument should be file() - see dao_types.hrl for more details <br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec get_file(File :: file()) -> {ok, file_doc()} | {error, any()} | no_return(). %% Throws file_not_found and invalid_data
%% ====================================================================
get_file({absolute_path, Path}) ->
    get_file({relative_path, Path, ""});
get_file({relative_path, [?PATH_SEPARATOR | _] = Path, Root}) ->
    get_file({relative_path, string:tokens(Path, [?PATH_SEPARATOR]), Root});
get_file({relative_path, [], []}) -> %% Root dir query
    {ok, #veil_document{uuid = "", record = #file{type = ?DIR_TYPE, perms = 8#777}}}; %% TODO: perms constants
get_file({relative_path, [Dir | Path], Root}) ->
    dao:set_db(?FILES_DB_NAME),
    Res = dao_helper:query_view(?FILE_TREE_VIEW#view_info.db_name, ?FILE_TREE_VIEW#view_info.design, ?FILE_TREE_VIEW#view_info.name,
                                #view_query_args{keys = [[dao_helper:name(Root), dao_helper:name(Dir)]],
                                    include_docs = case Path of [] -> true; _ -> false end}), %% Include doc representing leaf of our file path
    {NewRoot, FileDoc} =
        case dao_helper:parse_view_result(Res) of
            {ok, #view_result{rows = [#view_row{id = Id, doc = FDoc}]}} ->
                {Id, FDoc};
            {ok, #view_result{rows = []}} ->
                lager:error("File ~p not found (root = ~p)", [Dir, Root]),
                throw(file_not_found);
            _Other ->
                lager:error("Invalid view response: ~p", [_Other]),
                throw(invalid_data)
        end,
    case Path of
        [] -> {ok, FileDoc};
        _ -> get_file({relative_path, Path, NewRoot})
    end;
get_file({uuid, UUID}) ->
    %% TODO: type match checking
    dao:get_record(UUID).


%% get_path_info/1
%% ====================================================================<commit_msg>VFS-82: dao_vfs:get_file/1 return type check<commit_after>%% ===================================================================
%% @author <NAME>
%% @copyright (C): 2013 ACK CYFRONET AGH
%% This software is released under the MIT license 
%% cited in 'LICENSE.txt'.
%% @end
%% ===================================================================
%% @doc: This module gives high level DB API which contain veil file system specific methods.
%% All DAO API functions should not be called directly. Call dao:handle(_, {vfs, MethodName, ListOfArgs) instead.
%% See dao:handle/2 for more details.
%% @end
%% ===================================================================
-module(dao_vfs).

-include_lib("veil_modules/dao/dao.hrl").
-include_lib("veil_modules/dao/dao_helper.hrl").
-include_lib("veil_modules/dao/dao_types.hrl").
-include_lib("files_common.hrl").

%% API - File system management
-export([list_dir/3, rename_file/2, lock_file/3, unlock_file/3, test_init/0]). %% High level API functions
-export([save_descriptor/1, remove_descriptor/1, get_descriptor/1, list_descriptors/3]). %% Base descriptor management API functions
-export([save_file/1, remove_file/1, get_file/1, get_path_info/1]). %% Base file management API function


-ifdef(TEST).
-compile([export_all]).
-endif.

%% ===================================================================
%% API functions
%% ===================================================================

%% ===================================================================
%% File Descriptors Management
%% ===================================================================

%% save_descriptor/1
%% ====================================================================
%% @doc Saves file descriptor to DB. Argument should be either #file_descriptor{} record
%% (if you want to save it as new document) <br/>
%% or #veil_document{} that wraps #file_descriptor{} if you want to update descriptor in DB. <br/>
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec save_descriptor(Fd :: fd_info() | fd_doc()) -> {ok, uuid()} | {error, any()} | no_return().
%% ====================================================================
save_descriptor(#file_descriptor{} = Fd) ->
    save_descriptor(#veil_document{record = Fd});
save_descriptor(#veil_document{record = #file_descriptor{}} = FdDoc) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    dao:save_record(FdDoc).


%% remove_descriptor/1
%% ====================================================================
%% @doc Removes file descriptor from DB. Argument should be uuid() of #file_descriptor.
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec remove_descriptor(Fd :: fd()) -> ok | {error, any()} | no_return().
%% ====================================================================
remove_descriptor(Fd) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    dao:remove_record(Fd).


%% get_descriptor/1
%% ====================================================================
%% @doc Gets file descriptor from DB. Argument should be uuid() of #file_descriptor record
%% Non-error return value is always {ok, #veil_document{record = #file_descriptor}.
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec get_descriptor(Fd :: fd()) -> {ok, fd_doc()} | {error, any()} | no_return().
%% ====================================================================
get_descriptor(Fd) ->
    dao:set_db(?DESCRIPTORS_DB_NAME),
    %% TODO: record type checking
    dao:get_record(Fd).


%% list_descriptors/3
%% ====================================================================
%% @doc Lists file descriptor from DB. <br/>
%% First argument is a two-element tuple containing type of resource used to filter descriptors and resource itself<br/>
%% Currently only {by_file, File :: file()} is supported. <br/>
%% Second argument limits number of rows returned. 3rd argument sets offset of query (skips first Offset rows) <br/>
%% Non-error return value is always  {ok, [#veil_document{record = #file_descriptor]}.
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec list_descriptors(MatchCriteria, N :: pos_integer(), Offset :: non_neg_integer()) ->
    {ok, fd_doc()} | {error, any()} | no_return() when
    MatchCriteria :: {by_file, File :: file()}.
%% ====================================================================
list_descriptors({by_file, File}, N, Offset) ->
    {ok, #veil_document{uuid = FileId}} = get_file(File),
    Res = dao_helper:query_view(?FD_BY_FILE_VIEW#view_info.db_name, ?FD_BY_FILE_VIEW#view_info.design, ?FD_BY_FILE_VIEW#view_info.name,
        #view_query_args{keys = [dao_helper:name(FileId)], include_docs = true,
            limit = N, skip = Offset}),
    case dao_helper:parse_view_result(Res) of
        {ok, #view_result{rows = Rows}} ->
            {ok, [FdDoc || #view_row{doc = #veil_document{record = #file_descriptor{file = FileId1}} = FdDoc} <- Rows, FileId1 == FileId]};
        Data ->
            %% TODO: error handling
            throw({inavlid_data, Data})
    end;
list_descriptors({_Type, _Resouce}, _N, _Offset) ->
    not_yet_implemented.


%% ===================================================================
%% Files Management
%% ===================================================================

%% save_file/1
%% ====================================================================
%% @doc Saves file to DB. Argument should be either #file{} record
%% (if you want to save it as new document) <br/>
%% or #veil_document{} that wraps #file{} if you want to update file in DB. <br/>
%% See {@link dao:save_record/1} and {@link dao:get_record/1} for more details about #veil_document{} wrapper.<br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec save_file(File :: file_info() | file_doc()) -> {ok, uuid()} | {error, any()} | no_return().
%% ====================================================================
save_file(#file{} = File) ->
    save_file(#veil_document{record = File});
save_file(#veil_document{record = #file{}} = FileDoc) ->
    dao:set_db(?FILES_DB_NAME),
    dao:save_record(FileDoc).

%% remove_file/1
%% ====================================================================
%% @doc Removes file from DB. Argument should be file() - see dao_types.hrl for more details <br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec remove_file(File :: file()) -> ok | {error, any()} | no_return().
%% ====================================================================
remove_file(File) ->
    dao:set_db(?FILES_DB_NAME),
    {ok, FData} = get_file(File),
    dao:remove_record(FData#veil_document.uuid).

%% get_file/1
%% ====================================================================
%% @doc Gets file from DB. Argument should be file() - see dao_types.hrl for more details <br/>
%% Should not be used directly, use {@link dao:handle/2} instead (See {@link dao:handle/2} for more details).
%% @end
-spec get_file(File :: file()) -> {ok, file_doc()} | {error, any()} | no_return(). %% Throws file_not_found and invalid_data
%% ====================================================================
get_file({absolute_path, Path}) ->
    get_file({relative_path, Path, ""});
get_file({relative_path, [?PATH_SEPARATOR | _] = Path, Root}) ->
    get_file({relative_path, string:tokens(Path, [?PATH_SEPARATOR]), Root});
get_file({relative_path, [], []}) -> %% Root dir query
    {ok, #veil_document{uuid = "", record = #file{type = ?DIR_TYPE, perms = 8#777}}}; %% TODO: perms constants
get_file({relative_path, [Dir | Path], Root}) ->
    dao:set_db(?FILES_DB_NAME),
    Res = dao_helper:query_view(?FILE_TREE_VIEW#view_info.db_name, ?FILE_TREE_VIEW#view_info.design, ?FILE_TREE_VIEW#view_info.name,
                                #view_query_args{keys = [[dao_helper:name(Root), dao_helper:name(Dir)]],
                                    include_docs = case Path of [] -> true; _ -> false end}), %% Include doc representing leaf of our file path
    {NewRoot, FileDoc} =
        case dao_helper:parse_view_result(Res) of
            {ok, #view_result{rows = [#view_row{id = Id, doc = FDoc}]}} ->
                {Id, FDoc};
            {ok, #view_result{rows = []}} ->
                lager:error("File ~p not found (root = ~p)", [Dir, Root]),
                throw(file_not_found);
            _Other ->
                lager:error("Invalid view response: ~p", [_Other]),
                throw(invalid_data)
        end,
    case Path of
        [] -> {ok, FileDoc};
        _ -> get_file({relative_path, Path, NewRoot})
    end;
get_file({uuid, UUID}) ->
    case dao:get_record(UUID) of
        {ok, #veil_document{record = #file{}} = Doc} ->
            {ok, Doc};
        {ok, #veil_document{}} ->
            {error, invalid_file_record};
        Other ->
            Other
    end.


%% get_path_info/1
%% ====================================================================
{'fraction_non_alphanumeric': 0.30258457659172094, 'fraction_numerical': 0.005936121033830637, 'mean_word_length': 4.9434904776771775, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 96, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4491023', 'n_tokens_mistral': 5666, 'n_tokens_neox': 5415, 'n_words': 1443}