<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 file contains n2o website code.
%% The page is a file manager providing basic functionalities.
%% @end
%% ===================================================================

-module(page_file_manager).
-include("oneprovider_modules/control_panel/common.hrl").
-include("oneprovider_modules/fslogic/fslogic.hrl").
-include("oneprovider_modules/fslogic/fslogic_acl.hrl").
-include("oneprovider_modules/dao/dao_users.hrl").
-include("fuse_messages_pb.hrl").
-include_lib("ctool/include/logging.hrl").

% n2o API
-export([main/0, event/1, api_event/3]).
% Postback functions and other
-export([get_requested_hostname/0, comet_loop/1]).
-export([clear_manager/0, clear_workspace/0, sort_toggle/1, sort_reverse/0, navigate/1, up_one_level/0]).
-export([toggle_view/1, select_item/1, select_all/0, deselect_all/0, clear_clipboard/0, put_to_clipboard/1, paste_from_clipboard/0]).
-export([confirm_paste/0, submit_perms/2, show_permissions_info/0]).
-export([populate_acl_list/1, change_perms_type/1, add_acl/0, delete_acl/1, edit_acl/1, move_acl/2, submit_acl/6]).
-export([rename_item/2, create_directory/1, remove_selected/0, search/1, toggle_column/2, show_popup/1, hide_popup/0, path_navigator_body/1]).
-export([fs_list_dir/1, fs_mkdir/1, fs_remove/1, fs_remove_dir/1, fs_mv/2, fs_mv/3, fs_copy/2, fs_create_share/1]).
-export([fs_has_perms/2, fs_chmod/3, fs_get_acl/1, fs_set_acl/3]).

% All file attributes that are supported
-define(ALL_ATTRIBUTES, [perms, size, atime, mtime]).

% Attributes displayed by default
-define(DEFAULT_ATTRIBUTES, [size, atime, mtime]).

% How often should comet process check for changes in current dir
-define(AUTOREFRESH_PERIOD, 1000).


% Item is either a file or a dir represented in manager
-record(item, {
    id = <<"">>,
    path = <<"/">>,
    basename = <<"">>,
    is_shared = false,
    attr = #fileattributes{}}).


%% Check if user is logged in and has dn defined.
main() ->
    case opn_gui_utils:maybe_redirect(true, true) of
        true ->
            #dtl{file = "bare", app = ?APP_Name, bindings = [
                {title, <<"">>},
                {body, <<"">>},
                {custom, <<"">>},
                {css, <<"">>}
            ]};
        false ->
            #dtl{file = "bare", app = ?APP_Name, bindings = [
                {title, title()},
                {body, body()},
                {custom, custom()},
                {css, css()}
            ]}
    end.


%% Page title
title() -> <<"File manager">>.

%% This will be placed in the template instead of {{custom}} tag
custom() ->
    <<"<script src=\"/js/oneprovider_upload.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n",
    "    <script src=\"/js/file_manager.js\" type=\"text/javascript\" charset=\"utf-8\"></script>",
    "    <script src=\"/flatui/bootbox.min.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n">>.

%% This will be placed in the template instead of {{css}} tag
css() ->
    <<"<link rel=\"stylesheet\" href=\"/css/file_manager.css\" type=\"text/css\" media=\"screen\" charset=\"utf-8\" />">>.


%% This will be placed in the template instead of {{body}} tag
body() ->
    gui_jq:register_escape_event("escape_pressed_event"),
    gui_jq:wire(#api{name = "confirm_paste_event", tag = "confirm_paste_event"}, false),
    gui_jq:wire(#api{name = "change_perms_type_event", tag = "change_perms_type_event"}, false),
    gui_jq:wire(#api{name = "submit_perms_event", tag = "submit_perms_event"}, false),
    gui_jq:wire(#api{name = "add_acl_event", tag = "add_acl_event"}, false),
    gui_jq:wire(#api{name = "delete_acl_event", tag = "delete_acl_event"}, false),
    gui_jq:wire(#api{name = "edit_acl_event", tag = "edit_acl_event"}, false),
    gui_jq:wire(#api{name = "move_acl_event", tag = "move_acl_event"}, false),
    gui_jq:wire(#api{name = "submit_acl_event", tag = "submit_acl_event"}, false),
    Body = [
        #panel{id = <<"spinner">>, style = <<"position: absolute; top: 12px; left: 17px; z-index: 1234; width: 32px;">>, body = [
            #image{image = <<"/images/spinner.gif">>}
        ]},
        opn_gui_utils:top_menu(data_tab, manager_submenu()),
        manager_workspace(),
        footer_popup()
    ],
    Body.

% Submenu that will be glued below the top menu
manager_submenu() ->
    [
        #panel{class = <<"navbar-inner">>, style = <<"padding-top: 10px;">>, body = [
            #panel{class = <<"container">>, style = <<"position: relative; overflow: hidden;">>, body = [
                #list{class = <<"nav">>, body =
                tool_button_and_dummy(<<"tb_up_one_level">>, <<"Up one level">>, <<"padding: 10px 7px 10px 15px; font-size: 28px; margin: -2px 0 2px -8px;">>,
                    <<"icomoon-arrow-left">>, {action, up_one_level})},
                #panel{class = <<"breadcrumb-text breadcrumb-background">>, style = <<"overflow: hidden; margin-left: 15px;">>, body = [
                    #p{id = <<"path_navigator">>, class = <<"breadcrumb-content">>, body = <<"~">>}
                ]},
                #panel{class = <<"control-group">>, style = <<"position: absolute; right: 15px; top: 0;">>, body = [
                    #panel{class = <<"input-append">>, style = <<"; margin-bottom: 0px;">>, body = [
                        #textbox{id = wire_enter(<<"search_textbox">>, <<"search_button">>), class = <<"span2">>,
                            style = <<"width: 220px;">>, placeholder = <<"Search">>},
                        #panel{class = <<"btn-group">>, body = [
                            #button{id = wire_click(<<"search_button">>, {action, search, [{query_value, <<"search_textbox">>}]}, <<"search_textbox">>),
                                class = <<"btn">>, body = #span{class = <<"fui-search">>}}
                        ]}
                    ]}
                ]}
            ]}
        ]},
        #panel{class = <<"navbar-inner">>, style = <<"border-bottom: 1px solid gray; padding-bottom: 5px;">>, body = [
            #panel{class = <<"container">>, body = [
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                [#li{id = wire_click(<<"tb_create_dir">>, {action, show_popup, [create_directory]}), body = #link{title = <<"Create directory">>,
                    style = <<"padding: 16px 12px;">>, body = #span{class = <<"icomoon-folder-open">>, style = <<"font-size: 24px;">>,
                        body = #span{class = <<"icomoon-plus">>, style = <<"position: absolute; font-size: 10px; right: 5px; top: 16px;">>}}}}] ++
                    tool_button(<<"tb_upload_files">>, <<"Upload file(s)">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-upload">>, {action, show_popup, [file_upload]}) ++
                    tool_button_and_dummy(<<"tb_share_file">>, <<"Share">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-share">>, {action, show_popup, [share_file]})

                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_rename">>, <<"Rename">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-pencil2">>, {action, show_popup, [rename_item]}) ++
                    tool_button_and_dummy(<<"tb_chmod">>, <<"Change permissions">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-lock">>, {action, show_popup, [chmod]}) ++
                    tool_button_and_dummy(<<"tb_remove">>, <<"Remove">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-remove">>, {action, show_popup, [remove_selected]})
                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_cut">>, <<"Cut">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-scissors">>, {action, put_to_clipboard, [cut]}) ++
                %tool_button_and_dummy(<<"tb_copy">>, <<"Copy">>, <<"padding: 16px 12px;">>,
                %    <<"fui-windows">>, {action, put_to_clipboard, [copy]}) ++

                [#li{id = wire_click(<<"tb_paste">>, {action, paste_from_clipboard}), body = #link{title = <<"Paste">>, style = <<"padding: 16px 12px;">>,
                    body = #span{class = <<"icomoon-copy2">>, style = <<"font-size: 24px;">>, body = #span{id = <<"clipboard_size_label">>, class = <<"iconbar-unread">>,
                        style = <<"right: -2px; top: 9px; background-color: rgb(26, 188, 156);">>,
                        body = <<"0">>}}}},
                    #li{id = <<"tb_paste_dummy">>, class = <<"disabled hidden">>, body = #link{title = <<"Paste">>, style = <<"padding: 16px 12px;">>,
                        body = #span{style = <<"color: rgb(200, 200, 200); font-size: 24px;">>, class = <<"icomoon-copy2 ">>}}}]
                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_select_all">>, <<"Select all">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-checkbox-checked">>, {action, select_all}) ++
                tool_button_and_dummy(<<"tb_deselect_all">>, <<"Deselect all">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-checkbox-unchecked">>, {action, deselect_all})
                },

                #panel{class = <<"btn-toolbar pull-right no-margin">>, style = <<"padding: 12px 15px; overflow: hidden;">>, body = [
                    #panel{class = <<"btn-group no-margin">>, body = [
                        #link{id = wire_click(<<"list_view_button">>, {action, toggle_view, [list]}),
                            title = <<"List view">>, class = <<"btn btn-small btn-inverse">>,
                            body = #span{class = <<"fui-list-columned">>}},
                        #link{id = wire_click(<<"grid_view_button">>, {action, toggle_view, [grid]}),
                            title = <<"Grid view">>, class = <<"btn btn-small btn-inverse">>,
                            body = #span{class = <<"fui-list-small-thumbnails">>}}
                    ]}
                ]},

                #panel{class = <<"btn-group pull-right">>, style = <<"margin: 12px 15px">>, body = [
                    <<"<i class=\"dropdown-arrow dropdown-arrow-inverse\"></i>">>,
                    #button{id = wire_click(<<"button_sort_reverse">>, {action, sort_reverse}), title = <<"Reverse sorting">>,
                        class = <<"btn btn-inverse btn-small">>, body = <<"Sort">>},
                    #button{title = <<"Sort by">>, class = <<"btn btn-inverse btn-small dropdown-toggle">>,
                        data_fields = [{<<"data-toggle">>, <<"dropdown">>}], body = #span{class = <<"caret">>}},
                    #list{id = <<"sort_dropdown">>, class = <<"dropdown-menu dropdown-inverse">>, body = []}
                ]}
            ]}
        ]}
    ].


