diff options
-rw-r--r-- | bittorrent.cabal | 1 | ||||
-rw-r--r-- | src/Data/Torrent.hs | 270 | ||||
-rw-r--r-- | src/Data/Torrent/Layout.hs | 321 | ||||
-rw-r--r-- | src/Data/Torrent/Tree.hs | 2 | ||||
-rw-r--r-- | src/System/Torrent/FileMap.hs | 2 | ||||
-rw-r--r-- | src/System/Torrent/Storage.hs | 2 | ||||
-rw-r--r-- | tests/Data/Torrent/LayoutSpec.hs | 2 | ||||
-rw-r--r-- | tests/Data/Torrent/MetainfoSpec.hs | 3 | ||||
-rw-r--r-- | tests/System/Torrent/FileMapSpec.hs | 2 | ||||
-rw-r--r-- | tests/System/Torrent/StorageSpec.hs | 2 |
10 files changed, 276 insertions, 331 deletions
diff --git a/bittorrent.cabal b/bittorrent.cabal index 0861bf2b..9d687d7d 100644 --- a/bittorrent.cabal +++ b/bittorrent.cabal | |||
@@ -46,7 +46,6 @@ library | |||
46 | hs-source-dirs: src | 46 | hs-source-dirs: src |
47 | exposed-modules: Data.Torrent | 47 | exposed-modules: Data.Torrent |
48 | Data.Torrent.Bitfield | 48 | Data.Torrent.Bitfield |
49 | Data.Torrent.Layout | ||
50 | Data.Torrent.Piece | 49 | Data.Torrent.Piece |
51 | Data.Torrent.Progress | 50 | Data.Torrent.Progress |
52 | Data.Torrent.Tree | 51 | Data.Torrent.Tree |
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs index 5efff598..701da9dd 100644 --- a/src/Data/Torrent.hs +++ b/src/Data/Torrent.hs | |||
@@ -23,7 +23,11 @@ | |||
23 | {-# LANGUAGE MultiParamTypeClasses #-} | 23 | {-# LANGUAGE MultiParamTypeClasses #-} |
24 | {-# LANGUAGE BangPatterns #-} | 24 | {-# LANGUAGE BangPatterns #-} |
25 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | 25 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} |
26 | {-# LANGUAGE StandaloneDeriving #-} | ||
26 | {-# LANGUAGE DeriveDataTypeable #-} | 27 | {-# LANGUAGE DeriveDataTypeable #-} |
28 | {-# LANGUAGE DeriveFunctor #-} | ||
29 | {-# LANGUAGE DeriveFoldable #-} | ||
30 | {-# LANGUAGE DeriveTraversable #-} | ||
27 | {-# LANGUAGE TemplateHaskell #-} | 31 | {-# LANGUAGE TemplateHaskell #-} |
28 | {-# OPTIONS -fno-warn-orphans #-} | 32 | {-# OPTIONS -fno-warn-orphans #-} |
29 | module Data.Torrent | 33 | module Data.Torrent |
@@ -51,6 +55,37 @@ module Data.Torrent | |||
51 | , parseURN | 55 | , parseURN |
52 | , renderURN | 56 | , renderURN |
53 | 57 | ||
58 | -- * File layout | ||
59 | -- ** FileInfo | ||
60 | , FileOffset | ||
61 | , FileSize | ||
62 | , FileInfo (..) | ||
63 | , fileLength | ||
64 | , filePath | ||
65 | , fileMD5Sum | ||
66 | |||
67 | -- ** Layout info | ||
68 | , LayoutInfo (..) | ||
69 | , joinFilePath | ||
70 | , singleFile | ||
71 | , multiFile | ||
72 | , rootDirName | ||
73 | , isSingleFile | ||
74 | , isMultiFile | ||
75 | , suggestedName | ||
76 | , contentLength | ||
77 | , fileCount | ||
78 | , blockCount | ||
79 | |||
80 | -- ** Flat layout info | ||
81 | , FileLayout | ||
82 | , flatLayout | ||
83 | , accumPositions | ||
84 | , fileOffset | ||
85 | |||
86 | -- ** Internal | ||
87 | , sizeInBase | ||
88 | |||
54 | -- * Info dictionary | 89 | -- * Info dictionary |
55 | , InfoDict (..) | 90 | , InfoDict (..) |
56 | , infoDictionary | 91 | , infoDictionary |
@@ -109,6 +144,7 @@ import qualified Data.ByteString.Lazy as BL | |||
109 | import Data.Char | 144 | import Data.Char |
110 | import Data.Convertible | 145 | import Data.Convertible |
111 | import Data.Default | 146 | import Data.Default |
147 | import Data.Foldable as F | ||
112 | import Data.Hashable as Hashable | 148 | import Data.Hashable as Hashable |
113 | import qualified Data.List as L | 149 | import qualified Data.List as L |
114 | import Data.Map as M | 150 | import Data.Map as M |
@@ -128,8 +164,8 @@ import Text.ParserCombinators.ReadP as P | |||
128 | import Text.PrettyPrint as PP | 164 | import Text.PrettyPrint as PP |
129 | import Text.PrettyPrint.Class | 165 | import Text.PrettyPrint.Class |
130 | import System.FilePath | 166 | import System.FilePath |
167 | import System.Posix.Types | ||
131 | 168 | ||
132 | import Data.Torrent.Layout | ||
133 | import Data.Torrent.Piece | 169 | import Data.Torrent.Piece |
134 | import Network.BitTorrent.Core.NodeInfo | 170 | import Network.BitTorrent.Core.NodeInfo |
135 | 171 | ||
@@ -259,6 +295,238 @@ shortHex :: InfoHash -> Text | |||
259 | shortHex = T.take 7 . longHex | 295 | shortHex = T.take 7 . longHex |
260 | 296 | ||
261 | {----------------------------------------------------------------------- | 297 | {----------------------------------------------------------------------- |
298 | -- File info | ||
299 | -----------------------------------------------------------------------} | ||
300 | |||
301 | -- | Size of a file in bytes. | ||
302 | type FileSize = FileOffset | ||
303 | |||
304 | deriving instance BEncode FileOffset | ||
305 | |||
306 | -- | Contain metainfo about one single file. | ||
307 | data FileInfo a = FileInfo { | ||
308 | fiLength :: {-# UNPACK #-} !FileSize | ||
309 | -- ^ Length of the file in bytes. | ||
310 | |||
311 | -- TODO unpacked MD5 sum | ||
312 | , fiMD5Sum :: !(Maybe ByteString) | ||
313 | -- ^ 32 character long MD5 sum of the file. Used by third-party | ||
314 | -- tools, not by bittorrent protocol itself. | ||
315 | |||
316 | , fiName :: !a | ||
317 | -- ^ One or more string elements that together represent the | ||
318 | -- path and filename. Each element in the list corresponds to | ||
319 | -- either a directory name or (in the case of the last element) | ||
320 | -- the filename. For example, the file: | ||
321 | -- | ||
322 | -- > "dir1/dir2/file.ext" | ||
323 | -- | ||
324 | -- would consist of three string elements: | ||
325 | -- | ||
326 | -- > ["dir1", "dir2", "file.ext"] | ||
327 | -- | ||
328 | } deriving (Show, Read, Eq, Typeable | ||
329 | , Functor, Foldable | ||
330 | ) | ||
331 | |||
332 | makeLensesFor | ||
333 | [ ("fiLength", "fileLength") | ||
334 | , ("fiMD5Sum", "fileMD5Sum") | ||
335 | , ("fiName" , "filePath" ) | ||
336 | ] | ||
337 | ''FileInfo | ||
338 | |||
339 | instance NFData a => NFData (FileInfo a) where | ||
340 | rnf FileInfo {..} = rnf fiName | ||
341 | {-# INLINE rnf #-} | ||
342 | |||
343 | instance BEncode (FileInfo [ByteString]) where | ||
344 | toBEncode FileInfo {..} = toDict $ | ||
345 | "length" .=! fiLength | ||
346 | .: "md5sum" .=? fiMD5Sum | ||
347 | .: "path" .=! fiName | ||
348 | .: endDict | ||
349 | {-# INLINE toBEncode #-} | ||
350 | |||
351 | fromBEncode = fromDict $ do | ||
352 | FileInfo <$>! "length" | ||
353 | <*>? "md5sum" | ||
354 | <*>! "path" | ||
355 | {-# INLINE fromBEncode #-} | ||
356 | |||
357 | type Put a = a -> BDict -> BDict | ||
358 | |||
359 | putFileInfoSingle :: Data.Torrent.Put (FileInfo ByteString) | ||
360 | putFileInfoSingle FileInfo {..} cont = | ||
361 | "length" .=! fiLength | ||
362 | .: "md5sum" .=? fiMD5Sum | ||
363 | .: "name" .=! fiName | ||
364 | .: cont | ||
365 | |||
366 | getFileInfoSingle :: BE.Get (FileInfo ByteString) | ||
367 | getFileInfoSingle = do | ||
368 | FileInfo <$>! "length" | ||
369 | <*>? "md5sum" | ||
370 | <*>! "name" | ||
371 | |||
372 | instance BEncode (FileInfo ByteString) where | ||
373 | toBEncode = toDict . (`putFileInfoSingle` endDict) | ||
374 | {-# INLINE toBEncode #-} | ||
375 | |||
376 | fromBEncode = fromDict getFileInfoSingle | ||
377 | {-# INLINE fromBEncode #-} | ||
378 | |||
379 | instance Pretty (FileInfo BS.ByteString) where | ||
380 | pretty FileInfo {..} = | ||
381 | "Path: " <> text (T.unpack (T.decodeUtf8 fiName)) | ||
382 | $$ "Size: " <> text (show fiLength) | ||
383 | $$ maybe PP.empty ppMD5 fiMD5Sum | ||
384 | where | ||
385 | ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5)) | ||
386 | |||
387 | -- | Join file path. | ||
388 | joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString | ||
389 | joinFilePath = fmap (BS.intercalate "/") | ||
390 | |||
391 | {----------------------------------------------------------------------- | ||
392 | -- Layout info | ||
393 | -----------------------------------------------------------------------} | ||
394 | |||
395 | -- | Original (found in torrent file) layout info is either: | ||
396 | -- | ||
397 | -- * Single file with its /name/. | ||
398 | -- | ||
399 | -- * Multiple files with its relative file /paths/. | ||
400 | -- | ||
401 | data LayoutInfo | ||
402 | = SingleFile | ||
403 | { -- | Single file info. | ||
404 | liFile :: !(FileInfo ByteString) | ||
405 | } | ||
406 | | MultiFile | ||
407 | { -- | List of the all files that torrent contains. | ||
408 | liFiles :: ![FileInfo [ByteString]] | ||
409 | |||
410 | -- | The /suggested/ name of the root directory in which to | ||
411 | -- store all the files. | ||
412 | , liDirName :: !ByteString | ||
413 | } deriving (Show, Read, Eq, Typeable) | ||
414 | |||
415 | makeLensesFor | ||
416 | [ ("liFile" , "singleFile" ) | ||
417 | , ("liFiles" , "multiFile" ) | ||
418 | , ("liDirName", "rootDirName") | ||
419 | ] | ||
420 | ''LayoutInfo | ||
421 | |||
422 | instance NFData LayoutInfo where | ||
423 | rnf SingleFile {..} = () | ||
424 | rnf MultiFile {..} = rnf liFiles | ||
425 | |||
426 | -- | Empty multifile layout. | ||
427 | instance Default LayoutInfo where | ||
428 | def = MultiFile [] "" | ||
429 | |||
430 | getLayoutInfo :: BE.Get LayoutInfo | ||
431 | getLayoutInfo = single <|> multi | ||
432 | where | ||
433 | single = SingleFile <$> getFileInfoSingle | ||
434 | multi = MultiFile <$>! "files" <*>! "name" | ||
435 | |||
436 | putLayoutInfo :: Data.Torrent.Put LayoutInfo | ||
437 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
438 | putLayoutInfo MultiFile {..} = \ cont -> | ||
439 | "files" .=! liFiles | ||
440 | .: "name" .=! liDirName | ||
441 | .: cont | ||
442 | |||
443 | instance BEncode LayoutInfo where | ||
444 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
445 | fromBEncode = fromDict getLayoutInfo | ||
446 | |||
447 | instance Pretty LayoutInfo where | ||
448 | pretty SingleFile {..} = pretty liFile | ||
449 | pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles | ||
450 | |||
451 | -- | Test if this is single file torrent. | ||
452 | isSingleFile :: LayoutInfo -> Bool | ||
453 | isSingleFile SingleFile {} = True | ||
454 | isSingleFile _ = False | ||
455 | {-# INLINE isSingleFile #-} | ||
456 | |||
457 | -- | Test if this is multifile torrent. | ||
458 | isMultiFile :: LayoutInfo -> Bool | ||
459 | isMultiFile MultiFile {} = True | ||
460 | isMultiFile _ = False | ||
461 | {-# INLINE isMultiFile #-} | ||
462 | |||
463 | -- | Get name of the torrent based on the root path piece. | ||
464 | suggestedName :: LayoutInfo -> ByteString | ||
465 | suggestedName (SingleFile FileInfo {..}) = fiName | ||
466 | suggestedName MultiFile {..} = liDirName | ||
467 | {-# INLINE suggestedName #-} | ||
468 | |||
469 | -- | Find sum of sizes of the all torrent files. | ||
470 | contentLength :: LayoutInfo -> FileSize | ||
471 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
472 | contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs) | ||
473 | |||
474 | -- | Get number of all files in torrent. | ||
475 | fileCount :: LayoutInfo -> Int | ||
476 | fileCount SingleFile {..} = 1 | ||
477 | fileCount MultiFile {..} = L.length liFiles | ||
478 | |||
479 | -- | Find number of blocks of the specified size. If torrent size is | ||
480 | -- not a multiple of block size then the count is rounded up. | ||
481 | blockCount :: Int -> LayoutInfo -> Int | ||
482 | blockCount blkSize ci = contentLength ci `sizeInBase` blkSize | ||
483 | |||
484 | ------------------------------------------------------------------------ | ||
485 | |||
486 | -- | File layout specifies the order and the size of each file in the | ||
487 | -- storage. Note that order of files is highly important since we | ||
488 | -- coalesce all the files in the given order to get the linear block | ||
489 | -- address space. | ||
490 | -- | ||
491 | type FileLayout a = [(FilePath, a)] | ||
492 | |||
493 | -- | Extract files layout from torrent info with the given root path. | ||
494 | flatLayout | ||
495 | :: FilePath -- ^ Root path for the all torrent files. | ||
496 | -> LayoutInfo -- ^ Torrent content information. | ||
497 | -> FileLayout FileSize -- ^ The all file paths prefixed with the given root. | ||
498 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
499 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
500 | flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles | ||
501 | where -- TODO use utf8 encoding in name | ||
502 | mkPath FileInfo {..} = (path, fiLength) | ||
503 | where | ||
504 | path = prefixPath </> BC.unpack liDirName | ||
505 | </> joinPath (L.map BC.unpack fiName) | ||
506 | |||
507 | -- | Calculate offset of each file based on its length, incrementally. | ||
508 | accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize) | ||
509 | accumPositions = go 0 | ||
510 | where | ||
511 | go !_ [] = [] | ||
512 | go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs | ||
513 | |||
514 | -- | Gives global offset of a content file for a given full path. | ||
515 | fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset | ||
516 | fileOffset = L.lookup | ||
517 | {-# INLINE fileOffset #-} | ||
518 | |||
519 | ------------------------------------------------------------------------ | ||
520 | |||
521 | -- | Divide and round up. | ||
522 | sizeInBase :: Integral a => a -> Int -> Int | ||
523 | sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align | ||
524 | where | ||
525 | align = if n `mod` fromIntegral b == 0 then 0 else 1 | ||
526 | {-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-} | ||
527 | {-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-} | ||
528 | |||
529 | {----------------------------------------------------------------------- | ||
262 | -- Info dictionary | 530 | -- Info dictionary |
263 | -----------------------------------------------------------------------} | 531 | -----------------------------------------------------------------------} |
264 | 532 | ||
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs deleted file mode 100644 index cc529840..00000000 --- a/src/Data/Torrent/Layout.hs +++ /dev/null | |||
@@ -1,321 +0,0 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Layout of files in torrent. | ||
9 | -- | ||
10 | {-# LANGUAGE BangPatterns #-} | ||
11 | {-# LANGUAGE FlexibleInstances #-} | ||
12 | {-# LANGUAGE StandaloneDeriving #-} | ||
13 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
14 | {-# LANGUAGE DeriveDataTypeable #-} | ||
15 | {-# LANGUAGE DeriveFunctor #-} | ||
16 | {-# LANGUAGE DeriveFoldable #-} | ||
17 | {-# LANGUAGE DeriveTraversable #-} | ||
18 | {-# LANGUAGE TemplateHaskell #-} | ||
19 | {-# OPTIONS -fno-warn-orphans #-} | ||
20 | module Data.Torrent.Layout | ||
21 | ( -- * File attributes | ||
22 | FileOffset | ||
23 | , FileSize | ||
24 | |||
25 | -- * Single file info | ||
26 | , FileInfo (..) | ||
27 | |||
28 | -- ** Lens | ||
29 | , fileLength | ||
30 | , filePath | ||
31 | , fileMD5Sum | ||
32 | |||
33 | -- * File layout | ||
34 | , LayoutInfo (..) | ||
35 | , joinFilePath | ||
36 | |||
37 | -- ** Lens | ||
38 | , singleFile | ||
39 | , multiFile | ||
40 | , rootDirName | ||
41 | |||
42 | -- ** Predicates | ||
43 | , isSingleFile | ||
44 | , isMultiFile | ||
45 | |||
46 | -- ** Query | ||
47 | , suggestedName | ||
48 | , contentLength | ||
49 | , fileCount | ||
50 | , blockCount | ||
51 | |||
52 | -- * Flat file layout | ||
53 | , FileLayout | ||
54 | , flatLayout | ||
55 | , accumPositions | ||
56 | , fileOffset | ||
57 | |||
58 | -- * Internal | ||
59 | , sizeInBase | ||
60 | , getLayoutInfo | ||
61 | , putLayoutInfo | ||
62 | ) where | ||
63 | |||
64 | import Control.Applicative | ||
65 | import Control.DeepSeq | ||
66 | import Control.Lens | ||
67 | import Data.BEncode | ||
68 | import Data.BEncode.Types | ||
69 | import Data.ByteString as BS | ||
70 | import Data.ByteString.Base16 as Base16 | ||
71 | import Data.ByteString.Char8 as BC | ||
72 | import Data.Default | ||
73 | import Data.Foldable as F | ||
74 | import Data.List as L | ||
75 | import Data.Text as T | ||
76 | import Data.Text.Encoding as T | ||
77 | import Data.Typeable | ||
78 | import Text.PrettyPrint as PP | ||
79 | import Text.PrettyPrint.Class | ||
80 | import System.FilePath | ||
81 | import System.Posix.Types | ||
82 | |||
83 | {----------------------------------------------------------------------- | ||
84 | -- File attribytes | ||
85 | -----------------------------------------------------------------------} | ||
86 | |||
87 | -- | Size of a file in bytes. | ||
88 | type FileSize = FileOffset | ||
89 | |||
90 | deriving instance BEncode FileOffset | ||
91 | |||
92 | {----------------------------------------------------------------------- | ||
93 | -- File info both either from info dict or file list | ||
94 | -----------------------------------------------------------------------} | ||
95 | |||
96 | -- | Contain metainfo about one single file. | ||
97 | data FileInfo a = FileInfo { | ||
98 | fiLength :: {-# UNPACK #-} !FileSize | ||
99 | -- ^ Length of the file in bytes. | ||
100 | |||
101 | -- TODO unpacked MD5 sum | ||
102 | , fiMD5Sum :: !(Maybe ByteString) | ||
103 | -- ^ 32 character long MD5 sum of the file. Used by third-party | ||
104 | -- tools, not by bittorrent protocol itself. | ||
105 | |||
106 | , fiName :: !a | ||
107 | -- ^ One or more string elements that together represent the | ||
108 | -- path and filename. Each element in the list corresponds to | ||
109 | -- either a directory name or (in the case of the last element) | ||
110 | -- the filename. For example, the file: | ||
111 | -- | ||
112 | -- > "dir1/dir2/file.ext" | ||
113 | -- | ||
114 | -- would consist of three string elements: | ||
115 | -- | ||
116 | -- > ["dir1", "dir2", "file.ext"] | ||
117 | -- | ||
118 | } deriving (Show, Read, Eq, Typeable | ||
119 | , Functor, Foldable | ||
120 | ) | ||
121 | |||
122 | makeLensesFor | ||
123 | [ ("fiLength", "fileLength") | ||
124 | , ("fiMD5Sum", "fileMD5Sum") | ||
125 | , ("fiName" , "filePath" ) | ||
126 | ] | ||
127 | ''FileInfo | ||
128 | |||
129 | instance NFData a => NFData (FileInfo a) where | ||
130 | rnf FileInfo {..} = rnf fiName | ||
131 | {-# INLINE rnf #-} | ||
132 | |||
133 | instance BEncode (FileInfo [ByteString]) where | ||
134 | toBEncode FileInfo {..} = toDict $ | ||
135 | "length" .=! fiLength | ||
136 | .: "md5sum" .=? fiMD5Sum | ||
137 | .: "path" .=! fiName | ||
138 | .: endDict | ||
139 | {-# INLINE toBEncode #-} | ||
140 | |||
141 | fromBEncode = fromDict $ do | ||
142 | FileInfo <$>! "length" | ||
143 | <*>? "md5sum" | ||
144 | <*>! "path" | ||
145 | {-# INLINE fromBEncode #-} | ||
146 | |||
147 | type Put a = a -> BDict -> BDict | ||
148 | |||
149 | putFileInfoSingle :: Put (FileInfo ByteString) | ||
150 | putFileInfoSingle FileInfo {..} cont = | ||
151 | "length" .=! fiLength | ||
152 | .: "md5sum" .=? fiMD5Sum | ||
153 | .: "name" .=! fiName | ||
154 | .: cont | ||
155 | |||
156 | getFileInfoSingle :: Get (FileInfo ByteString) | ||
157 | getFileInfoSingle = do | ||
158 | FileInfo <$>! "length" | ||
159 | <*>? "md5sum" | ||
160 | <*>! "name" | ||
161 | |||
162 | instance BEncode (FileInfo ByteString) where | ||
163 | toBEncode = toDict . (`putFileInfoSingle` endDict) | ||
164 | {-# INLINE toBEncode #-} | ||
165 | |||
166 | fromBEncode = fromDict getFileInfoSingle | ||
167 | {-# INLINE fromBEncode #-} | ||
168 | |||
169 | instance Pretty (FileInfo BS.ByteString) where | ||
170 | pretty FileInfo {..} = | ||
171 | "Path: " <> text (T.unpack (T.decodeUtf8 fiName)) | ||
172 | $$ "Size: " <> text (show fiLength) | ||
173 | $$ maybe PP.empty ppMD5 fiMD5Sum | ||
174 | where | ||
175 | ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5)) | ||
176 | |||
177 | -- | Join file path. | ||
178 | joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString | ||
179 | joinFilePath = fmap (BS.intercalate "/") | ||
180 | |||
181 | {----------------------------------------------------------------------- | ||
182 | -- Original torrent file layout info | ||
183 | -----------------------------------------------------------------------} | ||
184 | |||
185 | -- | Original (found in torrent file) layout info is either: | ||
186 | -- | ||
187 | -- * Single file with its /name/. | ||
188 | -- | ||
189 | -- * Multiple files with its relative file /paths/. | ||
190 | -- | ||
191 | data LayoutInfo | ||
192 | = SingleFile | ||
193 | { -- | Single file info. | ||
194 | liFile :: !(FileInfo ByteString) | ||
195 | } | ||
196 | | MultiFile | ||
197 | { -- | List of the all files that torrent contains. | ||
198 | liFiles :: ![FileInfo [ByteString]] | ||
199 | |||
200 | -- | The /suggested/ name of the root directory in which to | ||
201 | -- store all the files. | ||
202 | , liDirName :: !ByteString | ||
203 | } deriving (Show, Read, Eq, Typeable) | ||
204 | |||
205 | makeLensesFor | ||
206 | [ ("liFile" , "singleFile" ) | ||
207 | , ("liFiles" , "multiFile" ) | ||
208 | , ("liDirName", "rootDirName") | ||
209 | ] | ||
210 | ''LayoutInfo | ||
211 | |||
212 | instance NFData LayoutInfo where | ||
213 | rnf SingleFile {..} = () | ||
214 | rnf MultiFile {..} = rnf liFiles | ||
215 | |||
216 | -- | Empty multifile layout. | ||
217 | instance Default LayoutInfo where | ||
218 | def = MultiFile [] "" | ||
219 | |||
220 | getLayoutInfo :: Get LayoutInfo | ||
221 | getLayoutInfo = single <|> multi | ||
222 | where | ||
223 | single = SingleFile <$> getFileInfoSingle | ||
224 | multi = MultiFile <$>! "files" <*>! "name" | ||
225 | |||
226 | putLayoutInfo :: Put LayoutInfo | ||
227 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
228 | putLayoutInfo MultiFile {..} = \ cont -> | ||
229 | "files" .=! liFiles | ||
230 | .: "name" .=! liDirName | ||
231 | .: cont | ||
232 | |||
233 | instance BEncode LayoutInfo where | ||
234 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
235 | fromBEncode = fromDict getLayoutInfo | ||
236 | |||
237 | instance Pretty LayoutInfo where | ||
238 | pretty SingleFile {..} = pretty liFile | ||
239 | pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles | ||
240 | |||
241 | -- | Test if this is single file torrent. | ||
242 | isSingleFile :: LayoutInfo -> Bool | ||
243 | isSingleFile SingleFile {} = True | ||
244 | isSingleFile _ = False | ||
245 | {-# INLINE isSingleFile #-} | ||
246 | |||
247 | -- | Test if this is multifile torrent. | ||
248 | isMultiFile :: LayoutInfo -> Bool | ||
249 | isMultiFile MultiFile {} = True | ||
250 | isMultiFile _ = False | ||
251 | {-# INLINE isMultiFile #-} | ||
252 | |||
253 | -- | Get name of the torrent based on the root path piece. | ||
254 | suggestedName :: LayoutInfo -> ByteString | ||
255 | suggestedName (SingleFile FileInfo {..}) = fiName | ||
256 | suggestedName MultiFile {..} = liDirName | ||
257 | {-# INLINE suggestedName #-} | ||
258 | |||
259 | -- | Find sum of sizes of the all torrent files. | ||
260 | contentLength :: LayoutInfo -> FileSize | ||
261 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
262 | contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs) | ||
263 | |||
264 | -- | Get number of all files in torrent. | ||
265 | fileCount :: LayoutInfo -> Int | ||
266 | fileCount SingleFile {..} = 1 | ||
267 | fileCount MultiFile {..} = L.length liFiles | ||
268 | |||
269 | -- | Find number of blocks of the specified size. If torrent size is | ||
270 | -- not a multiple of block size then the count is rounded up. | ||
271 | blockCount :: Int -> LayoutInfo -> Int | ||
272 | blockCount blkSize ci = contentLength ci `sizeInBase` blkSize | ||
273 | |||
274 | {----------------------------------------------------------------------- | ||
275 | -- Flat layout | ||
276 | -----------------------------------------------------------------------} | ||
277 | |||
278 | -- | File layout specifies the order and the size of each file in the | ||
279 | -- storage. Note that order of files is highly important since we | ||
280 | -- coalesce all the files in the given order to get the linear block | ||
281 | -- address space. | ||
282 | -- | ||
283 | type FileLayout a = [(FilePath, a)] | ||
284 | |||
285 | -- | Extract files layout from torrent info with the given root path. | ||
286 | flatLayout | ||
287 | :: FilePath -- ^ Root path for the all torrent files. | ||
288 | -> LayoutInfo -- ^ Torrent content information. | ||
289 | -> FileLayout FileSize -- ^ The all file paths prefixed with the given root. | ||
290 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
291 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
292 | flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles | ||
293 | where -- TODO use utf8 encoding in name | ||
294 | mkPath FileInfo {..} = (path, fiLength) | ||
295 | where | ||
296 | path = prefixPath </> BC.unpack liDirName | ||
297 | </> joinPath (L.map BC.unpack fiName) | ||
298 | |||
299 | -- | Calculate offset of each file based on its length, incrementally. | ||
300 | accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize) | ||
301 | accumPositions = go 0 | ||
302 | where | ||
303 | go !_ [] = [] | ||
304 | go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs | ||
305 | |||
306 | -- | Gives global offset of a content file for a given full path. | ||
307 | fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset | ||
308 | fileOffset = lookup | ||
309 | {-# INLINE fileOffset #-} | ||
310 | |||
311 | {----------------------------------------------------------------------- | ||
312 | -- Internal utilities | ||
313 | -----------------------------------------------------------------------} | ||
314 | |||
315 | -- | Divide and round up. | ||
316 | sizeInBase :: Integral a => a -> Int -> Int | ||
317 | sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align | ||
318 | where | ||
319 | align = if n `mod` fromIntegral b == 0 then 0 else 1 | ||
320 | {-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-} | ||
321 | {-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-} | ||
diff --git a/src/Data/Torrent/Tree.hs b/src/Data/Torrent/Tree.hs index 102f4dff..5825422f 100644 --- a/src/Data/Torrent/Tree.hs +++ b/src/Data/Torrent/Tree.hs | |||
@@ -31,7 +31,7 @@ import Data.List as L | |||
31 | import Data.Map as M | 31 | import Data.Map as M |
32 | import Data.Monoid | 32 | import Data.Monoid |
33 | 33 | ||
34 | import Data.Torrent.Layout | 34 | import Data.Torrent |
35 | 35 | ||
36 | 36 | ||
37 | -- | 'DirTree' is more convenient form of 'LayoutInfo'. | 37 | -- | 'DirTree' is more convenient form of 'LayoutInfo'. |
diff --git a/src/System/Torrent/FileMap.hs b/src/System/Torrent/FileMap.hs index 80907a30..6e8d7f5a 100644 --- a/src/System/Torrent/FileMap.hs +++ b/src/System/Torrent/FileMap.hs | |||
@@ -34,7 +34,7 @@ import Data.Vector as V -- TODO use unboxed vector | |||
34 | import Foreign | 34 | import Foreign |
35 | import System.IO.MMap | 35 | import System.IO.MMap |
36 | 36 | ||
37 | import Data.Torrent.Layout | 37 | import Data.Torrent |
38 | 38 | ||
39 | 39 | ||
40 | data FileEntry = FileEntry | 40 | data FileEntry = FileEntry |
diff --git a/src/System/Torrent/Storage.hs b/src/System/Torrent/Storage.hs index 003a4e98..697e3def 100644 --- a/src/System/Torrent/Storage.hs +++ b/src/System/Torrent/Storage.hs | |||
@@ -57,7 +57,7 @@ import Data.Typeable | |||
57 | 57 | ||
58 | import Data.Torrent | 58 | import Data.Torrent |
59 | import Data.Torrent.Bitfield as BF | 59 | import Data.Torrent.Bitfield as BF |
60 | import Data.Torrent.Layout | 60 | import Data.Torrent |
61 | import Data.Torrent.Piece | 61 | import Data.Torrent.Piece |
62 | import System.Torrent.FileMap as FM | 62 | import System.Torrent.FileMap as FM |
63 | 63 | ||
diff --git a/tests/Data/Torrent/LayoutSpec.hs b/tests/Data/Torrent/LayoutSpec.hs index d3966b3f..a3fe7c02 100644 --- a/tests/Data/Torrent/LayoutSpec.hs +++ b/tests/Data/Torrent/LayoutSpec.hs | |||
@@ -7,7 +7,7 @@ import Test.Hspec | |||
7 | import Test.QuickCheck | 7 | import Test.QuickCheck |
8 | import System.Posix.Types | 8 | import System.Posix.Types |
9 | 9 | ||
10 | import Data.Torrent.Layout | 10 | import Data.Torrent |
11 | 11 | ||
12 | 12 | ||
13 | instance Arbitrary COff where | 13 | instance Arbitrary COff where |
diff --git a/tests/Data/Torrent/MetainfoSpec.hs b/tests/Data/Torrent/MetainfoSpec.hs index 369c5e0f..537b3f99 100644 --- a/tests/Data/Torrent/MetainfoSpec.hs +++ b/tests/Data/Torrent/MetainfoSpec.hs | |||
@@ -13,9 +13,8 @@ import Test.Hspec | |||
13 | import Test.QuickCheck | 13 | import Test.QuickCheck |
14 | import Test.QuickCheck.Instances () | 14 | import Test.QuickCheck.Instances () |
15 | 15 | ||
16 | import Data.Torrent.Piece | ||
17 | import Data.Torrent.Layout | ||
18 | import Data.Torrent | 16 | import Data.Torrent |
17 | import Data.Torrent.Piece | ||
19 | import Data.Torrent.LayoutSpec () | 18 | import Data.Torrent.LayoutSpec () |
20 | import Network.BitTorrent.Core.NodeInfoSpec () | 19 | import Network.BitTorrent.Core.NodeInfoSpec () |
21 | 20 | ||
diff --git a/tests/System/Torrent/FileMapSpec.hs b/tests/System/Torrent/FileMapSpec.hs index 36632b3e..85180c0a 100644 --- a/tests/System/Torrent/FileMapSpec.hs +++ b/tests/System/Torrent/FileMapSpec.hs | |||
@@ -9,7 +9,7 @@ import System.FilePath | |||
9 | import System.IO.Unsafe | 9 | import System.IO.Unsafe |
10 | import Test.Hspec | 10 | import Test.Hspec |
11 | 11 | ||
12 | import Data.Torrent.Layout | 12 | import Data.Torrent |
13 | import System.Torrent.FileMap as FM | 13 | import System.Torrent.FileMap as FM |
14 | 14 | ||
15 | 15 | ||
diff --git a/tests/System/Torrent/StorageSpec.hs b/tests/System/Torrent/StorageSpec.hs index 30322545..ebf4fe3e 100644 --- a/tests/System/Torrent/StorageSpec.hs +++ b/tests/System/Torrent/StorageSpec.hs | |||
@@ -8,7 +8,7 @@ import System.IO.Unsafe | |||
8 | import Test.Hspec | 8 | import Test.Hspec |
9 | 9 | ||
10 | import Data.Torrent.Bitfield as BF | 10 | import Data.Torrent.Bitfield as BF |
11 | import Data.Torrent.Layout | 11 | import Data.Torrent |
12 | import Data.Torrent.Piece | 12 | import Data.Torrent.Piece |
13 | import System.Torrent.Storage | 13 | import System.Torrent.Storage |
14 | 14 | ||