diff --git a/elm.json b/elm.json index 02c63cee..0f70e409 100644 --- a/elm.json +++ b/elm.json @@ -33,6 +33,7 @@ "icidasset/elm-sha": "2.0.2", "justgage/tachyons-elm": "4.1.3", "mpizenberg/elm-pointer-events": "4.0.0", + "newlandsvalley/elm-binary-base64": "1.0.3", "noahzgordon/elm-color-extra": "1.0.2", "rtfeldman/elm-css": "16.0.0", "rtfeldman/elm-hex": "1.0.0", @@ -50,4 +51,4 @@ "direct": {}, "indirect": {} } -} +} \ No newline at end of file diff --git a/src/Library/Binary/Ext.elm b/src/Library/Binary/Ext.elm new file mode 100644 index 00000000..95f7755d --- /dev/null +++ b/src/Library/Binary/Ext.elm @@ -0,0 +1,242 @@ +module Binary.Ext exposing (fromBase64, intToBase64, toBase64) + +import Binary exposing (Bits) + + + +-- BASE64 + + +fromBase64 : String -> Bits +fromBase64 encoded = + Binary.empty + + +{-| To Base64. + + >>> import Binary + + >>> toBase64 (Binary.fromStringAsUtf8 "") + "" + + >>> toBase64 (Binary.fromStringAsUtf8 "f") + "Zg==" + + >>> toBase64 (Binary.fromStringAsUtf8 "fo") + "Zm8=" + + >>> toBase64 (Binary.fromStringAsUtf8 "foo") + "Zm9v" + + >>> toBase64 (Binary.fromStringAsUtf8 "foob") + "Zm9vYg==" + + >>> toBase64 (Binary.fromStringAsUtf8 "fooba") + "Zm9vYmE=" + + >>> toBase64 (Binary.fromStringAsUtf8 "foobar") + "Zm9vYmFy" + +-} +toBase64 : Bits -> String +toBase64 bits = + bits + |> Binary.chunksOf 6 + |> List.map (Binary.toDecimal >> intToBase64) + |> String.fromList + + +intToBase64 : Int -> Char +intToBase64 i = + case i of + 0 -> + 'A' + + 1 -> + 'B' + + 2 -> + 'C' + + 3 -> + 'D' + + 4 -> + 'E' + + 5 -> + 'F' + + 6 -> + 'G' + + 7 -> + 'H' + + 8 -> + 'I' + + 9 -> + 'J' + + 10 -> + 'K' + + 11 -> + 'L' + + 12 -> + 'M' + + 13 -> + 'N' + + 14 -> + 'O' + + 15 -> + 'P' + + 16 -> + 'Q' + + 17 -> + 'R' + + 18 -> + 'S' + + 19 -> + 'T' + + 20 -> + 'U' + + 21 -> + 'V' + + 22 -> + 'W' + + 23 -> + 'X' + + 24 -> + 'Y' + + 25 -> + 'Z' + + 26 -> + 'a' + + 27 -> + 'b' + + 28 -> + 'c' + + 29 -> + 'd' + + 30 -> + 'e' + + 31 -> + 'f' + + 32 -> + 'g' + + 33 -> + 'h' + + 34 -> + 'i' + + 35 -> + 'j' + + 36 -> + 'k' + + 37 -> + 'l' + + 38 -> + 'm' + + 39 -> + 'n' + + 40 -> + 'o' + + 41 -> + 'p' + + 42 -> + 'q' + + 43 -> + 'r' + + 44 -> + 's' + + 45 -> + 't' + + 46 -> + 'u' + + 47 -> + 'v' + + 48 -> + 'w' + + 49 -> + 'x' + + 50 -> + 'y' + + 51 -> + 'z' + + 52 -> + '0' + + 53 -> + '1' + + 54 -> + '2' + + 55 -> + '3' + + 56 -> + '4' + + 57 -> + '5' + + 58 -> + '6' + + 59 -> + '7' + + 60 -> + '8' + + 61 -> + '9' + + 62 -> + '+' + + _ -> + '/' diff --git a/src/Library/Common.elm b/src/Library/Common.elm index 35ae3e6b..1c00ab82 100644 --- a/src/Library/Common.elm +++ b/src/Library/Common.elm @@ -1,6 +1,8 @@ -module Common exposing (Switch(..), urlOrigin) +module Common exposing (Switch(..), queryString, urlOrigin) +import Tuple.Ext as Tuple import Url exposing (Protocol(..), Url) +import Url.Builder as Url @@ -16,6 +18,11 @@ type Switch -- πŸ”± +queryString : List ( String, String ) -> String +queryString = + List.map (Tuple.uncurry Url.string) >> Url.toQuery + + urlOrigin : Url -> String urlOrigin { host, port_, protocol } = let diff --git a/src/Library/Cryptography/Hmac.elm b/src/Library/Cryptography/Hmac.elm index 771a77dd..46ec9965 100644 --- a/src/Library/Cryptography/Hmac.elm +++ b/src/Library/Cryptography/Hmac.elm @@ -34,6 +34,18 @@ These include: SHA-0, SHA-1, SHA-224, SHA-256, MD5, etc. ..> |> String.toLower "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" + >>> Binary.fromHex "4a656665" + ..> |> encrypt64 SHA.sha256 "what do ya want for nothing?" + ..> |> Binary.toHex + ..> |> String.toLower + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" + + >>> Binary.fromHex "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ..> |> encrypt64 SHA.sha256 "Test Using Larger Than Block-Size Key - Hash Key First" + ..> |> Binary.toHex + ..> |> String.toLower + "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" + -} encrypt64 : HashFunction -> String -> Bits -> Bits encrypt64 = @@ -60,13 +72,10 @@ encrypt blockSize hash messageString key = keyWithBlockSize = if keySize > blockSize then - hash key + padRight blockSize (hash key) else if keySize < blockSize then - False - |> List.repeat (blockSize - keySize) - |> List.append (Binary.toBooleans key) - |> Binary.fromBooleans + padRight blockSize key else key @@ -85,6 +94,18 @@ encrypt blockSize hash messageString key = |> hash +padRight : Int -> Bits -> Bits +padRight int bits = + let + size = + Binary.width bits + in + False + |> List.repeat (int - size) + |> List.append (Binary.toBooleans bits) + |> Binary.fromBooleans + + -- PADDING diff --git a/src/Library/Sources.elm b/src/Library/Sources.elm index fdd6bbf8..cb74accd 100644 --- a/src/Library/Sources.elm +++ b/src/Library/Sources.elm @@ -40,6 +40,8 @@ type alias SourceData = type Service = AmazonS3 + | AzureBlob + | AzureFile | Dropbox | Google | Ipfs diff --git a/src/Library/Sources/Services.elm b/src/Library/Sources/Services.elm index 3e3a1561..a59d3646 100644 --- a/src/Library/Sources/Services.elm +++ b/src/Library/Sources/Services.elm @@ -7,6 +7,8 @@ import Http import Sources exposing (..) import Sources.Processing exposing (..) import Sources.Services.AmazonS3 as AmazonS3 +import Sources.Services.AzureBlob as AzureBlob +import Sources.Services.AzureFile as AzureFile import Sources.Services.Dropbox as Dropbox import Sources.Services.Google as Google import Sources.Services.Ipfs as Ipfs @@ -23,6 +25,12 @@ initialData service = AmazonS3 -> AmazonS3.initialData + AzureBlob -> + AzureBlob.initialData + + AzureFile -> + AzureFile.initialData + Dropbox -> Dropbox.initialData @@ -39,6 +47,12 @@ makeTrackUrl service = AmazonS3 -> AmazonS3.makeTrackUrl + AzureBlob -> + AzureBlob.makeTrackUrl + + AzureFile -> + AzureFile.makeTrackUrl + Dropbox -> Dropbox.makeTrackUrl @@ -61,6 +75,12 @@ makeTree service = AmazonS3 -> AmazonS3.makeTree + AzureBlob -> + AzureBlob.makeTree + + AzureFile -> + AzureFile.makeTree + Dropbox -> Dropbox.makeTree @@ -77,6 +97,12 @@ parseErrorResponse service = AmazonS3 -> AmazonS3.parseErrorResponse + AzureBlob -> + AzureBlob.parseErrorResponse + + AzureFile -> + AzureFile.parseErrorResponse + Dropbox -> Dropbox.parseErrorResponse @@ -93,6 +119,12 @@ parsePreparationResponse service = AmazonS3 -> AmazonS3.parsePreparationResponse + AzureBlob -> + AzureBlob.parsePreparationResponse + + AzureFile -> + AzureFile.parsePreparationResponse + Dropbox -> Dropbox.parsePreparationResponse @@ -109,6 +141,12 @@ parseTreeResponse service = AmazonS3 -> AmazonS3.parseTreeResponse + AzureBlob -> + AzureBlob.parseTreeResponse + + AzureFile -> + AzureFile.parseTreeResponse + Dropbox -> Dropbox.parseTreeResponse @@ -125,6 +163,12 @@ postProcessTree service = AmazonS3 -> AmazonS3.postProcessTree + AzureBlob -> + AzureBlob.postProcessTree + + AzureFile -> + AzureFile.postProcessTree + Dropbox -> Dropbox.postProcessTree @@ -147,6 +191,12 @@ prepare service = AmazonS3 -> AmazonS3.prepare + AzureBlob -> + AzureBlob.prepare + + AzureFile -> + AzureFile.prepare + Dropbox -> Dropbox.prepare @@ -163,6 +213,12 @@ properties service = AmazonS3 -> AmazonS3.properties + AzureBlob -> + AzureBlob.properties + + AzureFile -> + AzureFile.properties + Dropbox -> Dropbox.properties @@ -183,6 +239,12 @@ keyToType str = "AmazonS3" -> Just AmazonS3 + "AzureBlob" -> + Just AzureBlob + + "AzureFile" -> + Just AzureFile + "Dropbox" -> Just Dropbox @@ -202,6 +264,12 @@ typeToKey service = AmazonS3 -> "AmazonS3" + AzureBlob -> + "AzureBlob" + + AzureFile -> + "AzureFile" + Dropbox -> "Dropbox" @@ -218,6 +286,8 @@ Maps a service key to a label. labels : List ( String, String ) labels = [ ( typeToKey AmazonS3, "Amazon S3" ) + , ( typeToKey AzureBlob, "Azure Blob Storage" ) + , ( typeToKey AzureFile, "Azure File Storage" ) , ( typeToKey Dropbox, "Dropbox" ) , ( typeToKey Google, "Google Drive" ) , ( typeToKey Ipfs, "IPFS" ) diff --git a/src/Library/Sources/Services/AmazonS3/Presign.elm b/src/Library/Sources/Services/AmazonS3/Presign.elm index 045acbeb..85656738 100644 --- a/src/Library/Sources/Services/AmazonS3/Presign.elm +++ b/src/Library/Sources/Services/AmazonS3/Presign.elm @@ -1,6 +1,7 @@ module Sources.Services.AmazonS3.Presign exposing (presignedUrl) import Binary exposing (Bits) +import Common import Cryptography.HMAC as HMAC import DateFormat as Date import Dict @@ -14,7 +15,6 @@ import Sources.Processing exposing (HttpMethod, httpMethod) import String.Ext as String import Time import Url -import Url.Builder as Url @@ -127,8 +127,7 @@ presignedUrl method lifeExpectancyInSeconds extraParams currentTime srcData path ] |> List.append extraParams |> List.sortBy Tuple.first - |> List.map (\( a, b ) -> Url.string a b) - |> Url.toQuery + |> Common.queryString |> String.dropLeft 1 |> encodeAdditionalCharacters diff --git a/src/Library/Sources/Services/Azure/Authorization.elm b/src/Library/Sources/Services/Azure/Authorization.elm new file mode 100755 index 00000000..26661b20 --- /dev/null +++ b/src/Library/Sources/Services/Azure/Authorization.elm @@ -0,0 +1,228 @@ +module Sources.Services.Azure.Authorization exposing (Computation(..), SignatureDependencies, StorageMethod(..), makeSignature, presignedUrl) + +{-| Resources: + + - + - + +-} + +import Base64 +import Binary +import BinaryBase64 +import Common +import Cryptography.HMAC as Hmac +import DateFormat as Date +import Dict +import Dict.Ext as Dict +import SHA +import Sources exposing (SourceData) +import Sources.Processing exposing (HttpMethod) +import Sources.Services.Common as Utils +import String.Ext as String +import Time +import Url + + + +-- Types + + +type Computation + = List + | Read + + +type StorageMethod + = Blob + | File + + + +-- Public functions + + +presignedUrl : + StorageMethod + -> Computation + -> HttpMethod + -> Int + -> Time.Posix + -> SourceData + -> String + -> List ( String, String ) + -> String +presignedUrl storageMethod computation httpMethod hoursToLive currentTime srcData pathToFile params = + let + azure = + srcData + + accountName = + Dict.fetchUnknown "accountName" azure + + accountKey = + Dict.fetchUnknown "accountKey" azure + + container = + Dict.fetchUnknown "container" azure + + -- {var} Time (y-MM-ddTHH:mmZ) + expiryTime = + Date.format + [ Date.yearNumber + , Date.text "-" + , Date.monthFixed + , Date.text "-" + , Date.dayOfMonthFixed + , Date.text "T" + , Date.hourMilitaryFixed + , Date.text ":" + , Date.minuteFixed + , Date.text "Z" + ] + Time.utc + (currentTime + |> Time.posixToMillis + |> (+) 3600000 + |> Time.millisToPosix + ) + + -- {var} Other + permissions = + case computation of + List -> + "l" + + Read -> + "r" + + resourceType = + case storageMethod of + Blob -> + "blob" + + File -> + "file" + + resType = + case storageMethod of + Blob -> + "container" + + File -> + "directory" + + -- Signature + signatureStuff = + { accountKey = accountKey + , accountName = accountName + , expiryTime = expiryTime + , permissions = permissions + , protocol = "https" + , resources = "co" + , services = "bf" + , startTime = "" + , version = "2017-04-17" + } + in + String.concat + [ "https://" + , Url.percentEncode accountName + , "." + , Url.percentEncode resourceType + , ".core.windows.net/" + , Url.percentEncode container + , "/" + , Url.percentEncode (String.chopStart "/" pathToFile) + + -- Start query params + , case Common.queryString params of + "" -> + "?" + + qs -> + qs + + -- Query params for certain requests + , case computation of + List -> + "&restype=" ++ resType ++ "&comp=list" + + _ -> + "" + + -- Signature things + , "&sv=" + , Url.percentEncode signatureStuff.version + , "&ss=" + , Url.percentEncode signatureStuff.services + , "&srt=" + , Url.percentEncode signatureStuff.resources + , "&sp=" + , Url.percentEncode signatureStuff.permissions + , "&se=" + , Url.percentEncode signatureStuff.expiryTime + , "&spr=" + , Url.percentEncode signatureStuff.protocol + , "&sig=" + , Url.percentEncode (makeSignature signatureStuff) + ] + + + +-- Signature + + +type alias SignatureDependencies = + { accountKey : String + , accountName : String + , expiryTime : String + , permissions : String + , protocol : String + , resources : String + , services : String + , startTime : String + , version : String + } + + +{-| Make a signature. + + >>> makeSignature { accountKey = "93K17Co74T2lDHk2rA+wmb/avIAS6u6lPnZrk2hyT+9+aov82qNhrcXSNGZCzm9mjd4d75/oxxOr6r1JVpgTLA==", accountName = "tsmatsuzsttest0001", expiryTime = "2016-07-08T04:41:20Z", permissions = "rwdlacup", protocol = "https", resources = "sco", services = "bfqt", startTime = "2016-06-29T04:41:20Z", version = "2015-04-05" } + "+XuDjuLE1Sv/FrJTLz8YjsaDukWNTKX7e8G8Ew+5aps=" + +-} +makeSignature : SignatureDependencies -> String +makeSignature { accountKey, accountName, expiryTime, permissions, protocol, resources, services, startTime, version } = + let + message = + -- accountname + "\n" + + -- signedpermissions + "\n" + + -- signedservice + "\n" + + -- signedresourcetype + "\n" + + -- signedstart + "\n" + + -- signedexpiry + "\n" + + -- signedIP + "\n" + + -- signedProtocol + "\n" + + -- signedversion + "\n" + String.join "\n" + [ accountName + , permissions + , services + , resources + , startTime + , expiryTime + , "" + , protocol + , version ++ "\n" + ] + in + accountKey + |> BinaryBase64.decode + |> Result.withDefault [] + |> List.map (Binary.fromDecimal >> Binary.ensureSize 8) + |> Binary.concat + |> Hmac.encrypt64 SHA.sha256 message + |> Binary.chunksOf 8 + |> List.map Binary.toDecimal + |> BinaryBase64.encode diff --git a/src/Library/Sources/Services/Azure/BlobParser.elm b/src/Library/Sources/Services/Azure/BlobParser.elm new file mode 100755 index 00000000..bbb25014 --- /dev/null +++ b/src/Library/Sources/Services/Azure/BlobParser.elm @@ -0,0 +1,68 @@ +module Sources.Services.Azure.BlobParser exposing (parseErrorResponse, parseTreeResponse) + +import Sources.Processing exposing (Marker(..), TreeAnswer) +import Xml.Decode exposing (..) + + + +-- TREE + + +parseTreeResponse : String -> Marker -> TreeAnswer Marker +parseTreeResponse response _ = + response + |> decodeString + (map2 + (\f m -> { filePaths = f, marker = m }) + filePathsDecoder + markerDecoder + ) + |> Result.withDefault { filePaths = [], marker = TheEnd } + + +filePathsDecoder : Decoder (List String) +filePathsDecoder = + string + |> single + |> path [ "Name" ] + |> list + |> path [ "Blobs", "Blob" ] + + +markerDecoder : Decoder Marker +markerDecoder = + map + (\maybeNextMarker -> + case maybeNextMarker of + Just "" -> + TheEnd + + Just nextMarker -> + InProgress nextMarker + + Nothing -> + TheEnd + ) + (maybe <| path [ "NextMarker" ] <| single string) + + + +-- ERROR + + +parseErrorResponse : String -> String +parseErrorResponse response = + response + |> decodeString errorMessagesDecoder + |> Result.toMaybe + |> Maybe.andThen List.head + |> Maybe.withDefault "Invalid request" + + +errorMessagesDecoder : Decoder (List String) +errorMessagesDecoder = + string + |> single + |> path [ "Message" ] + |> list + |> path [ "Error" ] diff --git a/src/Library/Sources/Services/Azure/FileMarker.elm b/src/Library/Sources/Services/Azure/FileMarker.elm new file mode 100755 index 00000000..5bbcc497 --- /dev/null +++ b/src/Library/Sources/Services/Azure/FileMarker.elm @@ -0,0 +1,154 @@ +module Sources.Services.Azure.FileMarker exposing (MarkerItem(..), concat, itemToString, paramSeparator, prefixer, removeOne, separator, stringToItem, takeOne) + +{-| Custom `Marker` for the Azure File API. + +The Azure File API currently doesn't make a recursive list, +so we have to manage that ourselves. + +This custom marker is a combination of: + + - The default `marker` param, see URI parameters at + - Our custom logic to handle recursive listings + +Example: InProgress "dir=example ΒΆ param=defaultMarker" + +-} + +import Sources.Processing exposing (Marker(..)) + + +type MarkerItem + = Directory String + | Param { directory : String, marker : String } + + +separator : String +separator = + " Ι‘ " + + +prefixer : String +prefixer = + " Ξ² " + + +paramSeparator : String +paramSeparator = + " Ι£ " + + + +-- IN + + +concat : List MarkerItem -> Marker -> Marker +concat list marker = + let + listStringified = + List.map itemToString list + + result = + case marker of + InProgress m -> + [ listStringified, String.split separator m ] + |> List.concat + |> String.join separator + + _ -> + String.join separator listStringified + in + case result of + "" -> + TheEnd + + r -> + InProgress r + + + +-- OUT + + +{-| Take the first item and return it. +-} +takeOne : Marker -> Maybe MarkerItem +takeOne marker = + case marker of + InProgress m -> + m + |> String.split separator + |> List.head + |> Maybe.andThen stringToItem + + _ -> + Nothing + + +{-| Remove the first item if there is one. +-} +removeOne : Marker -> Marker +removeOne marker = + case marker of + InProgress m -> + let + tmp = + m + |> String.split separator + |> List.drop 1 + |> String.join separator + in + case tmp of + "" -> + TheEnd + + x -> + InProgress x + + _ -> + TheEnd + + + +-- CONVERSIONS + + +itemToString : MarkerItem -> String +itemToString item = + case item of + Directory d -> + "dir" ++ prefixer ++ d + + Param { directory, marker } -> + "par" ++ prefixer ++ directory ++ paramSeparator ++ marker + + +stringToItem : String -> Maybe MarkerItem +stringToItem string = + let + exploded = + String.split prefixer string + in + case List.head exploded of + Just "dir" -> + exploded + |> List.drop 1 + |> String.join prefixer + |> Directory + |> Just + + Just "par" -> + exploded + |> List.drop 1 + |> String.join prefixer + |> String.split paramSeparator + |> (\x -> + case x of + [ dir, mar ] -> + Just (Param { directory = dir, marker = mar }) + + _ -> + Nothing + ) + + _ -> + Nothing diff --git a/src/Library/Sources/Services/Azure/FileParser.elm b/src/Library/Sources/Services/Azure/FileParser.elm new file mode 100755 index 00000000..e9f217cc --- /dev/null +++ b/src/Library/Sources/Services/Azure/FileParser.elm @@ -0,0 +1,104 @@ +module Sources.Services.Azure.FileParser exposing (parseErrorResponse, parseTreeResponse) + +import Dict.Ext as Dict +import Sources.Processing exposing (Marker(..), TreeAnswer) +import Sources.Services.Azure.BlobParser +import Sources.Services.Azure.FileMarker as FileMarker exposing (MarkerItem(..)) +import Sources.Services.Common exposing (cleanPath) +import Xml.Decode exposing (..) + + + +-- TREE + + +parseTreeResponse : String -> Marker -> TreeAnswer Marker +parseTreeResponse response previousMarker = + response + |> decodeString (treeDecoder previousMarker) + |> Result.withDefault { filePaths = [], marker = TheEnd } + + +treeDecoder : Marker -> Decoder (TreeAnswer Marker) +treeDecoder previousMarker = + usedDirectoryDecoder + |> map cleanPath + |> andThen + (\usedDirectory -> + map2 + (\a b -> ( usedDirectory, a, b )) + (map (List.map <| String.append usedDirectory) filePathsDecoder) + (map (List.map <| String.append usedDirectory) directoryPathsDecoder) + ) + |> andThen + (\( usedDirectory, filePaths, directoryPaths ) -> + previousMarker + |> FileMarker.removeOne + |> FileMarker.concat (List.map Directory directoryPaths) + |> markerDecoder usedDirectory + |> map (\marker -> { filePaths = filePaths, marker = marker }) + ) + + +usedDirectoryDecoder : Decoder String +usedDirectoryDecoder = + stringAttr "DirectoryPath" + + +filePathsDecoder : Decoder (List String) +filePathsDecoder = + string + |> single + |> path [ "Name" ] + |> list + |> path [ "Entries", "File" ] + + +directoryPathsDecoder : Decoder (List String) +directoryPathsDecoder = + string + |> single + |> path [ "Name" ] + |> list + |> path [ "Entries", "Directory" ] + + +markerDecoder : String -> Marker -> Decoder Marker +markerDecoder usedDirectory markerWithDirectories = + map + (\maybeNextMarker -> + case maybeNextMarker of + Just "" -> + markerWithDirectories + + Just marker -> + FileMarker.concat + [ Param { directory = usedDirectory, marker = marker } ] + markerWithDirectories + + Nothing -> + markerWithDirectories + ) + (maybe <| path [ "NextMarker" ] <| single string) + + + +-- ERROR + + +parseErrorResponse : String -> String +parseErrorResponse response = + response + |> decodeString errorMessagesDecoder + |> Result.toMaybe + |> Maybe.andThen List.head + |> Maybe.withDefault "Invalid request" + + +errorMessagesDecoder : Decoder (List String) +errorMessagesDecoder = + string + |> single + |> path [ "Message" ] + |> list + |> path [ "Error" ] diff --git a/src/Library/Sources/Services/AzureBlob.elm b/src/Library/Sources/Services/AzureBlob.elm new file mode 100644 index 00000000..0769c097 --- /dev/null +++ b/src/Library/Sources/Services/AzureBlob.elm @@ -0,0 +1,168 @@ +module Sources.Services.AzureBlob exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) + +{-| Microsoft Azure Blob Service. + +Resources: + + - + +-} + +import Dict +import Http +import Sources exposing (Property, SourceData) +import Sources.Pick +import Sources.Processing exposing (..) +import Sources.Services.Azure.Authorization exposing (..) +import Sources.Services.Azure.BlobParser as Parser +import Sources.Services.Common exposing (cleanPath, noPrep) +import Time + + + +-- PROPERTIES +-- πŸ“Ÿ + + +defaults = + { directoryPath = "/" + , name = "Music from Azure Blob Storage" + } + + +{-| The list of properties we need from the user. + +Tuple: (property, label, placeholder, isPassword) +Will be used for the forms. + +-} +properties : List Property +properties = + [ { key = "accountName" + , label = "Account name" + , placeholder = "myaccount" + , password = False + } + , { key = "accountKey" + , label = "Account key" + , placeholder = "MXFPDkaN4KBT" + , password = True + } + , { key = "container" + , label = "Container" + , placeholder = "music" + , password = False + } + , { key = "directoryPath" + , label = "Directory (aka. Prefix)" + , placeholder = defaults.directoryPath + , password = False + } + ] + + +{-| Initial data set. +-} +initialData : SourceData +initialData = + Dict.fromList + [ ( "accountName", "" ) + , ( "accountKey", "" ) + , ( "container", "" ) + , ( "directoryPath", defaults.directoryPath ) + , ( "name", defaults.name ) + ] + + + +-- PREPARATION + + +prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) +prepare _ _ _ _ = + Nothing + + + +-- TREE + + +{-| Create a directory tree. + +List all the tracks in the container. +Or a specific directory in the container. + +-} +makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg +makeTree srcData marker currentTime toMsg = + let + directoryPath = + srcData + |> Dict.get "directoryPath" + |> Maybe.withDefault defaults.directoryPath + |> cleanPath + + baseParams = + [ ( "maxresults", "1000" ) ] + + params = + case marker of + InProgress s -> + [ ( "marker", s ) ] + + _ -> + [] + + url = + presignedUrl Blob List Get 1 currentTime srcData directoryPath (baseParams ++ params) + in + Http.get + { url = url + , expect = Http.expectString toMsg + } + + +{-| Re-export parser functions. +-} +parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse = + noPrep + + +parseTreeResponse : String -> Marker -> TreeAnswer Marker +parseTreeResponse = + Parser.parseTreeResponse + + +parseErrorResponse : String -> String +parseErrorResponse = + Parser.parseErrorResponse + + + +-- POST + + +{-| Post process the tree results. + +!!! Make sure we only use music files that we can use. + +-} +postProcessTree : List String -> List String +postProcessTree = + Sources.Pick.selectMusicFiles + + + +-- TRACK URL + + +{-| Create a public url for a file. + +We need this to play the track. +(!) Creates a presigned url that's valid for 48 hours + +-} +makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime srcData method pathToFile = + presignedUrl Blob Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/AzureFile.elm b/src/Library/Sources/Services/AzureFile.elm new file mode 100644 index 00000000..d429eeb7 --- /dev/null +++ b/src/Library/Sources/Services/AzureFile.elm @@ -0,0 +1,172 @@ +module Sources.Services.AzureFile exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) + +{-| Microsoft Azure File Service. + +Resources: + + - + +-} + +import Dict +import Http +import Sources exposing (Property, SourceData) +import Sources.Pick +import Sources.Processing exposing (..) +import Sources.Services.Azure.Authorization exposing (..) +import Sources.Services.Azure.FileMarker as FileMarker exposing (MarkerItem(..)) +import Sources.Services.Azure.FileParser as Parser +import Sources.Services.Common exposing (cleanPath, noPrep) +import Time + + + +-- PROPERTIES +-- πŸ“Ÿ + + +defaults = + { directoryPath = "/" + , name = "Music from Azure File Storage" + } + + +{-| The list of properties we need from the user. + +Tuple: (property, label, placeholder, isPassword) +Will be used for the forms. + +-} +properties : List Property +properties = + [ { key = "accountName" + , label = "Account name" + , placeholder = "myaccount" + , password = False + } + , { key = "accountKey" + , label = "Account key" + , placeholder = "MXFPDkaN4KBT" + , password = True + } + , { key = "container" + , label = "Share name" + , placeholder = "music" + , password = False + } + , { key = "directoryPath" + , label = "Directory (aka. Prefix)" + , placeholder = defaults.directoryPath + , password = False + } + ] + + +{-| Initial data set. +-} +initialData : SourceData +initialData = + Dict.fromList + [ ( "accountName", "" ) + , ( "accountKey", "" ) + , ( "container", "" ) + , ( "directoryPath", defaults.directoryPath ) + , ( "name", defaults.name ) + ] + + + +-- PREPARATION + + +prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) +prepare _ _ _ _ = + Nothing + + + +-- TREE + + +{-| Create a directory tree. + +List all the tracks in the container. +Or a specific directory in the container. + +-} +makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg +makeTree srcData marker currentTime toMsg = + let + directoryPathFromSrcData = + srcData + |> Dict.get "directoryPath" + |> Maybe.withDefault defaults.directoryPath + |> cleanPath + + baseParams = + [ ( "maxresults", "1000" ) ] + + ( directoryPath, params ) = + case FileMarker.takeOne marker of + Just (Directory directory) -> + Tuple.pair directory [] + + Just (Param param) -> + Tuple.pair param.directory [ ( "marker", param.marker ) ] + + _ -> + Tuple.pair directoryPathFromSrcData [] + + url = + presignedUrl File List Get 1 currentTime srcData directoryPath (baseParams ++ params) + in + Http.get + { url = url + , expect = Http.expectString toMsg + } + + +{-| Re-export parser functions. +-} +parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse = + noPrep + + +parseTreeResponse : String -> Marker -> TreeAnswer Marker +parseTreeResponse = + Parser.parseTreeResponse + + +parseErrorResponse : String -> String +parseErrorResponse = + Parser.parseErrorResponse + + + +-- POST + + +{-| Post process the tree results. + +!!! Make sure we only use music files that we can use. + +-} +postProcessTree : List String -> List String +postProcessTree = + Sources.Pick.selectMusicFiles + + + +-- TRACK URL + + +{-| Create a public url for a file. + +We need this to play the track. +(!) Creates a presigned url that's valid for 48 hours + +-} +makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime srcData method pathToFile = + presignedUrl File Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/Dropbox.elm b/src/Library/Sources/Services/Dropbox.elm index 07ff2431..3028e4fe 100644 --- a/src/Library/Sources/Services/Dropbox.elm +++ b/src/Library/Sources/Services/Dropbox.elm @@ -4,6 +4,7 @@ module Sources.Services.Dropbox exposing (authorizationSourceData, authorization -} import Base64 +import Common import Dict import Dict.Ext as Dict import Http @@ -16,8 +17,6 @@ import Sources.Processing exposing (..) import Sources.Services.Common exposing (cleanPath, noPrep) import Sources.Services.Dropbox.Parser as Parser import Time -import Url -import Url.Builder as Url @@ -96,8 +95,7 @@ authorizationUrl sourceData origin = , ( "redirect_uri", origin ++ "/sources/new/dropbox" ) , ( "state", state ) ] - |> List.map (\( a, b ) -> Url.string a b) - |> Url.toQuery + |> Common.queryString |> String.append "https://www.dropbox.com/oauth2/authorize" diff --git a/src/Library/Sources/Services/Google.elm b/src/Library/Sources/Services/Google.elm index 5f3c62ab..53e9abdf 100644 --- a/src/Library/Sources/Services/Google.elm +++ b/src/Library/Sources/Services/Google.elm @@ -4,6 +4,7 @@ module Sources.Services.Google exposing (authorizationSourceData, authorizationU -} import Base64 +import Common import Dict import Dict.Ext as Dict import Http @@ -15,8 +16,6 @@ import Sources.Pick import Sources.Processing exposing (..) import Sources.Services.Google.Parser as Parser import Time -import Url -import Url.Builder as Url @@ -113,8 +112,7 @@ authorizationUrl sourceData origin = , ( "scope", "https://www.googleapis.com/auth/drive.readonly" ) , ( "state", state ) ] - |> List.map (\( a, b ) -> Url.string a b) - |> Url.toQuery + |> Common.queryString |> String.append "https://accounts.google.com/o/oauth2/v2/auth" @@ -165,9 +163,7 @@ prepare origin srcData _ toMsg = ] query = - queryParams - |> List.map (\( a, b ) -> Url.string a b) - |> Url.toQuery + Common.queryString queryParams url = "https://www.googleapis.com/oauth2/v4/token" ++ query @@ -215,7 +211,7 @@ makeTree srcData marker currentTime toMsg = , ( "spaces", "drive" ) ] - params = + queryString = (case marker of InProgress cursor -> [ ( "pageToken", cursor ) @@ -225,13 +221,12 @@ makeTree srcData marker currentTime toMsg = [] ) |> List.append paramsBase - |> List.map (\( a, b ) -> Url.string a b) - |> Url.toQuery + |> Common.queryString in Http.request { method = "GET" , headers = [ Http.header "Authorization" ("Bearer " ++ accessToken) ] - , url = "https://www.googleapis.com/drive/v3/files" ++ params + , url = "https://www.googleapis.com/drive/v3/files" ++ queryString , body = Http.emptyBody , expect = Http.expectString toMsg , timeout = Nothing diff --git a/src/Library/Tuple/Ext.elm b/src/Library/Tuple/Ext.elm new file mode 100644 index 00000000..f52a6777 --- /dev/null +++ b/src/Library/Tuple/Ext.elm @@ -0,0 +1,8 @@ +module Tuple.Ext exposing (uncurry) + +-- πŸ”± + + +uncurry : (a -> b -> c) -> ( a, b ) -> c +uncurry fn ( a, b ) = + fn a b