% Working space of the explorer.
manager_workspace() ->
    #panel{id = <<"manager_workspace">>, style = <<"z-index: -1; margin: 170px 0 20px 0; overflow: hidden">>, body = []}.


% Footer popup to display prompts and forms.
footer_popup() ->
    #panel{id = <<"footer_popup">>, class = <<"dialog success-dialog wide hidden">>,
        style = <<"z-index: 2; position:fixed; bottom: 0; margin-bottom: 0px; padding: 20px 0px; width: 100%;">>, body = []
    }.


% Emits a button, properly wired (postback)
tool_button(ID, Title, Style, Icon, Postback) ->
    [
        #li{id = wire_click(ID, Postback), body = #link{title = Title, style = Style,
            body = #span{class = Icon, style = <<"font-size: 24px;">>}}}
    ].


% Emits a button, properly wired (postback) + its disabled clone
tool_button_and_dummy(ID, Title, Style, Icon, Postback) ->
    tool_button(ID, Title, Style, Icon, Postback) ++
    [
        #li{id = <<ID/binary, "_dummy">>, class = <<"disabled hidden">>, body = #link{title = Title, style = Style,
            body = #span{style = <<"color: rgb(200, 200, 200); font-size: 24px;">>, class = Icon}}}
    ].


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Wiring postbacks. Thanks to this wrapper every time a postback is initiated,
%% there will be spinner showing up in 150ms. It gets hidden when reply is received.
wire_click(ID, Tag) ->
    gui_jq:wire(gui_jq:postback_action(ID, Tag)),
    gui_jq:bind_element_click(ID, <<"function(e) { $('#spinner').delay(150).show(); }">>),
    ID.

wire_click(ID, Tag, Source) ->
    gui_jq:wire(gui_jq:form_submit_action(ID, Tag, Source)),
    gui_jq:bind_element_click(ID, <<"function(e) { $('#spinner').delay(150).show(); }">>),
    ID.

wire_enter(ID, ButtonToClickID) ->
    % No need to show the spinner, as this only performs a click on a submit button
    gui_jq:bind_enter_to_submit_button(ID, ButtonToClickID),
    ID.


%% Handling events
api_event("escape_pressed_event", _, _) ->
    event({action, hide_popup});

api_event("confirm_paste_event", _, _) ->
    event({action, confirm_paste});

api_event("submit_perms_event", Args, _Ctx) ->
    [Perms, Recursive] = mochijson2:decode(Args),
    event({action, submit_perms, [Perms, Recursive]});

api_event("change_perms_type_event", Args, _Ctx) ->
    EnableACL = mochijson2:decode(Args),
    event({action, change_perms_type, [EnableACL]});

api_event("add_acl_event", _Args, _) ->
    event({action, add_acl});

api_event("delete_acl_event", Args, _) ->
    IndexRaw = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, delete_acl, [Index]});

api_event("edit_acl_event", Args, _) ->
    IndexRaw = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, edit_acl, [Index]});

api_event("move_acl_event", Args, _) ->
    [IndexRaw, MoveUp] = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, move_acl, [Index, MoveUp]});

api_event("submit_acl_event", Args, _) ->
    [IndexRaw, Identifier, Type, Read, Write, Execute] = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, submit_acl, [Index, Identifier, Type, Read, Write, Execute]}).


event(init) ->
    case gui_ctx:user_logged_in() and opn_gui_utils:storage_defined() of
        false ->
            skip;
        true ->
            GRUID = opn_gui_utils:get_global_user_id(),
            AccessToken = opn_gui_utils:get_access_token(),
            Hostname = gui_ctx:get_requested_hostname(),
            {ok, Pid} = gui_comet:spawn(fun() -> comet_loop_init(GRUID, AccessToken, Hostname) end),
            put(comet_pid, Pid)
    end;


event(terminate) ->
    ok;


event({action, Fun}) ->
    event({action, Fun, []});


