summaryrefslogtreecommitdiff
path: root/src/Data
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2014-04-04 21:16:34 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2014-04-04 21:16:34 +0400
commit88ef120511caae5ed74a48a87617b43aec4b7f76 (patch)
treeb3bb561ea041a7da6c7168496a2e522b00b14456 /src/Data
parent7a892425de92efd88b98576e848bebc725a9bf14 (diff)
Move layout info to Torrent module
Diffstat (limited to 'src/Data')
-rw-r--r--src/Data/Torrent.hs270
-rw-r--r--src/Data/Torrent/Layout.hs321
-rw-r--r--src/Data/Torrent/Tree.hs2
3 files changed, 270 insertions, 323 deletions
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 #-}
29module Data.Torrent 33module 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
109import Data.Char 144import Data.Char
110import Data.Convertible 145import Data.Convertible
111import Data.Default 146import Data.Default
147import Data.Foldable as F
112import Data.Hashable as Hashable 148import Data.Hashable as Hashable
113import qualified Data.List as L 149import qualified Data.List as L
114import Data.Map as M 150import Data.Map as M
@@ -128,8 +164,8 @@ import Text.ParserCombinators.ReadP as P
128import Text.PrettyPrint as PP 164import Text.PrettyPrint as PP
129import Text.PrettyPrint.Class 165import Text.PrettyPrint.Class
130import System.FilePath 166import System.FilePath
167import System.Posix.Types
131 168
132import Data.Torrent.Layout
133import Data.Torrent.Piece 169import Data.Torrent.Piece
134import Network.BitTorrent.Core.NodeInfo 170import Network.BitTorrent.Core.NodeInfo
135 171
@@ -259,6 +295,238 @@ shortHex :: InfoHash -> Text
259shortHex = T.take 7 . longHex 295shortHex = T.take 7 . longHex
260 296
261{----------------------------------------------------------------------- 297{-----------------------------------------------------------------------
298-- File info
299-----------------------------------------------------------------------}
300
301-- | Size of a file in bytes.
302type FileSize = FileOffset
303
304deriving instance BEncode FileOffset
305
306-- | Contain metainfo about one single file.
307data 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
332makeLensesFor
333 [ ("fiLength", "fileLength")
334 , ("fiMD5Sum", "fileMD5Sum")
335 , ("fiName" , "filePath" )
336 ]
337 ''FileInfo
338
339instance NFData a => NFData (FileInfo a) where
340 rnf FileInfo {..} = rnf fiName
341 {-# INLINE rnf #-}
342
343instance 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
357type Put a = a -> BDict -> BDict
358
359putFileInfoSingle :: Data.Torrent.Put (FileInfo ByteString)
360putFileInfoSingle FileInfo {..} cont =
361 "length" .=! fiLength
362 .: "md5sum" .=? fiMD5Sum
363 .: "name" .=! fiName
364 .: cont
365
366getFileInfoSingle :: BE.Get (FileInfo ByteString)
367getFileInfoSingle = do
368 FileInfo <$>! "length"
369 <*>? "md5sum"
370 <*>! "name"
371
372instance BEncode (FileInfo ByteString) where
373 toBEncode = toDict . (`putFileInfoSingle` endDict)
374 {-# INLINE toBEncode #-}
375
376 fromBEncode = fromDict getFileInfoSingle
377 {-# INLINE fromBEncode #-}
378
379instance 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.
388joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString
389joinFilePath = 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--
401data 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
415makeLensesFor
416 [ ("liFile" , "singleFile" )
417 , ("liFiles" , "multiFile" )
418 , ("liDirName", "rootDirName")
419 ]
420 ''LayoutInfo
421
422instance NFData LayoutInfo where
423 rnf SingleFile {..} = ()
424 rnf MultiFile {..} = rnf liFiles
425
426-- | Empty multifile layout.
427instance Default LayoutInfo where
428 def = MultiFile [] ""
429
430getLayoutInfo :: BE.Get LayoutInfo
431getLayoutInfo = single <|> multi
432 where
433 single = SingleFile <$> getFileInfoSingle
434 multi = MultiFile <$>! "files" <*>! "name"
435
436putLayoutInfo :: Data.Torrent.Put LayoutInfo
437putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
438putLayoutInfo MultiFile {..} = \ cont ->
439 "files" .=! liFiles
440 .: "name" .=! liDirName
441 .: cont
442
443instance BEncode LayoutInfo where
444 toBEncode = toDict . (`putLayoutInfo` endDict)
445 fromBEncode = fromDict getLayoutInfo
446
447instance 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.
452isSingleFile :: LayoutInfo -> Bool
453isSingleFile SingleFile {} = True
454isSingleFile _ = False
455{-# INLINE isSingleFile #-}
456
457-- | Test if this is multifile torrent.
458isMultiFile :: LayoutInfo -> Bool
459isMultiFile MultiFile {} = True
460isMultiFile _ = False
461{-# INLINE isMultiFile #-}
462
463-- | Get name of the torrent based on the root path piece.
464suggestedName :: LayoutInfo -> ByteString
465suggestedName (SingleFile FileInfo {..}) = fiName
466suggestedName MultiFile {..} = liDirName
467{-# INLINE suggestedName #-}
468
469-- | Find sum of sizes of the all torrent files.
470contentLength :: LayoutInfo -> FileSize
471contentLength SingleFile { liFile = FileInfo {..} } = fiLength
472contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
473
474-- | Get number of all files in torrent.
475fileCount :: LayoutInfo -> Int
476fileCount SingleFile {..} = 1
477fileCount 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.
481blockCount :: Int -> LayoutInfo -> Int
482blockCount 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--
491type FileLayout a = [(FilePath, a)]
492
493-- | Extract files layout from torrent info with the given root path.
494flatLayout
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.
498flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
499 = [(prefixPath </> BC.unpack fiName, fiLength)]
500flatLayout 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.
508accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
509accumPositions = 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.
515fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
516fileOffset = L.lookup
517{-# INLINE fileOffset #-}
518
519------------------------------------------------------------------------
520
521-- | Divide and round up.
522sizeInBase :: Integral a => a -> Int -> Int
523sizeInBase 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 #-}
20module 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
64import Control.Applicative
65import Control.DeepSeq
66import Control.Lens
67import Data.BEncode
68import Data.BEncode.Types
69import Data.ByteString as BS
70import Data.ByteString.Base16 as Base16
71import Data.ByteString.Char8 as BC
72import Data.Default
73import Data.Foldable as F
74import Data.List as L
75import Data.Text as T
76import Data.Text.Encoding as T
77import Data.Typeable
78import Text.PrettyPrint as PP
79import Text.PrettyPrint.Class
80import System.FilePath
81import System.Posix.Types
82
83{-----------------------------------------------------------------------
84-- File attribytes
85-----------------------------------------------------------------------}
86
87-- | Size of a file in bytes.
88type FileSize = FileOffset
89
90deriving 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.
97data 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
122makeLensesFor
123 [ ("fiLength", "fileLength")
124 , ("fiMD5Sum", "fileMD5Sum")
125 , ("fiName" , "filePath" )
126 ]
127 ''FileInfo
128
129instance NFData a => NFData (FileInfo a) where
130 rnf FileInfo {..} = rnf fiName
131 {-# INLINE rnf #-}
132
133instance 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
147type Put a = a -> BDict -> BDict
148
149putFileInfoSingle :: Put (FileInfo ByteString)
150putFileInfoSingle FileInfo {..} cont =
151 "length" .=! fiLength
152 .: "md5sum" .=? fiMD5Sum
153 .: "name" .=! fiName
154 .: cont
155
156getFileInfoSingle :: Get (FileInfo ByteString)
157getFileInfoSingle = do
158 FileInfo <$>! "length"
159 <*>? "md5sum"
160 <*>! "name"
161
162instance BEncode (FileInfo ByteString) where
163 toBEncode = toDict . (`putFileInfoSingle` endDict)
164 {-# INLINE toBEncode #-}
165
166 fromBEncode = fromDict getFileInfoSingle
167 {-# INLINE fromBEncode #-}
168
169instance 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.
178joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString
179joinFilePath = 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--
191data 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
205makeLensesFor
206 [ ("liFile" , "singleFile" )
207 , ("liFiles" , "multiFile" )
208 , ("liDirName", "rootDirName")
209 ]
210 ''LayoutInfo
211
212instance NFData LayoutInfo where
213 rnf SingleFile {..} = ()
214 rnf MultiFile {..} = rnf liFiles
215
216-- | Empty multifile layout.
217instance Default LayoutInfo where
218 def = MultiFile [] ""
219
220getLayoutInfo :: Get LayoutInfo
221getLayoutInfo = single <|> multi
222 where
223 single = SingleFile <$> getFileInfoSingle
224 multi = MultiFile <$>! "files" <*>! "name"
225
226putLayoutInfo :: Put LayoutInfo
227putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
228putLayoutInfo MultiFile {..} = \ cont ->
229 "files" .=! liFiles
230 .: "name" .=! liDirName
231 .: cont
232
233instance BEncode LayoutInfo where
234 toBEncode = toDict . (`putLayoutInfo` endDict)
235 fromBEncode = fromDict getLayoutInfo
236
237instance 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.
242isSingleFile :: LayoutInfo -> Bool
243isSingleFile SingleFile {} = True
244isSingleFile _ = False
245{-# INLINE isSingleFile #-}
246
247-- | Test if this is multifile torrent.
248isMultiFile :: LayoutInfo -> Bool
249isMultiFile MultiFile {} = True
250isMultiFile _ = False
251{-# INLINE isMultiFile #-}
252
253-- | Get name of the torrent based on the root path piece.
254suggestedName :: LayoutInfo -> ByteString
255suggestedName (SingleFile FileInfo {..}) = fiName
256suggestedName MultiFile {..} = liDirName
257{-# INLINE suggestedName #-}
258
259-- | Find sum of sizes of the all torrent files.
260contentLength :: LayoutInfo -> FileSize
261contentLength SingleFile { liFile = FileInfo {..} } = fiLength
262contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
263
264-- | Get number of all files in torrent.
265fileCount :: LayoutInfo -> Int
266fileCount SingleFile {..} = 1
267fileCount 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.
271blockCount :: Int -> LayoutInfo -> Int
272blockCount 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--
283type FileLayout a = [(FilePath, a)]
284
285-- | Extract files layout from torrent info with the given root path.
286flatLayout
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.
290flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
291 = [(prefixPath </> BC.unpack fiName, fiLength)]
292flatLayout 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.
300accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
301accumPositions = 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.
307fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
308fileOffset = lookup
309{-# INLINE fileOffset #-}
310
311{-----------------------------------------------------------------------
312-- Internal utilities
313-----------------------------------------------------------------------}
314
315-- | Divide and round up.
316sizeInBase :: Integral a => a -> Int -> Int
317sizeInBase 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
31import Data.Map as M 31import Data.Map as M
32import Data.Monoid 32import Data.Monoid
33 33
34import Data.Torrent.Layout 34import Data.Torrent
35 35
36 36
37-- | 'DirTree' is more convenient form of 'LayoutInfo'. 37-- | 'DirTree' is more convenient form of 'LayoutInfo'.