From dfffc353671f9cb53f4c95fcd81cac807a07201e Mon Sep 17 00:00:00 2001
From: Ramakrishnan Muthukrishnan <ram@rkrishnan.org>
Date: Tue, 15 Sep 2015 21:41:36 +0530
Subject: [PATCH] Resume download from where it was left off last time

This commit looks at the downloaded file, verifies the hashes of
the downloaded pieces, marks them as "have" pieces and proceeds
to download those that are not in its posession.
---
 src/FuncTorrent/Fileops.hs | 19 +++++++++++++++----
 src/FuncTorrent/Peer.hs    | 27 ++++++++++++++++++++++++---
 src/main/Main.hs           | 10 +++++++---
 3 files changed, 46 insertions(+), 10 deletions(-)

diff --git a/src/FuncTorrent/Fileops.hs b/src/FuncTorrent/Fileops.hs
index f1bbefe..cab73c7 100644
--- a/src/FuncTorrent/Fileops.hs
+++ b/src/FuncTorrent/Fileops.hs
@@ -1,17 +1,23 @@
 module FuncTorrent.Fileops
        (createDummyFile,
-        writeFileAtOffset
+        writeFileAtOffset,
+        readFileAtOffset
        ) where
 
 import Prelude hiding (writeFile)
 
 import System.IO (withFile, hSeek, IOMode(..), SeekMode(..))
-import Data.ByteString (ByteString, writeFile, hPut)
+import System.Directory (doesFileExist)
+import Data.ByteString (ByteString, writeFile, hPut, hGet)
 import qualified Data.ByteString.Char8 as BC (replicate)
 
 createDummyFile :: FilePath -> Int -> IO ()
-createDummyFile path size =
-  writeFile path (BC.replicate size '\0')
+createDummyFile path size = do
+  dfe <- doesFileExist path
+  if dfe
+    then return ()
+    else
+    writeFile path (BC.replicate size '\0')
 
 -- write into a file at a specific offet
 writeFileAtOffset :: FilePath -> Integer -> ByteString -> IO ()
@@ -19,3 +25,8 @@ writeFileAtOffset path offset block =
   withFile path ReadWriteMode (\h -> do
                                   hSeek h AbsoluteSeek offset
                                   hPut h block)
+readFileAtOffset :: FilePath -> Integer -> Integer -> IO ByteString
+readFileAtOffset path offset len =
+  withFile path ReadWriteMode (\h -> do
+                                  hSeek h AbsoluteSeek offset
+                                  hGet h (fromInteger len))
diff --git a/src/FuncTorrent/Peer.hs b/src/FuncTorrent/Peer.hs
index 61b5112..2b9ff5f 100644
--- a/src/FuncTorrent/Peer.hs
+++ b/src/FuncTorrent/Peer.hs
@@ -8,19 +8,20 @@ module FuncTorrent.Peer
 import Prelude hiding (lookup, concat, replicate, splitAt, take, filter)
 
 import System.IO (Handle, BufferMode(..), hSetBuffering)
+import System.Directory (doesFileExist)
 import Data.ByteString (ByteString, unpack, concat, hGet, hPut, take, empty)
 import qualified Data.ByteString.Char8 as BC (length)
 import Network (connectTo, PortID(..))
 import Control.Monad.State
 import Data.Bits
 import Data.Word (Word8)
-import Data.Map (Map, fromList, toList, (!), mapWithKey, adjust, filter)
+import Data.Map (Map, fromList, toList, (!), mapWithKey, traverseWithKey, adjust, filter)
 import qualified Crypto.Hash.SHA1 as SHA1 (hash)
 import Safe (headMay)
 
 import FuncTorrent.Metainfo (Info(..), Metainfo(..))
 import FuncTorrent.Utils (splitN, splitNum)
-import FuncTorrent.Fileops (createDummyFile, writeFileAtOffset)
+import FuncTorrent.Fileops (createDummyFile, writeFileAtOffset, readFileAtOffset)
 import FuncTorrent.PeerMsgs (Peer(..), PeerMsg(..), sendMsg, getMsg, genHandshakeMsg)
 
 data PState = PState { handle :: Handle
@@ -61,6 +62,25 @@ initPieceMap pieceHash fileLen pieceLen = fromList kvs
     hashes = splitN 20 pieceHash
     pLengths = (splitNum fileLen pieceLen)
 
+updatePieceMap :: FilePath -> PieceMap -> IO PieceMap
+updatePieceMap filePath pieceMap = do
+  dfe <- doesFileExist filePath
+  -- TODO: this is not enough, file should have the same size as well
+  if dfe
+    then pieceMapFromFile filePath pieceMap
+    else return pieceMap
+
+pieceMapFromFile :: FilePath -> PieceMap -> IO PieceMap
+pieceMapFromFile filePath pieceMap = do
+  traverseWithKey f pieceMap
+    where
+      f k v = do
+        let offset = if k == 0 then 0 else k * len (pieceMap ! (k - 1))
+        isHashValid <- (flip verifyHash) (hash v) <$> (readFileAtOffset filePath offset (len v))
+        if isHashValid
+          then return $ v { dlstate = Have }
+          else return $ v
+
 havePiece :: PieceMap -> Integer -> Bool
 havePiece pm index =
   dlstate (pm ! index) == Have
@@ -130,8 +150,9 @@ handlePeerMsgs p peerId m = do
       fileLen = lengthInBytes (info m)
       fileName = name (info m)
       pieceStatus = initPieceMap pieceHash fileLen pLen
+  pieceStatus' <- updatePieceMap fileName pieceStatus
   createDummyFile fileName (fromIntegral fileLen)
-  _ <- runStateT (msgLoop pieceStatus fileName) pstate
+  _ <- runStateT (msgLoop pieceStatus' fileName) pstate
   return ()
 
 msgLoop :: PieceMap -> FilePath -> StateT PState IO ()
diff --git a/src/main/Main.hs b/src/main/Main.hs
index 0a51410..ae3c035 100644
--- a/src/main/Main.hs
+++ b/src/main/Main.hs
@@ -57,9 +57,13 @@ main = do
     case torrentToMetainfo torrentStr of
      Left e -> logError e log
      Right m -> do
-       log "Input File OK"
-       log $ "Downloading file : " ++ name (info m)
-
+       let p = name (info m)
+       log $ "Downloading file : " ++ p
+       -- if we had downloaded the file before (partly or completely)
+       -- then we should check the current directory for the existence
+       -- of the file and then update the map of each piece' availability.
+       -- This can be donw by reading each piece and verifying the checksum.
+       -- If the checksum does not match, we don't have that piece.
        log $ "starting server"
        (serverSock, (PortNumber portnum)) <- Server.start
        log $ "server started on " ++ show portnum
-- 
2.45.2