-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathFS.hs
201 lines (186 loc) · 8.76 KB
/
FS.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
-- | 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
,Integer
,Integer
)] -- ^ (File handle, file chunk offset, 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')) offs size
| size <= 0 =
fail "FS: Should have already stopped projection"
| null handles =
fail "FS: Attempt to read beyond torrent length"
| offs >= length1 =
projectHandles (Handles handles') (offs - length1) size
| otherwise =
let size1 = length1 - offs -- How much of h1 to take?
in if size1 >= size
then [(h1, offs, size)]
else (h1, offs, size1) :
projectHandles (Handles handles') 0 (size - size1)
projectHandles (Handles []) _ _ = fail "FS: Empty Handles list, can't happen"
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, offs, size) ->
do hSeek h AbsoluteSeek offs
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, offs, size) ->
do hSeek h AbsoluteSeek offs
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_ (\content (h, offs, size) ->
do let size' = fromInteger size
(toStore, rest) = B.splitAt size' content
hSeek h AbsoluteSeek offs
B.hPut h $ toStore
hFlush h
return rest
) blkData (projection (position pInfo) (fromIntegral $ B.length blkData))
return ()
where
projection = {-# SCC "projectHandles" #-} projectHandles handles
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, offs, size) ->
do hSeek h AbsoluteSeek offs
L.hGet h (fromInteger size)
)
return (D.digest bs == 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, l) ->
do let dir = joinPath $ init path
when (dir /= "") $
createDirectoryIfMissing True dir
let fpath = joinPath path
h <- openBinaryFile fpath ReadWriteMode
return (h, l)
)
have <- checkFile handles pieceMap
return (handles, have, pieceMap)
where Just files = BCode.infoFiles bc
Just pieceMap = mkPieceMap bc