diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2013-10-31 11:25:59 +0400 |
commit | 01cef3fafc27d39d88c94cacdcd8e204c5f66b86 (patch) | |
tree | 01040aca19e49f4e7937383fef53b8c82bcec12b /src/Data/Torrent/Layout.hs | |
parent | c1fec260f47084300ac30de2e43d52966316a2c7 (diff) |
Merge bittorrent package with torrent-content
Diffstat (limited to 'src/Data/Torrent/Layout.hs')
-rw-r--r-- | src/Data/Torrent/Layout.hs | 273 |
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 #-} | ||
15 | module 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 | |||
48 | import Control.Applicative | ||
49 | import Control.DeepSeq | ||
50 | import Control.Lens | ||
51 | import Data.Aeson.TH | ||
52 | import Data.Aeson.Types (FromJSON, ToJSON) | ||
53 | import Data.BEncode | ||
54 | import Data.BEncode.Types | ||
55 | import Data.ByteString as BS | ||
56 | import Data.ByteString.Char8 as BC | ||
57 | import Data.Char | ||
58 | import Data.List as L | ||
59 | import Data.Typeable | ||
60 | import System.FilePath | ||
61 | import System.Posix.Types | ||
62 | |||
63 | |||
64 | {----------------------------------------------------------------------- | ||
65 | -- File attribytes | ||
66 | -----------------------------------------------------------------------} | ||
67 | |||
68 | type FileSize = FileOffset | ||
69 | |||
70 | deriving instance FromJSON FileOffset | ||
71 | deriving instance ToJSON FileOffset | ||
72 | deriving 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. | ||
79 | data 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 | |||
104 | makeLensesFor | ||
105 | [ ("fiLength", "fileLength") | ||
106 | , ("fiMD5Sum", "fileMD5Sum") | ||
107 | , ("fiName" , "filePath" ) | ||
108 | ] | ||
109 | ''FileInfo | ||
110 | |||
111 | instance NFData a => NFData (FileInfo a) where | ||
112 | rnf FileInfo {..} = rnf fiName | ||
113 | {-# INLINE rnf #-} | ||
114 | |||
115 | instance 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 | |||
129 | type Put a = a -> BDict -> BDict | ||
130 | |||
131 | putFileInfoSingle :: Put (FileInfo ByteString) | ||
132 | putFileInfoSingle FileInfo {..} cont = | ||
133 | "length" .=! fiLength | ||
134 | .: "md5sum" .=? fiMD5Sum | ||
135 | .: "name" .=! fiName | ||
136 | .: cont | ||
137 | |||
138 | getFileInfoSingle :: Get (FileInfo ByteString) | ||
139 | getFileInfoSingle = do | ||
140 | FileInfo <$>! "length" | ||
141 | <*>? "md5sum" | ||
142 | <*>! "name" | ||
143 | |||
144 | instance 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 | |||
155 | data 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 | |||
170 | makeLensesFor | ||
171 | [ ("liFile" , "singleFile" ) | ||
172 | , ("liFiles" , "multiFile" ) | ||
173 | , ("liDirName", "rootDirName") | ||
174 | ] | ||
175 | ''LayoutInfo | ||
176 | |||
177 | instance NFData LayoutInfo where | ||
178 | rnf SingleFile {..} = () | ||
179 | rnf MultiFile {..} = rnf liFiles | ||
180 | |||
181 | getLayoutInfo :: Get LayoutInfo | ||
182 | getLayoutInfo = single <|> multi | ||
183 | where | ||
184 | single = SingleFile <$> getFileInfoSingle | ||
185 | multi = MultiFile <$>! "files" <*>! "name" | ||
186 | |||
187 | putLayoutInfo :: Put LayoutInfo | ||
188 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
189 | putLayoutInfo MultiFile {..} = \ cont -> | ||
190 | "files" .=! liFiles | ||
191 | .: "name" .=! liDirName | ||
192 | .: cont | ||
193 | |||
194 | instance BEncode LayoutInfo where | ||
195 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
196 | fromBEncode = fromDict getLayoutInfo | ||
197 | |||
198 | -- | Test if this is single file torrent. | ||
199 | isSingleFile :: LayoutInfo -> Bool | ||
200 | isSingleFile SingleFile {} = True | ||
201 | isSingleFile _ = False | ||
202 | {-# INLINE isSingleFile #-} | ||
203 | |||
204 | -- | Test if this is multifile torrent. | ||
205 | isMultiFile :: LayoutInfo -> Bool | ||
206 | isMultiFile MultiFile {} = True | ||
207 | isMultiFile _ = False | ||
208 | {-# INLINE isMultiFile #-} | ||
209 | |||
210 | -- | Find sum of sizes of the all torrent files. | ||
211 | contentLength :: LayoutInfo -> FileSize | ||
212 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
213 | contentLength MultiFile { liFiles = tfs } = sum (L.map fiLength tfs) | ||
214 | |||
215 | -- | Get count of all files in torrent. | ||
216 | fileNumber :: LayoutInfo -> Int | ||
217 | fileNumber SingleFile {..} = 1 | ||
218 | fileNumber 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. | ||
222 | blockCount :: Int -- ^ Block size. | ||
223 | -> LayoutInfo -- ^ Torrent content info. | ||
224 | -> Int -- ^ Number of blocks. | ||
225 | blockCount 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 | -- | ||
236 | type Layout a = [(FilePath, a)] | ||
237 | |||
238 | -- | Extract files layout from torrent info with the given root path. | ||
239 | flatLayout | ||
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. | ||
243 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
244 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
245 | flatLayout 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 | |||
252 | accumOffsets :: Layout FileSize -> Layout FileOffset | ||
253 | accumOffsets = 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. | ||
259 | fileOffset :: FilePath -> Layout FileOffset -> Maybe FileOffset | ||
260 | fileOffset = lookup | ||
261 | {-# INLINE fileOffset #-} | ||
262 | |||
263 | {----------------------------------------------------------------------- | ||
264 | -- Internal utilities | ||
265 | -----------------------------------------------------------------------} | ||
266 | |||
267 | -- | Divide and round up. | ||
268 | sizeInBase :: Integral a => a -> Int -> Int | ||
269 | sizeInBase 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 #-} | ||