%%%---------------------------------------------------------------------- %%% File : pubsub_migrate.erl %%% Author : Christophe Romain %%% Purpose : Migration/Upgrade code put out of mod_pubsub %%% Created : 26 Jul 2014 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2020 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(pubsub_migrate). -dialyzer({no_return, report_and_stop/2}). -include("pubsub.hrl"). -include("logger.hrl"). -export([update_node_database/2, update_state_database/2]). -export([update_item_database/2, update_lastitem_database/2]). update_item_database(_Host, _ServerHost) -> convert_list_items(). update_node_database(Host, ServerHost) -> mnesia:del_table_index(pubsub_node, type), mnesia:del_table_index(pubsub_node, parentid), case catch mnesia:table_info(pubsub_node, attributes) of [host_node, host_parent, info] -> ?INFO_MSG("Upgrading pubsub nodes table...", []), F = fun () -> {Result, LastIdx} = lists:foldl(fun ({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, {RecList, NodeIdx}) -> ItemsList = lists:foldl(fun ({item, IID, Publisher, Payload}, Acc) -> C = {unknown, Publisher}, M = {erlang:timestamp(), Publisher}, mnesia:write(#pubsub_item{itemid = {IID, NodeIdx}, creation = C, modification = M, payload = Payload}), [{Publisher, IID} | Acc] end, [], Items), Owners = dict:fold(fun (JID, {entity, Aff, Sub}, Acc) -> UsrItems = lists:foldl(fun ({P, I}, IAcc) -> case P of JID -> [I | IAcc]; _ -> IAcc end end, [], ItemsList), mnesia:write({pubsub_state, {JID, NodeIdx}, UsrItems, Aff, Sub}), case Aff of owner -> [JID | Acc]; _ -> Acc end end, [], Entities), mnesia:delete({pubsub_node, NodeId}), {[#pubsub_node{nodeid = NodeId, id = NodeIdx, parents = [element(2, ParentId)], owners = Owners, options = Options} | RecList], NodeIdx + 1} end, {[], 1}, mnesia:match_object({pubsub_node, {Host, '_'}, '_', '_'})), mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}), Result end, {atomic, NewRecords} = mnesia:transaction(F), {atomic, ok} = mnesia:delete_table(pubsub_node), {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_node, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_node)}]), FNew = fun () -> lists:foreach(fun (Record) -> mnesia:write(Record) end, NewRecords) end, case mnesia:transaction(FNew) of {atomic, Result} -> ?INFO_MSG("Pubsub nodes table upgraded: ~p", [Result]); {aborted, Reason} -> ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", [Reason]) end; [nodeid, parentid, type, owners, options] -> F = fun ({pubsub_node, NodeId, {_, Parent}, Type, Owners, Options}) -> #pubsub_node{nodeid = NodeId, id = 0, parents = [Parent], type = Type, owners = Owners, options = Options} end, mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), FNew = fun () -> LastIdx = lists:foldl(fun (#pubsub_node{nodeid = NodeId} = PubsubNode, NodeIdx) -> mnesia:write(PubsubNode#pubsub_node{id = NodeIdx}), lists:foreach(fun (#pubsub_state{stateid = StateId} = State) -> {JID, _} = StateId, mnesia:delete({pubsub_state, StateId}), mnesia:write(State#pubsub_state{stateid = {JID, NodeIdx}}) end, mnesia:match_object(#pubsub_state{stateid = {'_', NodeId}, _ = '_'})), lists:foreach(fun (#pubsub_item{itemid = ItemId} = Item) -> {IID, _} = ItemId, {M1, M2} = Item#pubsub_item.modification, {C1, C2} = Item#pubsub_item.creation, mnesia:delete({pubsub_item, ItemId}), mnesia:write(Item#pubsub_item{itemid = {IID, NodeIdx}, modification = {M2, M1}, creation = {C2, C1}}) end, mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'})), NodeIdx + 1 end, 1, mnesia:match_object({pubsub_node, {Host, '_'}, '_', '_', '_', '_', '_'}) ++ mnesia:match_object({pubsub_node, {{'_', ServerHost, '_'}, '_'}, '_', '_', '_', '_', '_'})), mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}) end, case mnesia:transaction(FNew) of {atomic, Result} -> rename_default_nodeplugin(), ?INFO_MSG("Pubsub nodes table upgraded: ~p", [Result]); {aborted, Reason} -> ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", [Reason]) end; [nodeid, id, parent, type, owners, options] -> F = fun ({pubsub_node, NodeId, Id, Parent, Type, Owners, Options}) -> #pubsub_node{nodeid = NodeId, id = Id, parents = [Parent], type = Type, owners = Owners, options = Options} end, mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), rename_default_nodeplugin(); _ -> ok end, convert_list_nodes(). rename_default_nodeplugin() -> lists:foreach(fun (Node) -> mnesia:dirty_write(Node#pubsub_node{type = <<"hometree">>}) end, mnesia:dirty_match_object(#pubsub_node{type = <<"default">>, _ = '_'})). update_state_database(_Host, _ServerHost) -> % useless starting from ejabberd 17.04 % case catch mnesia:table_info(pubsub_state, attributes) of % [stateid, nodeidx, items, affiliation, subscriptions] -> % ?INFO_MSG("Upgrading pubsub states table...", []), % F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> % JID = {U,S,R}, % Subs = case Sub of % none -> % []; % [] -> % []; % _ -> % SubID = pubsub_subscription:make_subid(), % [{Sub, SubID}] % end, % NewState = #pubsub_state{stateid = {JID, NodeID}, % items = Items, % affiliation = Aff, % subscriptions = Subs}, % [NewState | Acc] % end, % {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, % [F, [], pubsub_state]), % {atomic, ok} = mnesia:delete_table(pubsub_state), % {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_state, % [{disc_copies, [node()]}, % {attributes, record_info(fields, pubsub_state)}]), % FNew = fun () -> % lists:foreach(fun mnesia:write/1, NewRecs) % end, % case mnesia:transaction(FNew) of % {atomic, Result} -> % ?INFO_MSG("Pubsub states table upgraded: ~p", % [Result]); % {aborted, Reason} -> % ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", % [Reason]) % end; % _ -> % ok % end, convert_list_subscriptions(), convert_list_states(). update_lastitem_database(_Host, _ServerHost) -> convert_list_lasts(). %% binarization from old 2.1.x convert_list_items() -> convert_list_records( pubsub_item, record_info(fields, pubsub_item), fun(#pubsub_item{itemid = {I, _}}) -> I end, fun(#pubsub_item{itemid = {I, Nidx}, creation = {C, CKey}, modification = {M, MKey}, payload = Els} = R) -> R#pubsub_item{itemid = {bin(I), Nidx}, creation = {C, binusr(CKey)}, modification = {M, binusr(MKey)}, payload = [fxml:to_xmlel(El) || El<-Els]} end). convert_list_states() -> convert_list_records( pubsub_state, record_info(fields, pubsub_state), fun(#pubsub_state{stateid = {{U,_,_}, _}}) -> U end, fun(#pubsub_state{stateid = {U, Nidx}, items = Is, affiliation = A, subscriptions = Ss} = R) -> R#pubsub_state{stateid = {binusr(U), Nidx}, items = [bin(I) || I<-Is], affiliation = A, subscriptions = [{S,bin(Sid)} || {S,Sid}<-Ss]} end). convert_list_nodes() -> convert_list_records( pubsub_node, record_info(fields, pubsub_node), fun(#pubsub_node{nodeid = {{U,_,_}, _}}) -> U; (#pubsub_node{nodeid = {H, _}}) -> H end, fun(#pubsub_node{nodeid = {H, N}, id = I, parents = Ps, type = T, owners = Os, options = Opts} = R) -> R#pubsub_node{nodeid = {binhost(H), bin(N)}, id = I, parents = [bin(P) || P<-Ps], type = bin(T), owners = [binusr(O) || O<-Os], options = Opts} end). convert_list_subscriptions() -> [convert_list_records( pubsub_subscription, record_info(fields, pubsub_subscription), fun(#pubsub_subscription{subid = I}) -> I end, fun(#pubsub_subscription{subid = I, options = Opts} = R) -> R#pubsub_subscription{subid = bin(I), options = Opts} end) || lists:member(pubsub_subscription, mnesia:system_info(tables))]. convert_list_lasts() -> convert_list_records( pubsub_last_item, record_info(fields, pubsub_last_item), fun(#pubsub_last_item{itemid = I}) -> I end, fun(#pubsub_last_item{itemid = I, nodeid = Nidx, creation = {C, CKey}, payload = Payload} = R) -> R#pubsub_last_item{itemid = bin(I), nodeid = Nidx, creation = {C, binusr(CKey)}, payload = fxml:to_xmlel(Payload)} end). %% internal tools convert_list_records(Tab, Fields, DetectFun, ConvertFun) -> case mnesia:table_info(Tab, attributes) of Fields -> convert_table_to_binary( Tab, Fields, set, DetectFun, ConvertFun); _ -> ?INFO_MSG("Recreating ~p table", [Tab]), mnesia:transform_table(Tab, ignore, Fields), convert_list_records(Tab, Fields, DetectFun, ConvertFun) end. binhost({U,S,R}) -> binusr({U,S,R}); binhost(L) -> bin(L). binusr({U,S,R}) -> {bin(U), bin(S), bin(R)}. bin(L) -> iolist_to_binary(L). %% The code should be updated to support new ejabberd_mnesia %% transform functions (i.e. need_transform/1 and transform/1) convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> case is_table_still_list(Tab, DetectFun) of true -> ?INFO_MSG("Converting '~ts' table from strings to binaries.", [Tab]), TmpTab = list_to_atom(atom_to_list(Tab) ++ "_tmp_table"), catch mnesia:delete_table(TmpTab), case ejabberd_mnesia:create(?MODULE, TmpTab, [{disc_only_copies, [node()]}, {type, Type}, {local_content, true}, {record_name, Tab}, {attributes, Fields}]) of {atomic, ok} -> mnesia:transform_table(Tab, ignore, Fields), case mnesia:transaction( fun() -> mnesia:write_lock_table(TmpTab), mnesia:foldl( fun(R, _) -> NewR = ConvertFun(R), mnesia:dirty_write(TmpTab, NewR) end, ok, Tab) end) of {atomic, ok} -> mnesia:clear_table(Tab), case mnesia:transaction( fun() -> mnesia:write_lock_table(Tab), mnesia:foldl( fun(R, _) -> mnesia:dirty_write(R) end, ok, TmpTab) end) of {atomic, ok} -> mnesia:delete_table(TmpTab); Err -> report_and_stop(Tab, Err) end; Err -> report_and_stop(Tab, Err) end; Err -> report_and_stop(Tab, Err) end; false -> ok end. is_table_still_list(Tab, DetectFun) -> is_table_still_list(Tab, DetectFun, mnesia:dirty_first(Tab)). is_table_still_list(_Tab, _DetectFun, '$end_of_table') -> false; is_table_still_list(Tab, DetectFun, Key) -> Rs = mnesia:dirty_read(Tab, Key), Res = lists:foldl(fun(_, true) -> true; (_, false) -> false; (R, _) -> case DetectFun(R) of '$next' -> '$next'; El -> is_list(El) end end, '$next', Rs), case Res of true -> true; false -> false; '$next' -> is_table_still_list(Tab, DetectFun, mnesia:dirty_next(Tab, Key)) end. report_and_stop(Tab, Err) -> ErrTxt = lists:flatten( io_lib:format( "Failed to convert '~ts' table to binary: ~p", [Tab, Err])), ?CRITICAL_MSG(ErrTxt, []), ejabberd:halt().