event({action, Fun, Args}) ->
    NewArgs = lists:map(
        fun(Arg) ->
            case Arg of
                {query_value, FieldName} ->
                    % This tuple means, that element with id=FieldName has to be queried
                    % and the result be put in function args
                    gui_ctx:postback_param(FieldName);
                Other ->
                    Other
            end
        end, Args),
    opn_gui_utils:apply_or_redirect(erlang, send, [get(comet_pid), {action, Fun, NewArgs}]).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Comet loop and functions evaluated by comet
comet_loop_init(GRUID, UserAccessToken, RequestedHostname) ->
    % Initialize page state
    fslogic_context:set_gr_auth(GRUID, UserAccessToken),

    set_requested_hostname(RequestedHostname),
    set_working_directory(<<"/">>),
    set_selected_items([]),
    set_display_style(list),
    set_sort_by(name),
    set_sort_ascending(true),
    set_item_counter(1),
    set_item_list(fs_list_dir(get_working_directory())),
    set_item_list_rev(item_list_md5(get_item_list())),
    set_clipboard_items([]),
    set_clipboard_type(none),
    refresh_workspace(),
    gui_jq:hide(<<"spinner">>),
    gui_comet:flush(),

    % Enter comet loop for event processing and autorefreshing
    comet_loop(false).


comet_loop(IsUploadInProgress) ->
    NewIsUploadInProgress =
        try
            receive
                {action, Fun, Args} ->
                    case IsUploadInProgress of
                        true ->
                            gui_jq:info_popup(<<"Upload in progress">>,
                                <<"Please wait for the upload to finish.">>, <<"">>),
                            gui_comet:flush();
                        false ->
                            erlang:apply(?MODULE, Fun, Args)
                    end,
                    gui_jq:hide(<<"spinner">>),
                    gui_comet:flush(),
                    IsUploadInProgress;
                upload_started ->
                    true;
                upload_finished ->
                    false;
                Other ->
                    ?debug("Unrecognized comet message in page_file_manager: ~p", [Other]),
                    IsUploadInProgress

            after ?AUTOREFRESH_PERIOD ->
                % Refresh file list if it has changed
                CurrentItemList = fs_list_dir(get_working_directory()),
                CurrentMD5 = item_list_md5(CurrentItemList),
                case get_item_list_rev() of
                    CurrentMD5 ->
                        skip;
                    _ ->
                        set_item_list(CurrentItemList),
                        set_item_list_rev(CurrentMD5),
                        refresh_workspace(),
                        gui_comet:flush()
                end,
                IsUploadInProgress
            end

        catch Type:Message ->
            ?error_stacktrace("Error in file_manager comet_loop - ~p:~p", [Type, Message]),
            page_error:redirect_with_error(?error_internal_server_error),
            gui_comet:flush(),
            error
        end,
    case NewIsUploadInProgress of
        error -> ok; % Comet process will terminate
        _ -> ?MODULE:comet_loop(NewIsUploadInProgress)
    end.


%%%%%%%%%%%%%%%
%% Event handling
clear_manager() ->
    hide_popup(),
    gui_jq:update(<<"path_navigator">>, path_navigator_body(get_working_directory())),
    clear_workspace().


clear_workspace() ->
    set_item_list(fs_list_dir(get_working_directory())),
    set_item_list_rev(item_list_md5(get_item_list())),
    refresh_workspace().


refresh_workspace() ->
    set_selected_items([]),
    refresh_tool_buttons(),
    sort_item_list(),
    NewBody = case get_display_style() of
                  list -> list_view_body();
                  grid -> grid_view_body()
              end,
    gui_jq:update(<<"manager_workspace">>, NewBody).


sort_item_list() ->
    AllItems = get_item_list(),
    {SpacesDirList, ItemList} = lists:partition(
        fun(I) ->
            is_space_dir(item_path(I))
        end, AllItems),
    Attr = get_sort_by(),
    SortAscending = get_sort_ascending(),
    SortedItems = lists:sort(
        fun(Item1, Item2) ->
            {V1, V2} = case item_attr(Attr, Item1) of
                           L when is_list(L) ->
                               {string:to_lower(item_attr(Attr, Item1)),
                                   string:to_lower(item_attr(Attr, Item2))};
                           _ ->
                               {item_attr(Attr, Item1), item_attr(Attr, Item2)}
                       end,
            if
                V1 < V2 -> true;
                V1 > V2 -> false;
                V1 =:= V2 -> item_attr(name, Item1) =< item_attr(name, Item2)
            end
        end, ItemList),
    Result = case SortAscending of
                 true -> SortedItems;
                 false -> lists:reverse(SortedItems)
             end,
    FinalResult = case Attr of
                      name ->
                          {Dirs, Files} = lists:partition(fun(I) -> item_is_dir(I) end, Result),
                          Dirs ++ Files;
                      _ -> Result
                  end,
    set_item_list(SpacesDirList ++ FinalResult).


sort_toggle(Type) ->
    case get_sort_by() of
        Type -> sort_reverse();
        _ ->
            set_sort_by(Type),
            set_sort_ascending(true),
            refresh_workspace()
    end.


sort_reverse() ->
    set_sort_ascending(not get_sort_ascending()),
    refresh_workspace().


