summaryrefslogtreecommitdiff
path: root/src/Data/Torrent/Layout.hs
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-10-31 11:25:59 +0400
commit01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch)
tree01040aca19e49f4e7937383fef53b8c82bcec12b /src/Data/Torrent/Layout.hs
parentc1fec260f47084300ac30de2e43d52966316a2c7 (diff)
Merge bittorrent package with torrent-content
Diffstat (limited to 'src/Data/Torrent/Layout.hs')
-rw-r--r--src/Data/Torrent/Layout.hs273
1 files changed, 273 insertions, 0 deletions
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs
new file mode 100644
index 00000000..409426be
--- /dev/null
+++ b/src/Data/Torrent/Layout.hs
@@ -0,0 +1,273 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE BangPatterns #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE StandaloneDeriving #-}
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12{-# LANGUAGE DeriveDataTypeable #-}
13{-# LANGUAGE TemplateHaskell #-}
14{-# OPTIONS -fno-warn-orphans #-}
15module Data.Torrent.Layout
16 ( -- * File attribytes
17 FileOffset
18 , FileSize
19
20 -- * Single file info
21 , FileInfo (..)
22 , fileLength
23 , filePath
24 , fileMD5Sum
25
26 -- * File layout
27 , LayoutInfo (..)
28 , singleFile
29 , multiFile
30 , rootDirName
31 , isSingleFile
32 , isMultiFile
33 , fileNumber
34 , contentLength
35 , blockCount
36
37 -- * Flat file layout
38 , Layout
39 , flatLayout
40 , accumOffsets
41 , fileOffset
42
43 -- * Internal
44 , getLayoutInfo
45 , putLayoutInfo
46 ) where
47
48import Control.Applicative
49import Control.DeepSeq
50import Control.Lens
51import Data.Aeson.TH
52import Data.Aeson.Types (FromJSON, ToJSON)
53import Data.BEncode
54import Data.BEncode.Types
55import Data.ByteString as BS
56import Data.ByteString.Char8 as BC
57import Data.Char
58import Data.List as L
59import Data.Typeable
60import System.FilePath
61import System.Posix.Types
62
63
64{-----------------------------------------------------------------------
65-- File attribytes
66-----------------------------------------------------------------------}
67
68type FileSize = FileOffset
69
70deriving instance FromJSON FileOffset
71deriving instance ToJSON FileOffset
72deriving instance BEncode FileOffset
73
74{-----------------------------------------------------------------------
75-- File info both either from info dict or file list
76-----------------------------------------------------------------------}
77
78-- | Contain info about one single file.
79data FileInfo a = FileInfo {
80 fiLength :: {-# UNPACK #-} !FileSize
81 -- ^ Length of the file in bytes.
82
83 -- TODO unpacked MD5 sum
84 , fiMD5Sum :: !(Maybe ByteString)
85 -- ^ 32 character long MD5 sum of the file. Used by third-party
86 -- tools, not by bittorrent protocol itself.
87
88 , fiName :: !a
89 -- ^ One or more string elements that together represent the
90 -- path and filename. Each element in the list corresponds to
91 -- either a directory name or (in the case of the last
92 -- element) the filename. For example, the file:
93 --
94 -- > "dir1/dir2/file.ext"
95 --
96 -- would consist of three string elements:
97 --
98 -- > ["dir1", "dir2", "file.ext"]
99 --
100 } deriving (Show, Read, Eq, Typeable)
101
102$(deriveJSON (L.map toLower . L.dropWhile isLower) ''FileInfo)
103
104makeLensesFor
105 [ ("fiLength", "fileLength")
106 , ("fiMD5Sum", "fileMD5Sum")
107 , ("fiName" , "filePath" )
108 ]
109 ''FileInfo
110
111instance NFData a => NFData (FileInfo a) where
112 rnf FileInfo {..} = rnf fiName
113 {-# INLINE rnf #-}
114
115instance BEncode (FileInfo [ByteString]) where
116 toBEncode FileInfo {..} = toDict $
117 "length" .=! fiLength
118 .: "md5sum" .=? fiMD5Sum
119 .: "path" .=! fiName
120 .: endDict
121 {-# INLINE toBEncode #-}
122
123 fromBEncode = fromDict $ do
124 FileInfo <$>! "length"
125 <*>? "md5sum"
126 <*>! "path"
127 {-# INLINE fromBEncode #-}
128
129type Put a = a -> BDict -> BDict
130
131putFileInfoSingle :: Put (FileInfo ByteString)
132putFileInfoSingle FileInfo {..} cont =
133 "length" .=! fiLength
134 .: "md5sum" .=? fiMD5Sum
135 .: "name" .=! fiName
136 .: cont
137
138getFileInfoSingle :: Get (FileInfo ByteString)
139getFileInfoSingle = do
140 FileInfo <$>! "length"
141 <*>? "md5sum"
142 <*>! "name"
143
144instance BEncode (FileInfo ByteString) where
145 toBEncode = toDict . (`putFileInfoSingle` endDict)
146 {-# INLINE toBEncode #-}
147
148 fromBEncode = fromDict getFileInfoSingle
149 {-# INLINE fromBEncode #-}
150
151{-----------------------------------------------------------------------
152-- Original torrent file layout info
153-----------------------------------------------------------------------}
154
155data LayoutInfo
156 = SingleFile
157 { liFile :: !(FileInfo ByteString)
158 }
159 | MultiFile
160 { -- | List of the all files that torrent contains.
161 liFiles :: ![FileInfo [ByteString]]
162
163 -- | The /suggested/ name of the root directory in which to
164 -- store all the files.
165 , liDirName :: !ByteString
166 } deriving (Show, Read, Eq, Typeable)
167
168$(deriveJSON (L.map toLower . L.dropWhile isLower) ''LayoutInfo)
169
170makeLensesFor
171 [ ("liFile" , "singleFile" )
172 , ("liFiles" , "multiFile" )
173 , ("liDirName", "rootDirName")
174 ]
175 ''LayoutInfo
176
177instance NFData LayoutInfo where
178 rnf SingleFile {..} = ()
179 rnf MultiFile {..} = rnf liFiles
180
181getLayoutInfo :: Get LayoutInfo
182getLayoutInfo = single <|> multi
183 where
184 single = SingleFile <$> getFileInfoSingle
185 multi = MultiFile <$>! "files" <*>! "name"
186
187putLayoutInfo :: Put LayoutInfo
188putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
189putLayoutInfo MultiFile {..} = \ cont ->
190 "files" .=! liFiles
191 .: "name" .=! liDirName
192 .: cont
193
194instance BEncode LayoutInfo where
195 toBEncode = toDict . (`putLayoutInfo` endDict)
196 fromBEncode = fromDict getLayoutInfo
197
198-- | Test if this is single file torrent.
199isSingleFile :: LayoutInfo -> Bool
200isSingleFile SingleFile {} = True
201isSingleFile _ = False
202{-# INLINE isSingleFile #-}
203
204-- | Test if this is multifile torrent.
205isMultiFile :: LayoutInfo -> Bool
206isMultiFile MultiFile {} = True
207isMultiFile _ = False
208{-# INLINE isMultiFile #-}
209
210-- | Find sum of sizes of the all torrent files.
211contentLength :: LayoutInfo -> FileSize
212contentLength SingleFile { liFile = FileInfo {..} } = fiLength
213contentLength MultiFile { liFiles = tfs } = sum (L.map fiLength tfs)
214
215-- | Get count of all files in torrent.
216fileNumber :: LayoutInfo -> Int
217fileNumber SingleFile {..} = 1
218fileNumber MultiFile {..} = L.length liFiles
219
220-- | Find number of blocks of the specified size. If torrent size is
221-- not a multiple of block size then the count is rounded up.
222blockCount :: Int -- ^ Block size.
223 -> LayoutInfo -- ^ Torrent content info.
224 -> Int -- ^ Number of blocks.
225blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
226
227{-----------------------------------------------------------------------
228-- Flat layout
229-----------------------------------------------------------------------}
230
231-- | File layout specifies the order and the size of each file in the
232-- storage. Note that order of files is highly important since we
233-- coalesce all the files in the given order to get the linear block
234-- address space.
235--
236type Layout a = [(FilePath, a)]
237
238-- | Extract files layout from torrent info with the given root path.
239flatLayout
240 :: FilePath -- ^ Root path for the all torrent files.
241 -> LayoutInfo -- ^ Torrent content information.
242 -> Layout FileSize -- ^ The all file paths prefixed with the given root.
243flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
244 = [(prefixPath </> BC.unpack fiName, fiLength)]
245flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
246 where -- TODO use utf8 encoding in name
247 mkPath FileInfo {..} = (path, fiLength)
248 where
249 path = prefixPath </> BC.unpack liDirName
250 </> joinPath (L.map BC.unpack fiName)
251
252accumOffsets :: Layout FileSize -> Layout FileOffset
253accumOffsets = go 0
254 where
255 go !_ [] = []
256 go !offset ((n, s) : xs) = (n, offset) : go (offset + s) xs
257
258-- | Gives global offset of a content file for a given full path.
259fileOffset :: FilePath -> Layout FileOffset -> Maybe FileOffset
260fileOffset = lookup
261{-# INLINE fileOffset #-}
262
263{-----------------------------------------------------------------------
264-- Internal utilities
265-----------------------------------------------------------------------}
266
267-- | Divide and round up.
268sizeInBase :: Integral a => a -> Int -> Int
269sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
270 where
271 align = if n `mod` fromIntegral b == 0 then 0 else 1
272{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
273{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}