Permalink
Browse files

Fix running of etorrent without building a release.

Allow to start with start-dev.sh without building a release.

Port changes.
  • Loading branch information...
1 parent 337586c commit fd088e7e21d46674768e5a2a43238449fa2df0ce @arcusfelis arcusfelis committed Jan 10, 2012
View
3 .gitignore
@@ -1,5 +1,4 @@
*.beam
deps/
ebin/*.app
-*.swp
-
+log/*
View
8 README.md
@@ -0,0 +1,8 @@
+How to run
+==========
+
+```
+rebar get-deps compile
+cp "priv/config.example" "~/.config/etorrent.config"
+./start-dev.sh
+```
View
112 priv/config.example
@@ -0,0 +1,112 @@
+%% -*- mode: Erlang; -*-
+[{etorrent_core,
+ [
+ %% The port entry tells etorrent which port it should listen on. It
+ %% can currently not be limited to listen on certain interfaces
+ %% only. It will instead bind to every available interface present.
+ {port, 1729 },
+
+ %% The port to listen on when retrieving UDP responses from the tracker
+ {udp_port, 1730 },
+
+ %% The dht entry enables the DHT subsystem, it is used to
+ %% retrieve information of which peers are available if there
+ %% are no trackers available.
+ {dht, true },
+
+ %% The DHT subsystem will also bind to all interfaces.
+ {dht_port, 6882 },
+
+ %% The DHT subsystem stores its internal state between runs in a state file
+ %% The following setting defines the location of this file
+ {dht_state, "/home/user/etorrent/spool/dht_state.dets"},
+
+ %% Enable UPnP subsystem, which tries to open port mappings in
+ %% UPnP-aware routers for etorrent.
+ {use_upnp, true },
+
+ %% The directory to watch for .torrent files and the directory to download data into
+ {dir, "/home/user/etorrent/torrent_data"},
+
+ %% The directory to download data into. It is optional, if not defined used 'dir' value.
+ {download_dir, "/home/user/etorrent/torrent_data"},
+
+ %% Interval in seconds to check directory for new .torrent files
+ {dirwatch_interval, 20 },
+
+ %% Location of the log file
+ {logger_dir, "log"},
+
+ %% Name of the log file. Etorrent will stamp out simple messages here whenever progress
+ %% is made in the system.
+ {logger_fname, "etorrent.log"},
+
+ %% Location of the fast resume file. If present this file is used to populate the fast-
+ %% resume table, so startup is much faster. Every 5 minutes the file is stamped out,
+ %% so an eventual death of the system won't affect too much. It is also written upon
+ %% graceful termination.
+ %% NOTE: The directory for the fast resume file must exist, or etorrent will crash.
+ {fast_resume_file, "/home/user/etorrent/spool/fast_resume_state.dets"},
+
+ %% Limit on the number of peers the system can maximally be connected to
+ {max_peers, 200},
+
+ %% The download rate of the system.
+ {max_download_rate, 1200 },
+
+ %% The upload rate of the system.
+ {max_upload_rate, 1200 },
+
+ %% Number of upload slots. Either an integer or 'auto'. We recommend 'auto' as this
+ %% will calculate a sane number of upload slots from the upload_rate. If this is set
+ %% too low, you will not saturate the outbound bandwidth. If set too high, peers will
+ %% not like the client as it can only give bad rates to all peers.
+ {max_upload_slots, auto},
+
+ %% High and low watermarks for the file system processes. Etorrent will not open more
+ %% on-disk files than the limit given here.
+ {fs_watermark_high, 128},
+ {fs_watermark_low, 100},
+
+ %% Number of optimistic upload slots. If your line is really fast, consider increasing
+ %% this a little bit.
+ {min_uploads, 2},
+
+ %% The preallocation strategy to use when creating new files. The default is "sparse"
+ %% which creates a sparse file on the disk. Some file systems are bad at working with
+ %% sparse files, most notably FreeBSDs default file system. The other option here is
+ %% "preallocate" which means the system will fill the file up on disk before using it.
+ {preallocation_strategy, sparse },
+
+ %% Enable the Web user interface in etorrent, on 127.0.0.1:8080
+ {webui, true },
+
+ %% Enable logging in the webui
+ {webui_logger_dir, "log/webui"},
+
+ %% The address to bind the webui on. Notice that is has to be given as a tuple for an IP address
+ %% and as a string for a domain name.
+ {webui_bind_address, {127,0,0,1}},
+
+ %% The port to use for the webui
+ {webui_port, 8080},
+
+ %% Enable profiling; do not enable unless you need it
+ {profiling, false}
+ ]},
+ {lager,
+ [{handlers,
+ [{lager_console_backend, [debug, true]},
+ {lager_file_backend,
+ [{"log/error.log", error, 10485760, "$D0", 5},
+ {"log/console.log", info, 10485760, "$D0", 5},
+ {"log/debug.log", debug, 10485760, "$D0", 5}
+ ]}
+ ]}
+ ]},
+ {kernel,
+ [{start_timer, true}]},
+ {sasl,
+ [{sasl_error_logger, {file, "log/sasl/sasl-error.log"}},
+ {errlog_type, error}]}
+].
View
3 rebar.config
@@ -26,5 +26,6 @@
{meck, ".*", {git, "git://github.com/klaar/meck.git", "file-bif-passthrough"}},
{proper, ".*", {git, "git://github.com/manopapad/proper.git", "master"}},
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "master"}},
- {rlimit, ".*", {git, "git://github.com/jlouis/rlimit.git", "master"}}
+ {rlimit, ".*", {git, "git://github.com/jlouis/rlimit.git", "master"}},
+ {cascadae, ".*", {git, "git://github.com/freeakk/cascadae.git", "master"}}
]}.
View
13 src/etorrent_app.erl
@@ -16,23 +16,26 @@
-export([start/2, stop/1, prep_stop/1, profile_output/0]).
-define(RANDOM_MAX_SIZE, 999999999999).
+-define(APP, etorrent_core).
start() ->
start([]).
start(Config) ->
load_config(Config),
- {ok, Deps} = application:get_key(etorrent, applications),
+ % Load app file.
+ application:load(?APP),
+ {ok, Deps} = application:get_key(?APP, applications),
true = lists:all(fun ensure_started/1, Deps),
- application:start(etorrent).
+ application:start(?APP).
stop() ->
- application:stop(etorrent).
+ application:stop(?APP).
load_config([]) ->
ok;
load_config([{Key, Val} | Next]) ->
- application:set_env(etorrent, Key, Val),
+ application:set_env(?APP, Key, Val),
load_config(Next).
%% @private
@@ -85,6 +88,8 @@ stop(_State) ->
ok.
start_webui() ->
+ cascadae:start(),
+
Dispatch = [ {'_', [{'_', etorrent_cowboy_handler, []}]} ],
{ok, _Pid} =
cowboy:start_listener(http, 10,
View
126 src/etorrent_chunkset.erl
@@ -5,14 +5,18 @@
-endif.
-export([new/2,
+ new/1,
size/1,
min/1,
min/2,
+ extract/2,
is_empty/1,
delete/2,
delete/3,
insert/3,
- from_list/3]).
+ from_list/3,
+ in/3,
+ subtract/3]).
-record(chunkset, {
piece_len :: pos_integer(),
@@ -32,6 +36,12 @@ new(PieceLen, ChunkLen) ->
chunk_len=ChunkLen,
chunks=[{0, PieceLen - 1}]}.
+
+%% @doc Create am empty copy of the chunkset.
+new(Prototype) ->
+ Prototype#chunkset{chunks=[]}.
+
+
from_list(PieceLen, ChunkLen, Chunks) ->
#chunkset{
piece_len=PieceLen,
@@ -86,12 +96,89 @@ min_(_, 0) ->
min_(Chunkset, Numchunks) ->
case min_(Chunkset) of
none -> [];
- {Start, End}=Chunk ->
- Without = delete(Start, End, Chunkset),
+ {Start, Size}=Chunk ->
+ Without = delete(Start, Size, Chunkset),
[Chunk|min_(Without, Numchunks - 1)]
end.
+%% @doc This operation combines min/2 and delete.
+extract(Chunkset, Numchunks) when Numchunks >= 0 ->
+ extract_(Chunkset, Numchunks, []).
+
+
+extract_(Chunkset, 0, Acc) ->
+ {lists:reverse(Acc), Chunkset, 0};
+
+extract_(Chunkset, Numchunks, Acc) ->
+ case min_(Chunkset) of
+ none ->
+ {lists:reverse(Acc), Chunkset, Numchunks};
+
+ {Start, Size}=Chunk ->
+ Without = delete(Start, Size, Chunkset),
+ extract_(Without, Numchunks - 1, [Chunk|Acc])
+ end.
+
+
+
+in(Offset, Size, Chunkset) ->
+ NewChunkset = delete(Offset, Size, Chunkset),
+ OldSize = ?MODULE:size(Chunkset),
+ NewSize = ?MODULE:size(NewChunkset),
+ Size =:= (OldSize - NewSize).
+
+
+%% @doc This operation run `delete/3' and return result and
+%% the list of the deleted values.
+subtract(Offset, Length, Chunkset) when Length > 0, Offset >= 0 ->
+ #chunkset{chunks=Chunks} = Chunkset,
+ {NewChunks, Deleted} = sub_(Offset, Offset + Length - 1, Chunks, [], []),
+ {Chunkset#chunkset{chunks=NewChunks}, rev_deleted_(Deleted, [])}.
+
+%% E = S + L - 1.
+%% L = E - S + 1.
+rev_deleted_([{S, E}|T], Acc) ->
+ L = E - S + 1,
+ rev_deleted_(T, [{S, L}|Acc]);
+
+rev_deleted_([], Acc) -> Acc.
+
+
+sub_(CS, CE, [{S, E}=H|T], Res, Acc)
+ when is_integer(S), is_integer(CS),
+ is_integer(E), is_integer(CE) ->
+ if
+ CS =< S, CE =:= E ->
+ % full match
+ {lists:reverse(Res, T), [H|Acc]}; % H
+ CS =< S, CE < E, CE > S ->
+ % try delete smaller piece
+ {lists:reverse(Res, [{CE+1, E}|T]), [{S, CE}|Acc]};
+ CS =< S, CE < S ->
+ % skip range
+ {lists:reverse(Res, [H|T]), Acc};
+ CS =< S, CE > E ->
+ % try delete bigger piece
+ sub_(E+1, CE, T, Res, [H|Acc]);
+ CS > S, CE =:= E ->
+ % try delete smaller piece
+ {lists:reverse(Res, [{S, CS-1}|T]), [{CS, E}|Acc]};
+ CS > S, CE < E ->
+ % try delete smaller piece
+ {lists:reverse(Res, [{S, CS-1},{CE+1, E}|T]),
+ [{CS, CE}|Acc]};
+ CS > S, CS < E, CE > E ->
+ % try delete bigger piece
+ sub_(E+1, CE, T, [{S, CS-1}|Res], [{CS, E}|Acc]);
+ CS > E ->
+ % chunk is higher
+ sub_(E+1, CE, T, [H|Res], Acc)
+ end;
+sub_(_CS, _CE, [], Res, Acc) ->
+ {lists:reverse(Res), Acc}.
+
+
%% @doc Check is a chunkset is empty
%% @end
is_empty(Chunkset) ->
@@ -108,12 +195,16 @@ delete([{Offset, Length}|T], Chunkset) ->
delete(T, delete(Offset, Length, Chunkset)).
+insert([], Chunkset) ->
+ Chunkset;
+insert([{Offset, Length}|T], Chunkset) ->
+ insert(T, insert(Offset, Length, Chunkset)).
+
+
%% @doc
%%
%% @end
-delete(_, Length, _) when Length < 1 ->
- erlang:error(badarg);
-delete(Offset, _, _) when Offset < 0 ->
+delete(Offset, Length, Chunkset) when Length < 1; Offset < 0 ->
erlang:error(badarg);
delete(Offset, Length, Chunkset) ->
#chunkset{chunks=Chunks} = Chunkset,
@@ -287,4 +378,27 @@ insert_past_end_test() ->
Set0 = ?set:new(32, 2),
?assertError(badarg, ?set:insert(0, 33, Set0)).
+in_test_() ->
+ Set0 = ?set:from_list(32, 2, [{0, 31}]),
+ Set1 = ?set:from_list(32, 2, [{0, 10}, {20, 31}]),
+ [ ?_assertEqual(true, ?set:in(5, 6, Set0))
+ , ?_assertEqual(true, ?set:in(0, 32, Set0))
+ , ?_assertEqual(false, ?set:in(0, 35, Set0))
+ , ?_assertEqual(false, ?set:in(0, 55, Set0))
+
+ , ?_assertEqual(true, ?set:in(3, 4, Set1))
+ , ?_assertEqual(false, ?set:in(10, 4, Set1))
+ , ?_assertEqual(false, ?set:in(10, 21, Set1))
+ , ?_assertEqual(false, ?set:in(0, 31, Set1))
+ ].
+
+subtract_test_() ->
+ T0 = ?set:from_list(32, 2, [{10, 20}]),
+ T1 = ?set:from_list(32, 2, [{10, 10}, {15, 20}]),
+ T2 = ?set:from_list(32, 2, [{15, 20}]),
+ [ ?_assertEqual(subtract(11, 4, T0), {T1, [{11,4}]})
+ , ?_assertEqual(subtract(5, 10, T0), {T2, [{10,5}]})
+ , ?_assertEqual(subtract(21, 5, T0), {T0, []})
+ ].
+
-endif.
View
2 src/etorrent_core.app.src
@@ -31,7 +31,7 @@
]},
{applications, [kernel,
stdlib,
- lager,
+ compiler, syntax_tools, lager,
cowboy,
crypto,
public_key,
View
3 src/etorrent_cowboy_handler.erl
@@ -132,7 +132,8 @@ list_torrents() ->
seeding -> "sparkline-seed";
leeching -> "sparkline-leech";
endgame -> "sparkline-leech";
- unknown -> "sparkline-leech"
+ unknown -> "sparkline-leech";
+ paused -> "sparkline-leech"
end,
show_sparkline(lists:reverse(SL)),
round(lists:max(SL) / 1024),
View
27 src/etorrent_ctl.erl
@@ -13,7 +13,7 @@
-export([start_link/1,
start/1, start/2, stop/1,
- check/1]).
+ check/1, pause/1, continue/1]).
-export([handle_cast/2, handle_call/3, init/1, terminate/2]).
-export([handle_info/2, code_change/3]).
@@ -53,6 +53,18 @@ start(File, CallBack) ->
check(Id) ->
gen_server:cast(?SERVER, {check, Id}).
+% @doc Set the torrent on pause
+% @end
+-spec pause(integer()) -> ok.
+pause(Id) ->
+ gen_server:cast(?SERVER, {pause, Id}).
+
+% @doc Set the torrent on play :)
+% @end
+-spec continue(integer()) -> ok.
+continue(Id) ->
+ gen_server:cast(?SERVER, {continue, Id}).
+
% @doc Ask the manager process to stop a torrent, identified by File.
% @end
-spec stop(string()) -> ok.
@@ -69,9 +81,20 @@ init([PeerId]) ->
%% @private
handle_cast({check, Id}, S) ->
- Child = gproc:lookup_local_name({torrent, Id, control}),
+ Child = etorrent_torrent_ctl:lookup_server(Id),
etorrent_torrent_ctl:check_torrent(Child),
{noreply, S};
+
+handle_cast({pause, Id}, S) ->
+ Child = etorrent_torrent_ctl:lookup_server(Id),
+ etorrent_torrent_ctl:pause_torrent(Child),
+ {noreply, S};
+
+handle_cast({continue, Id}, S) ->
+ Child = etorrent_torrent_ctl:lookup_server(Id),
+ etorrent_torrent_ctl:continue_torrent(Child),
+ {noreply, S};
+
handle_cast({stop, F}, S) ->
stop_torrent(F),
{noreply, S}.
View
107 src/etorrent_download.erl
@@ -1,5 +1,6 @@
-module(etorrent_download).
+
%% exported functions
-export([await_servers/1,
request_chunks/3,
@@ -8,8 +9,14 @@
chunk_fetched/4,
chunk_stored/4]).
+%% gproc registry entries
+-export([register_server/1,
+ unregister_server/1,
+ lookup_server/1,
+ await_server/1]).
+
%% update functions
--export([activate_endgame/1,
+-export([switch_assignor/2,
update/2]).
-type torrent_id() :: etorrent_types:torrent_id().
@@ -18,108 +25,106 @@
-type chunk_offset() :: etorrent_types:chunk_offset().
-type chunk_length() :: etorrent_types:chunk_len().
-type chunkspec() :: {pieceindex(), chunk_offset(), chunk_length()}.
+-type update_query() :: {assignor, pid()}.
--type tupdate() :: {endgame, boolean()}.
-record(tservices, {
torrent_id :: torrent_id(),
- in_endgame :: boolean(),
+ assignor :: pid(),
pending :: pid(),
- progress :: pid(),
- histogram :: pid(),
- endgame :: pid()}).
--define(endgame(Handle), (Handle#tservices.in_endgame)).
+ histogram :: pid()}).
-opaque tservices() :: #tservices{}.
-export_type([tservices/0]).
+-spec register_server(torrent_id()) -> true.
+register_server(TorrentID) ->
+ etorrent_utils:register(server_name(TorrentID)).
+
+-spec unregister_server(torrent_id()) -> true.
+unregister_server(TorrentID) ->
+ etorrent_utils:unregister(server_name(TorrentID)).
+
+-spec lookup_server(torrent_id()) -> pid().
+lookup_server(TorrentID) ->
+ etorrent_utils:lookup(server_name(TorrentID)).
+
+-spec await_server(torrent_id()) -> pid().
+await_server(TorrentID) ->
+ etorrent_utils:await(server_name(TorrentID)).
+
+server_name(TorrentID) ->
+ {etorrent, TorrentID, assignor}.
+
+
+
%% @doc
%% @end
-spec await_servers(torrent_id()) -> tservices().
await_servers(TorrentID) ->
Pending = etorrent_pending:await_server(TorrentID),
- Progress = etorrent_progress:await_server(TorrentID),
+ Assignor = await_server(TorrentID),
Histogram = etorrent_scarcity:await_server(TorrentID),
- Endgame = etorrent_endgame:await_server(TorrentID),
- Inendgame = etorrent_endgame:is_active(Endgame),
ok = etorrent_pending:register(Pending),
Handle = #tservices{
torrent_id=TorrentID,
- in_endgame=Inendgame,
pending=Pending,
- progress=Progress,
- histogram=Histogram,
- endgame=Endgame},
+ assignor=Assignor,
+ histogram=Histogram},
Handle.
-%% @doc
+%% @doc Run `update/2' for the peer with Pid.
+%% It allows to change the Hangle tuple.
%% @end
--spec activate_endgame(pid()) -> ok.
-activate_endgame(Pid) ->
- Pid ! {download, {endgame, true}},
+-spec switch_assignor(pid(), pid()) -> ok.
+switch_assignor(PeerPid, Assignor) ->
+ PeerPid ! {download, {assignor, Assignor}},
ok.
-%% @doc
+%% @doc This function is called by peer.
%% @end
--spec update(tupdate(), tservices()) -> tservices().
-update({endgame, Inendgame}, Handle) when is_boolean(Inendgame) ->
- Handle#tservices{in_endgame=Inendgame}.
+-spec update(update_query(), tservices()) -> tservices().
+update({assignor, Assignor}, Handle) when is_pid(Assignor) ->
+ Handle#tservices{assignor=Assignor}.
%% @doc
%% @end
-spec request_chunks(non_neg_integer(), pieceset(), tservices()) ->
{ok, assigned | not_interested | [chunkspec()]}.
-request_chunks(Numchunks, Peerset, Handle) when ?endgame(Handle) ->
- #tservices{endgame=Endgame} = Handle,
- etorrent_chunkstate:request(Numchunks, Peerset, Endgame);
-
request_chunks(Numchunks, Peerset, Handle) ->
- #tservices{progress=Progress} = Handle,
- etorrent_chunkstate:request(Numchunks, Peerset, Progress).
+ #tservices{assignor=Assignor} = Handle,
+ etorrent_chunkstate:request(Numchunks, Peerset, Assignor).
%% @doc
%% @end
-spec chunk_dropped(pieceindex(),
chunk_offset(), chunk_length(), tservices()) -> ok.
-chunk_dropped(Piece, Offset, Length, Handle) when ?endgame(Handle) ->
- #tservices{pending=Pending, endgame=Endgame} = Handle,
- ok = etorrent_chunkstate:dropped(Piece, Offset, Length, self(), Endgame),
- ok = etorrent_chunkstate:dropped(Piece, Offset, Length, self(), Pending);
-
-chunk_dropped(Piece, Offset, Length, Handle) ->
- #tservices{pending=Pending, progress=Progress} = Handle,
- ok = etorrent_chunkstate:dropped(Piece, Offset, Length, self(), Progress),
+chunk_dropped(Piece, Offset, Length, Handle)->
+ #tservices{pending=Pending, assignor=Assignor} = Handle,
+ ok = etorrent_chunkstate:dropped(Piece, Offset, Length, self(), Assignor),
ok = etorrent_chunkstate:dropped(Piece, Offset, Length, self(), Pending).
%% @doc
%% @end
-spec chunks_dropped([chunkspec()], tservices()) -> ok.
-chunks_dropped(Chunks, Handle) when ?endgame(Handle) ->
- #tservices{pending=Pending, endgame=Endgame} = Handle,
- ok = etorrent_chunkstate:dropped(Chunks, self(), Endgame),
- ok = etorrent_chunkstate:dropped(Chunks, self(), Pending);
-
chunks_dropped(Chunks, Handle) ->
- #tservices{pending=Pending, progress=Progress} = Handle,
- ok = etorrent_chunkstate:dropped(Chunks, self(), Progress),
+ #tservices{pending=Pending, assignor=Assignor} = Handle,
+ ok = etorrent_chunkstate:dropped(Chunks, self(), Assignor),
ok = etorrent_chunkstate:dropped(Chunks, self(), Pending).
%% @doc
%% @end
-spec chunk_fetched(pieceindex(),
chunk_offset(), chunk_length(), tservices()) -> ok.
-chunk_fetched(Piece, Offset, Length, Handle) when ?endgame(Handle) ->
- #tservices{endgame=Endgame} = Handle,
- ok = etorrent_chunkstate:fetched(Piece, Offset, Length, self(), Endgame);
-
-chunk_fetched(_, _, _, _) ->
- ok.
+chunk_fetched(Piece, Offset, Length, Handle) ->
+ #tservices{assignor=Assignor} = Handle,
+ ok = etorrent_chunkstate:fetched(Piece, Offset, Length, self(), Assignor).
%% @doc
@@ -128,6 +133,6 @@ chunk_fetched(_, _, _, _) ->
chunk_offset(), chunk_length(), tservices())
-> ok.
chunk_stored(Piece, Offset, Length, Handle) ->
- #tservices{pending=Pending, progress=Progress} = Handle,
- ok = etorrent_chunkstate:stored(Piece, Offset, Length, self(), Progress),
+ #tservices{pending=Pending, assignor=Assignor} = Handle,
+ ok = etorrent_chunkstate:stored(Piece, Offset, Length, self(), Assignor),
ok = etorrent_chunkstate:stored(Piece, Offset, Length, self(), Pending).
View
41 src/etorrent_endgame.erl
@@ -2,9 +2,7 @@
-behaviour(gen_server).
%% exported functions
--export([start_link/1,
- is_active/1,
- activate/1]).
+-export([start_link/1]).
%% gproc registry entries
-export([register_server/1,
@@ -27,7 +25,6 @@
-record(state, {
torrent_id = exit(required) :: torrent_id(),
- active = exit(required) :: boolean(),
pending = exit(required) :: pid(),
assigned = exit(required) :: gb_tree(),
fetched = exit(required) :: gb_tree(),
@@ -64,27 +61,17 @@ start_link(TorrentID) ->
gen_server:start_link(?MODULE, [TorrentID], []).
-%% @doc
-%% @end
--spec is_active(pid()) -> boolean().
-is_active(SrvPid) ->
- gen_server:call(SrvPid, is_active).
-
-
-%% @doc
-%% @end
--spec activate(pid()) -> ok.
-activate(SrvPid) ->
- gen_server:call(SrvPid, activate).
-
-
%% @private
init([TorrentID]) ->
+ lager:info([{endgame, TorrentID}],
+ "Endgame active ~w", [TorrentID]),
+
true = register_server(TorrentID),
+ true = etorrent_download:register_server(TorrentID),
Pending = etorrent_pending:await_server(TorrentID),
+ ok = etorrent_pending:receiver(self(), Pending),
InitState = #state{
torrent_id=TorrentID,
- active=false,
pending=Pending,
assigned=gb_trees:empty(),
fetched=gb_trees:empty(),
@@ -93,22 +80,8 @@ init([TorrentID]) ->
%% @private
-handle_call(is_active, _, State) ->
- #state{active=IsActive} = State,
- {reply, IsActive, State};
-
-handle_call(activate, _, State) ->
- #state{active=false, torrent_id=TorrentID} = State,
- lager:info([{endgame, TorrentID}],
- "Endgame active ~w", [TorrentID]),
- NewState = State#state{active=true},
- Peers = etorrent_peer_control:lookup_peers(TorrentID),
- [etorrent_download:activate_endgame(Peer) || Peer <- Peers],
- {reply, ok, NewState};
-
handle_call({chunk, {request, _, Peerset, Pid}}, _, State) ->
#state{
- active=true,
pending=Pending,
assigned=Assigned} = State,
Request = etorrent_utils:find(
@@ -178,7 +151,6 @@ handle_info({chunk, {dropped, Index, Offset, Length, Pid}}, State) ->
handle_info({chunk, {fetched, Index, Offset, Length, Pid}}, State) ->
#state{
- active=true,
assigned=Assigned,
fetched=Fetched,
stored=Stored} = State,
@@ -204,7 +176,6 @@ handle_info({chunk, {fetched, Index, Offset, Length, Pid}}, State) ->
handle_info({chunk, {stored, Index, Offset, Length, Pid}}, State) ->
#state{
- active=true,
fetched=Fetched,
stored=Stored} = State,
Chunk = {Index, Offset, Length},
View
24 src/etorrent_event.erl
@@ -10,16 +10,23 @@
%% Installation/deinstallation of the event mgr
-export([start_link/0,
- add_handler/2,
- delete_handler/2]).
+ add_handler/2,
+ delete_handler/2]).
-%% Notifications
+%% Torrent Notifications
-export([notify/1,
started_torrent/1,
+ stopped_torrent/1,
checking_torrent/1,
- completed_torrent/1,
+ completed_torrent/1,
seeding_torrent/1]).
+%% Task Notifications
+-export([added_task/1,
+ completed_task/1,
+ failed_task/2]).
+
+
-define(SERVER, ?MODULE).
%% =======================================================================
@@ -42,6 +49,10 @@ add_handler(Handler, Args) ->
delete_handler(Handler, Args) ->
gen_event:delete_handler(?SERVER, Handler, Args).
+%% @equiv notify({stopped_torrent, Id})
+-spec stopped_torrent(integer()) -> ok.
+stopped_torrent(Id) -> notify({stopped_torrent, Id}).
+
%% @equiv notify({started_torrent, Id})
-spec started_torrent(integer()) -> ok.
started_torrent(Id) -> notify({started_torrent, Id}).
@@ -58,6 +69,11 @@ seeding_torrent(Id) -> notify({seeding_torrent, Id}).
-spec completed_torrent(integer()) -> ok.
completed_torrent(Id) -> notify({completed_torrent, Id}).
+%% @doc New task was added.
+added_task(Props) -> notify({added_task, Props}).
+completed_task(Props) -> notify({completed_task, Props}).
+failed_task(Props, Reason) -> notify({failed_task, Props, Reason}).
+
%% ====================================================================
%% @doc Start the event handler
View
113 src/etorrent_fast_resume.erl
@@ -52,7 +52,13 @@ list() ->
gen_server:call(srv_name(), list).
%% @doc Query for the state of TorrentID, ID.
-%% <p>The function returns one of several possible values:</p>
+%% <p>The function returns the proplist.
+%% [{state, State},
+%% {bitfield, Bitfield}, optional
+%% {uploaded, Uploaded},
+%% {downloaded, Downloaded}]
+
+%% State has one of several possible values:</p>
%% <dl>
%% <dt>unknown</dt>
%% <dd>
@@ -61,19 +67,19 @@ list() ->
%% if we had just started it
%% </dd>
%%
-%% <dt>seeding</dt>
+%% <dt>seeding, paused, Left = 0</dt>
%% <dd>
-%% We are currently seeding this torrent
+%% We are currently seeding this torrent.
%% </dd>
%%
-%% <dt>{bitfield, BF}</dt>
+%% <dt>leaching, paused, Left > 0</dt>
%% <dd>
%% Here is the bitfield of known good pieces.
%% The rest are in an unknown state.
%% </dd>
%% </dl>
%% @end
--spec query_state(integer()) -> unknown | {value, [{term(), term()}]}.
+-spec query_state(integer()) -> [{term(), term()}].
query_state(ID) ->
gen_server:call(srv_name(), {query_state, ID}).
@@ -89,6 +95,8 @@ srv_name() ->
update() ->
gen_server:call(srv_name(), update).
+
+
%% ==================================================================
%% @private
@@ -115,13 +123,24 @@ init([]) ->
handle_call({query_state, ID}, _From, State) ->
#state{table=Table} = State,
{value, Properties} = etorrent_table:get_torrent(ID),
- Torrentfile = proplists:get_value(filename, Properties),
- Reply = case dets:lookup(Table, Torrentfile) of
- [] ->
- unknown;
- [{_, Torrentstate}] ->
- {value, Torrentstate}
- end,
+ TorrentFile = proplists:get_value(filename, Properties),
+
+ Reply = case dets:lookup(Table, TorrentFile) of
+ [{_, [_|_] = TorrentState}] ->
+ case proplists:get_value('state', TorrentState, 0) of
+ X when is_atom(X) ->
+ % check the fact that state is atom, not a tuple.
+ TorrentState;
+ _ ->
+ % data is from old version of code.
+ []
+ end;
+
+ _ ->
+ % there is no data.
+ []
+ end,
+
{reply, Reply, State};
handle_call(list, _, #state { table = Table } = State) ->
@@ -144,6 +163,7 @@ handle_call(update, _, State) ->
dets:sync(Table),
{reply, ok, State}.
+
%% @private
handle_cast(_, State) ->
{noreply, State}.
@@ -161,35 +181,50 @@ code_change(_, State, _) ->
{ok, State}.
%% Enter a torrent into the tracking table
-track_torrent(ID, Filename, Table) ->
- case etorrent_torrent:lookup(ID) of
+track_torrent(Id, Filename, Table) ->
+ case etorrent_torrent:lookup(Id) of
not_found ->
ignore;
{value, Props} ->
- UploadTotal = proplists:get_value(all_time_uploaded, Props),
- UploadDiff = proplists:get_value(uploaded, Props),
- Uploaded = UploadTotal + UploadDiff,
-
- DownloadTotal = proplists:get_value(all_time_downloaded, Props),
- DownloadDiff = proplists:get_value(downloaded, Props),
- Downloaded = DownloadTotal + DownloadDiff,
-
- case proplists:get_value(state, Props) of
- unknown ->
- ignore;
- seeding ->
- dets:insert(Table,
- {Filename, [
- {state, seeding},
- {uploaded, Uploaded},
- {downloaded, Downloaded}]});
- _ ->
- TorrentPid = etorrent_torrent_ctl:lookup_server(ID),
- {ok, Valid} = etorrent_torrent_ctl:valid_pieces(TorrentPid),
- Bitfield = etorrent_pieceset:to_binary(Valid),
- dets:insert(Table,
- {Filename, [{state, {bitfield, Bitfield}},
- {uploaded, Uploaded},
- {downloaded, Downloaded}]})
+ case form_entry(Id, Props) of
+ ignore -> ignore;
+ Entry ->
+ dets:insert(Table, {Filename, Entry})
end
end.
+
+%% @private
+form_entry(Id, Props) ->
+ UploadTotal = proplists:get_value(all_time_uploaded, Props),
+ UploadDiff = proplists:get_value(uploaded, Props),
+ Uploaded = UploadTotal + UploadDiff,
+
+ DownloadTotal = proplists:get_value(all_time_downloaded, Props),
+ DownloadDiff = proplists:get_value(downloaded, Props),
+ Downloaded = DownloadTotal + DownloadDiff,
+
+ Left = proplists:get_value(left, Props),
+
+ case proplists:get_value(state, Props) of
+ %% not prepared
+ unknown ->
+ ignore;
+
+ %% downloaded
+ State when Left =:= 0 ->
+ [{state, State}
+ ,{uploaded, Uploaded}
+ ,{downloaded, Downloaded}];
+
+ %% not downloaded
+ State ->
+ TorrentPid = etorrent_torrent_ctl:lookup_server(Id),
+ {ok, Valid} = etorrent_torrent_ctl:valid_pieces(TorrentPid),
+ Bitfield = etorrent_pieceset:to_binary(Valid),
+ {ok, Wishes} = etorrent_torrent_ctl:get_permanent_wishes(Id),
+ [{state, State}
+ ,{bitfield, Bitfield}
+ ,{wishes, Wishes}
+ ,{uploaded, Uploaded}
+ ,{downloaded, Downloaded}]
+ end.
View
682 src/etorrent_info.erl
@@ -0,0 +1,682 @@
+% @author Uvarov Michail <freeakk@gmail.com>
+
+-module(etorrent_info).
+-behaviour(gen_server).
+
+-define(AWAIT_TIMEOUT, 10*1000).
+-define(DEFAULT_CHUNK_SIZE, 16#4000). % TODO - get this value from a configuration file
+
+
+-export([start_link/2,
+ register_server/1,
+ lookup_server/1,
+ await_server/1]).
+
+-export([get_mask/2,
+ get_mask/4,
+ tree_children/2,
+ minimize_filelist/2]).
+
+%% Info API
+-export([long_file_name/2,
+ file_name/2,
+ full_file_name/2,
+ file_position/2,
+ file_size/2, %
+ piece_size/1, %
+ piece_count/1, %
+ chunk_size/1 %
+ ]).
+
+
+-export([init/1,
+ handle_call/3,
+ terminate/2,
+ code_change/3]).
+
+
+-type block_len() :: etorrent_types:block_len().
+-type block_offset() :: etorrent_types:block_offset().
+-type bcode() :: etorrent_types:bcode().
+-type piece_bin() :: etorrent_types:piece_bin().
+-type chunk_len() :: etorrent_types:chunk_len().
+-type chunk_offset() :: etorrent_types:chunk_offset().
+-type chunk_bin() :: etorrent_types:chunk_bin().
+-type piece_index() :: etorrent_types:piece_index().
+-type file_path() :: etorrent_types:file_path().
+-type torrent_id() :: etorrent_types:torrent_id().
+-type file_id() :: etorrent_types:file_id().
+-type block_pos() :: {string(), block_offset(), block_len()}.
+-type pieceset() :: etorrent_pieceset:pieceset().
+
+-record(io_file, {
+ rel_path :: file_path(),
+ process :: pid(),
+ monitor :: reference(),
+ accessed :: {integer(), integer(), integer()}}).
+
+
+-record(state, {
+ torrent :: torrent_id(),
+ static_file_info :: array(),
+ total_size :: non_neg_integer(),
+ piece_size :: non_neg_integer(),
+ chunk_size = ?DEFAULT_CHUNK_SIZE :: non_neg_integer(),
+ piece_count :: non_neg_integer()
+ }).
+
+
+-record(file_info, {
+ id :: file_id(),
+ %% Relative name, used in file_sup
+ name :: string(),
+ %% Label for nodes of cascadae file tree
+ short_name :: binary(),
+ type = file :: directory | file,
+ children = [] :: [file_id()],
+ % How many files are in this node?
+ capacity = 0 :: non_neg_integer(),
+ size = 0 :: non_neg_integer(),
+ % byte offset from 0
+ position = 0 :: non_neg_integer(),
+ pieces :: array()
+}).
+
+-type file_info() :: #file_info{}.
+
+
+%% @doc Start the File I/O Server
+%% @end
+-spec start_link(torrent_id(), bcode()) -> {'ok', pid()}.
+start_link(TorrentID, Torrent) ->
+ gen_server:start_link(?MODULE, [TorrentID, Torrent], [{timeout,15000}]).
+
+
+
+server_name(TorrentID) ->
+ {etorrent, TorrentID, info}.
+
+
+%% @doc
+%% Register the current process as the directory server for
+%% the given torrent.
+%% @end
+-spec register_server(torrent_id()) -> true.
+register_server(TorrentID) ->
+ etorrent_utils:register(server_name(TorrentID)).
+
+%% @doc
+%% Lookup the process id of the directory server responsible
+%% for the given torrent. If there is no such server registered
+%% this function will crash.
+%% @end
+-spec lookup_server(torrent_id()) -> pid().
+lookup_server(TorrentID) ->
+ etorrent_utils:lookup(server_name(TorrentID)).
+
+%% @doc
+%% Wait for the directory server for this torrent to appear
+%% in the process registry.
+%% @end
+-spec await_server(torrent_id()) -> pid().
+await_server(TorrentID) ->
+ etorrent_utils:await(server_name(TorrentID), ?AWAIT_TIMEOUT).
+
+
+
+%% @doc Build a mask of the file in the torrent.
+-spec get_mask(torrent_id(), file_id()) -> pieceset().
+get_mask(TorrentID, FileID) when is_integer(FileID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Mask} = gen_server:call(DirPid, {get_mask, FileID}),
+ Mask;
+
+%% List of files with same priority.
+get_mask(TorrentID, [_|_] = IdList) ->
+ true = lists:all(fun is_integer/1, IdList),
+ DirPid = await_server(TorrentID),
+ MapFn = fun(FileID) ->
+ {ok, Mask} = gen_server:call(DirPid, {get_mask, FileID}),
+ Mask
+ end,
+
+ %% Do map
+ Masks = lists:map(MapFn, IdList),
+ %% Do reduce
+ etorrent_pieceset:union(Masks).
+
+
+%% @doc Build a mask of the part of the file in the torrent.
+get_mask(TorrentID, FileID, PartStart, PartSize)
+ when PartStart >= 0, PartSize >= 0,
+ is_integer(TorrentID), is_integer(FileID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Mask} = gen_server:call(DirPid, {get_mask, FileID, PartStart, PartSize}),
+ Mask.
+
+
+piece_size(TorrentID) when is_integer(TorrentID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Size} = gen_server:call(DirPid, piece_size),
+ Size.
+
+
+chunk_size(TorrentID) when is_integer(TorrentID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Size} = gen_server:call(DirPid, chunk_size),
+ Size.
+
+
+piece_count(TorrentID) when is_integer(TorrentID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Count} = gen_server:call(DirPid, piece_count),
+ Count.
+
+
+file_position(TorrentID, FileID) when is_integer(TorrentID), is_integer(FileID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Pos} = gen_server:call(DirPid, {position, FileID}),
+ Pos.
+
+
+file_size(TorrentID, FileID) when is_integer(TorrentID), is_integer(FileID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Size} = gen_server:call(DirPid, {size, FileID}),
+ Size.
+
+
+-spec tree_children(torrent_id(), file_id()) -> [{atom(), term()}].
+tree_children(TorrentID, FileID) when is_integer(TorrentID), is_integer(FileID) ->
+ %% get children
+ DirPid = await_server(TorrentID),
+ {ok, Records} = gen_server:call(DirPid, {tree_children, FileID}),
+
+ %% get valid pieceset
+ CtlPid = etorrent_torrent_ctl:lookup_server(TorrentID),
+ {ok, Valid} = etorrent_torrent_ctl:valid_pieces(CtlPid),
+
+ lists:map(fun(X) ->
+ ValidFP = etorrent_pieceset:intersection(X#file_info.pieces, Valid),
+ SizeFP = etorrent_pieceset:size(X#file_info.pieces),
+ ValidSizeFP = etorrent_pieceset:size(ValidFP),
+ [{id, X#file_info.id}
+ ,{name, X#file_info.short_name}
+ ,{size, X#file_info.size}
+ ,{capacity, X#file_info.capacity}
+ ,{is_leaf, (X#file_info.children == [])}
+ ,{progress, ValidSizeFP / SizeFP}
+ ]
+ end, Records).
+
+
+%% @doc Form minimal version of the filelist with the same pieceset.
+minimize_filelist(TorrentID, FileIds) when is_integer(TorrentID) ->
+ SortedFiles = lists:sort(FileIds),
+ DirPid = await_server(TorrentID),
+ {ok, Ids} = gen_server:call(DirPid, {minimize_filelist, SortedFiles}),
+ Ids.
+
+
+%% @doc This name is used in cascadae wish view.
+-spec long_file_name(torrent_id(), file_id() | [file_id()]) -> binary().
+long_file_name(TorrentID, FileID) when is_integer(FileID) ->
+ long_file_name(TorrentID, [FileID]);
+
+long_file_name(TorrentID, FileID) when is_list(FileID), is_integer(TorrentID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Name} = gen_server:call(DirPid, {long_file_name, FileID}),
+ Name.
+
+
+full_file_name(TorrentID, FileID) when is_integer(FileID), is_integer(TorrentID) ->
+ RelName = etorrent_io:file_name(TorrentID, FileID),
+ FileServer = etorrent_io:lookup_file_server(TorrentID, RelName),
+ {ok, Name} = etorrent_io_file:full_path(FileServer),
+ Name.
+
+
+%% @doc Convert FileID to relative file name.
+file_name(TorrentID, FileID) when is_integer(FileID) ->
+ DirPid = await_server(TorrentID),
+ {ok, Name} = gen_server:call(DirPid, {file_name, FileID}),
+ Name.
+
+
+%% ----------------------------------------------------------------------
+
+%% @private
+init([TorrentID, Torrent]) ->
+ Info = collect_static_file_info(Torrent),
+
+ {Static, PLen, TLen} = Info,
+
+ true = register_server(TorrentID),
+
+ InitState = #state{
+ torrent=TorrentID,
+ static_file_info=Static,
+ total_size=TLen,
+ piece_size=PLen,
+ piece_count=(TLen div PLen) +
+ (case TLen rem PLen of 0 -> 0; _ -> 1 end)
+ },
+ {ok, InitState}.
+
+
+%% @private
+
+handle_call({get_info, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ X=#file_info{} ->
+ {reply, {ok, X}, State}
+ end;
+
+handle_call({position, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info{position=P} ->
+ {reply, {ok, P}, State}
+ end;
+
+handle_call({size, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info{size=Size} ->
+ {reply, {ok, Size}, State}
+ end;
+
+handle_call(chunk_size, _, State=#state{chunk_size=S}) ->
+ {reply, {ok, S}, State};
+
+handle_call(piece_size, _, State=#state{piece_size=S}) ->
+ {reply, {ok, S}, State};
+
+handle_call(piece_count, _, State=#state{piece_count=C}) ->
+ {reply, {ok, C}, State};
+
+handle_call({get_mask, FileID, PartStart, PartSize}, _, State) ->
+ #state{static_file_info=Arr, total_size=TLen, piece_size=PLen} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info {position = FileStart, size = FileSize} ->
+ %% true = PartSize =< FileSize,
+
+ %% Start from beginning of the torrent
+ From = FileStart + PartStart,
+ To = From + PartSize,
+ Mask = make_mask(From, To, PLen, TLen),
+ Set = etorrent_pieceset:from_bitstring(Mask),
+
+ {reply, {ok, Set}, State}
+ end;
+
+handle_call({get_mask, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info {pieces = Mask} ->
+ {reply, {ok, Mask}, State}
+ end;
+
+handle_call({long_file_name, FileIDs}, _, State) ->
+ #state{static_file_info=Arr} = State,
+
+ F = fun(FileID) ->
+ Rec = array:get(FileID, Arr),
+ Rec#file_info.name
+ end,
+
+ Reply = try
+ NameList = lists:map(F, FileIDs),
+ NameBinary = list_to_binary(string:join(NameList, ", ")),
+ {ok, NameBinary}
+ catch error:_ ->
+ lager:error("List of ids ~w caused an error.",
+ [FileIDs]),
+ {error, badid}
+ end,
+
+ {reply, Reply, State};
+
+handle_call({file_name, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info {name = Name} ->
+ {reply, {ok, Name}, State}
+ end;
+
+handle_call({minimize_filelist, FileIDs}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ RecList = [ array:get(FileID, Arr) || FileID <- FileIDs ],
+ FilteredIDs = [Rec#file_info.id || Rec <- minimize_reclist(RecList)],
+ {reply, {ok, FilteredIDs}, State};
+
+handle_call({tree_children, FileID}, _, State) ->
+ #state{static_file_info=Arr} = State,
+ case array:get(FileID, Arr) of
+ undefined ->
+ {reply, {error, badid}, State};
+ #file_info {children = Ids} ->
+ Children = [array:get(Id, Arr) || Id <- Ids],
+ {reply, {ok, Children}, State}
+ end.
+
+
+
+%% @private
+terminate(_, _) ->
+ ok.
+
+%% @private
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% ----------------------------------------------------------------------
+
+
+
+%% -\/-----------------FILE INFO API----------------------\/-
+%% @private
+collect_static_file_info(Torrent) ->
+ PieceLength = etorrent_metainfo:get_piece_length(Torrent),
+ FileLengths = etorrent_metainfo:file_path_len(Torrent),
+ CurrentDirectory = "",
+ Acc = [],
+ Pos = 0,
+ %% Rec1, Rec2, .. are lists of nodes.
+ %% Calculate positions, create records. They are still not prepared.
+ {TLen, Rec1} = flen_to_record(FileLengths, Pos, Acc),
+ %% Add directories as additional nodes.
+ Rec2 = add_directories(Rec1),
+ %% Fill `pieces' field.
+ %% A mask is a set of pieces which contains the file.
+ Rec3 = fill_pieces(Rec2, PieceLength, TLen),
+ Rec4 = fill_ids(Rec3),
+ {array:from_list(Rec4), PieceLength, TLen}.
+
+
+%% @private
+flen_to_record([{Name, FLen} | T], From, Acc) ->
+ To = From + FLen,
+ X = #file_info {
+ type = file,
+ name = Name,
+ position = From,
+ size = FLen
+ },
+ flen_to_record(T, To, [X|Acc]);
+
+flen_to_record([], TotalLen, Acc) ->
+ {TotalLen+1, lists:reverse(Acc)}.
+
+
+%% @private
+add_directories(Rec1) ->
+ Idx = 1,
+ {Rec2, Children, Idx1, []} = add_directories_(Rec1, Idx, "", [], []),
+ [Last|_] = Rec2,
+ Rec3 = lists:reverse(Rec2),
+
+ #file_info {
+ size = LastSize,
+ position = LastPos
+ } = Last,
+
+ Root = #file_info {
+ name = "",
+ % total size
+ size = (LastSize + LastPos),
+ position = 0,
+ children = Children,
+ capacity = Idx1 - Idx
+ },
+
+ [Root|Rec3].
+
+
+%% "test/t1.txt"
+%% "t2.txt"
+%% "dir1/dir/x.x"
+%% ==>
+%% "."
+%% "test"
+%% "test/t1.txt"
+%% "t2.txt"
+%% "dir1"
+%% "dir1/dir"
+%% "dir1/dir/x.x"
+
+%% @private
+dirname_(Name) ->
+ case filename:dirname(Name) of
+ "." -> "";
+ Dir -> Dir
+ end.
+
+%% @private
+first_token_(Path) ->
+ case filename:split(Path) of
+ ["/", Token | _] -> Token;
+ [Token | _] -> Token
+ end.
+
+%% @private
+file_join_(L, R) ->
+ case filename:join(L, R) of
+ "/" ++ X -> X;
+ X -> X
+ end.
+
+file_prefix_(S1, S2) ->
+ lists:prefix(filename:split(S1), filename:split(S2)).
+
+
+%% @private
+add_directories_([], Idx, Cur, Children, Acc) ->
+ {Acc, lists:reverse(Children), Idx, []};
+
+%% @private
+add_directories_([H|T], Idx, Cur, Children, Acc) ->
+ #file_info{ name = Name, position = CurPos } = H,
+ Dir = dirname_(Name),
+ Action = case Dir of
+ Cur -> 'equal';
+ _ ->
+ case file_prefix_(Cur, Dir) of
+ true -> 'prefix';
+ false -> 'other'
+ end
+ end,
+
+ case Action of
+ %% file is in the same directory
+ 'equal' ->
+ add_directories_(T, Idx+1, Dir, [Idx|Children], [H|Acc]);
+
+ %% file is in child directory
+ 'prefix' ->
+ Sub = Dir -- Cur,
+ Part = first_token_(Sub),
+ NextDir = file_join_(Cur, Part),
+
+ {SubAcc, SubCh, Idx1, SubT}
+ = add_directories_([H|T], Idx+1, NextDir, [], []),
+ [#file_info{ position = LastPos, size = LastSize }|_] = SubAcc,
+
+ DirRec = #file_info {
+ name = NextDir,
+ size = (LastPos + LastSize - CurPos),
+ position = CurPos,
+ children = SubCh,
+ capacity = Idx1 - Idx
+ },
+ NewAcc = SubAcc ++ [DirRec|Acc],
+ add_directories_(SubT, Idx1, Cur, [Idx|Children], NewAcc);
+
+ %% file is in the other directory
+ 'other' ->
+ {Acc, lists:reverse(Children), Idx, [H|T]}
+ end.
+
+
+%% @private
+fill_pieces(RecList, PLen, TLen) ->
+ F = fun(#file_info{position = From, size = Size} = Rec) ->
+ To = From + Size,
+ Mask = make_mask(From, To, PLen, TLen),
+ Set = etorrent_pieceset:from_bitstring(Mask),
+ Rec#file_info{pieces = Set}
+ end,
+
+ lists:map(F, RecList).
+
+
+fill_ids(RecList) ->
+ fill_ids_(RecList, 0, []).
+
+
+fill_ids_([H1=#file_info{name=Name}|T], Id, Acc) ->
+ % set id, prepare name for cascadae
+ H2 = H1#file_info{
+ id = Id,
+ short_name = list_to_binary(filename:basename(Name))
+ },
+ fill_ids_(T, Id+1, [H2|Acc]);
+fill_ids_([], _Id, Acc) ->
+ lists:reverse(Acc).
+
+
+%% @private
+make_mask(From, To, PLen, TLen)
+ when PLen =< TLen, From =< To,
+ To < TLen, From >= 0 ->
+ %% __Bytes__: 1 <= From <= To <= TLen
+ %%
+ %% Calculate how many __pieces__ before, in and after the file.
+ %% Be greedy: when the file ends inside a piece, then put this piece
+ %% both into this file and into the next file.
+ %% [0..X1 ) [X1..X2] (X2..MaxPieces]
+ %% [before) [ in ] ( after ]
+ PTotal = (TLen div PLen)
+ + case TLen rem PLen of 0 -> 0; _ -> 1 end,
+
+ %% indexing from 0
+ PFrom = From div PLen,
+ PTo = To div PLen,
+
+ PBefore = PFrom,
+ PIn = PTo - PFrom + 1,
+ PAfter = PTotal - PFrom - PIn,
+ <<0:PBefore, (bnot 0):PIn, 0:PAfter>>.
+
+
+%% @private
+minimize_reclist(RecList) ->
+ minimize_(RecList, []).
+
+
+minimize_([H|T], []) ->
+ minimize_(T, [H]);
+
+
+%% H is a ancestor of the previous element. Skip H.
+minimize_([H=#file_info{position=Pos}|T],
+ [#file_info{size=PrevSize, position=PrevPos}|_] = Acc)
+ when Pos < (PrevPos + PrevSize) ->
+ minimize_(T, Acc);
+
+minimize_([H|T], Acc) ->
+ minimize_(T, [H|Acc]);
+
+minimize_([], Acc) ->
+ lists:reverse(Acc).
+
+
+%% -/\-----------------FILE INFO API----------------------/\-
+
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+make_mask_test_() ->
+ F = fun make_mask/4,
+ % make_index(From, To, PLen, TLen)
+ [?_assertEqual(F(2, 5, 4, 10), <<2#110:3>>)
+ ,?_assertEqual(F(2, 5, 3, 10), <<2#1100:4>>)
+ ,?_assertEqual(F(2, 5, 2, 10), <<2#01100:5>>)
+ ,?_assertEqual(F(2, 5, 1, 10), <<2#0011110000:10>>)
+ ,?_assertEqual(F(2, 5, 10, 10), <<1:1>>)
+ ,?_assertEqual(F(2, 5, 9, 10), <<1:1, 0:1>>)
+ ,?_assertEqual(F(0, 5, 3, 10), <<2#1100:4>>)
+ ,?_assertEqual(F(8, 9, 3, 10), <<2#0011:4>>)
+ ].
+
+add_directories_test_() ->
+ Rec = add_directories(
+ [#file_info{position=0, size=3, name="test/t1.txt"}
+ ,#file_info{position=3, size=2, name="t2.txt"}
+ ,#file_info{position=5, size=1, name="dir1/dir/x.x"}
+ ,#file_info{position=6, size=2, name="dir1/dir/x.y"}
+ ]),
+ Names = el(Rec, #file_info.name),
+ Sizes = el(Rec, #file_info.size),
+ Positions = el(Rec, #file_info.position),
+ Children = el(Rec, #file_info.children),
+
+ [Root|Elems] = Rec,
+ MinNames = el(minimize_reclist(Elems), #file_info.name),
+
+ %% {NumberOfFile, Name, Size, Position, ChildNumbers}
+ List = [{0, "", 8, 0, [1, 3, 4]}
+ ,{1, "test", 3, 0, [2]}
+ ,{2, "test/t1.txt", 3, 0, []}
+ ,{3, "t2.txt", 2, 3, []}
+ ,{4, "dir1", 3, 5, [5]}
+ ,{5, "dir1/dir", 3, 5, [6, 7]}
+ ,{6, "dir1/dir/x.x", 1, 5, []}
+ ,{7, "dir1/dir/x.y", 2, 6, []}
+ ],
+ ExpNames = el(List, 2),
+ ExpSizes = el(List, 3),
+ ExpPositions = el(List, 4),
+ ExpChildren = el(List, 5),
+
+ [?_assertEqual(Names, ExpNames)
+ ,?_assertEqual(Sizes, ExpSizes)
+ ,?_assertEqual(Positions, ExpPositions)
+ ,?_assertEqual(Children, ExpChildren)
+ ,?_assertEqual(MinNames, ["test", "t2.txt", "dir1"])
+ ].
+
+
+el(List, Pos) ->
+ Children = [element(Pos, X) || X <- List].
+
+
+
+add_directories_test() ->
+ add_directories(
+ [#file_info{position=0, size=3, name=
+ "BBC.7.BigToe/Eoin Colfer. Artemis Fowl/artemis_04.mp3"}
+ ,#file_info{position=3, size=2, name=
+ "BBC.7.BigToe/Eoin Colfer. Artemis Fowl. The Arctic Incident/artemis2_03.mp3"}
+ ]).
+
+% H = {file_info,undefined,
+% "BBC.7.BigToe/Eoin Colfer. Artemis Fowl. The Arctic Incident/artemis2_03.mp3",
+% undefined,file,[],0,5753284,1633920175,undefined}
+% NextDir = "BBC.7.BigToe/Eoin Colfer. Artemis Fowl/. The Arctic Incident
+
+-endif.
View
8 src/etorrent_io.erl
@@ -47,7 +47,7 @@
-include_lib("eunit/include/eunit.hrl").
-endif.
--define(AWAIT_TIMEOUT, 10*1000).
+-define(AWAIT_TIMEOUT, 60*1000).
-export([start_link/2,
allocate/1,
@@ -107,7 +107,7 @@
%% @end
-spec start_link(torrent_id(), bcode()) -> {'ok', pid()}.
start_link(TorrentID, Torrent) ->
- gen_server:start_link(?MODULE, [TorrentID, Torrent], []).
+ gen_server:start_link(?MODULE, [TorrentID, Torrent], [{timeout, 10000}]).
%% @doc Allocate bytes in the end of files in a torrent
%% @end
@@ -296,6 +296,7 @@ register_directory(TorrentID) ->
%% @doc
-spec register_file_server(torrent_id(), file_path()) -> true.
register_file_server(TorrentID, Path) ->
+% io:format("Register ~p ~p ~n", [TorrentID, Path]),
etorrent_utils:register(file_server_name(TorrentID, Path)).
%% @doc
@@ -348,6 +349,7 @@ unregister_open_file(TorrentID, Path) ->
%% @end
-spec await_file_server(torrent_id(), file_path()) -> pid().
await_file_server(TorrentID, Path) ->
+% io:format("Await file ~p ~p ~n", [TorrentID, Path]),
etorrent_utils:await(file_server_name(TorrentID, Path), ?AWAIT_TIMEOUT).
%% @doc
@@ -404,9 +406,9 @@ init([TorrentID, Torrent]) ->
% Let the user define a limit on the amount of files
% that will be open at the same time
MaxFiles = etorrent_config:max_files(),
- true = register_directory(TorrentID),
PieceMap = make_piece_map(Torrent),
Files = make_file_list(Torrent),
+ true = register_directory(TorrentID),
InitState = #state{
torrent=TorrentID,
pieces=PieceMap,
View
29 src/etorrent_io_file.erl
@@ -15,7 +15,9 @@
close/1,
read/3,
write/3,
- allocate/2]).
+ full_path/1,
+ allocate/2]).
+
-export([init/1,
handle_call/3,
@@ -46,7 +48,9 @@
%% @end
-spec start_link(torrent_id(), file_path(), file_path()) -> {'ok', pid()}.
start_link(TorrentID, Path, FullPath) ->
- gen_server:start_link(?MODULE, [TorrentID, Path, FullPath], [{spawn_opt, [{fullsweep_after, 0}]}]).
+ Args = [TorrentID, Path, FullPath],
+ Opts =[{spawn_opt, [{fullsweep_after, 0}]}],
+ gen_server:start_link(?MODULE, Args, Opts).
%% @doc Request to open the file
%% @end
@@ -83,6 +87,10 @@ write(FilePid, Offset, Chunk) ->
allocate(FilePid, Size) ->
gen_server:call(FilePid, {allocate, Size}, infinity).
+full_path(FilePid) ->
+ gen_server:call(FilePid, full_path, ?CALL_TIMEOUT).
+
+
%% @private
init([TorrentID, RelPath, FullPath]) ->
true = etorrent_io:register_file_server(TorrentID, RelPath),
@@ -96,22 +104,39 @@ init([TorrentID, RelPath, FullPath]) ->
%% @private
handle_call({read, _, _}, _, State) when State#state.handle == closed ->
{reply, {error, eagain}, State, ?GC_TIMEOUT};
+
handle_call({write, _, _}, _, State) when State#state.handle == closed ->
{reply, {error, eagain}, State, ?GC_TIMEOUT};
+
+%% Who was call it?
+%% It is a mini hack for files with an actual length of zero.
+handle_call({read, 0, 0}, _, State) ->
+ Chunk = <<>>,
+ {reply, {ok, Chunk}, State, ?GC_TIMEOUT};
+
handle_call({read, Offset, Length}, _, State) ->
#state{handle=Handle} = State,
+ %% If file length is 0, then this function will returns eof (badmatch)
{ok, Chunk} = file:pread(Handle, Offset, Length),
{reply, {ok, Chunk}, State, ?GC_TIMEOUT};
+
handle_call({write, Offset, Chunk}, _, State) ->
#state{handle=Handle} = State,
ok = file:pwrite(Handle, Offset, Chunk),
{reply, ok, State, ?GC_TIMEOUT};
+
+handle_call(full_path, _, State) ->
+ #state{fullpath=Path} = State,
+ {reply, {ok, Path}, State, ?GC_TIMEOUT};
+
handle_call({allocate, _Sz}, _From, #state { handle = closed } = S) ->
{reply, {error, eagain}, S, ?GC_TIMEOUT};
+
handle_call({allocate, Sz}, _From, #state { handle = FD } = S) ->
fill_file(FD, Sz),
{reply, ok, S, ?GC_TIMEOUT}.
+
%% @private
handle_cast(open, State) ->
#state{
View
2 src/etorrent_io_sup.erl
@@ -29,7 +29,7 @@ init([TorrentID, Torrent]) ->
DirServer = directory_server_spec(TorrentID, Torrent),
Dldir = etorrent_config:download_dir(),
FileSup = file_server_sup_spec(TorrentID, Dldir, Files),
- {ok, {{one_for_one, 1, 60}, [DirServer, FileSup]}}.
+ {ok, {{one_for_one, 1, 60}, [FileSup, DirServer]}}.
%% ----------------------------------------------------------------------
directory_server_spec(TorrentID, Torrent) ->
View
18 src/etorrent_peer_control.erl
@@ -21,6 +21,7 @@
initialize/2,
incoming_msg/2,
check_choke/1,
+ update_queue/1,
stop/1]).
%% gproc registry entries
@@ -120,6 +121,13 @@ choke(Pid) ->
unchoke(Pid) ->
gen_server:cast(Pid, unchoke).
+%% @doc Rerun `poll_local_rqueue'.
+%% <p>The intended caller of this function is the {@link etorrent_reordered}</p>
+%% @end
+update_queue(Pid) ->
+ gen_server:cast(Pid, update_queue).
+
+
%% @doc Initialize the connection.
%% <p>The `Way' parameter tells the client of the connection is
%% `incoming' or `outgoing'. They are handled differently since part
@@ -415,7 +423,8 @@ handle_info({chunk, {contents, Index, Offset, Length, Data}}, State) ->
NewRemote = etorrent_peerstate:requests(NewRequests, Remote),
NewState = State#state{remote=NewRemote},
ok = etorrent_peer_send:piece(SendPid, Index, Offset, Length, Data),
- ok = etorrent_torrent:statechange(TorrentID, [{add_upload, Length}]),
+ % Already in peer_send? It cause double-rating!
+ % ok = etorrent_torrent:statechange(TorrentID, [{add_upload, Length}]),
ok = pop_remote_rqueue_hook(TorrentID, NewRequests),
{noreply, NewState};
%% Same as clause #2. Peer returned to unchoked state. Non empty queue
@@ -439,6 +448,13 @@ handle_info({piece, {unassigned, _}}, State) ->
NewState = State#state{local=NewLocal},
{noreply, NewState};
+%% The chunk manager wants new chunks.
+handle_info(update_queue, State) ->
+ #state{download=Download, send_pid=SendPid, local=Local, remote=Remote} = State,
+ NewLocal = poll_local_rqueue(Download, SendPid, Remote, Local),
+ NewState = State#state{local=NewLocal},
+ {noreply, NewState};
+
%% etorrent_peerstate:interested(self()),
handle_info({peer, {check, seeder}}, State) ->
{noreply, State};
View
40 src/etorrent_pieceset.erl
@@ -3,13 +3,15 @@
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-undef(LET).
+-define(PROPER_NO_IMPORTS, true).
-include_lib("proper/include/proper.hrl").
-endif.
-export([new/1,
empty/1,
full/1,
from_binary/2,
+ from_bitstring/1,
to_binary/1,
from_list/2,
to_list/1,
@@ -25,7 +27,10 @@
capacity/1,
first/2,
foldl/3,
- min/1]).
+ min/1,
+ union/1,
+ union/2,
+ progress/1]).
-record(pieceset, {
size :: non_neg_integer(),
@@ -82,6 +87,12 @@ from_binary(Bin, Size) when is_binary(Bin) ->
_ -> erlang:error(badarg)
end.
+%% @doc Construct pieceset from bitstring.
+from_bitstring(Bin) ->
+ Size = bit_size(Bin),
+ #pieceset{size=Size, elements=Bin}.
+
+
%% @doc
%% Convert a piece set to a bitfield, the bitfield will
%% be padded with at most 7 bits set to zero.
@@ -253,6 +264,26 @@ intersection(Set0, Set1) ->
#pieceset{size=Size0, elements=Intersection}
end.
+-spec union(pieceset(), pieceset()) -> pieceset().
+union(Set0, Set1) ->
+ #pieceset{size=Size, elements = Elements0} = Set0,
+ #pieceset{size=Size, elements = Elements1} = Set1,
+ <<E0:Size>> = Elements0,
+ <<E1:Size>> = Elements1,
+ Union = <<(E0 bor E1):Size>>,
+ #pieceset{size=Size, elements=Union}.
+
+union([H|T]) ->
+ #pieceset{size=Size, elements = ElementsH} = H,
+ <<EH:Size>> = ElementsH,
+ F = fun(X, Acc) ->
+ #pieceset{size=Size, elements = <<EX:Size>>} = X,
+ EX bor Acc
+ end,
+ Union = lists:foldl(F, EH, T),
+ #pieceset{size = Size, elements = <<Union:Size>>}.
+
+
%% @doc
%% Return a piece set where each member is a member of the first
%% but not a member of the second set.
@@ -295,6 +326,13 @@ capacity(Pieceset) ->
#pieceset{size=Size} = Pieceset,
Size.
+
+%% @doc Return float from 0 to 1.
+-spec progress(pieceset()) -> float().
+progress(Pieceset) ->
+ etorrent_pieceset:size(Pieceset) / capacity(Pieceset).
+
+
%% @doc Return the first member of the list that is a member of the set
%% If no element of the list is a member of the set the function exits
%% with reason badarg. This function assumes that all pieces in the lists
View
224 src/etorrent_progress.erl
@@ -56,7 +56,7 @@
%% peer API
-export([start_link/1,
- start_link/5,
+ start_link/6,
mark_valid/2]).
%% stats API
@@ -68,6 +68,12 @@
num_stored/1,
num_valid/1]).
+%% wish API
+-export([set_wishes/2]).
+
+-export([show_assigned/1,
+ show_valid/1]).
+
%% gproc registry entries
-export([register_server/1,
unregister_server/1,
@@ -83,9 +89,9 @@
terminate/2,
code_change/3]).
--import(gen_server, [call/2, cast/2]).
--compile({no_auto_import,[monitor/2, error/1]}).
--import(erlang, [monitor/2, error/1]).
+%% Private
+-export([stored_chunks/1]).
+
-type pieceset() :: etorrent_pieceset:pieceset().
-type torrent_id() :: etorrent_types:torrent_id().
@@ -123,8 +129,11 @@
piece_priority :: #pieceprio{},
%% Chunk assignment processes
pending = exit(required) :: pid(),
- endgame = exit(required) :: pid(),
- in_endgame = exit(required) :: boolean()}).
+ user_wishes = [] :: [pieceset()],
+ %% If active = false, the process will not handle messages.
+ %% When we want to switch assignor, we mark active as `false'
+ %% and wait when the torrent supervisor kills this process.
+ active = true :: boolean()}).
%% # Piece states
%% As the download of a torrent progresses, and regresses, each piece
@@ -243,13 +252,14 @@ start_link(Args) ->
%% Start a new chunk server for a set of pieces, a subset of the
%% pieces may already have been fetched.
%% @end
-start_link(TorrentID, ChunkSize, Fetched, Sizes, TorrentPid) ->
+start_link(TorrentID, ChunkSize, Fetched, Sizes, TorrentPid, Wishes) ->
Args = [
{torrentid, TorrentID},
{chunksize, ChunkSize},
{fetched, Fetched},
{piecesizes, Sizes},
- {torrentpid, TorrentPid}],
+ {torrentpid, TorrentPid},
+ {user_wishes, Wishes}],
start_link(Args).
%% @doc
@@ -307,7 +317,7 @@ num_valid(TorrentID) ->
-spec state_members(torrent_id(), piece_state()) -> pieceset().
state_members(TorrentID, Piecestate) ->
ChunkSrv = lookup_server(TorrentID),
- call(ChunkSrv, {state_members, Piecestate}).
+ gen_server:call(ChunkSrv, {state_members, Piecestate}).
%% @private
-spec num_state_members(torrent_id(), piece_state()) -> integer().
@@ -323,19 +333,41 @@ num_state_members(TorrentID, Piecestate) ->
end.
+show_assigned(TorrentID) ->
+ Assigned = state_members(TorrentID, assigned),
+ etorrent_pieceset:to_string(Assigned).
+
+
+show_valid(TorrentID) ->
+ Valid = state_members(TorrentID, valid),
+ etorrent_pieceset:to_string(Valid).
+
+
+set_wishes(TorrentID, Wishes) ->
+ ChunkSrv = lookup_server(TorrentID),
+ ok = gen_server:call(ChunkSrv, {set_wishes, Wishes}).
+
+
+stored_chunks(TorrentID) ->
+ ChunkSrv = lookup_server(TorrentID),
+ {ok, Chunks} = gen_server:call(ChunkSrv, stored_chunks),
+ Chunks.
+
+
%% @private
init(Serverargs) ->
Args = orddict:from_list(Serverargs),
TorrentID = orddict:fetch(torrentid, Args),
true = register_server(TorrentID),
+ true = etorrent_download:register_server(TorrentID),
ChunkSize = orddict:fetch(chunksize, Args),
PiecesValid = orddict:fetch(fetched, Args),
PieceSizes = orddict:fetch(piecesizes, Args),
+ Wishes = proplists:get_value(user_wishes, Serverargs, []),
TorrentPid = etorrent_torrent_ctl:await_server(TorrentID),
_ScarcityPid = etorrent_scarcity:await_server(TorrentID),
Pending = etorrent_pending:await_server(TorrentID),
- Endgame = etorrent_endgame:await_server(TorrentID),
ok = etorrent_pending:receiver(self(), Pending),
NumPieces = length(PieceSizes),
@@ -380,26 +412,26 @@ init(Serverargs) ->
chunks_stored=ChunkSets,
piece_priority=PiecePriority,
pending=Pending,
- endgame=Endgame,
- in_endgame=false},
+ user_wishes=Wishes
+ },
{ok, InitS