refresh_tool_buttons() ->
    % View toggling buttons
    {EnableID, DisableID} = case get_display_style() of
                                list -> {<<"list_view_button">>, <<"grid_view_button">>};
                                grid -> {<<"grid_view_button">>, <<"list_view_button">>}
                            end,
    gui_jq:add_class(EnableID, <<"active">>),
    gui_jq:remove_class(DisableID, <<"active">>),

    % Sort dropdown
    DropdownBody = case get_display_style() of
                       grid ->
                           #li{id = wire_click(<<"grid_sort_by_name">>, {action, sort_toggle, [name]}),
                               class = <<"active">>, body = #link{body = <<"Name">>}};
                       list ->
                           lists:foldl(
                               fun(Attr, Acc) ->
                                   Class = case get_sort_by() of
                                               Attr -> <<"active">>;
                                               _ -> <<"">>
                                           end,
                                   Acc ++ [#li{id = wire_click(<<"list_sort_by_", (atom_to_binary(Attr, latin1))/binary>>, {action, sort_toggle, [Attr]}),
                                       class = Class, body = #link{body = attr_to_name(Attr)}}]
                               end, [], [name | get_displayed_file_attributes()])
                   end,
    gui_jq:update(<<"sort_dropdown">>, DropdownBody),

    Count = length(get_selected_items()),
    NFiles = length(get_item_list()),
    IsDir = try item_is_dir(item_find(element(1, lists:nth(1, get_selected_items())))) catch _:_ -> false end,
    enable_tool_button(<<"tb_up_one_level">>, get_working_directory() /= <<"/">>),
    enable_tool_button(<<"tb_share_file">>, (Count =:= 1) andalso (not IsDir)),
    enable_tool_button(<<"tb_rename">>, Count =:= 1),
    enable_tool_button(<<"tb_chmod">>, Count > 0),
    enable_tool_button(<<"tb_remove">>, Count > 0),
    enable_tool_button(<<"tb_cut">>, Count > 0),
    enable_tool_button(<<"tb_copy">>, false),
    enable_tool_button(<<"tb_paste">>, length(get_clipboard_items()) > 0),
    gui_jq:update(<<"clipboard_size_label">>, integer_to_binary(length(get_clipboard_items()))),
    enable_tool_button(<<"tb_select_all">>, Count < NFiles),
    enable_tool_button(<<"tb_deselect_all">>, Count > 0).


enable_tool_button(ID, Flag) ->
    case Flag of
        true ->
            gui_jq:remove_class(ID, <<"hidden">>),
            gui_jq:add_class(<<ID/binary, "_dummy">>, <<"hidden">>),
            gui_jq:show(ID),
            gui_jq:hide(<<ID/binary, "_dummy">>);
        false ->
            gui_jq:add_class(ID, <<"hidden">>),
            gui_jq:remove_class(<<ID/binary, "_dummy">>, <<"hidden">>),
            gui_jq:hide(ID),
            gui_jq:show(<<ID/binary, "_dummy">>)
    end.


navigate(Path) ->
    set_working_directory(Path),
    clear_manager().


up_one_level() ->
    navigate(filename:dirname(filename:absname(get_working_directory()))).


toggle_view(Type) ->
    set_display_style(Type),
    set_sort_by(name),
    set_sort_ascending(true),
    clear_workspace().


select_item(Path) ->
    case item_find(Path) of
        undefined ->
            skip;
        Item ->
            SelectedItems = get_selected_items(),
            Basename = item_basename(Item),
            case lists:member({Path, Basename}, SelectedItems) of
                false ->
                    set_selected_items(SelectedItems ++ [{Path, Basename}]),
                    gui_jq:add_class(item_id(Item), <<"selected-item">>);
                true ->
                    set_selected_items(SelectedItems -- [{Path, Basename}]),
                    gui_jq:remove_class(item_id(Item), <<"selected-item">>)
            end
    end,
    refresh_tool_buttons().


select_all() ->
    set_selected_items([]),
    lists:foreach(
        fun(Item) ->
            set_selected_items(get_selected_items() ++ [{item_path(Item), item_basename(Item)}]),
            gui_jq:add_class(item_id(Item), <<"selected-item">>)
        end, get_item_list()),
    refresh_tool_buttons().


deselect_all() ->
    lists:foreach(
        fun(Item) ->
            gui_jq:remove_class(item_id(Item), <<"selected-item">>)
        end, get_item_list()),
    set_selected_items([]),
    refresh_tool_buttons().


clear_clipboard() ->
    set_clipboard_items([]),
    set_clipboard_type(none).


put_to_clipboard(Type) ->
    SelectedItems = get_selected_items(),
    set_clipboard_type(Type),
    set_clipboard_items(SelectedItems),
    clear_workspace().


paste_from_clipboard() ->
    [{FirstPath, _} | _] = get_clipboard_items(),
    % Check if mv is between different spaces
    case is_the_same_space(filename:dirname(FirstPath), get_working_directory()) of
        false ->
            gui_jq:confirm_popup(<<"Do you really want to move your file(s) between spaces and ",
            "share them with all members of the target space?">>, <<"confirm_paste_event();">>);
        true ->
            confirm_paste()
    end.


confirm_paste() ->
    ClipboardItems = get_clipboard_items(),
    ClipboardType = get_clipboard_type(),
    WorkingDirectory = get_working_directory(),
    clear_clipboard(),
    ErrorMessage = lists:foldl(
        fun({Path, Basename}, Acc) ->
            case ClipboardType of
                cut ->
                    case fs_mv(Path, WorkingDirectory) of
                        ok ->
                            Acc;
                        {logical_file_system_error, "eperm"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - insufficient permissions.\r\n">>;
                        {logical_file_system_error, "eexist"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - file exists.\r\n">>;
                        {logical_file_system_error, "eacces"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - insufficient permissions.\r\n">>;
                        _ ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - error occured.\r\n">>
                    end;
                copy ->
                    % Not yet implemented
                    fs_copy(Path, WorkingDirectory),
                    Acc
            end
        end, <<"">>, ClipboardItems),
    case ErrorMessage of
        <<"">> ->
            ok;
        _ ->
            gui_jq:info_popup(<<"Error(s) occured">>, ErrorMessage, <<"">>)
    end,
    clear_workspace().


submit_perms(Perms, Recursive) ->
    {Files, ACLEnabled, ACLEntries} = get_perms_state(),
    {Failed, Message} = case ACLEnabled of
                            true ->
                                FailedFiles = lists:foldl(
                                    fun(Path, Acc) ->
                                        {_Successful, Failed} = fs_set_acl(Path, ACLEntries, Recursive),
                                        Acc ++ Failed
                                    end, [], Files),
                                {FailedFiles, <<"Unable to set ACL for following file(s):">>};
                            false ->
                                FailedFiles = lists:foldl(
                                    fun(Path, Acc) ->
                                        {_Successful, Failed} = fs_chmod(Path, Perms, Recursive),
                                        Acc ++ Failed
                                    end, [], Files),
                                {FailedFiles, <<"Unable to change permissions for following file(s):">>}
                        end,
    case Failed of
        [] ->
            ok;
        _ ->
            FailedList = lists:foldl(
                fun({Path, Reason}, Acc) ->
                    ReasonBin = case Reason of
                                    {logical_file_system_error, "eacces"} ->
                                        <<"insufficient permissions">>;
                                    _ ->
                                        <<"error occured">>
                                end,
                    <<Acc/binary, Path/binary, ": ", ReasonBin/binary, "\r\n">>
                end, <<"">>, Failed),
            gui_jq:info_popup(<<"Error(s) occured">>,
                <<Message/binary, "\r\n\r\n", FailedList/binary>>, <<"">>)
    end,
    clear_manager().


rename_item(OldPath, NewName) ->
    OldName = filename:basename(OldPath),
    case NewName of
        [] -> hide_popup();
        undefined -> hide_popup();
        OldName -> hide_popup();
        _ ->
            NewPath = filename:absname(NewName, get_working_directory()),
            ErrorMessage = case fs_mv(OldPath, get_working_directory(), NewName) of
                               ok ->
                                   clear_clipboard(),
                                   clear_manager(),
                                   select_item(NewPath),
                                   none;
                               {logical_file_system_error, "eperm"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - insufficient permissions.">>;
                               {logical_file_system_error, "eexist"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - file exists.">>;
                               {logical_file_system_error, "eacces"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - insufficient permissions.">>;
                               _ ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - error occured.">>
                           end,
            case ErrorMessage of
                none -> ok;
                _ -> gui_jq:info_popup(<<"Error(s) occured">>, ErrorMessage, <<"">>)<commit_msg>VFS-886, update alerts<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 file contains n2o website code.
%% The page is a file manager providing basic functionalities.
%% @end
%% ===================================================================

-module(page_file_manager).
-include("oneprovider_modules/control_panel/common.hrl").
-include("oneprovider_modules/fslogic/fslogic.hrl").
-include("oneprovider_modules/fslogic/fslogic_acl.hrl").
-include("oneprovider_modules/dao/dao_users.hrl").
-include("fuse_messages_pb.hrl").
-include_lib("ctool/include/logging.hrl").

% n2o API
-export([main/0, event/1, api_event/3]).
% Postback functions and other
-export([get_requested_hostname/0, comet_loop/1]).
-export([clear_manager/0, clear_workspace/0, sort_toggle/1, sort_reverse/0, navigate/1, up_one_level/0]).
-export([toggle_view/1, select_item/1, select_all/0, deselect_all/0, clear_clipboard/0, put_to_clipboard/1, paste_from_clipboard/0]).
-export([confirm_paste/0, submit_perms/2, show_permissions_info/0]).
-export([populate_acl_list/1, change_perms_type/1, add_acl/0, delete_acl/1, edit_acl/1, move_acl/2, submit_acl/6]).
-export([rename_item/2, create_directory/1, remove_selected/0, search/1, toggle_column/2, show_popup/1, hide_popup/0, path_navigator_body/1]).
-export([fs_list_dir/1, fs_mkdir/1, fs_remove/1, fs_remove_dir/1, fs_mv/2, fs_mv/3, fs_copy/2, fs_create_share/1]).
-export([fs_has_perms/2, fs_chmod/3, fs_get_acl/1, fs_set_acl/3]).

% All file attributes that are supported
-define(ALL_ATTRIBUTES, [perms, size, atime, mtime]).

% Attributes displayed by default
-define(DEFAULT_ATTRIBUTES, [size, atime, mtime]).

% How often should comet process check for changes in current dir
-define(AUTOREFRESH_PERIOD, 1000).


% Item is either a file or a dir represented in manager
-record(item, {
    id = <<"">>,
    path = <<"/">>,
    basename = <<"">>,
    is_shared = false,
    attr = #fileattributes{}}).


%% Check if user is logged in and has dn defined.
main() ->
    case opn_gui_utils:maybe_redirect(true, true) of
        true ->
            #dtl{file = "bare", app = ?APP_Name, bindings = [
                {title, <<"">>},
                {body, <<"">>},
                {custom, <<"">>},
                {css, <<"">>}
            ]};
        false ->
            #dtl{file = "bare", app = ?APP_Name, bindings = [
                {title, title()},
                {body, body()},
                {custom, custom()},
                {css, css()}
            ]}
    end.


%% Page title
title() -> <<"File manager">>.

%% This will be placed in the template instead of {{custom}} tag
custom() ->
    <<"<script src=\"/js/oneprovider_upload.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n",
    "    <script src=\"/js/file_manager.js\" type=\"text/javascript\" charset=\"utf-8\"></script>",
    "    <script src=\"/flatui/bootbox.min.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n">>.

%% This will be placed in the template instead of {{css}} tag
css() ->
    <<"<link rel=\"stylesheet\" href=\"/css/file_manager.css\" type=\"text/css\" media=\"screen\" charset=\"utf-8\" />">>.


%% This will be placed in the template instead of {{body}} tag
body() ->
    gui_jq:register_escape_event("escape_pressed_event"),
    gui_jq:wire(#api{name = "confirm_paste_event", tag = "confirm_paste_event"}, false),
    gui_jq:wire(#api{name = "change_perms_type_event", tag = "change_perms_type_event"}, false),
    gui_jq:wire(#api{name = "submit_perms_event", tag = "submit_perms_event"}, false),
    gui_jq:wire(#api{name = "add_acl_event", tag = "add_acl_event"}, false),
    gui_jq:wire(#api{name = "delete_acl_event", tag = "delete_acl_event"}, false),
    gui_jq:wire(#api{name = "edit_acl_event", tag = "edit_acl_event"}, false),
    gui_jq:wire(#api{name = "move_acl_event", tag = "move_acl_event"}, false),
    gui_jq:wire(#api{name = "submit_acl_event", tag = "submit_acl_event"}, false),
    Body = [
        #panel{id = <<"spinner">>, style = <<"position: absolute; top: 12px; left: 17px; z-index: 1234; width: 32px;">>, body = [
            #image{image = <<"/images/spinner.gif">>}
        ]},
        opn_gui_utils:top_menu(data_tab, manager_submenu()),
        manager_workspace(),
        footer_popup()
    ],
    Body.

% Submenu that will be glued below the top menu
manager_submenu() ->
    [
        #panel{class = <<"navbar-inner">>, style = <<"padding-top: 10px;">>, body = [
            #panel{class = <<"container">>, style = <<"position: relative; overflow: hidden;">>, body = [
                #list{class = <<"nav">>, body =
                tool_button_and_dummy(<<"tb_up_one_level">>, <<"Up one level">>, <<"padding: 10px 7px 10px 15px; font-size: 28px; margin: -2px 0 2px -8px;">>,
                    <<"icomoon-arrow-left">>, {action, up_one_level})},
                #panel{class = <<"breadcrumb-text breadcrumb-background">>, style = <<"overflow: hidden; margin-left: 15px;">>, body = [
                    #p{id = <<"path_navigator">>, class = <<"breadcrumb-content">>, body = <<"~">>}
                ]},
                #panel{class = <<"control-group">>, style = <<"position: absolute; right: 15px; top: 0;">>, body = [
                    #panel{class = <<"input-append">>, style = <<"; margin-bottom: 0px;">>, body = [
                        #textbox{id = wire_enter(<<"search_textbox">>, <<"search_button">>), class = <<"span2">>,
                            style = <<"width: 220px;">>, placeholder = <<"Search">>},
                        #panel{class = <<"btn-group">>, body = [
                            #button{id = wire_click(<<"search_button">>, {action, search, [{query_value, <<"search_textbox">>}]}, <<"search_textbox">>),
                                class = <<"btn">>, body = #span{class = <<"fui-search">>}}
                        ]}
                    ]}
                ]}
            ]}
        ]},
        #panel{class = <<"navbar-inner">>, style = <<"border-bottom: 1px solid gray; padding-bottom: 5px;">>, body = [
            #panel{class = <<"container">>, body = [
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                [#li{id = wire_click(<<"tb_create_dir">>, {action, show_popup, [create_directory]}), body = #link{title = <<"Create directory">>,
                    style = <<"padding: 16px 12px;">>, body = #span{class = <<"icomoon-folder-open">>, style = <<"font-size: 24px;">>,
                        body = #span{class = <<"icomoon-plus">>, style = <<"position: absolute; font-size: 10px; right: 5px; top: 16px;">>}}}}] ++
                    tool_button(<<"tb_upload_files">>, <<"Upload file(s)">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-upload">>, {action, show_popup, [file_upload]}) ++
                    tool_button_and_dummy(<<"tb_share_file">>, <<"Share">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-share">>, {action, show_popup, [share_file]})

                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_rename">>, <<"Rename">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-pencil2">>, {action, show_popup, [rename_item]}) ++
                    tool_button_and_dummy(<<"tb_chmod">>, <<"Change permissions">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-lock">>, {action, show_popup, [chmod]}) ++
                    tool_button_and_dummy(<<"tb_remove">>, <<"Remove">>, <<"padding: 16px 12px;">>,
                        <<"icomoon-remove">>, {action, show_popup, [remove_selected]})
                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_cut">>, <<"Cut">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-scissors">>, {action, put_to_clipboard, [cut]}) ++
                %tool_button_and_dummy(<<"tb_copy">>, <<"Copy">>, <<"padding: 16px 12px;">>,
                %    <<"fui-windows">>, {action, put_to_clipboard, [copy]}) ++

                [#li{id = wire_click(<<"tb_paste">>, {action, paste_from_clipboard}), body = #link{title = <<"Paste">>, style = <<"padding: 16px 12px;">>,
                    body = #span{class = <<"icomoon-copy2">>, style = <<"font-size: 24px;">>, body = #span{id = <<"clipboard_size_label">>, class = <<"iconbar-unread">>,
                        style = <<"right: -2px; top: 9px; background-color: rgb(26, 188, 156);">>,
                        body = <<"0">>}}}},
                    #li{id = <<"tb_paste_dummy">>, class = <<"disabled hidden">>, body = #link{title = <<"Paste">>, style = <<"padding: 16px 12px;">>,
                        body = #span{style = <<"color: rgb(200, 200, 200); font-size: 24px;">>, class = <<"icomoon-copy2 ">>}}}]
                },
                #list{class = <<"nav">>, style = <<"margin-right: 30px;">>, body =
                tool_button_and_dummy(<<"tb_select_all">>, <<"Select all">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-checkbox-checked">>, {action, select_all}) ++
                tool_button_and_dummy(<<"tb_deselect_all">>, <<"Deselect all">>, <<"padding: 16px 12px;">>,
                    <<"icomoon-checkbox-unchecked">>, {action, deselect_all})
                },

                #panel{class = <<"btn-toolbar pull-right no-margin">>, style = <<"padding: 12px 15px; overflow: hidden;">>, body = [
                    #panel{class = <<"btn-group no-margin">>, body = [
                        #link{id = wire_click(<<"list_view_button">>, {action, toggle_view, [list]}),
                            title = <<"List view">>, class = <<"btn btn-small btn-inverse">>,
                            body = #span{class = <<"fui-list-columned">>}},
                        #link{id = wire_click(<<"grid_view_button">>, {action, toggle_view, [grid]}),
                            title = <<"Grid view">>, class = <<"btn btn-small btn-inverse">>,
                            body = #span{class = <<"fui-list-small-thumbnails">>}}
                    ]}
                ]},

                #panel{class = <<"btn-group pull-right">>, style = <<"margin: 12px 15px">>, body = [
                    <<"<i class=\"dropdown-arrow dropdown-arrow-inverse\"></i>">>,
                    #button{id = wire_click(<<"button_sort_reverse">>, {action, sort_reverse}), title = <<"Reverse sorting">>,
                        class = <<"btn btn-inverse btn-small">>, body = <<"Sort">>},
                    #button{title = <<"Sort by">>, class = <<"btn btn-inverse btn-small dropdown-toggle">>,
                        data_fields = [{<<"data-toggle">>, <<"dropdown">>}], body = #span{class = <<"caret">>}},
                    #list{id = <<"sort_dropdown">>, class = <<"dropdown-menu dropdown-inverse">>, body = []}
                ]}
            ]}
        ]}
    ].


