A music player that connects to your cloud/distributed storage.
1module Sources.Services.Ipfs exposing (..)
2
3{-| IPFS Service.
4
5Resources:
6
7 - <https://ipfs.io/docs/api/>
8
9-}
10
11import Common exposing (boolFromString, boolToString)
12import Conditional exposing (ifThenElse)
13import Dict
14import Dict.Ext as Dict
15import Http
16import Json.Decode as Json
17import Sources exposing (Property, SourceData)
18import Sources.Processing exposing (..)
19import Sources.Services.Ipfs.Marker as Marker
20import Sources.Services.Ipfs.Parser as Parser
21import String.Ext as String
22import Task
23import Time
24import Url
25
26
27
28-- PROPERTIES
29-- 📟
30
31
32defaults =
33 { gateway = ""
34 , local = boolToString False
35 , name = "Music from IPFS"
36 , ipns = boolToString False
37 }
38
39
40defaultGateway =
41 "https://ipfs.io"
42
43
44{-| The list of properties we need from the user.
45
46Tuple: (property, label, placeholder, isPassword)
47Will be used for the forms.
48
49-}
50properties : List Property
51properties =
52 [ { key = "directoryHash"
53 , label = "Directory hash / DNSLink domain"
54 , placeholder = "QmVLDAhCY3X9P2u"
55 , password = False
56 }
57 , { key = "ipns"
58 , label = "Resolve using IPNS"
59 , placeholder = defaults.ipns
60 , password = False
61 }
62 , { key = "gateway"
63 , label = "Gateway (Optional)"
64 , placeholder = defaultGateway
65 , password = False
66 }
67 , { key = "local"
68 , label = "Resolve IPNS locally"
69 , placeholder = defaults.local
70 , password = False
71 }
72 ]
73
74
75{-| Initial data set.
76-}
77initialData : SourceData
78initialData =
79 Dict.fromList
80 [ ( "directoryHash", "" )
81 , ( "gateway", defaults.gateway )
82 , ( "ipns", defaults.ipns )
83 , ( "local", defaults.local )
84 , ( "name", defaults.name )
85 ]
86
87
88
89-- PREPARATION
90
91
92prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg)
93prepare _ srcData _ toMsg =
94 let
95 domainName =
96 srcData
97 |> Dict.get "directoryHash"
98 |> Maybe.withDefault ""
99 |> String.chopStart "http://"
100 |> String.chopStart "https://"
101 |> String.chopEnd "/"
102 |> String.chopStart "_dnslink."
103 in
104 if isDnsLink srcData then
105 (Just << Http.request)
106 { method = "POST"
107 , headers = []
108 , url = extractGateway srcData ++ "/api/v0/dns?arg=" ++ domainName
109 , body = Http.emptyBody
110 , expect = Http.expectString toMsg
111 , timeout = Nothing
112 , tracker = Nothing
113 }
114
115 else
116 Nothing
117
118
119
120-- TREE
121
122
123{-| Create a directory tree.
124-}
125makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg
126makeTree srcData marker _ resultMsg =
127 let
128 gateway =
129 extractGateway srcData
130
131 resolveWithIpns =
132 case marker of
133 InProgress _ ->
134 False
135
136 _ ->
137 srcData
138 |> Dict.fetch "ipns" defaults.ipns
139 |> boolFromString
140
141 resolveLocally =
142 srcData
143 |> Dict.fetch "local" defaults.local
144 |> boolFromString
145 |> (\b -> ifThenElse b "true" "false")
146
147 root =
148 rootHash srcData
149
150 path =
151 case marker of
152 InProgress _ ->
153 marker
154 |> Marker.takeOne
155 |> Maybe.map (\p -> root ++ "/" ++ p)
156 |> Maybe.withDefault ""
157
158 _ ->
159 root
160 in
161 (if resolveWithIpns then
162 Http.task
163 { method = "POST"
164 , headers = []
165 , url = gateway ++ "/api/v0/name/resolve?arg=" ++ encodedPath path ++ "&local=" ++ resolveLocally ++ "&encoding=json"
166 , body = Http.emptyBody
167 , resolver = Http.stringResolver ipnsResolver
168 , timeout = Just (60 * 15 * 1000)
169 }
170
171 else
172 Task.succeed { ipfsPath = path }
173 )
174 |> Task.andThen
175 (\{ ipfsPath } ->
176 Http.task
177 { method = "POST"
178 , headers = []
179 , url = gateway ++ "/api/v0/ls?arg=" ++ encodedPath ipfsPath ++ "&encoding=json"
180 , body = Http.emptyBody
181 , resolver = Http.stringResolver Common.translateHttpResponse
182 , timeout = Just (60 * 15 * 1000)
183 }
184 )
185 |> Task.attempt resultMsg
186
187
188ipnsResolver : Http.Response String -> Result Http.Error { ipfsPath : String }
189ipnsResolver response =
190 case response of
191 Http.BadUrl_ u ->
192 Err (Http.BadUrl u)
193
194 Http.Timeout_ ->
195 Err Http.Timeout
196
197 Http.NetworkError_ ->
198 Err Http.NetworkError
199
200 Http.BadStatus_ _ body ->
201 Err (Http.BadBody body)
202
203 Http.GoodStatus_ _ body ->
204 body
205 |> Json.decodeString (Json.field "Path" Json.string)
206 |> Result.map (\path -> { ipfsPath = String.chopStart "/ipfs/" path })
207 |> Result.mapError (Json.errorToString >> Http.BadBody)
208
209
210{-| Re-export parser functions.
211-}
212parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker
213parsePreparationResponse =
214 Parser.parseDnsLookup
215
216
217parseTreeResponse : String -> Marker -> TreeAnswer Marker
218parseTreeResponse =
219 Parser.parseTreeResponse
220
221
222parseErrorResponse : String -> Maybe String
223parseErrorResponse =
224 Parser.parseErrorResponse
225
226
227
228-- POST
229
230
231{-| Post process the tree results.
232
233!!! Make sure we only use music files that we can use.
234
235-}
236postProcessTree : List String -> List String
237postProcessTree =
238 identity
239
240
241
242-- TRACK URL
243
244
245{-| Create a public url for a file.
246
247We need this to play the track.
248
249-}
250makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String
251makeTrackUrl _ _ srcData _ path =
252 if not (String.contains "/" path) && not (String.contains "." path) then
253 -- If it still uses the old way of doing things
254 -- (ie. each path was a cid)
255 extractGateway srcData ++ "/ipfs/" ++ path
256
257 else
258 -- Or the new way
259 extractGateway srcData ++ "/ipfs/" ++ rootHash srcData ++ "/" ++ encodedPath path
260
261
262
263-- ⚗️
264
265
266encodedPath : String -> String
267encodedPath path =
268 path
269 |> String.split "/"
270 |> List.map Url.percentEncode
271 |> String.join "/"
272
273
274extractGateway : SourceData -> String
275extractGateway srcData =
276 srcData
277 |> Dict.get "gateway"
278 |> Maybe.map String.trim
279 |> Maybe.andThen
280 (\s ->
281 case s of
282 "" ->
283 Nothing
284
285 _ ->
286 Just s
287 )
288 |> Maybe.map (String.chopEnd "/")
289 |> Maybe.withDefault defaultGateway
290
291
292isDnsLink : SourceData -> Bool
293isDnsLink srcData =
294 srcData
295 |> Dict.get "directoryHash"
296 |> Maybe.map pathIsDnsLink
297 |> Maybe.withDefault False
298
299
300pathIsDnsLink : String -> Bool
301pathIsDnsLink =
302 String.contains "."
303
304
305rootHash : SourceData -> String
306rootHash srcData =
307 srcData
308 |> Dict.get "directoryHash"
309 |> Maybe.andThen
310 (\path ->
311 if pathIsDnsLink path then
312 Dict.get "directoryHashFromDnsLink" srcData
313
314 else
315 Just path
316 )
317 |> Maybe.withDefault ""
318 |> String.chopEnd "/"