-- | Filesystem routines. These are used for working with and -- manipulating files in the filesystem. module FS (PieceInfo(..), PieceMap, Handles, readPiece, readBlock, writeBlock, mkPieceMap, checkFile, checkPiece, openAndCheckFile, canSeed) where import Control.Monad.State import Data.Array import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import qualified Data.Map as M import System.IO import System.Directory (createDirectoryIfMissing) import System.FilePath (joinPath) import Protocol.BCode as BCode import qualified Digest as D import Torrent -- | For multi-file torrents we've got to maintain multiple file -- handles. The data structure may as well be a Map Range Handle, -- but that's detailto only @projectHandles@. More importantly, -- functions operating on the files must be aware that a -- piece/block can span multiple files. -- -- FIXME: Replace this with a handle cache later. Many peers & many -- tiny files will make us overstep the fd limit (usually -- 1024). newtype Handles = Handles [(Handle, Integer)] -- ^[(fileHandle, fileLength)] projectHandles :: Handles -> Integer -- ^Torrent offset -> Integer -- ^Torrent size -> [(Handle -- ^File handle ,Integer -- ^File chunk offset ,Integer -- ^File chunk size )] {- projectHandles handles offset size = let r = projectHandles' handles offset size in trace ("projectHandles " ++ show handles ++ " " ++ show offset ++ " " ++ show size ++ " = " ++ show r ) $ r -} projectHandles (Handles handles@((h1, length1):handles')) offset size | size <= 0 = fail "FS: Should have already stopped projection" | null handles = fail "FS: Attempt to read beyond torrent length" | offset >= length1 = projectHandles (Handles handles') (offset - length1) size | otherwise = let size1 = length1 - offset -- ^How much of h1 to take? in if size1 >= size then [(h1, offset, size)] else (h1, offset, size1) : projectHandles (Handles handles') 0 (size - size1) pInfoLookup :: PieceNum -> PieceMap -> IO PieceInfo pInfoLookup pn mp = return $ mp ! pn -- | FIXME: minor code duplication with @readBlock@ readPiece :: PieceNum -> Handles -> PieceMap -> IO L.ByteString readPiece pn handles mp = {-# SCC "readPiece" #-} do pInfo <- pInfoLookup pn mp bs <- L.concat `fmap` forM (projectHandles handles (offset pInfo) (len pInfo)) (\(h, offset, size) -> do hSeek h AbsoluteSeek offset L.hGet h (fromInteger size) ) if L.length bs == (fromInteger . len $ pInfo) then return bs else fail "FS: Wrong number of bytes read" -- | FIXME: concatenating strict ByteStrings may turn out -- expensive. Returning lazy ones may be more appropriate. readBlock :: PieceNum -> Block -> Handles -> PieceMap -> IO B.ByteString readBlock pn blk handles mp = {-# SCC "readBlock" #-} do pInfo <- pInfoLookup pn mp B.concat `fmap` forM (projectHandles handles (offset pInfo + (fromIntegral $ blockOffset blk)) (fromIntegral $ blockSize blk)) (\(h, offset, size) -> do hSeek h AbsoluteSeek offset B.hGet h $ fromInteger size ) -- | The call @writeBlock h n blk pm blkData@ will write the contents of @blkData@ -- to the file pointed to by handle at the correct position in the file. If the -- block is of a wrong length, the call will fail. writeBlock :: Handles -> PieceNum -> Block -> PieceMap -> B.ByteString -> IO () writeBlock handles n blk pm blkData = {-# SCC "writeBlock" #-} do when lenFail $ fail "Writing block of wrong length" pInfo <- pInfoLookup n pm foldM_ (\blkData (h, offset, size) -> do let size' = fromInteger size hSeek h AbsoluteSeek offset B.hPut h $ B.take size' blkData hFlush h return $ B.drop size' blkData ) blkData (projectHandles handles (position pInfo) (fromIntegral $ B.length blkData)) return () where position :: PieceInfo -> Integer position pinfo = (offset pinfo) + fromIntegral (blockOffset blk) lenFail = B.length blkData /= blockSize blk -- | The @checkPiece h inf@ checks the file system for correctness of a given piece, namely if -- the piece described by @inf@ is correct inside the file pointed to by @h@. checkPiece :: PieceInfo -> Handles -> IO Bool checkPiece inf handles = {-# SCC "checkPiece" #-} do bs <- L.concat `fmap` forM (projectHandles handles (offset inf) (fromInteger $ len inf)) (\(h, offset, size) -> do hSeek h AbsoluteSeek offset L.hGet h (fromInteger size) ) dgs <- liftIO $ D.digest bs return (dgs == digest inf) -- | Create a MissingMap from a file handle and a piecemap. The system will read each part of -- the file and then check it against the digest. It will create a map of what we are missing -- in the file as a missing map. We could alternatively choose a list of pieces missing rather -- then creating the data structure here. This is perhaps better in the long run. checkFile :: Handles -> PieceMap -> IO PiecesDoneMap checkFile handles pm = do l <- mapM checkP pieces return $ M.fromList l where pieces = assocs pm checkP :: (PieceNum, PieceInfo) -> IO (PieceNum, Bool) checkP (pn, pInfo) = do b <- checkPiece pInfo handles return (pn, b) -- | Extract the PieceMap from a bcoded structure -- Needs some more defense in the long run. mkPieceMap :: BCode -> Maybe PieceMap mkPieceMap bc = fetchData where fetchData = do pLen <- infoPieceLength bc pieceData <- infoPieces bc tLen <- infoLength bc let pis = extract pLen tLen 0 pieceData l = length pis pm = array (0, l-1) (zip [0..] pis) when ( tLen /= (sum $ map len $ elems pm) ) (error "PieceMap construction size assertion failed") return pm extract :: Integer -> Integer -> Integer -> [B.ByteString] -> [PieceInfo] extract _ 0 _ [] = [] extract plen tlen offst (p : ps) | tlen < plen = PieceInfo { offset = offst, len = tlen, digest = p } : extract plen 0 (offst + plen) ps | otherwise = inf : extract plen (tlen - plen) (offst + plen) ps where inf = PieceInfo { offset = offst, len = plen, digest = p } extract _ _ _ _ = error "mkPieceMap: the impossible happened!" -- | Predicate function. True if nothing is missing from the map. canSeed :: PiecesDoneMap -> Bool canSeed = M.fold (&&) True -- | Process a BCoded torrent file. Create directories, open the files -- in question, check it and return Handles plus a haveMap for the -- file openAndCheckFile :: BCode -> IO (Handles, PiecesDoneMap, PieceMap) openAndCheckFile bc = do handles <- Handles `fmap` forM files (\(path, length) -> do let dir = joinPath $ init path when (dir /= "") $ createDirectoryIfMissing True dir let fpath = joinPath path h <- openBinaryFile fpath ReadWriteMode return (h, length) ) have <- checkFile handles pieceMap return (handles, have, pieceMap) where Just files = BCode.infoFiles bc Just pieceMap = mkPieceMap bc