% Working space of the explorer.
manager_workspace() ->
    #panel{id = <<"manager_workspace">>, style = <<"z-index: -1; margin: 170px 0 20px 0; overflow: hidden">>, body = []}.


% Footer popup to display prompts and forms.
footer_popup() ->
    #panel{id = <<"footer_popup">>, class = <<"dialog success-dialog wide hidden">>,
        style = <<"z-index: 2; position:fixed; bottom: 0; margin-bottom: 0px; padding: 20px 0px; width: 100%;">>, body = []
    }.


% Emits a button, properly wired (postback)
tool_button(ID, Title, Style, Icon, Postback) ->
    [
        #li{id = wire_click(ID, Postback), body = #link{title = Title, style = Style,
            body = #span{class = Icon, style = <<"font-size: 24px;">>}}}
    ].


% Emits a button, properly wired (postback) + its disabled clone
tool_button_and_dummy(ID, Title, Style, Icon, Postback) ->
    tool_button(ID, Title, Style, Icon, Postback) ++
    [
        #li{id = <<ID/binary, "_dummy">>, class = <<"disabled hidden">>, body = #link{title = Title, style = Style,
            body = #span{style = <<"color: rgb(200, 200, 200); font-size: 24px;">>, class = Icon}}}
    ].


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Wiring postbacks. Thanks to this wrapper every time a postback is initiated,
%% there will be spinner showing up in 150ms. It gets hidden when reply is received.
wire_click(ID, Tag) ->
    gui_jq:wire(gui_jq:postback_action(ID, Tag)),
    gui_jq:bind_element_click(ID, <<"function(e) { $('#spinner').delay(150).show(); }">>),
    ID.

