summaryrefslogtreecommitdiff
path: root/src/couch/test/eunit/couchdb_views_tests.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couch/test/eunit/couchdb_views_tests.erl')
-rw-r--r--src/couch/test/eunit/couchdb_views_tests.erl668
1 files changed, 0 insertions, 668 deletions
diff --git a/src/couch/test/eunit/couchdb_views_tests.erl b/src/couch/test/eunit/couchdb_views_tests.erl
deleted file mode 100644
index 06e2f03eb..000000000
--- a/src/couch/test/eunit/couchdb_views_tests.erl
+++ /dev/null
@@ -1,668 +0,0 @@
-% Licensed under the Apache License, Version 2.0 (the "License"); you may not
-% use this file except in compliance with the License. You may obtain a copy of
-% the License at
-%
-% http://www.apache.org/licenses/LICENSE-2.0
-%
-% Unless required by applicable law or agreed to in writing, software
-% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-% License for the specific language governing permissions and limitations under
-% the License.
-
--module(couchdb_views_tests).
-
--include_lib("couch/include/couch_eunit.hrl").
--include_lib("couch/include/couch_db.hrl").
--include_lib("couch_mrview/include/couch_mrview.hrl").
-
--define(DELAY, 100).
--define(TIMEOUT, 1000).
--define(WAIT_DELAY_COUNT, 40).
-
-setup() ->
- DbName = ?tempdb(),
- {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
- ok = couch_db:close(Db),
- FooRev = create_design_doc(DbName, <<"_design/foo">>, <<"bar">>),
- query_view(DbName, "foo", "bar"),
- BooRev = create_design_doc(DbName, <<"_design/boo">>, <<"baz">>),
- query_view(DbName, "boo", "baz"),
- {DbName, {FooRev, BooRev}}.
-
-setup_with_docs() ->
- DbName = ?tempdb(),
- {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
- ok = couch_db:close(Db),
- create_docs(DbName),
- create_design_doc(DbName, <<"_design/foo">>, <<"bar">>),
- DbName.
-
-setup_legacy() ->
- DbName = <<"test">>,
- DbFileName = "test.couch",
- OldDbFilePath = filename:join([?FIXTURESDIR, DbFileName]),
- OldViewName = "6cf2c2f766f87b618edf6630b00f8736.view",
- FixtureViewFilePath = filename:join([?FIXTURESDIR, OldViewName]),
- NewViewName = "a1c5929f912aca32f13446122cc6ce50.view",
-
- DbDir = config:get("couchdb", "database_dir"),
- ViewDir = config:get("couchdb", "view_index_dir"),
- OldViewFilePath = filename:join([ViewDir, ".test_design", "mrview",
- OldViewName]),
- NewViewFilePath = filename:join([ViewDir, ".test_design", "mrview",
- NewViewName]),
-
- NewDbFilePath = filename:join([DbDir, DbFileName]),
-
- Files = [NewDbFilePath, OldViewFilePath, NewViewFilePath],
-
- %% make sure there is no left over
- lists:foreach(fun(File) -> file:delete(File) end, Files),
-
- % copy old db file into db dir
- {ok, _} = file:copy(OldDbFilePath, NewDbFilePath),
-
- % copy old view file into view dir
- ok = filelib:ensure_dir(OldViewFilePath),
-
- {ok, _} = file:copy(FixtureViewFilePath, OldViewFilePath),
-
- {DbName, Files}.
-
-teardown({DbName, _}) ->
- teardown(DbName);
-teardown(DbName) when is_binary(DbName) ->
- couch_server:delete(DbName, [?ADMIN_CTX]),
- ok.
-
-teardown_legacy({_DbName, Files}) ->
- lists:foreach(fun(File) -> file:delete(File) end, Files).
-
-view_indexes_cleanup_test_() ->
- {
- "View indexes cleanup",
- {
- setup,
- fun test_util:start_couch/0, fun test_util:stop_couch/1,
- {
- foreach,
- fun setup/0, fun teardown/1,
- [
- fun should_have_two_indexes_alive_before_deletion/1,
- fun should_cleanup_index_file_after_ddoc_deletion/1,
- fun should_cleanup_all_index_files/1
- ]
- }
- }
- }.
-
-view_group_db_leaks_test_() ->
- {
- "View group db leaks",
- {
- setup,
- fun test_util:start_couch/0, fun test_util:stop_couch/1,
- {
- foreach,
- fun setup_with_docs/0, fun teardown/1,
- [
- fun couchdb_1138/1,
- fun couchdb_1309/1
- ]
- }
- }
- }.
-
-view_group_shutdown_test_() ->
- {
- "View group shutdown",
- {
- setup,
- fun() ->
- meck:new(couch_mrview_index, [passthrough]),
- test_util:start_couch()
- end,
- fun(Ctx) ->
- test_util:stop_couch(Ctx),
- meck:unload()
- end,
- [couchdb_1283()]
- }
- }.
-
-backup_restore_test_() ->
- {
- "Upgrade and bugs related tests",
- {
- setup,
- fun test_util:start_couch/0, fun test_util:stop_couch/1,
- {
- foreach,
- fun setup_with_docs/0, fun teardown/1,
- [
- fun should_not_remember_docs_in_index_after_backup_restore/1
- ]
- }
- }
- }.
-
-
-upgrade_test_() ->
- {
- "Upgrade tests",
- {
- setup,
- fun test_util:start_couch/0, fun test_util:stop_couch/1,
- {
- foreach,
- fun setup_legacy/0, fun teardown_legacy/1,
- [
- fun should_upgrade_legacy_view_files/1
- ]
- }
- }
- }.
-
-should_not_remember_docs_in_index_after_backup_restore(DbName) ->
- ?_test(begin
- %% COUCHDB-640
-
- ok = backup_db_file(DbName),
- create_doc(DbName, "doc666"),
-
- Rows0 = query_view(DbName, "foo", "bar"),
- ?assert(has_doc("doc1", Rows0)),
- ?assert(has_doc("doc2", Rows0)),
- ?assert(has_doc("doc3", Rows0)),
- ?assert(has_doc("doc666", Rows0)),
-
- ?assertEqual(ok, restore_backup_db_file(DbName)),
-
- Rows1 = query_view(DbName, "foo", "bar"),
- ?assert(has_doc("doc1", Rows1)),
- ?assert(has_doc("doc2", Rows1)),
- ?assert(has_doc("doc3", Rows1)),
- ?assertNot(has_doc("doc666", Rows1))
- end).
-
-should_upgrade_legacy_view_files({DbName, Files}) ->
- ?_test(begin
- [_NewDbFilePath, OldViewFilePath, NewViewFilePath] = Files,
- ok = config:set("query_server_config", "commit_freq", "0", false),
-
- % ensure old header
- OldHeader = read_header(OldViewFilePath),
- ?assertEqual(6, tuple_size(OldHeader)),
- ?assertMatch(mrheader, element(1, OldHeader)),
-
- % query view for expected results
- Rows0 = query_view(DbName, "test", "test"),
- ?assertEqual(3, length(Rows0)),
-
- % ensure old file gone
- ?assertNot(filelib:is_regular(OldViewFilePath)),
-
- % add doc to trigger update
- DocUrl = db_url(DbName) ++ "/bar",
- {ok, _, _, _} = test_request:put(
- DocUrl, [{"Content-Type", "application/json"}], <<"{\"a\":4}">>),
-
- % query view for expected results
- Rows1 = query_view(DbName, "test", "test"),
- ?assertEqual(4, length(Rows1)),
-
- % ensure new header
- timer:sleep(2000), % have to wait for awhile to upgrade the index
- NewHeader = read_header(NewViewFilePath),
- ?assertMatch(#mrheader{}, NewHeader),
- NewViewStatus = hd(NewHeader#mrheader.view_states),
- ?assertEqual(3, tuple_size(NewViewStatus))
- end).
-
-
-should_have_two_indexes_alive_before_deletion({DbName, _}) ->
- view_cleanup(DbName),
- ?_assertEqual(2, count_index_files(DbName)).
-
-should_cleanup_index_file_after_ddoc_deletion({DbName, {FooRev, _}}) ->
- delete_design_doc(DbName, <<"_design/foo">>, FooRev),
- view_cleanup(DbName),
- ?_assertEqual(1, count_index_files(DbName)).
-
-should_cleanup_all_index_files({DbName, {FooRev, BooRev}})->
- delete_design_doc(DbName, <<"_design/foo">>, FooRev),
- delete_design_doc(DbName, <<"_design/boo">>, BooRev),
- view_cleanup(DbName),
- ?_assertEqual(0, count_index_files(DbName)).
-
-couchdb_1138(DbName) ->
- ?_test(begin
- {ok, IndexerPid} = couch_index_server:get_index(
- couch_mrview_index, DbName, <<"_design/foo">>),
- ?assert(is_pid(IndexerPid)),
- ?assert(is_process_alive(IndexerPid)),
- ?assertEqual(2, count_users(DbName)),
-
- wait_indexer(IndexerPid),
-
- Rows0 = query_view(DbName, "foo", "bar"),
- ?assertEqual(3, length(Rows0)),
- ?assertEqual(2, count_users(DbName)),
- ?assert(is_process_alive(IndexerPid)),
-
- create_doc(DbName, "doc1000"),
- Rows1 = query_view(DbName, "foo", "bar"),
- ?assertEqual(4, length(Rows1)),
- ?assertEqual(2, count_users(DbName)),
-
- ?assert(is_process_alive(IndexerPid)),
-
- compact_db(DbName),
- ?assert(is_process_alive(IndexerPid)),
-
- compact_view_group(DbName, "foo"),
- ?assertEqual(2, count_users(DbName)),
-
- ?assert(is_process_alive(IndexerPid)),
-
- create_doc(DbName, "doc1001"),
- Rows2 = query_view(DbName, "foo", "bar"),
- ?assertEqual(5, length(Rows2)),
- ?assertEqual(2, count_users(DbName)),
-
- ?assert(is_process_alive(IndexerPid))
- end).
-
-couchdb_1309(DbName) ->
- ?_test(begin
- {ok, IndexerPid} = couch_index_server:get_index(
- couch_mrview_index, DbName, <<"_design/foo">>),
- ?assert(is_pid(IndexerPid)),
- ?assert(is_process_alive(IndexerPid)),
- ?assertEqual(2, count_users(DbName)),
-
- wait_indexer(IndexerPid),
-
- create_doc(DbName, "doc1001"),
- Rows0 = query_view(DbName, "foo", "bar"),
- check_rows_value(Rows0, null),
- ?assertEqual(4, length(Rows0)),
- ?assertEqual(2, count_users(DbName)),
-
- ?assert(is_process_alive(IndexerPid)),
-
- update_design_doc(DbName, <<"_design/foo">>, <<"bar">>),
- {ok, NewIndexerPid} = couch_index_server:get_index(
- couch_mrview_index, DbName, <<"_design/foo">>),
- ?assert(is_pid(NewIndexerPid)),
- ?assert(is_process_alive(NewIndexerPid)),
- ?assertNotEqual(IndexerPid, NewIndexerPid),
- UserCnt = case count_users(DbName) of
- N when N > 2 ->
- timer:sleep(1000),
- count_users(DbName);
- N -> N
- end,
- ?assertEqual(2, UserCnt),
-
- Rows1 = query_view(DbName, "foo", "bar", ok),
- ?assertEqual(0, length(Rows1)),
- Rows2 = query_view(DbName, "foo", "bar"),
- check_rows_value(Rows2, 1),
- ?assertEqual(4, length(Rows2)),
-
- ok = stop_indexer( %% FIXME we need to grab monitor earlier
- fun() -> ok end,
- IndexerPid, ?LINE,
- "old view group is not dead after ddoc update"),
-
- ok = stop_indexer(
- fun() -> couch_server:delete(DbName, [?ADMIN_USER]) end,
- NewIndexerPid, ?LINE,
- "new view group did not die after DB deletion")
- end).
-
-couchdb_1283() ->
- ?_test(begin
- ok = config:set("couchdb", "max_dbs_open", "3", false),
-
- {ok, MDb1} = couch_db:create(?tempdb(), [?ADMIN_CTX]),
- DDoc = couch_doc:from_json_obj({[
- {<<"_id">>, <<"_design/foo">>},
- {<<"language">>, <<"javascript">>},
- {<<"views">>, {[
- {<<"foo">>, {[
- {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
- ]}},
- {<<"foo2">>, {[
- {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
- ]}},
- {<<"foo3">>, {[
- {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
- ]}},
- {<<"foo4">>, {[
- {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
- ]}},
- {<<"foo5">>, {[
- {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
- ]}}
- ]}}
- ]}),
- {ok, _} = couch_db:update_doc(MDb1, DDoc, []),
- ok = populate_db(MDb1, 100, 100),
- query_view(couch_db:name(MDb1), "foo", "foo"),
- ok = couch_db:close(MDb1),
-
- {ok, Pid} = couch_index_server:get_index(
- couch_mrview_index, couch_db:name(MDb1), <<"_design/foo">>),
-
- % Start and pause compacton
- WaitRef = erlang:make_ref(),
- meck:expect(couch_mrview_index, compact, fun(Db, State, Opts) ->
- receive {WaitRef, From, init} -> ok end,
- From ! {WaitRef, inited},
- receive {WaitRef, go} -> ok end,
- meck:passthrough([Db, State, Opts])
- end),
-
- {ok, CPid} = gen_server:call(Pid, compact),
- CRef = erlang:monitor(process, CPid),
- ?assert(is_process_alive(CPid)),
-
- % Make sure that our compactor is waiting for us
- % before we continue our assertions
- CPid ! {WaitRef, self(), init},
- receive {WaitRef, inited} -> ok end,
-
- % Make sure that a compaction process takes a monitor
- % on the database's main_pid
- ?assertEqual(true, lists:member(CPid, couch_db:monitored_by(MDb1))),
-
- % Finish compaction to and make sure the monitor
- % disappears
- CPid ! {WaitRef, go},
- wait_for_process_shutdown(CRef, normal,
- {reason, "Failure compacting view group"}),
-
- % Make sure that the monitor was removed
- ?assertEqual(false, lists:member(CPid, couch_db:monitored_by(MDb1)))
- end).
-
-wait_for_process_shutdown(Pid, ExpectedReason, Error) ->
- receive
- {'DOWN', Pid, process, _, Reason} ->
- ?assertEqual(ExpectedReason, Reason)
- after ?TIMEOUT ->
- erlang:error(
- {assertion_failed,
- [{module, ?MODULE}, {line, ?LINE}, Error]})
- end.
-
-
-create_doc(DbName, DocId) when is_list(DocId) ->
- create_doc(DbName, ?l2b(DocId));
-create_doc(DbName, DocId) when is_binary(DocId) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- Doc666 = couch_doc:from_json_obj({[
- {<<"_id">>, DocId},
- {<<"value">>, 999}
- ]}),
- {ok, _} = couch_db:update_docs(Db, [Doc666]),
- couch_db:close(Db).
-
-create_docs(DbName) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- Doc1 = couch_doc:from_json_obj({[
- {<<"_id">>, <<"doc1">>},
- {<<"value">>, 1}
-
- ]}),
- Doc2 = couch_doc:from_json_obj({[
- {<<"_id">>, <<"doc2">>},
- {<<"value">>, 2}
-
- ]}),
- Doc3 = couch_doc:from_json_obj({[
- {<<"_id">>, <<"doc3">>},
- {<<"value">>, 3}
-
- ]}),
- {ok, _} = couch_db:update_docs(Db, [Doc1, Doc2, Doc3]),
- couch_db:close(Db).
-
-populate_db(Db, BatchSize, N) when N > 0 ->
- Docs = lists:map(
- fun(_) ->
- couch_doc:from_json_obj({[
- {<<"_id">>, couch_uuids:new()},
- {<<"value">>, base64:encode(crypto:strong_rand_bytes(1000))}
- ]})
- end,
- lists:seq(1, BatchSize)),
- {ok, _} = couch_db:update_docs(Db, Docs, []),
- populate_db(Db, BatchSize, N - length(Docs));
-populate_db(_Db, _, _) ->
- ok.
-
-create_design_doc(DbName, DDName, ViewName) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- DDoc = couch_doc:from_json_obj({[
- {<<"_id">>, DDName},
- {<<"language">>, <<"javascript">>},
- {<<"views">>, {[
- {ViewName, {[
- {<<"map">>, <<"function(doc) { emit(doc.value, null); }">>}
- ]}}
- ]}}
- ]}),
- {ok, Rev} = couch_db:update_doc(Db, DDoc, []),
- couch_db:close(Db),
- Rev.
-
-update_design_doc(DbName, DDName, ViewName) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- {ok, Doc} = couch_db:open_doc(Db, DDName, [?ADMIN_CTX]),
- {Props} = couch_doc:to_json_obj(Doc, []),
- Rev = couch_util:get_value(<<"_rev">>, Props),
- DDoc = couch_doc:from_json_obj({[
- {<<"_id">>, DDName},
- {<<"_rev">>, Rev},
- {<<"language">>, <<"javascript">>},
- {<<"views">>, {[
- {ViewName, {[
- {<<"map">>, <<"function(doc) { emit(doc.value, 1); }">>}
- ]}}
- ]}}
- ]}),
- {ok, NewRev} = couch_db:update_doc(Db, DDoc, [?ADMIN_CTX]),
- couch_db:close(Db),
- NewRev.
-
-delete_design_doc(DbName, DDName, Rev) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- DDoc = couch_doc:from_json_obj({[
- {<<"_id">>, DDName},
- {<<"_rev">>, couch_doc:rev_to_str(Rev)},
- {<<"_deleted">>, true}
- ]}),
- {ok, _} = couch_db:update_doc(Db, DDoc, [Rev]),
- couch_db:close(Db).
-
-db_url(DbName) ->
- Addr = config:get("httpd", "bind_address", "127.0.0.1"),
- Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)),
- "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(DbName).
-
-query_view(DbName, DDoc, View) ->
- query_view(DbName, DDoc, View, false).
-
-query_view(DbName, DDoc, View, Stale) ->
- {ok, Code, _Headers, Body} = test_request:get(
- db_url(DbName) ++ "/_design/" ++ DDoc ++ "/_view/" ++ View
- ++ case Stale of
- false -> [];
- _ -> "?stale=" ++ atom_to_list(Stale)
- end),
- ?assertEqual(200, Code),
- {Props} = jiffy:decode(Body),
- couch_util:get_value(<<"rows">>, Props, []).
-
-check_rows_value(Rows, Value) ->
- lists:foreach(
- fun({Row}) ->
- ?assertEqual(Value, couch_util:get_value(<<"value">>, Row))
- end, Rows).
-
-view_cleanup(DbName) ->
- {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
- couch_mrview:cleanup(Db),
- couch_db:close(Db).
-
-count_users(DbName) ->
- {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX]),
- DbPid = couch_db:get_pid(Db),
- {monitored_by, Monitors0} = process_info(DbPid, monitored_by),
- Monitors = lists:filter(fun is_pid/1, Monitors0),
- CouchFiles = [P || P <- Monitors, couch_file:process_info(P) =/= undefined],
- ok = couch_db:close(Db),
- length(lists:usort(Monitors) -- [self() | CouchFiles]).
-
-count_index_files(DbName) ->
- % call server to fetch the index files
- RootDir = config:get("couchdb", "view_index_dir"),
- length(filelib:wildcard(RootDir ++ "/." ++
- binary_to_list(DbName) ++ "_design"++"/mrview/*")).
-
-has_doc(DocId1, Rows) ->
- DocId = iolist_to_binary(DocId1),
- lists:any(fun({R}) -> lists:member({<<"id">>, DocId}, R) end, Rows).
-
-backup_db_file(DbName) ->
- {ok, Db} = couch_db:open_int(DbName, []),
- try
- SrcPath = couch_db:get_filepath(Db),
- Src = if
- is_list(SrcPath) -> SrcPath;
- true -> binary_to_list(SrcPath)
- end,
- ok = copy_tree(Src, Src ++ ".backup")
- after
- couch_db:close(Db)
- end.
-
-restore_backup_db_file(DbName) ->
- {ok, Db} = couch_db:open_int(DbName, []),
- Src = couch_db:get_filepath(Db),
- ok = couch_db:close(Db),
- DbPid = couch_db:get_pid(Db),
- exit(DbPid, shutdown),
- ok = copy_tree(Src ++ ".backup", Src),
-
- test_util:wait(fun() ->
- case couch_server:open(DbName, [{timeout, ?TIMEOUT}]) of
- {ok, WaitDb} ->
- case couch_db:get_pid(WaitDb) == DbPid of
- true -> wait;
- false -> ok
- end;
- Else ->
- Else
- end
- end, ?TIMEOUT, ?DELAY).
-
-compact_db(DbName) ->
- {ok, Db} = couch_db:open_int(DbName, []),
- {ok, _} = couch_db:start_compact(Db),
- ok = couch_db:close(Db),
- wait_db_compact_done(DbName, ?WAIT_DELAY_COUNT).
-
-wait_db_compact_done(_DbName, 0) ->
- erlang:error({assertion_failed,
- [{module, ?MODULE},
- {line, ?LINE},
- {reason, "DB compaction failed to finish"}]});
-wait_db_compact_done(DbName, N) ->
- {ok, Db} = couch_db:open_int(DbName, []),
- ok = couch_db:close(Db),
- CompactorPid = couch_db:get_compactor_pid(Db),
- case is_pid(CompactorPid) of
- false ->
- ok;
- true ->
- ok = timer:sleep(?DELAY),
- wait_db_compact_done(DbName, N - 1)
- end.
-
-compact_view_group(DbName, DDocId) when is_list(DDocId) ->
- compact_view_group(DbName, ?l2b("_design/" ++ DDocId));
-compact_view_group(DbName, DDocId) when is_binary(DDocId) ->
- ok = couch_mrview:compact(DbName, DDocId),
- wait_view_compact_done(DbName, DDocId, 10).
-
-wait_view_compact_done(_DbName, _DDocId, 0) ->
- erlang:error({assertion_failed,
- [{module, ?MODULE},
- {line, ?LINE},
- {reason, "DB compaction failed to finish"}]});
-wait_view_compact_done(DbName, DDocId, N) ->
- {ok, Code, _Headers, Body} = test_request:get(
- db_url(DbName) ++ "/" ++ ?b2l(DDocId) ++ "/_info"),
- ?assertEqual(200, Code),
- {Info} = jiffy:decode(Body),
- {IndexInfo} = couch_util:get_value(<<"view_index">>, Info),
- CompactRunning = couch_util:get_value(<<"compact_running">>, IndexInfo),
- case CompactRunning of
- false ->
- ok;
- true ->
- ok = timer:sleep(?DELAY),
- wait_view_compact_done(DbName, DDocId, N - 1)
- end.
-
-read_header(File) ->
- {ok, Fd} = couch_file:open(File),
- {ok, {_Sig, Header}} = couch_file:read_header(Fd),
- couch_file:close(Fd),
- Header.
-
-stop_indexer(StopFun, Pid, Line, Reason) ->
- case test_util:stop_sync(Pid, StopFun) of
- timeout ->
- erlang:error(
- {assertion_failed,
- [{module, ?MODULE}, {line, Line},
- {reason, Reason}]});
- ok ->
- ok
- end.
-
-wait_indexer(IndexerPid) ->
- test_util:wait(fun() ->
- {ok, Info} = couch_index:get_info(IndexerPid),
- case couch_util:get_value(compact_running, Info) of
- true ->
- wait;
- false ->
- ok
- end
- end).
-
-copy_tree(Src, Dst) ->
- case filelib:is_dir(Src) of
- true ->
- {ok, Files} = file:list_dir(Src),
- copy_tree(Files, Src, Dst);
- false ->
- ok = filelib:ensure_dir(Dst),
- {ok, _} = file:copy(Src, Dst),
- ok
- end.
-
-copy_tree([], _Src, _Dst) ->
- ok;
-copy_tree([File | Rest], Src, Dst) ->
- FullSrc = filename:join(Src, File),
- FullDst = filename:join(Dst, File),
- ok = copy_tree(FullSrc, FullDst),
- copy_tree(Rest, Src, Dst).