wire_click(ID, Tag, Source) ->
    gui_jq:wire(gui_jq:form_submit_action(ID, Tag, Source)),
    gui_jq:bind_element_click(ID, <<"function(e) { $('#spinner').delay(150).show(); }">>),
    ID.

wire_enter(ID, ButtonToClickID) ->
    % No need to show the spinner, as this only performs a click on a submit button
    gui_jq:bind_enter_to_submit_button(ID, ButtonToClickID),
    ID.


%% Handling events
api_event("escape_pressed_event", _, _) ->
    event({action, hide_popup});

api_event("confirm_paste_event", _, _) ->
    event({action, confirm_paste});

api_event("submit_perms_event", Args, _Ctx) ->
    [Perms, Recursive] = mochijson2:decode(Args),
    event({action, submit_perms, [Perms, Recursive]});

api_event("change_perms_type_event", Args, _Ctx) ->
    EnableACL = mochijson2:decode(Args),
    event({action, change_perms_type, [EnableACL]});

api_event("add_acl_event", _Args, _) ->
    event({action, add_acl});

api_event("delete_acl_event", Args, _) ->
    IndexRaw = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, delete_acl, [Index]});

api_event("edit_acl_event", Args, _) ->
    IndexRaw = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, edit_acl, [Index]});

api_event("move_acl_event", Args, _) ->
    [IndexRaw, MoveUp] = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, move_acl, [Index, MoveUp]});

api_event("submit_acl_event", Args, _) ->
    [IndexRaw, Identifier, Type, Read, Write, Execute] = mochijson2:decode(Args),
    Index = case IndexRaw of
                I when is_integer(I) -> I;
                Bin when is_binary(Bin) -> binary_to_integer(Bin)
            end,
    event({action, submit_acl, [Index, Identifier, Type, Read, Write, Execute]}).


event(init) ->
    case gui_ctx:user_logged_in() and opn_gui_utils:storage_defined() of
        false ->
            skip;
        true ->
            GRUID = opn_gui_utils:get_global_user_id(),
            AccessToken = opn_gui_utils:get_access_token(),
            Hostname = gui_ctx:get_requested_hostname(),
            {ok, Pid} = gui_comet:spawn(fun() -> comet_loop_init(GRUID, AccessToken, Hostname) end),
            put(comet_pid, Pid)
    end;


event(terminate) ->
    ok;


event({action, Fun}) ->
    event({action, Fun, []});


event({action, Fun, Args}) ->
    NewArgs = lists:map(
        fun(Arg) ->
            case Arg of
                {query_value, FieldName} ->
                    % This tuple means, that element with id=FieldName has to be queried
                    % and the result be put in function args
                    gui_ctx:postback_param(FieldName);
                Other ->
                    Other
            end
        end, Args),
    opn_gui_utils:apply_or_redirect(erlang, send, [get(comet_pid), {action, Fun, NewArgs}]).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Comet loop and functions evaluated by comet
comet_loop_init(GRUID, UserAccessToken, RequestedHostname) ->
    % Initialize page state
    fslogic_context:set_gr_auth(GRUID, UserAccessToken),

    set_requested_hostname(RequestedHostname),
    set_working_directory(<<"/">>),
    set_selected_items([]),
    set_display_style(list),
    set_sort_by(name),
    set_sort_ascending(true),
    set_item_counter(1),
    set_item_list(fs_list_dir(get_working_directory())),
    set_item_list_rev(item_list_md5(get_item_list())),
    set_clipboard_items([]),
    set_clipboard_type(none),
    refresh_workspace(),
    gui_jq:hide(<<"spinner">>),
    gui_comet:flush(),

    % Enter comet loop for event processing and autorefreshing
    comet_loop(false).


comet_loop(IsUploadInProgress) ->
    NewIsUploadInProgress =
        try
            receive
                {action, Fun, Args} ->
                    case IsUploadInProgress of
                        true ->
                            gui_jq:info_popup(<<"Upload in progress">>,
                                <<"Please wait for the upload to finish.">>, <<"">>),
                            gui_comet:flush();
                        false ->
                            erlang:apply(?MODULE, Fun, Args)
                    end,
                    gui_jq:hide(<<"spinner">>),
                    gui_comet:flush(),
                    IsUploadInProgress;
                upload_started ->
                    true;
                upload_finished ->
                    false;
                Other ->
                    ?debug("Unrecognized comet message in page_file_manager: ~p", [Other]),
                    IsUploadInProgress

            after ?AUTOREFRESH_PERIOD ->
                % Refresh file list if it has changed
                CurrentItemList = fs_list_dir(get_working_directory()),
                CurrentMD5 = item_list_md5(CurrentItemList),
                case get_item_list_rev() of
                    CurrentMD5 ->
                        skip;
                    _ ->
                        set_item_list(CurrentItemList),
                        set_item_list_rev(CurrentMD5),
                        refresh_workspace(),
                        gui_comet:flush()
                end,
                IsUploadInProgress
            end

        catch Type:Message ->
            ?error_stacktrace("Error in file_manager comet_loop - ~p:~p", [Type, Message]),
            page_error:redirect_with_error(?error_internal_server_error),
            gui_comet:flush(),
            error
        end,
    case NewIsUploadInProgress of
        error -> ok; % Comet process will terminate
        _ -> ?MODULE:comet_loop(NewIsUploadInProgress)
    end.


%%%%%%%%%%%%%%%
%% Event handling
clear_manager() ->
    hide_popup(),
    gui_jq:update(<<"path_navigator">>, path_navigator_body(get_working_directory())),
    clear_workspace().


clear_workspace() ->
    set_item_list(fs_list_dir(get_working_directory())),
    set_item_list_rev(item_list_md5(get_item_list())),
    refresh_workspace().


refresh_workspace() ->
    set_selected_items([]),
    refresh_tool_buttons(),
    sort_item_list(),
    NewBody = case get_display_style() of
                  list -> list_view_body();
                  grid -> grid_view_body()
              end,
    gui_jq:update(<<"manager_workspace">>, NewBody).


sort_item_list() ->
    AllItems = get_item_list(),
    {SpacesDirList, ItemList} = lists:partition(
        fun(I) ->
            is_space_dir(item_path(I))
        end, AllItems),
    Attr = get_sort_by(),
    SortAscending = get_sort_ascending(),
    SortedItems = lists:sort(
        fun(Item1, Item2) ->
            {V1, V2} = case item_attr(Attr, Item1) of
                           L when is_list(L) ->
                               {string:to_lower(item_attr(Attr, Item1)),
                                   string:to_lower(item_attr(Attr, Item2))};
                           _ ->
                               {item_attr(Attr, Item1), item_attr(Attr, Item2)}
                       end,
            if
                V1 < V2 -> true;
                V1 > V2 -> false;
                V1 =:= V2 -> item_attr(name, Item1) =< item_attr(name, Item2)
            end
        end, ItemList),
    Result = case SortAscending of
                 true -> SortedItems;
                 false -> lists:reverse(SortedItems)
             end,
    FinalResult = case Attr of
                      name ->
                          {Dirs, Files} = lists:partition(fun(I) -> item_is_dir(I) end, Result),
                          Dirs ++ Files;
                      _ -> Result
                  end,
    set_item_list(SpacesDirList ++ FinalResult).


sort_toggle(Type) ->
    case get_sort_by() of
        Type -> sort_reverse();
        _ ->
            set_sort_by(Type),
            set_sort_ascending(true),
            refresh_workspace()
    end.


sort_reverse() ->
    set_sort_ascending(not get_sort_ascending()),
    refresh_workspace().


refresh_tool_buttons() ->
    % View toggling buttons
    {EnableID, DisableID} = case get_display_style() of
                                list -> {<<"list_view_button">>, <<"grid_view_button">>};
                                grid -> {<<"grid_view_button">>, <<"list_view_button">>}
                            end,
    gui_jq:add_class(EnableID, <<"active">>),
    gui_jq:remove_class(DisableID, <<"active">>),

    % Sort dropdown
    DropdownBody = case get_display_style() of
                       grid ->
                           #li{id = wire_click(<<"grid_sort_by_name">>, {action, sort_toggle, [name]}),
                               class = <<"active">>, body = #link{body = <<"Name">>}};
                       list ->
                           lists:foldl(
                               fun(Attr, Acc) ->
                                   Class = case get_sort_by() of
                                               Attr -> <<"active">>;
                                               _ -> <<"">>
                                           end,
                                   Acc ++ [#li{id = wire_click(<<"list_sort_by_", (atom_to_binary(Attr, latin1))/binary>>, {action, sort_toggle, [Attr]}),
                                       class = Class, body = #link{body = attr_to_name(Attr)}}]
                               end, [], [name | get_displayed_file_attributes()])
                   end,
    gui_jq:update(<<"sort_dropdown">>, DropdownBody),

    Count = length(get_selected_items()),
    NFiles = length(get_item_list()),
    IsDir = try item_is_dir(item_find(element(1, lists:nth(1, get_selected_items())))) catch _:_ -> false end,
    enable_tool_button(<<"tb_up_one_level">>, get_working_directory() /= <<"/">>),
    enable_tool_button(<<"tb_share_file">>, (Count =:= 1) andalso (not IsDir)),
    enable_tool_button(<<"tb_rename">>, Count =:= 1),
    enable_tool_button(<<"tb_chmod">>, Count > 0),
    enable_tool_button(<<"tb_remove">>, Count > 0),
    enable_tool_button(<<"tb_cut">>, Count > 0),
    enable_tool_button(<<"tb_copy">>, false),
    enable_tool_button(<<"tb_paste">>, length(get_clipboard_items()) > 0),
    gui_jq:update(<<"clipboard_size_label">>, integer_to_binary(length(get_clipboard_items()))),
    enable_tool_button(<<"tb_select_all">>, Count < NFiles),
    enable_tool_button(<<"tb_deselect_all">>, Count > 0).


enable_tool_button(ID, Flag) ->
    case Flag of
        true ->
            gui_jq:remove_class(ID, <<"hidden">>),
            gui_jq:add_class(<<ID/binary, "_dummy">>, <<"hidden">>),
            gui_jq:show(ID),
            gui_jq:hide(<<ID/binary, "_dummy">>);
        false ->
            gui_jq:add_class(ID, <<"hidden">>),
            gui_jq:remove_class(<<ID/binary, "_dummy">>, <<"hidden">>),
            gui_jq:hide(ID),
            gui_jq:show(<<ID/binary, "_dummy">>)
    end.


navigate(Path) ->
    set_working_directory(Path),
    clear_manager().


up_one_level() ->
    navigate(filename:dirname(filename:absname(get_working_directory()))).


toggle_view(Type) ->
    set_display_style(Type),
    set_sort_by(name),
    set_sort_ascending(true),
    clear_workspace().


select_item(Path) ->
    case item_find(Path) of
        undefined ->
            skip;
        Item ->
            SelectedItems = get_selected_items(),
            Basename = item_basename(Item),
            case lists:member({Path, Basename}, SelectedItems) of
                false ->
                    set_selected_items(SelectedItems ++ [{Path, Basename}]),
                    gui_jq:add_class(item_id(Item), <<"selected-item">>);
                true ->
                    set_selected_items(SelectedItems -- [{Path, Basename}]),
                    gui_jq:remove_class(item_id(Item), <<"selected-item">>)
            end
    end,
    refresh_tool_buttons().


select_all() ->
    set_selected_items([]),
    lists:foreach(
        fun(Item) ->
            set_selected_items(get_selected_items() ++ [{item_path(Item), item_basename(Item)}]),
            gui_jq:add_class(item_id(Item), <<"selected-item">>)
        end, get_item_list()),
    refresh_tool_buttons().


deselect_all() ->
    lists:foreach(
        fun(Item) ->
            gui_jq:remove_class(item_id(Item), <<"selected-item">>)
        end, get_item_list()),
    set_selected_items([]),
    refresh_tool_buttons().


clear_clipboard() ->
    set_clipboard_items([]),
    set_clipboard_type(none).


put_to_clipboard(Type) ->
    SelectedItems = get_selected_items(),
    set_clipboard_type(Type),
    set_clipboard_items(SelectedItems),
    clear_workspace().


paste_from_clipboard() ->
    [{FirstPath, _} | _] = get_clipboard_items(),
    % Check if mv is between different spaces
    case is_the_same_space(filename:dirname(FirstPath), get_working_directory()) of
        false ->
            gui_jq:confirm_popup(<<"Do you really want to move your file(s) between spaces and ",
            "share them with all members of the target space?">>, <<"confirm_paste_event();">>);
        true ->
            confirm_paste()
    end.


confirm_paste() ->
    ClipboardItems = get_clipboard_items(),
    ClipboardType = get_clipboard_type(),
    WorkingDirectory = get_working_directory(),
    clear_clipboard(),
    ErrorMessage = lists:foldl(
        fun({Path, Basename}, Acc) ->
            case ClipboardType of
                cut ->
                    case fs_mv(Path, WorkingDirectory) of
                        ok ->
                            Acc;
                        {logical_file_system_error, "eperm"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - insufficient permissions.<br />">>;
                        {logical_file_system_error, "eexist"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - file exists.<br />">>;
                        {logical_file_system_error, "eacces"} ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - insufficient permissions.<br />">>;
                        _ ->
                            <<Acc/binary, "Unable to move ", Basename/binary, " - error occured.<br />">>
                    end;
                copy ->
                    % Not yet implemented
                    fs_copy(Path, WorkingDirectory),
                    Acc
            end
        end, <<"">>, ClipboardItems),
    case ErrorMessage of
        <<"">> ->
            ok;
        _ ->
            gui_jq:info_popup(<<"Error(s) occured">>, ErrorMessage, <<"">>)
    end,
    clear_workspace().


submit_perms(Perms, Recursive) ->
    {Files, ACLEnabled, ACLEntries} = get_perms_state(),
    {Failed, Message} = case ACLEnabled of
                            true ->
                                FailedFiles = lists:foldl(
                                    fun(Path, Acc) ->
                                        {_Successful, Failed} = fs_set_acl(Path, ACLEntries, Recursive),
                                        Acc ++ Failed
                                    end, [], Files),
                                {FailedFiles, <<"Unable to set ACL for following file(s):">>};
                            false ->
                                FailedFiles = lists:foldl(
                                    fun(Path, Acc) ->
                                        {_Successful, Failed} = fs_chmod(Path, Perms, Recursive),
                                        Acc ++ Failed
                                    end, [], Files),
                                {FailedFiles, <<"Unable to change permissions for following file(s):">>}
                        end,
    case Failed of
        [] ->
            ok;
        _ ->
            FailedList = lists:foldl(
                fun({Path, Reason}, Acc) ->
                    ReasonBin = case Reason of
                                    {logical_file_system_error, "eacces"} ->
                                        <<"insufficient permissions">>;
                                    _ ->
                                        <<"error occured">>
                                end,
                    <<Acc/binary, Path/binary, ": ", ReasonBin/binary, "<br />">>
                end, <<"">>, Failed),
            gui_jq:info_popup(<<"Error(s) occured">>,
                <<Message/binary, "<br /><br />", FailedList/binary>>, <<"">>)
    end,
    clear_manager().


rename_item(OldPath, NewName) ->
    OldName = filename:basename(OldPath),
    case NewName of
        [] -> hide_popup();
        undefined -> hide_popup();
        OldName -> hide_popup();
        _ ->
            NewPath = filename:absname(NewName, get_working_directory()),
            ErrorMessage = case fs_mv(OldPath, get_working_directory(), NewName) of
                               ok ->
                                   clear_clipboard(),
                                   clear_manager(),
                                   select_item(NewPath),
                                   none;
                               {logical_file_system_error, "eperm"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - insufficient permissions.">>;
                               {logical_file_system_error, "eexist"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - file exists.">>;
                               {logical_file_system_error, "eacces"} ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - insufficient permissions.">>;
                               _ ->
                                   <<"Unable to rename ", (gui_str:to_binary(OldName))/binary, " - error occured.">>
                           end,
            case ErrorMessage of
                none -> ok;
                _ -> gui_jq:info_popup(<<"Error(s) occured">>, ErrorMessage, <<"">>)
{'fraction_non_alphanumeric': 0.16454426303781491, 'fraction_numerical': 0.008980437209753887, 'mean_word_length': 2.0692292411601114, 'pattern_counts': {'":': 2, '<': 904, '<?xml version=': 0, '>': 1190, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 68, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5529019', 'n_tokens_mistral': 19619, 'n_tokens_neox': 18348, 'n_words': 3617}